diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000000..aec086257a --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,76 @@ +name: Documentation + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + build: + if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')" + name: run + runs-on: ubuntu-latest + container: discourse/discourse_test:slim + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Setup Git + run: | + git config --global user.email "ci@ci.invalid" + git config --global user.name "Discourse CI" + + - name: Bundler cache + uses: actions/cache@v3 + with: + path: vendor/bundle + key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gem- + + - name: Setup gems + run: | + gem install bundler --conservative -v $(awk '/BUNDLED WITH/ { getline; gsub(/ /,""); print $0 }' Gemfile.lock) + bundle config --local path vendor/bundle + bundle config --local deployment true + bundle config --local without development + bundle install --jobs 4 + bundle clean + + - name: Get yarn cache directory + id: yarn-cache-dir + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - name: Yarn cache + uses: actions/cache@v3 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Yarn install + run: yarn install + + - name: Check Chat documentation + run: | + LOAD_PLUGINS=1 bin/rake chat:doc + + if [ ! -z "$(git status --porcelain plugins/chat/docs/)" ]; then + echo "Chat documentation is not up to date. To resolve, run:" + echo " LOAD_PLUGINS=1 bin/rake chat:doc" + echo + echo "Or manually apply the diff printed below:" + echo "---------------------------------------------" + git -c color.ui=always diff plugins/chat/docs/ + exit 1 + fi + timeout-minutes: 30 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 17393c8517..9374c85d70 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,9 +18,9 @@ permissions: jobs: build: if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')" - name: ${{ matrix.target }} ${{ matrix.build_type }} + name: ${{ matrix.target }} ${{ matrix.build_type }} ${{ matrix.ruby }} runs-on: ${{ (matrix.build_type == 'annotations') && 'ubuntu-latest' || 'ubuntu-20.04-8core' }} - container: discourse/discourse_test:slim${{ (matrix.build_type == 'frontend' || matrix.build_type == 'system') && '-browsers' || '' }} + container: discourse/discourse_test:slim${{ (matrix.build_type == 'frontend' || matrix.build_type == 'system') && '-browsers' || '' }}${{ (matrix.ruby == '3.2') && '-ruby-3.2.0' || '' }} timeout-minutes: 20 env: @@ -38,6 +38,7 @@ jobs: matrix: build_type: [backend, frontend, system, annotations] target: [core, plugins] + ruby: ['3.1'] exclude: - build_type: annotations target: plugins @@ -68,9 +69,9 @@ jobs: uses: actions/cache@v3 with: path: vendor/bundle - key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} + key: ${{ runner.os }}-${{ matrix.ruby }}-gem-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | - ${{ runner.os }}-gem- + ${{ runner.os }}-${{ matrix.ruby }}-gem- - name: Setup gems run: | diff --git a/.jsdoc b/.jsdoc new file mode 100644 index 0000000000..92345fda22 --- /dev/null +++ b/.jsdoc @@ -0,0 +1,7 @@ +// jsdoc doesn't accept paths starting with _ (which is the case on github runners) +// so we need to alter the default config +{ + "source": { + "excludePattern": "" + } +} diff --git a/Gemfile b/Gemfile index ef77228f74..4bd110bf4b 100644 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,7 @@ else # this allows us to include the bits of rails we use without pieces we do not. # # To issue a rails update bump the version number here - rails_version = "7.0.3.1" + rails_version = "7.0.4.1" gem "actionmailer", rails_version gem "actionpack", rails_version gem "actionview", rails_version @@ -32,8 +32,8 @@ end gem "json" # TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals -# This is a desired upgrade we should get to. -gem "sprockets", "3.7.2" +# We intend to drop sprockets rather than upgrade to 4.x +gem "sprockets", git: "https://github.com/rails/sprockets", branch: "3.x" # this will eventually be added to rails, # allows us to precompile all our templates in the unicorn master @@ -261,12 +261,7 @@ if ENV["IMPORT"] == "1" gem "parallel", require: false end -# workaround for openssl 3.0, see -# https://github.com/pushpad/web-push/pull/2 -gem "web-push", - require: false, - git: "https://github.com/xfalcox/web-push", - branch: "openssl-3-compat" +gem "web-push" gem "colored2", require: false gem "maxminddb" diff --git a/Gemfile.lock b/Gemfile.lock index 06f20a01c2..0f22d6f063 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,37 +6,36 @@ GIT mini_mime (>= 0.1.1) GIT - remote: https://github.com/xfalcox/web-push - revision: 369df8f475a4cd4832a7679bec16576665f24d24 - branch: openssl-3-compat + remote: https://github.com/rails/sprockets + revision: f4d3dae71ef29c44b75a49cfbf8032cce07b423a + branch: 3.x specs: - web-push (2.1.0) - hkdf (~> 1.0) - jwt (~> 2.0) - openssl (~> 3.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) GEM remote: https://rubygems.org/ specs: - actionmailer (7.0.3.1) - actionpack (= 7.0.3.1) - actionview (= 7.0.3.1) - activejob (= 7.0.3.1) - activesupport (= 7.0.3.1) + actionmailer (7.0.4.1) + actionpack (= 7.0.4.1) + actionview (= 7.0.4.1) + activejob (= 7.0.4.1) + activesupport (= 7.0.4.1) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.3.1) - actionview (= 7.0.3.1) - activesupport (= 7.0.3.1) + actionpack (7.0.4.1) + actionview (= 7.0.4.1) + activesupport (= 7.0.4.1) rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actionview (7.0.3.1) - activesupport (= 7.0.3.1) + actionview (7.0.4.1) + activesupport (= 7.0.4.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -45,15 +44,15 @@ GEM actionview (>= 6.0.a) active_model_serializers (0.8.4) activemodel (>= 3.0) - activejob (7.0.3.1) - activesupport (= 7.0.3.1) + activejob (7.0.4.1) + activesupport (= 7.0.4.1) globalid (>= 0.3.6) - activemodel (7.0.3.1) - activesupport (= 7.0.3.1) - activerecord (7.0.3.1) - activemodel (= 7.0.3.1) - activesupport (= 7.0.3.1) - activesupport (7.0.3.1) + activemodel (7.0.4.1) + activesupport (= 7.0.4.1) + activerecord (7.0.4.1) + activemodel (= 7.0.4.1) + activesupport (= 7.0.4.1) + activesupport (7.0.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -111,7 +110,7 @@ GEM chunky_png (1.4.0) coderay (1.1.3) colored2 (3.1.2) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.0) connection_pool (2.3.0) cose (1.3.0) cbor (~> 0.5.9) @@ -120,8 +119,9 @@ GEM crack (0.4.5) rexml crass (1.0.6) - css_parser (1.13.0) + css_parser (1.14.0) addressable + date (3.3.3) debug_inspector (1.1.0) diff-lcs (1.5.0) diffy (3.4.2) @@ -137,15 +137,15 @@ GEM ecma-re-validator (0.4.0) regexp_parser (~> 2.2) email_reply_trimmer (0.1.13) - erubi (1.11.0) - excon (0.96.0) + erubi (1.12.0) + excon (0.97.2) execjs (2.8.1) exifr (1.3.10) fabrication (2.30.0) faker (2.23.0) i18n (>= 1.8.11, < 2) fakeweb (1.3.0) - faraday (2.7.2) + faraday (2.7.4) faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) @@ -157,7 +157,7 @@ GEM ffi (1.15.5) fspath (3.1.2) gc_tracer (1.5.1) - globalid (1.0.0) + globalid (1.0.1) activesupport (>= 5.0) guess_html_encoding (0.0.11) hana (1.3.7) @@ -215,7 +215,7 @@ GEM matrix (0.4.2) maxminddb (0.1.22) memory_profiler (1.0.1) - message_bus (4.3.1) + message_bus (4.3.2) rack (>= 1.1.3) method_source (1.0.0) mini_mime (1.1.2) @@ -236,7 +236,8 @@ GEM mustache (1.1.1) net-http (0.3.2) uri - net-imap (0.3.1) + net-imap (0.3.4) + date net-protocol net-pop (0.1.2) net-protocol @@ -245,16 +246,16 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) - nokogiri (1.13.10) + nokogiri (1.14.0) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.10-aarch64-linux) + nokogiri (1.14.0-aarch64-linux) racc (~> 1.4) - nokogiri (1.13.10-arm64-darwin) + nokogiri (1.14.0-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.10-x86_64-darwin) + nokogiri (1.14.0-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.10-x86_64-linux) + nokogiri (1.14.0-x86_64-linux) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) @@ -296,7 +297,7 @@ GEM openssl (> 2.0, < 3.1) optimist (3.0.1) parallel (1.22.1) - parallel_tests (4.0.0) + parallel_tests (4.1.0) parallel parser (3.2.0.0) ast (~> 2.4.1) @@ -316,7 +317,7 @@ GEM nio4r (~> 2.0) r2 (0.2.7) racc (1.6.2) - rack (2.2.5) + rack (2.2.6.2) rack-mini-profiler (3.0.0) rack (>= 1.2.0) rack-protection (3.0.5) @@ -326,7 +327,7 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.4) + rails-html-sanitizer (1.5.0) loofah (~> 2.19, >= 2.19.1) rails_failover (0.8.1) activerecord (> 6.0, < 7.1) @@ -335,9 +336,9 @@ GEM rails_multisite (4.0.1) activerecord (> 5.0, < 7.1) railties (> 5.0, < 7.1) - railties (7.0.3.1) - actionpack (= 7.0.3.1) - activesupport (= 7.0.3.1) + railties (7.0.4.1) + actionpack (= 7.0.4.1) + activesupport (= 7.0.4.1) method_source rake (>= 12.2) thor (~> 1.0) @@ -354,9 +355,9 @@ GEM optimist (>= 3.0.0) rchardet (1.8.0) redis (4.8.0) - redis-namespace (1.9.0) + redis-namespace (1.10.0) redis (>= 4) - regexp_parser (2.6.1) + regexp_parser (2.6.2) request_store (1.5.1) rack (>= 1.4) rexml (3.2.5) @@ -378,7 +379,7 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.12.2) + rspec-mocks (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.1) @@ -397,7 +398,7 @@ GEM json-schema (>= 2.2, < 4.0) railties (>= 3.1, < 7.1) rspec-core (>= 2.14) - rubocop (1.43.0) + rubocop (1.44.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) @@ -409,11 +410,14 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.24.1) parser (>= 3.1.1.0) + rubocop-capybara (2.17.0) + rubocop (~> 1.41) rubocop-discourse (3.0.3) rubocop (>= 1.1.0) rubocop-rspec (>= 2.0.0) - rubocop-rspec (2.16.0) + rubocop-rspec (2.18.1) rubocop (~> 1.33) + rubocop-capybara (~> 2.17) ruby-prof (1.4.5) ruby-progressbar (1.11.0) ruby-readability (0.7.0) @@ -433,7 +437,7 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.7.1) + selenium-webdriver (4.8.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -452,9 +456,6 @@ GEM snaky_hash (2.0.1) hashie version_gem (~> 1.1, >= 1.1.1) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) @@ -483,6 +484,10 @@ GEM uri (0.12.0) uri_template (0.7.0) version_gem (1.1.1) + web-push (3.0.0) + hkdf (~> 1.0) + jwt (~> 2.0) + openssl (~> 3.0) webdrivers (5.2.0) nokogiri (~> 1.6) rubyzip (>= 1.3.0) @@ -496,7 +501,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - yaml-lint (0.0.10) + yaml-lint (0.1.2) zeitwerk (2.6.6) PLATFORMS @@ -509,14 +514,14 @@ PLATFORMS x86_64-linux DEPENDENCIES - actionmailer (= 7.0.3.1) - actionpack (= 7.0.3.1) - actionview (= 7.0.3.1) + actionmailer (= 7.0.4.1) + actionpack (= 7.0.4.1) + actionview (= 7.0.4.1) actionview_precompiler active_model_serializers (~> 0.8.3) - activemodel (= 7.0.3.1) - activerecord (= 7.0.3.1) - activesupport (= 7.0.3.1) + activemodel (= 7.0.4.1) + activerecord (= 7.0.4.1) + activesupport (= 7.0.4.1) addressable annotate aws-sdk-s3 @@ -601,7 +606,7 @@ DEPENDENCIES rack-protection rails_failover rails_multisite - railties (= 7.0.3.1) + railties (= 7.0.4.1) rake rb-fsevent rbtrace @@ -627,7 +632,7 @@ DEPENDENCIES shoulda-matchers sidekiq simplecov - sprockets (= 3.7.2) + sprockets! sprockets-rails sshkey stackprof @@ -638,7 +643,7 @@ DEPENDENCIES uglifier unf unicorn - web-push! + web-push webdrivers webmock webrick diff --git a/README.md b/README.md index 4673f84cc3..2b914eab91 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ To get your environment setup, follow the community setup guide for your operati If you're familiar with how Rails works and are comfortable setting up your own environment, you can also try out the [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md), which is aimed primarily at Ubuntu and macOS environments. -Before you get started, ensure you have the following minimum versions: [Ruby 2.7+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 13+](https://www.postgresql.org/download/), [Redis 6.2+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first! +Before you get started, ensure you have the following minimum versions: [Ruby 3.1+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 13](https://www.postgresql.org/download/), [Redis 7](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first! ## Setting up Discourse @@ -51,7 +51,7 @@ Discourse supports the **latest, stable releases** of all major browsers and pla | Microsoft Edge | | | | Mozilla Firefox | | | -Additionally, we aim to support Safari on iOS 12.5+ until January 2023 (Discourse 3.0). +Additionally, we aim to support Safari on iOS 15.7+. ## Built With diff --git a/app/assets/javascripts/admin/addon/templates/modal/admin-penalize-user.hbs b/app/assets/javascripts/admin/addon/templates/modal/admin-penalize-user.hbs index 1303d01d80..ea8ae78a8f 100644 --- a/app/assets/javascripts/admin/addon/templates/modal/admin-penalize-user.hbs +++ b/app/assets/javascripts/admin/addon/templates/modal/admin-penalize-user.hbs @@ -57,7 +57,6 @@ /> {{/if}} -
{{html-safe this.penaltyHistory}}
{{else}} {{#if (eq this.penaltyType "suspend")}}
{{i18n "admin.user.cant_suspend"}}
@@ -65,6 +64,7 @@
{{i18n "admin.user.cant_silence"}}
{{/if}} {{/if}} +
{{html-safe this.penaltyHistory}}
diff --git a/app/assets/javascripts/admin/package.json b/app/assets/javascripts/admin/package.json index 03e1828c42..5b5c0c7582 100644 --- a/app/assets/javascripts/admin/package.json +++ b/app/assets/javascripts/admin/package.json @@ -15,7 +15,7 @@ "start": "ember serve" }, "dependencies": { - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "webpack": "^5.75.0", @@ -24,7 +24,7 @@ "devDependencies": { "@babel/core": "^7.20.12", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^2.0.2", + "@embroider/test-setup": "^2.1.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/bootstrap-json/index.js b/app/assets/javascripts/bootstrap-json/index.js index a2087fdc16..b795588ce7 100644 --- a/app/assets/javascripts/bootstrap-json/index.js +++ b/app/assets/javascripts/bootstrap-json/index.js @@ -252,6 +252,11 @@ async function buildFromBootstrap(proxy, baseURL, req, response, preload) { url.searchParams.append("safe_mode", reqUrlSafeMode); } + const enableSidebar = forUrlSearchParams.get("enable_sidebar"); + if (enableSidebar) { + url.searchParams.append("enable_sidebar", enableSidebar); + } + const reqUrlPreviewThemeId = forUrlSearchParams.get("preview_theme_id"); if (reqUrlPreviewThemeId) { url.searchParams.append("preview_theme_id", reqUrlPreviewThemeId); @@ -458,6 +463,13 @@ to serve API requests. For example: baseURL = rootURL === "" ? "/" : cleanBaseURL(rootURL || baseURL); const rawMiddleware = express.raw({ type: () => true, limit: "100mb" }); + const pathRestrictedRawMiddleware = (req, res, next) => { + if (this.shouldHandleRequest(req, baseURL)) { + return rawMiddleware(req, res, next); + } else { + return next(); + } + }; app.use( "/favicon.ico", @@ -469,9 +481,9 @@ to serve API requests. For example: ) ); - app.use(rawMiddleware, async (req, res, next) => { + app.use(pathRestrictedRawMiddleware, async (req, res, next) => { try { - if (this.shouldForwardRequest(req, baseURL)) { + if (this.shouldHandleRequest(req, baseURL)) { await handleRequest(proxy, baseURL, req, res); } else { // Fixes issues when using e.g. "localhost" instead of loopback IP address @@ -492,7 +504,7 @@ to serve API requests. For example: }); }, - shouldForwardRequest(request, baseURL) { + shouldHandleRequest(request, baseURL) { if ( [ `${baseURL}tests/index.html`, @@ -508,6 +520,10 @@ to serve API requests. For example: return false; } + if (request.path.startsWith(`${baseURL}message-bus/`)) { + return false; + } + return true; }, }; diff --git a/app/assets/javascripts/bootstrap-json/package.json b/app/assets/javascripts/bootstrap-json/package.json index f35b3b0e82..b425bf6a2c 100644 --- a/app/assets/javascripts/bootstrap-json/package.json +++ b/app/assets/javascripts/bootstrap-json/package.json @@ -24,7 +24,7 @@ "discourse-plugins": "1.0.0", "express": "^4.18.2", "html-entities": "^2.3.3", - "jsdom": "^21.0.0", + "jsdom": "^21.1.0", "node-fetch": "^3.3.0" } } diff --git a/app/assets/javascripts/dialog-holder/package.json b/app/assets/javascripts/dialog-holder/package.json index 91add909f9..aa568e76c3 100644 --- a/app/assets/javascripts/dialog-holder/package.json +++ b/app/assets/javascripts/dialog-holder/package.json @@ -9,7 +9,7 @@ ], "dependencies": { "a11y-dialog": "7.5.2", - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1" }, diff --git a/app/assets/javascripts/discourse-common/package.json b/app/assets/javascripts/discourse-common/package.json index 0491353069..edc7848c63 100644 --- a/app/assets/javascripts/discourse-common/package.json +++ b/app/assets/javascripts/discourse-common/package.json @@ -21,7 +21,7 @@ "@uppy/drop-target": "^2.0.1", "@uppy/utils": "^5.1.1", "@uppy/xhr-upload": "^3.0.4", - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "ember-resolver": "^8.0.3", @@ -32,7 +32,7 @@ "devDependencies": { "@babel/core": "^7.20.12", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^2.0.2", + "@embroider/test-setup": "^2.1.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/discourse-hbr/package.json b/app/assets/javascripts/discourse-hbr/package.json index 0010518e81..f2b8001fae 100644 --- a/app/assets/javascripts/discourse-hbr/package.json +++ b/app/assets/javascripts/discourse-hbr/package.json @@ -15,7 +15,7 @@ "start": "ember serve" }, "dependencies": { - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "handlebars": "^4.7.6", @@ -24,7 +24,7 @@ "devDependencies": { "@babel/core": "^7.20.12", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^2.0.2", + "@embroider/test-setup": "^2.1.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/discourse-plugins/package.json b/app/assets/javascripts/discourse-plugins/package.json index 27c6ce8014..0b26f91a25 100644 --- a/app/assets/javascripts/discourse-plugins/package.json +++ b/app/assets/javascripts/discourse-plugins/package.json @@ -10,7 +10,7 @@ "repository": "", "dependencies": { "discourse-widget-hbs": "1.0.0", - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli": "~3.28.5", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", diff --git a/app/assets/javascripts/discourse-widget-hbs/package.json b/app/assets/javascripts/discourse-widget-hbs/package.json index 38a885d444..8eccbf31ca 100644 --- a/app/assets/javascripts/discourse-widget-hbs/package.json +++ b/app/assets/javascripts/discourse-widget-hbs/package.json @@ -15,7 +15,7 @@ "start": "ember serve" }, "dependencies": { - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "handlebars": "^4.7.6", @@ -24,7 +24,7 @@ "devDependencies": { "@babel/core": "^7.20.12", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^2.0.2", + "@embroider/test-setup": "^2.1.0", "@glimmer/component": "^1.1.2", "@glimmer/syntax": "^0.84.2", "broccoli-asset-rev": "^3.0.0", diff --git a/app/assets/javascripts/discourse/app/components/bulk-group-member-dropdown.js b/app/assets/javascripts/discourse/app/components/bulk-group-member-dropdown.js index baeddb0746..76fe26e6d5 100644 --- a/app/assets/javascripts/discourse/app/components/bulk-group-member-dropdown.js +++ b/app/assets/javascripts/discourse/app/components/bulk-group-member-dropdown.js @@ -39,22 +39,24 @@ export default DropdownSelectBoxComponent.extend({ }); } - if (this.bulkSelection.some((m) => !m.primary)) { - items.push({ - id: "setPrimary", - name: I18n.t("groups.members.make_all_primary"), - description: I18n.t("groups.members.make_all_primary_description"), - icon: "id-card", - }); - } + if (this.currentUser.staff) { + if (this.bulkSelection.some((m) => !m.primary)) { + items.push({ + id: "setPrimary", + name: I18n.t("groups.members.make_all_primary"), + description: I18n.t("groups.members.make_all_primary_description"), + icon: "id-card", + }); + } - if (this.bulkSelection.some((m) => m.primary)) { - items.push({ - id: "unsetPrimary", - name: I18n.t("groups.members.remove_all_primary"), - description: I18n.t("groups.members.remove_all_primary_description"), - icon: "id-card", - }); + if (this.bulkSelection.some((m) => m.primary)) { + items.push({ + id: "unsetPrimary", + name: I18n.t("groups.members.remove_all_primary"), + description: I18n.t("groups.members.remove_all_primary_description"), + icon: "id-card", + }); + } } return items; diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index 2c8cd34338..7388a6fc3d 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -106,8 +106,6 @@ export default Component.extend(ComposerUploadUppy, { fileUploadElementId: "file-uploader", mobileFileUploaderId: "mobile-file-upload", - // TODO (martin) Remove this once the chat plugin is using the new composerEventPrefix - eventPrefix: "composer", composerEventPrefix: "composer", uploadType: "composer", uppyId: "composer-editor-uppy", diff --git a/app/assets/javascripts/discourse/app/components/d-navigation-item.js b/app/assets/javascripts/discourse/app/components/d-navigation-item.js index bb32a38490..ec9e17dd22 100644 --- a/app/assets/javascripts/discourse/app/components/d-navigation-item.js +++ b/app/assets/javascripts/discourse/app/components/d-navigation-item.js @@ -11,7 +11,31 @@ export default Component.extend({ attributeBindings: ["ariaCurrent:aria-current", "title"], - ariaCurrent: computed("router.currentRouteName", "route", function () { - return this.router.currentRouteName === this.route ? "page" : null; - }), + ariaCurrent: computed( + "router.currentRouteName", + "router.currentRoute.parent.name", + "route", + "ariaCurrentContext", + function () { + let ariaCurrentValue = "page"; + + // when there are multiple levels of navigation + // we want the active parent to get `aria-current="page"` + // and the active child to get `aria-current="location"` + if (this.ariaCurrentContext === "subNav") { + ariaCurrentValue = "location"; + } else if (this.ariaCurrentContext === "parentNav") { + if ( + this.router.currentRouteName !== this.route && // not the current route + this.router.currentRoute.parent.name.includes(this.route) // but is the current parent route + ) { + return "page"; + } + } + + return this.router.currentRouteName === this.route + ? ariaCurrentValue + : null; + } + ), }); diff --git a/app/assets/javascripts/discourse/app/components/group-member-dropdown.js b/app/assets/javascripts/discourse/app/components/group-member-dropdown.js index bd6fa1006a..f8bff73391 100644 --- a/app/assets/javascripts/discourse/app/components/group-member-dropdown.js +++ b/app/assets/javascripts/discourse/app/components/group-member-dropdown.js @@ -43,6 +43,15 @@ export default DropdownSelectBoxComponent.extend({ icon: "shield-alt", }); } + } else if (this.canEditGroup && !this.member.owner) { + items.push({ + id: "makeOwner", + name: I18n.t("groups.members.make_owner"), + description: I18n.t("groups.members.make_owner_description", { + username: this.get("member.username"), + }), + icon: "shield-alt", + }); } if (this.currentUser.staff) { diff --git a/app/assets/javascripts/discourse/app/components/user-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav.hbs index 96b8f220c9..4221692a4c 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav.hbs @@ -1,55 +1,63 @@
- + {{#unless @user.profile_hidden}} -
  • - - {{d-icon "user"}} - {{i18n "user.summary.title"}} - -
  • + + {{d-icon "user"}} + {{i18n "user.summary.title"}} + + + + {{d-icon "stream"}} + {{i18n "user.activity_stream"}} + -
  • - - {{d-icon "stream"}} - {{i18n "user.activity_stream"}} - -
  • {{/unless}} {{#if @showNotificationsTab}} -
  • - - {{d-icon "bell" class="glyph"}} - {{i18n "user.notifications"}} - -
  • + + {{d-icon "bell" class="glyph"}} + {{i18n "user.notifications"}} + {{/if}} {{#if @showPrivateMessages}} -
  • - - {{d-icon "envelope"}} - {{i18n "user.private_messages"}} - -
  • + + {{d-icon "envelope"}} + {{i18n "user.private_messages"}} + {{/if}} {{#if @canInviteToForum}} -
  • - - {{d-icon "user-plus"}} - {{i18n "user.invited.title"}} - -
  • + + {{d-icon "user-plus"}} + {{i18n "user.invited.title"}} + {{/if}} {{#if @showBadges}} -
  • - - {{d-icon "certificate"}} - {{i18n "badges.title"}} - -
  • + + {{d-icon "certificate"}} + {{i18n "badges.title"}} + {{/if}} {{#if @user.can_edit}} -
  • - - {{d-icon "cog"}} - {{i18n "user.preferences"}} - -
  • + + {{d-icon "cog"}} + {{i18n "user.preferences"}} + {{/if}} {{#if (and this.site.mobileView this.currentUser.staff)}} -
  • +
  • {{d-icon "wrench"}} {{i18n "admin.user.manage_user"}} diff --git a/app/assets/javascripts/discourse/app/components/user-nav/activity-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav/activity-nav.hbs index b0f5ed0c48..85107fa1e9 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav/activity-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav/activity-nav.hbs @@ -1,44 +1,77 @@ - + {{d-icon "stream"}} {{i18n "user.filters.all"}} - + {{d-icon "list-ul"}} {{i18n "user_action_groups.4"}} - + {{d-icon "reply"}} {{i18n "user_action_groups.5"}} {{#if @user.showRead}} - + {{d-icon "history"}} {{i18n "user.read"}} {{/if}} {{#if @user.showDrafts}} - + {{d-icon "pencil-alt"}} {{@draftLabel}} {{/if}} {{#if (gt @model.pending_posts_count 0)}} - + {{d-icon "clock"}} {{@pendingLabel}} {{/if}} - + {{d-icon "heart"}} {{i18n "user_action_groups.1"}} {{#if @user.showBookmarks}} - + {{d-icon "bookmark"}} {{i18n "user_action_groups.3"}} diff --git a/app/assets/javascripts/discourse/app/components/user-nav/notifications-nav.hbs b/app/assets/javascripts/discourse/app/components/user-nav/notifications-nav.hbs index 75cd13dc92..bfda5d1e23 100644 --- a/app/assets/javascripts/discourse/app/components/user-nav/notifications-nav.hbs +++ b/app/assets/javascripts/discourse/app/components/user-nav/notifications-nav.hbs @@ -1,39 +1,49 @@ -
  • - - {{d-icon "bell"}} - {{i18n "user.filters.all"}} - -
  • + + {{d-icon "bell"}} + {{i18n "user.filters.all"}} + -
  • - - {{d-icon "reply"}} - {{i18n "user_action_groups.6"}} - -
  • + + {{d-icon "reply"}} + {{i18n "user_action_groups.6"}} + -
  • - - {{d-icon "heart"}} - {{i18n "user_action_groups.2"}} - -
  • + + {{d-icon "heart"}} + {{i18n "user_action_groups.2"}} + {{#if @siteSettings.enable_mentions}} -
  • - - {{d-icon "at"}} - {{i18n "user_action_groups.7"}} - -
  • + + {{d-icon "at"}} + {{i18n "user_action_groups.7"}} + {{/if}} -
  • - - {{d-icon "pencil-alt"}} - {{i18n "user_action_groups.11"}} - -
  • + + {{d-icon "pencil-alt"}} + {{i18n "user_action_groups.11"}} +
    "; + buffer = `
    `; if (tags) { for (let i = 0; i < tags.length; i++) { buffer += diff --git a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js index 8a20a5858b..d5dab6cce9 100644 --- a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js +++ b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js @@ -74,7 +74,7 @@ export default Mixin.create(UploadDebugging, { } }, - // TODO (martin) This and _onPreProcessComplete will need to be tweaked + // NOTE: This and _onPreProcessComplete will need to be tweaked // if we ever add support for "determinate" preprocessors for uppy, which // means the progress will have a value rather than a started/complete // state ("indeterminate"). diff --git a/app/assets/javascripts/discourse/app/mixins/upload-debugging.js b/app/assets/javascripts/discourse/app/mixins/upload-debugging.js index 0c7a935f3e..07c6d59854 100644 --- a/app/assets/javascripts/discourse/app/mixins/upload-debugging.js +++ b/app/assets/javascripts/discourse/app/mixins/upload-debugging.js @@ -31,7 +31,6 @@ export default Mixin.create({ _instrumentUploadTimings() { if (!this._performanceApiSupport()) { - // TODO (martin) (2021-01-23) Check if FireFox fixed this yet. warn( "Some browsers do not return a PerformanceMeasure when calling performance.mark, disabling instrumentation. See https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure#return_value and https://bugzilla.mozilla.org/show_bug.cgi?id=1724645", { id: "discourse.upload-debugging" } diff --git a/app/assets/javascripts/discourse/app/models/group.js b/app/assets/javascripts/discourse/app/models/group.js index fc1b4725fb..f43812a8d3 100644 --- a/app/assets/javascripts/discourse/app/models/group.js +++ b/app/assets/javascripts/discourse/app/models/group.js @@ -148,9 +148,9 @@ const Group = RestModel.extend({ }, async addOwners(usernames, filter, notifyUsers) { - const response = await ajax(`/admin/groups/${this.id}/owners.json`, { + const response = await ajax(`/groups/${this.id}/owners.json`, { type: "PUT", - data: { group: { usernames, notify_users: notifyUsers } }, + data: { usernames, notify_users: notifyUsers }, }); if (filter) { diff --git a/app/assets/javascripts/discourse/app/models/post.js b/app/assets/javascripts/discourse/app/models/post.js index 25a8f40168..1f9f1573dd 100644 --- a/app/assets/javascripts/discourse/app/models/post.js +++ b/app/assets/javascripts/discourse/app/models/post.js @@ -461,6 +461,12 @@ Post.reopenClass({ }); }, + permanentlyDeleteRevisions(postId) { + return ajax(`/posts/${postId}/revisions/permanently_delete`, { + type: "DELETE", + }); + }, + showRevision(postId, version) { return ajax(`/posts/${postId}/revisions/${version}/show`, { type: "PUT", diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js index f472948337..4667825058 100644 --- a/app/assets/javascripts/discourse/app/models/topic.js +++ b/app/assets/javascripts/discourse/app/models/topic.js @@ -471,6 +471,10 @@ const Topic = RestModel.extend({ !deleted_by.staff && !deleted_by.groups.some( (group) => group.name === this.category.reviewable_by_group_name + ) && + !( + this.siteSettings.tl4_delete_posts_and_topics && + deleted_by.trust_level >= 4 ) ) { DiscourseURL.redirectTo("/"); diff --git a/app/assets/javascripts/discourse/app/routes/preferences-account.js b/app/assets/javascripts/discourse/app/routes/preferences-account.js index e7516e2f20..032ef98f34 100644 --- a/app/assets/javascripts/discourse/app/routes/preferences-account.js +++ b/app/assets/javascripts/discourse/app/routes/preferences-account.js @@ -2,6 +2,7 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; import UserBadge from "discourse/models/user-badge"; import showModal from "discourse/lib/show-modal"; import { action } from "@ember/object"; +import I18n from "I18n"; export default RestrictedUserRoute.extend({ showFooter: true, @@ -32,6 +33,7 @@ export default RestrictedUserRoute.extend({ newPrimaryGroupInput: user.get("primary_group_id"), newFlairGroupId: user.get("flair_group_id"), newStatus: user.status, + subpageTitle: I18n.t("user.preferences_nav.account"), }); }, diff --git a/app/assets/javascripts/discourse/app/routes/preferences.js b/app/assets/javascripts/discourse/app/routes/preferences.js index 8c157859da..83bc53b2f2 100644 --- a/app/assets/javascripts/discourse/app/routes/preferences.js +++ b/app/assets/javascripts/discourse/app/routes/preferences.js @@ -1,7 +1,19 @@ import RestrictedUserRoute from "discourse/routes/restricted-user"; +import I18n from "I18n"; +import { inject as service } from "@ember/service"; export default RestrictedUserRoute.extend({ + router: service(), + model() { return this.modelFor("user"); }, + + titleToken() { + let controller = this.controllerFor(this.router.currentRouteName); + let subpageTitle = controller?.subpageTitle; + return subpageTitle + ? `${subpageTitle} - ${I18n.t("user.preferences")}` + : I18n.t("user.preferences"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/tag-show.js b/app/assets/javascripts/discourse/app/routes/tag-show.js index 9c73f3b1d8..2482f9159d 100644 --- a/app/assets/javascripts/discourse/app/routes/tag-show.js +++ b/app/assets/javascripts/discourse/app/routes/tag-show.js @@ -136,7 +136,21 @@ export default DiscourseRoute.extend(FilterModeMixin, { noSubcategories, loading: false, }); - this.searchService.set("searchContext", model.tag.searchContext); + + if (model.category || model.additionalTags) { + const tagIntersectionSearchContext = { + type: "tagIntersection", + tagId: model.tag.id, + tag: model.tag, + additionalTags: model.additionalTags || null, + categoryId: model.category?.id || null, + category: model.category || null, + }; + + this.searchService.set("searchContext", tagIntersectionSearchContext); + } else { + this.searchService.set("searchContext", model.tag.searchContext); + } }, titleToken() { diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-bookmarks.js b/app/assets/javascripts/discourse/app/routes/user-activity-bookmarks.js index a29d1124f4..af88f4f95c 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-bookmarks.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-bookmarks.js @@ -2,6 +2,7 @@ import { action } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import DiscourseRoute from "discourse/routes/discourse"; import { Promise } from "rsvp"; +import I18n from "I18n"; export default DiscourseRoute.extend({ queryParams: { @@ -50,6 +51,10 @@ export default DiscourseRoute.extend({ this.render("user_bookmarks"); }, + titleToken() { + return I18n.t("user_action_groups.3"); + }, + @action didTransition() { this.controllerFor("user-activity")._showFooter(); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js b/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js index 5a5d30507b..c73975624e 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js @@ -38,6 +38,10 @@ export default DiscourseRoute.extend({ this.appEvents.off("draft:destroyed", this, this.refresh); }, + titleToken() { + return I18n.t("user_action_groups.15"); + }, + @action didTransition() { this.controllerFor("user-activity")._showFooter(); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-index.js b/app/assets/javascripts/discourse/app/routes/user-activity-index.js index 88bb0b37cd..4907997ed0 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-index.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-index.js @@ -25,4 +25,8 @@ export default UserActivityStreamRoute.extend({ return { title, body }; }, + + titleToken() { + return I18n.t("user.filters.all"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-likes-given.js b/app/assets/javascripts/discourse/app/routes/user-activity-likes-given.js index 901c890e24..0965c24d57 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-likes-given.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-likes-given.js @@ -25,6 +25,10 @@ export default UserActivityStreamRoute.extend({ return { title, body }; }, + titleToken() { + return I18n.t("user_action_groups.1"); + }, + @action didTransition() { this.controllerFor("application").set("showFooter", true); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-read.js b/app/assets/javascripts/discourse/app/routes/user-activity-read.js index 5746b96d1a..c988fad68e 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-read.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-read.js @@ -42,6 +42,10 @@ export default UserTopicListRoute.extend({ return { title, body }; }, + titleToken() { + return `${I18n.t("user.read")}`; + }, + @action triggerRefresh() { this.refresh(); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-replies.js b/app/assets/javascripts/discourse/app/routes/user-activity-replies.js index 2db4981c4f..04ab75c18e 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-replies.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-replies.js @@ -29,6 +29,10 @@ export default UserActivityStreamRoute.extend({ return { title, body }; }, + titleToken() { + return I18n.t("user_action_groups.5"); + }, + @action didTransition() { this.controllerFor("application").set("showFooter", true); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js index 7059e4acb8..6e02037447 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js @@ -50,6 +50,10 @@ export default UserTopicListRoute.extend({ return { title, body }; }, + titleToken() { + return I18n.t("user_action_groups.4"); + }, + @action triggerRefresh() { this.refresh(); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity.js b/app/assets/javascripts/discourse/app/routes/user-activity.js index 9a94a47d8d..2638e1d7af 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity.js @@ -1,4 +1,5 @@ import DiscourseRoute from "discourse/routes/discourse"; +import I18n from "I18n"; export default DiscourseRoute.extend({ model() { @@ -19,4 +20,8 @@ export default DiscourseRoute.extend({ setupController(controller, user) { this.controllerFor("user-activity").set("model", user); }, + + titleToken() { + return I18n.t("user.activity_stream"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-badges.js b/app/assets/javascripts/discourse/app/routes/user-badges.js index 6feb940c50..e604e7ccb3 100644 --- a/app/assets/javascripts/discourse/app/routes/user-badges.js +++ b/app/assets/javascripts/discourse/app/routes/user-badges.js @@ -2,6 +2,7 @@ import DiscourseRoute from "discourse/routes/discourse"; import UserBadge from "discourse/models/user-badge"; import ViewingActionType from "discourse/mixins/viewing-action-type"; import { action } from "@ember/object"; +import I18n from "I18n"; export default DiscourseRoute.extend(ViewingActionType, { model() { @@ -20,6 +21,10 @@ export default DiscourseRoute.extend(ViewingActionType, { this.render("user/badges"); }, + titleToken() { + return I18n.t("badges.title"); + }, + @action didTransition() { this.controllerFor("application").set("showFooter", true); diff --git a/app/assets/javascripts/discourse/app/routes/user-invited-show.js b/app/assets/javascripts/discourse/app/routes/user-invited-show.js index af6f4a3eeb..bfa5ce2136 100644 --- a/app/assets/javascripts/discourse/app/routes/user-invited-show.js +++ b/app/assets/javascripts/discourse/app/routes/user-invited-show.js @@ -1,6 +1,7 @@ import DiscourseRoute from "discourse/routes/discourse"; import Invite from "discourse/models/invite"; import { action } from "@ember/object"; +import I18n from "I18n"; export default DiscourseRoute.extend({ model(params) { @@ -27,6 +28,10 @@ export default DiscourseRoute.extend({ }); }, + titleToken() { + return I18n.t("user.invited." + this.inviteFilter + "_tab"); + }, + @action triggerRefresh() { this.refresh(); diff --git a/app/assets/javascripts/discourse/app/routes/user-invited.js b/app/assets/javascripts/discourse/app/routes/user-invited.js index ae432d3ecc..11053e1660 100644 --- a/app/assets/javascripts/discourse/app/routes/user-invited.js +++ b/app/assets/javascripts/discourse/app/routes/user-invited.js @@ -1,4 +1,5 @@ import DiscourseRoute from "discourse/routes/discourse"; +import I18n from "I18n"; export default DiscourseRoute.extend({ setupController(controller) { @@ -10,4 +11,8 @@ export default DiscourseRoute.extend({ can_see_invite_details, }); }, + + titleToken() { + return I18n.t("user.invited.title"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-notifications-edits.js b/app/assets/javascripts/discourse/app/routes/user-notifications-edits.js index 8be7b15b7b..90b469a3c4 100644 --- a/app/assets/javascripts/discourse/app/routes/user-notifications-edits.js +++ b/app/assets/javascripts/discourse/app/routes/user-notifications-edits.js @@ -1,6 +1,11 @@ import UserAction from "discourse/models/user-action"; import UserActivityStreamRoute from "discourse/routes/user-activity-stream"; +import I18n from "I18n"; export default UserActivityStreamRoute.extend({ userActionType: UserAction.TYPES["edits"], + + titleToken() { + return I18n.t("user_action_groups.11"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-notifications-index.js b/app/assets/javascripts/discourse/app/routes/user-notifications-index.js index 45af6681c7..99709d47de 100644 --- a/app/assets/javascripts/discourse/app/routes/user-notifications-index.js +++ b/app/assets/javascripts/discourse/app/routes/user-notifications-index.js @@ -1,4 +1,5 @@ import DiscourseRoute from "discourse/routes/discourse"; +import I18n from "I18n"; export default DiscourseRoute.extend({ controllerName: "user-notifications", @@ -6,6 +7,10 @@ export default DiscourseRoute.extend({ this.render("user/notifications-index"); }, + titleToken() { + return I18n.t("user.filters.all"); + }, + afterModel(model) { if (!model) { this.transitionTo("userNotifications.responses"); diff --git a/app/assets/javascripts/discourse/app/routes/user-notifications-likes-received.js b/app/assets/javascripts/discourse/app/routes/user-notifications-likes-received.js index 1bfaa46b84..c913ae16df 100644 --- a/app/assets/javascripts/discourse/app/routes/user-notifications-likes-received.js +++ b/app/assets/javascripts/discourse/app/routes/user-notifications-likes-received.js @@ -1,6 +1,11 @@ import UserAction from "discourse/models/user-action"; import UserActivityStreamRoute from "discourse/routes/user-activity-stream"; +import I18n from "I18n"; export default UserActivityStreamRoute.extend({ userActionType: UserAction.TYPES["likes_received"], + + titleToken() { + return I18n.t("user_action_groups.1"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-notifications-mentions.js b/app/assets/javascripts/discourse/app/routes/user-notifications-mentions.js index 2d8c2622da..449a2dbf33 100644 --- a/app/assets/javascripts/discourse/app/routes/user-notifications-mentions.js +++ b/app/assets/javascripts/discourse/app/routes/user-notifications-mentions.js @@ -1,6 +1,11 @@ import UserAction from "discourse/models/user-action"; import UserActivityStreamRoute from "discourse/routes/user-activity-stream"; +import I18n from "I18n"; export default UserActivityStreamRoute.extend({ userActionType: UserAction.TYPES["mentions"], + + titleToken() { + return I18n.t("user_action_groups.7"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-notifications-responses.js b/app/assets/javascripts/discourse/app/routes/user-notifications-responses.js index 52604ccdc5..12f43ff2c6 100644 --- a/app/assets/javascripts/discourse/app/routes/user-notifications-responses.js +++ b/app/assets/javascripts/discourse/app/routes/user-notifications-responses.js @@ -1,6 +1,11 @@ import UserAction from "discourse/models/user-action"; import UserActivityStreamRoute from "discourse/routes/user-activity-stream"; +import I18n from "I18n"; export default UserActivityStreamRoute.extend({ userActionType: UserAction.TYPES["replies"], + + titleToken() { + return I18n.t("user_action_groups.6"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-notifications.js b/app/assets/javascripts/discourse/app/routes/user-notifications.js index aeb118579c..aa93fa7a8a 100644 --- a/app/assets/javascripts/discourse/app/routes/user-notifications.js +++ b/app/assets/javascripts/discourse/app/routes/user-notifications.js @@ -1,6 +1,7 @@ import DiscourseRoute from "discourse/routes/discourse"; import ViewingActionType from "discourse/mixins/viewing-action-type"; import { action } from "@ember/object"; +import I18n from "I18n"; export default DiscourseRoute.extend(ViewingActionType, { controllerName: "user-notifications", @@ -31,4 +32,8 @@ export default DiscourseRoute.extend(ViewingActionType, { controller.set("user", this.modelFor("user")); this.viewingActionType(-1); }, + + titleToken() { + return I18n.t("user.notifications"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-summary.js b/app/assets/javascripts/discourse/app/routes/user-summary.js index d7fefead28..7f06eabd8c 100644 --- a/app/assets/javascripts/discourse/app/routes/user-summary.js +++ b/app/assets/javascripts/discourse/app/routes/user-summary.js @@ -1,4 +1,5 @@ import DiscourseRoute from "discourse/routes/discourse"; +import I18n from "I18n"; export default DiscourseRoute.extend({ showFooter: true, @@ -11,4 +12,8 @@ export default DiscourseRoute.extend({ return user.summary(); }, + + titleToken() { + return I18n.t("user.summary.title"); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user.js b/app/assets/javascripts/discourse/app/routes/user.js index b3efdef687..aa0af01979 100644 --- a/app/assets/javascripts/discourse/app/routes/user.js +++ b/app/assets/javascripts/discourse/app/routes/user.js @@ -1,5 +1,4 @@ import DiscourseRoute from "discourse/routes/discourse"; -import I18n from "I18n"; import User from "discourse/models/user"; import { action } from "@ember/object"; import { bind } from "discourse-common/utils/decorators"; @@ -98,9 +97,7 @@ export default DiscourseRoute.extend({ titleToken() { const username = this.modelFor("user").username; - if (username) { - return [I18n.t("user.profile"), username]; - } + return username ? username : null; }, @action diff --git a/app/assets/javascripts/discourse/app/templates/components/badge-title.hbs b/app/assets/javascripts/discourse/app/templates/components/badge-title.hbs index 62e7b1e886..4423882019 100644 --- a/app/assets/javascripts/discourse/app/templates/components/badge-title.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/badge-title.hbs @@ -1,5 +1,5 @@
    -
    +
    diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs index 2080982b7a..5d1ab2c237 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-settings.hbs @@ -181,20 +181,6 @@
    - {{#if this.siteSettings.tagging_enabled}} -
    - - -
    - {{/if}} -
    \ No newline at end of file +
    diff --git a/app/assets/javascripts/discourse/app/templates/components/flag-action-type.hbs b/app/assets/javascripts/discourse/app/templates/components/flag-action-type.hbs index 8907e1caf0..684a7f8bc5 100644 --- a/app/assets/javascripts/discourse/app/templates/components/flag-action-type.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/flag-action-type.hbs @@ -16,6 +16,7 @@ name="message" class="flag-message" placeholder={{this.customPlaceholder}} + aria-label={{i18n "flagging.notify_user_textarea_label"}} @value={{this.message}} />
    -
    +
    {{outlet}}
    \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/group/manage.hbs b/app/assets/javascripts/discourse/app/templates/group/manage.hbs index 4e1d80cfc3..f103e7f3ed 100644 --- a/app/assets/javascripts/discourse/app/templates/group/manage.hbs +++ b/app/assets/javascripts/discourse/app/templates/group/manage.hbs @@ -12,6 +12,6 @@ {{/each}}
    -
    +
    {{outlet}}
    \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/group/messages.hbs b/app/assets/javascripts/discourse/app/templates/group/messages.hbs index dc985edf67..b5919924c6 100644 --- a/app/assets/javascripts/discourse/app/templates/group/messages.hbs +++ b/app/assets/javascripts/discourse/app/templates/group/messages.hbs @@ -12,6 +12,6 @@
    -
    +
    {{outlet}}
    \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/group/permissions.hbs b/app/assets/javascripts/discourse/app/templates/group/permissions.hbs index 6ad2d05354..fcd231f6b1 100644 --- a/app/assets/javascripts/discourse/app/templates/group/permissions.hbs +++ b/app/assets/javascripts/discourse/app/templates/group/permissions.hbs @@ -1,4 +1,4 @@ -
    +
    {{#if this.model.permissions}} - + diff --git a/app/assets/javascripts/discourse/app/templates/preferences.hbs b/app/assets/javascripts/discourse/app/templates/preferences.hbs index 88f978b73a..d95715f11a 100644 --- a/app/assets/javascripts/discourse/app/templates/preferences.hbs +++ b/app/assets/javascripts/discourse/app/templates/preferences.hbs @@ -1,11 +1,12 @@ {{#if this.currentUser.redesigned_user_page_nav_enabled}}
    - +
    @@ -103,7 +104,7 @@
    {{/if}} -
    +
    diff --git a/app/assets/javascripts/discourse/app/templates/user-invited.hbs b/app/assets/javascripts/discourse/app/templates/user-invited.hbs index 1d3ebe4fe3..7997e48b74 100644 --- a/app/assets/javascripts/discourse/app/templates/user-invited.hbs +++ b/app/assets/javascripts/discourse/app/templates/user-invited.hbs @@ -3,7 +3,7 @@
    - + -
  • - - {{d-icon "envelope"}} - {{i18n "categories.latest"}} - -
  • + + + {{d-icon "envelope"}} + {{i18n "categories.latest"}} + {{#if this.viewingSelf}} -
  • - - {{d-icon "exclamation-circle"}} - {{this.newLinkText}} - -
  • + + {{d-icon "exclamation-circle"}} + {{this.newLinkText}} + -
  • - - {{d-icon "plus-circle"}} - {{this.unreadLinkText}} - -
  • + + {{d-icon "plus-circle"}} + {{this.unreadLinkText}} + -
  • - - {{d-icon "archive"}} - {{i18n "user.messages.archive"}} - -
  • + + {{d-icon "archive"}} + {{i18n "user.messages.archive"}} + {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/user-private-messages-user.hbs b/app/assets/javascripts/discourse/app/templates/user-private-messages-user.hbs index 0ddb6b1ee0..7106d8717d 100644 --- a/app/assets/javascripts/discourse/app/templates/user-private-messages-user.hbs +++ b/app/assets/javascripts/discourse/app/templates/user-private-messages-user.hbs @@ -5,50 +5,59 @@ {{/if}} -
  • - - {{d-icon "envelope"}} - {{i18n "categories.latest"}} - -
  • + + {{d-icon "envelope"}} + {{i18n "categories.latest"}} + -
  • - - {{d-icon "reply"}} - {{i18n "user.messages.sent"}} - -
  • + + {{d-icon "reply"}} + {{i18n "user.messages.sent"}} + {{#if this.viewingSelf}} -
  • - - {{d-icon "exclamation-circle"}} - {{this.newLinkText}} - -
  • + + {{d-icon "exclamation-circle"}} + {{this.newLinkText}} + + + + {{d-icon "plus-circle"}} + {{this.unreadLinkText}} + -
  • - - {{d-icon "plus-circle"}} - {{this.unreadLinkText}} - -
  • {{/if}} -
  • - - {{d-icon "archive"}} - {{i18n "user.messages.archive"}} - -
  • + + {{d-icon "archive"}} + {{i18n "user.messages.archive"}} + +
    {{outlet}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/user.hbs b/app/assets/javascripts/discourse/app/templates/user.hbs index 803efde906..d28c3349a7 100644 --- a/app/assets/javascripts/discourse/app/templates/user.hbs +++ b/app/assets/javascripts/discourse/app/templates/user.hbs @@ -10,6 +10,9 @@ {{this.primaryGroup}}" > +
    -
    - -
    -
    + {{#if this.model.staged}} -

    {{i18n "user.staged"}}

    + {{/if}} {{#if this.model.title}} -

    {{this.model.title}}

    + {{/if}} {{#if this.showFeaturedTopic}} - +
    {{/if}} -

    +

    +
    {{#if this.model.suspended}} @@ -310,6 +269,62 @@ @args={{hash model=this.model}} />
    + +
    + +
    diff --git a/app/assets/javascripts/discourse/app/templates/user/activity.hbs b/app/assets/javascripts/discourse/app/templates/user/activity.hbs index cfbbdc5215..e1e326f644 100644 --- a/app/assets/javascripts/discourse/app/templates/user/activity.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/activity.hbs @@ -2,7 +2,7 @@
    - +
    @@ -88,6 +89,6 @@ {{/if}} {{/if}} -
    +
    {{outlet}}
    \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/user/badges.hbs b/app/assets/javascripts/discourse/app/templates/user/badges.hbs index 00dd25bc3b..28e7b9ef65 100644 --- a/app/assets/javascripts/discourse/app/templates/user/badges.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/badges.hbs @@ -1,4 +1,4 @@ - +

    {{i18n "badges.favorite_count" diff --git a/app/assets/javascripts/discourse/app/templates/user/messages.hbs b/app/assets/javascripts/discourse/app/templates/user/messages.hbs index f4260d2692..a665dd4b31 100644 --- a/app/assets/javascripts/discourse/app/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/messages.hbs @@ -14,6 +14,7 @@ @@ -183,7 +184,7 @@ {{/unless}} {{/if}} -

    +
    {{#unless this.currentUser.redesigned_user_page_nav_enabled}}
    {{#if this.site.mobileView}} diff --git a/app/assets/javascripts/discourse/app/templates/user/notifications.hbs b/app/assets/javascripts/discourse/app/templates/user/notifications.hbs index 15ec2c6753..6dcdf70fa6 100644 --- a/app/assets/javascripts/discourse/app/templates/user/notifications.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/notifications.hbs @@ -2,10 +2,11 @@
    - + @@ -79,7 +80,7 @@ {{/if}} {{/if}} -
    +
    -
    +
    { + if (item.additionalTags) { + prefix = term?.split(" ").slice(0, -1).join(" ").trim() || ""; + } else { + prefix = term?.split("#")[0].trim() || ""; + } + + if (prefix.length) { + prefix = `${prefix} `; + } + + content.push( + this.attach("search-menu-assistant-item", { + prefix, + tag: item.tagName, + additionalTags: item.additionalTags, + category: item.category, + slug: term, + withInLabel: attrs.withInLabel, + isIntersection: true, + }) + ); + }); + break; case "#": attrs.results.forEach((item) => { if (item.model) { @@ -572,6 +601,36 @@ createWidget("search-menu-initial-options", { }) ); break; + case "tagIntersection": + let tagTerm; + if (ctx.additionalTags) { + const tags = [ctx.tagId, ...ctx.additionalTags]; + tagTerm = `${term} tags:${tags.join("+")}`; + } else { + tagTerm = `${term} #${ctx.tagId}`; + } + let suggestionOptions = { + tagName: ctx.tagId, + additionalTags: ctx.additionalTags, + }; + if (ctx.category) { + const categorySlug = ctx.category.parentCategory + ? `#${ctx.category.parentCategory.slug}:${ctx.category.slug}` + : `#${ctx.category.slug}`; + suggestionOptions.categoryName = categorySlug; + suggestionOptions.category = ctx.category; + tagTerm = tagTerm + ` ${categorySlug}`; + } + + content.push( + this.attach("search-menu-assistant", { + term: tagTerm, + suggestionKeyword: "+", + results: [suggestionOptions], + withInLabel: true, + }) + ); + break; case "user": content.push( this.attach("search-menu-assistant-item", { @@ -617,7 +676,7 @@ createWidget("search-menu-initial-options", { slug: term, extraHint: I18n.t("search.enter_hint"), label: [ - h("span.keyword", `${term} `), + h("span.keyword", `${term}`), opts.withLabel ? h("span.label-suffix", I18n.t("search.in_topics_posts")) : null, @@ -677,11 +736,20 @@ createWidget("search-menu-assistant-item", { link: false, }) ); - } else if (attrs.tag) { - attributes.href = getURL(`/tag/${attrs.tag}`); - content.push(iconNode("tag")); - content.push(h("span.search-item-tag", attrs.tag)); + // category and tag combination + if (attrs.tag && attrs.isIntersection) { + attributes.href = getURL(`/tag/${attrs.tag}`); + content.push(h("span.search-item-tag", [iconNode("tag"), attrs.tag])); + } + } else if (attrs.tag) { + if (attrs.isIntersection && attrs.additionalTags?.length) { + const tags = [attrs.tag, ...attrs.additionalTags]; + content.push(h("span.search-item-tag", `tags:${tags.join("+")}`)); + } else { + attributes.href = getURL(`/tag/${attrs.tag}`); + content.push(h("span.search-item-tag", [iconNode("tag"), attrs.tag])); + } } else if (attrs.user) { const userResult = [ avatarImg("small", { diff --git a/app/assets/javascripts/discourse/config/targets.js b/app/assets/javascripts/discourse/config/targets.js index 13213df378..09210acbf6 100644 --- a/app/assets/javascripts/discourse/config/targets.js +++ b/app/assets/javascripts/discourse/config/targets.js @@ -10,8 +10,7 @@ const browsers = [ ]; if (isCI || isProduction) { - // https://meta.discourse.org/t/224747 - browsers.push("Safari 12"); + browsers.push("Safari 15"); } module.exports = { diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index c4aa0e7d36..a10d5795bf 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -17,13 +17,13 @@ }, "dependencies": { "@babel/core": "^7.20.12", - "@babel/standalone": "^7.20.12", + "@babel/standalone": "^7.20.13", "@discourse/backburner.js": "^2.7.1-0", "@discourse/itsatrap": "^2.0.10", "@ember-compat/tracked-built-ins": "^0.9.1", "@ember/jquery": "^2.0.0", "@ember/optional-features": "^2.0.0", - "@ember/render-modifiers": "^2.0.4", + "@ember/render-modifiers": "^2.0.5", "@ember/test-helpers": "^2.9.3", "@glimmer/component": "^1.1.2", "@glimmer/syntax": "^0.84.2", @@ -49,7 +49,7 @@ "discourse-hbr": "1.0.0", "discourse-plugins": "1.0.0", "discourse-widget-hbs": "1.0.0", - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-auto-import-chunks-json-generator": "^1.0.0", "ember-buffered-proxy": "^2.1.1", "ember-cached-decorator-polyfill": "^1.0.1", @@ -69,25 +69,25 @@ "ember-modifier": "^4.0.0", "ember-on-resize-modifier": "^1.1.0", "ember-qunit": "^6.1.1", - "ember-rfc176-data": "^0.3.17", + "ember-rfc176-data": "^0.3.18", "ember-source": "~3.28.11", "ember-test-selectors": "^6.0.0", - "eslint": "^8.31.0", + "eslint": "^8.32.0", "eslint-plugin-qunit": "^7.3.4", "handlebars": "^4.7.7", "html-entities": "^2.3.3", "imports-loader": "^4.0.1", "js-yaml": "^4.1.0", - "jsdom": "^21.0.0", + "jsdom": "^21.1.0", "loader.js": "^4.7.0", "markdown-it": "^13.0.1", - "message-bus-client": "^4.3.0", + "message-bus-client": "^4.3.2", "messageformat": "0.1.5", "pretender": "^3.4.7", "pretty-text": "1.0.0", - "qunit": "^2.19.3", + "qunit": "^2.19.4", "qunit-dom": "^2.0.0", - "sass": "^1.57.0", + "sass": "^1.57.1", "select-kit": "1.0.0", "sinon": "^15.0.1", "source-map": "^0.7.4", diff --git a/app/assets/javascripts/discourse/scripts/browser-detect.js b/app/assets/javascripts/discourse/scripts/browser-detect.js index 86cce506a8..b205e684eb 100644 --- a/app/assets/javascripts/discourse/scripts/browser-detect.js +++ b/app/assets/javascripts/discourse/scripts/browser-detect.js @@ -1,7 +1,14 @@ /* eslint-disable no-var */ // `let` is not supported in very old browsers (function () { - if (!window.WeakMap || !window.Promise || typeof globalThis === "undefined") { + if ( + !window.WeakMap || + !window.Promise || + typeof globalThis === "undefined" || + !String.prototype.replaceAll || + !CSS.supports || + !CSS.supports("aspect-ratio: 1") + ) { window.unsupportedBrowser = true; } else { // Some implementations of `WeakMap.prototype.has` do not accept false diff --git a/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js b/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js index 9f777d5877..a315b60e72 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js @@ -77,6 +77,8 @@ acceptance("Category Edit", function (needs) { test("Editing required tag groups", async function (assert) { await visit("/c/bug/edit/tags"); + assert.ok(exists(".minimum-required-tags")); + assert.ok(exists(".required-tag-groups")); assert.strictEqual(count(".required-tag-group-row"), 0); diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js index f38fdbaf41..e421601e62 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-test.js @@ -1269,7 +1269,7 @@ acceptance("Composer - Default category", function (needs) { name: "General", slug: "general", permission: 1, - ltopic_template: null, + topic_template: null, }, { id: 2, @@ -1306,7 +1306,7 @@ acceptance("Composer - Uncategorized category", function (needs) { name: "General", slug: "general", permission: 1, - ltopic_template: null, + topic_template: null, }, { id: 2, @@ -1337,7 +1337,7 @@ acceptance("Composer - default category not set", function (needs) { name: "General", slug: "general", permission: 1, - ltopic_template: null, + topic_template: null, }, { id: 2, diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-index-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-index-test.js index 020a5a4463..2707adf91d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-index-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-index-test.js @@ -39,7 +39,7 @@ acceptance("Group Members", function (needs) { needs.user(); needs.pretender((server, helper) => { - server.put("/admin/groups/47/owners.json", () => { + server.put("/groups/47/owners.json", () => { return helper.response({ success: true }); }); }); @@ -72,10 +72,9 @@ acceptance("Group Members", function (needs) { ); }); - test("Shows bulk actions", async function (assert) { + test("Shows bulk actions as an admin user", async function (assert) { await visit("/g/discourse"); - assert.ok(exists("button.bulk-select")); await click("button.bulk-select"); await click(queryAll("input.bulk-select")[0]); @@ -83,7 +82,50 @@ acceptance("Group Members", function (needs) { const memberDropdown = selectKit(".bulk-group-member-dropdown"); await memberDropdown.expand(); - await memberDropdown.selectRowByValue("makeOwners"); + + assert.ok( + exists('[data-value="removeMembers"]'), + "it includes remove member option" + ); + + assert.ok( + exists('[data-value="makeOwners"]'), + "it includes make owners option" + ); + + assert.ok( + exists('[data-value="setPrimary"]'), + "it includes set primary option" + ); + }); + + test("Shows bulk actions as a group owner", async function (assert) { + updateCurrentUser({ moderator: false, admin: false }); + + await visit("/g/discourse"); + + await click("button.bulk-select"); + + await click(queryAll("input.bulk-select")[0]); + await click(queryAll("input.bulk-select")[1]); + + const memberDropdown = selectKit(".bulk-group-member-dropdown"); + await memberDropdown.expand(); + + assert.ok( + exists('[data-value="removeMembers"]'), + "it includes remove member option" + ); + + assert.ok( + exists('[data-value="makeOwners"]'), + "it includes make owners option" + ); + + assert.notOk( + exists('[data-value="setPrimary"]'), + "it does not include set primary (staff only) option" + ); }); test("Bulk actions - Menu, Select all and Clear all buttons", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-test.js index f0b21db01e..62806071af 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/search-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/search-test.js @@ -73,7 +73,7 @@ acceptance("Search - Anonymous", function (needs) { query( ".search-menu .results ul.search-menu-initial-options li:first-child .search-item-slug" ).innerText.trim(), - `dev ${I18n.t("search.in_topics_posts")}`, + `dev${I18n.t("search.in_topics_posts")}`, "shows topic search as first dropdown item" ); @@ -757,6 +757,47 @@ acceptance("Search - assistant", function (needs) { return helper.response(searchFixtures["search/query"]); }); + server.get("/tag/dev/notifications", () => { + return helper.response({ + tag_notification: { id: "dev", notification_level: 2 }, + }); + }); + + server.get("/tags/c/bug/1/dev/l/latest.json", () => { + return helper.response({ + users: [], + primary_groups: [], + topic_list: { + can_create_topic: true, + draft: null, + draft_key: "new_topic", + draft_sequence: 1, + per_page: 30, + tags: [ + { + id: 1, + name: "dev", + topic_count: 1, + }, + ], + topics: [], + }, + }); + }); + + server.get("/tags/intersection/dev/foo.json", () => { + return helper.response({ + topic_list: { + can_create_topic: true, + draft: null, + draft_key: "new_topic", + draft_sequence: 1, + per_page: 30, + topics: [], + }, + }); + }); + server.get("/u/search/users", () => { return helper.response({ users: [ @@ -810,13 +851,92 @@ acceptance("Search - assistant", function (needs) { query( ".search-menu .results ul.search-menu-assistant .search-item-prefix" ).innerText, - "sam " + "sam" ); await click(firstCategory); assert.strictEqual(query("#search-term").value, `sam #${firstResultSlug}`); }); + test("Shows category / tag combination shortcut when both are present", async function (assert) { + await visit("/tags/c/bug/dev"); + await click("#search-button"); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .category-name") + .innerText, + "bug", + "Category is displayed" + ); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .search-item-tag") + .innerText, + "dev", + "Tag is displayed" + ); + }); + + test("Updates tag / category combination search suggestion when typing", async function (assert) { + await visit("/tags/c/bug/dev"); + await click("#search-button"); + await fillIn("#search-term", "foo bar"); + + assert.strictEqual( + query( + ".search-menu .results ul.search-menu-assistant .search-item-prefix" + ).innerText, + "foo bar", + "Input is applied to search query" + ); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .category-name") + .innerText, + "bug" + ); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .search-item-tag") + .innerText, + "dev", + "Tag is displayed" + ); + }); + + test("Shows tag combination shortcut when visiting tag intersection", async function (assert) { + await visit("/tags/intersection/dev/foo"); + await click("#search-button"); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .search-item-tag") + .innerText, + "tags:dev+foo", + "Tags are displayed" + ); + }); + + test("Updates tag intersection search suggestion when typing", async function (assert) { + await visit("/tags/intersection/dev/foo"); + await click("#search-button"); + await fillIn("#search-term", "foo bar"); + + assert.strictEqual( + query( + ".search-menu .results ul.search-menu-assistant .search-item-prefix" + ).innerText, + "foo bar", + "Input is applied to search query" + ); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .search-item-tag") + .innerText, + "tags:dev+foo", + "Tags are displayed" + ); + }); + test("shows in: shortcuts", async function (assert) { await visit("/"); await click("#search-button"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js index 9ac356da93..6875db5cf3 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-private-messages-test.js @@ -346,17 +346,31 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { await publishUnreadToMessageBus({ topicId: 1 }); await publishNewToMessageBus({ topicId: 2 }); - assert.strictEqual( - query(".messages-nav li a.new").innerText.trim(), - I18n.t("user.messages.new_with_count", { count: 1 }), - "displays the right count" - ); + if (customUserProps?.redesigned_user_page_nav_enabled) { + assert.strictEqual( + query(".user-nav__messages-new").innerText.trim(), + I18n.t("user.messages.new_with_count", { count: 1 }), + "displays the right count" + ); - assert.strictEqual( - query(".messages-nav li a.unread").innerText.trim(), - I18n.t("user.messages.unread_with_count", { count: 1 }), - "displays the right count" - ); + assert.strictEqual( + query(".user-nav__messages-unread").innerText.trim(), + I18n.t("user.messages.unread_with_count", { count: 1 }), + "displays the right count" + ); + } else { + assert.strictEqual( + query(".messages-nav li a.new").innerText.trim(), + I18n.t("user.messages.new_with_count", { count: 1 }), + "displays the right count" + ); + + assert.strictEqual( + query(".messages-nav li a.unread").innerText.trim(), + I18n.t("user.messages.unread_with_count", { count: 1 }), + "displays the right count" + ); + } }); test("incoming new messages while viewing new", async function (assert) { @@ -364,11 +378,19 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { await publishNewToMessageBus({ topicId: 1 }); - assert.strictEqual( - query(".messages-nav li a.new").innerText.trim(), - I18n.t("user.messages.new_with_count", { count: 1 }), - "displays the right count" - ); + if (customUserProps?.redesigned_user_page_nav_enabled) { + assert.strictEqual( + query(".messages-nav .user-nav__messages-new").innerText.trim(), + I18n.t("user.messages.new_with_count", { count: 1 }), + "displays the right count" + ); + } else { + assert.strictEqual( + query(".messages-nav li a.new").innerText.trim(), + I18n.t("user.messages.new_with_count", { count: 1 }), + "displays the right count" + ); + } assert.ok(exists(".show-mores"), "displays the topic incoming info"); }); @@ -378,11 +400,19 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { await publishUnreadToMessageBus(); - assert.strictEqual( - query(".messages-nav li a.unread").innerText.trim(), - I18n.t("user.messages.unread_with_count", { count: 1 }), - "displays the right count" - ); + if (customUserProps?.redesigned_user_page_nav_enabled) { + assert.strictEqual( + query(".messages-nav .user-nav__messages-unread").innerText.trim(), + I18n.t("user.messages.unread_with_count", { count: 1 }), + "displays the right count" + ); + } else { + assert.strictEqual( + query(".messages-nav li a.unread").innerText.trim(), + I18n.t("user.messages.unread_with_count", { count: 1 }), + "displays the right count" + ); + } assert.ok(exists(".show-mores"), "displays the topic incoming info"); }); @@ -393,33 +423,65 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { await publishUnreadToMessageBus({ groupIds: [14], topicId: 1 }); await publishNewToMessageBus({ groupIds: [14], topicId: 2 }); - assert.strictEqual( - query(".messages-nav li a.unread").innerText.trim(), - I18n.t("user.messages.unread_with_count", { count: 1 }), - "displays the right count" - ); + if (customUserProps?.redesigned_user_page_nav_enabled) { + assert.strictEqual( + query( + ".messages-nav .user-nav__messages-group-unread" + ).innerText.trim(), + I18n.t("user.messages.unread_with_count", { count: 1 }), + "displays the right count" + ); - assert.strictEqual( - query(".messages-nav li a.new").innerText.trim(), - I18n.t("user.messages.new_with_count", { count: 1 }), - "displays the right count" - ); + assert.strictEqual( + query(".messages-nav .user-nav__messages-group-new").innerText.trim(), + I18n.t("user.messages.new_with_count", { count: 1 }), + "displays the right count" + ); - assert.ok(exists(".show-mores"), "displays the topic incoming info"); + assert.ok(exists(".show-mores"), "displays the topic incoming info"); - await visit("/u/charlie/messages/unread"); + await visit("/u/charlie/messages/unread"); - assert.strictEqual( - query(".messages-nav li a.unread").innerText.trim(), - I18n.t("user.messages.unread"), - "displays the right count" - ); + assert.strictEqual( + query(".messages-nav .user-nav__messages-unread").innerText.trim(), + I18n.t("user.messages.unread"), + "displays the right count" + ); - assert.strictEqual( - query(".messages-nav li a.new").innerText.trim(), - I18n.t("user.messages.new"), - "displays the right count" - ); + assert.strictEqual( + query(".messages-nav .user-nav__messages-new").innerText.trim(), + I18n.t("user.messages.new"), + "displays the right count" + ); + } else { + assert.strictEqual( + query(".messages-nav a.unread").innerText.trim(), + I18n.t("user.messages.unread_with_count", { count: 1 }), + "displays the right count" + ); + + assert.strictEqual( + query(".messages-nav a.new").innerText.trim(), + I18n.t("user.messages.new_with_count", { count: 1 }), + "displays the right count" + ); + + assert.ok(exists(".show-mores"), "displays the topic incoming info"); + + await visit("/u/charlie/messages/unread"); + + assert.strictEqual( + query(".messages-nav a.unread").innerText.trim(), + I18n.t("user.messages.unread"), + "displays the right count" + ); + + assert.strictEqual( + query(".messages-nav a.new").innerText.trim(), + I18n.t("user.messages.new"), + "displays the right count" + ); + } }); test("incoming messages is not tracked on non user messages route", async function (assert) { @@ -452,11 +514,19 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { await click(".btn.dismiss-read"); await click("#dismiss-read-confirm"); - assert.strictEqual( - query(".messages-nav li a.unread").innerText.trim(), - I18n.t("user.messages.unread"), - "displays the right count" - ); + if (customUserProps?.redesigned_user_page_nav_enabled) { + assert.strictEqual( + query(".user-nav__messages-unread").innerText.trim(), + I18n.t("user.messages.unread"), + "displays the right count" + ); + } else { + assert.strictEqual( + query(".messages-nav li a.unread").innerText.trim(), + I18n.t("user.messages.unread"), + "displays the right count" + ); + } assert.strictEqual( count(".topic-list-item"), @@ -518,11 +588,19 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { await click(".btn.dismiss-read"); - assert.strictEqual( - query(".messages-nav li a.new").innerText.trim(), - I18n.t("user.messages.new"), - "displays the right count" - ); + if (customUserProps?.redesigned_user_page_nav_enabled) { + assert.strictEqual( + query(".messages-nav .user-nav__messages-new").innerText.trim(), + I18n.t("user.messages.new"), + "displays the right count" + ); + } else { + assert.strictEqual( + query(".messages-nav li a.new").innerText.trim(), + I18n.t("user.messages.new"), + "displays the right count" + ); + } assert.strictEqual( count(".topic-list-item"), @@ -701,7 +779,11 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { "User personal inbox is selected in dropdown" ); - await click(".messages-sent"); + if (customUserProps?.redesigned_user_page_nav_enabled) { + await click(".user-nav__messages-sent"); + } else { + await click(".messages-sent"); + } assert.strictEqual( messagesDropdown.header().name(), @@ -724,7 +806,11 @@ function testUserPrivateMessagesWithGroupMessages(needs, customUserProps) { "Group inbox is selected in dropdown" ); - await click(".messages-group-new"); + if (customUserProps?.redesigned_user_page_nav_enabled) { + await click(".user-nav__messages-group-new"); + } else { + await click(".messages-group-new"); + } assert.strictEqual( messagesDropdown.header().name(), diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/single-select-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/single-select-test.js index cc8a13006d..c06eeadf84 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/single-select-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/single-select-test.js @@ -393,6 +393,24 @@ module("Integration | Component | select-kit/single-select", function (hooks) { ); }); + test("row index", async function (assert) { + this.setProperties({ + content: [ + { id: 1, name: "john" }, + { id: 2, name: "jane" }, + ], + value: null, + }); + + await render( + hbs`` + ); + await this.subject.expand(); + + assert.dom('.select-kit-row[data-index="0"][data-value="1"]').exists(); + assert.dom('.select-kit-row[data-index="1"][data-value="2"]').exists(); + }); + test("options.verticalOffset", async function (assert) { setDefaultState(this, { verticalOffset: -50 }); await render(hbs` @@ -411,4 +429,17 @@ module("Integration | Component | select-kit/single-select", function (hooks) { assert.ok(header.bottom > body.top, "it correctly offsets the body"); }); + + test("options.expandedOnInsert", async function (assert) { + setDefaultState(this); + await render(hbs` + + `); + + assert.dom(".single-select.is-expanded").exists(); + }); }); diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/quick-access-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/quick-access-item-test.js index 9e3dbb2616..4b17281f38 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/widgets/quick-access-item-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/quick-access-item-test.js @@ -32,5 +32,21 @@ module( const contentDiv = query(CONTENT_DIV_SELECTOR); assert.strictEqual(contentDiv.innerText, '"quote"'); }); + + test("Renders the notification content with no username when username is not present", async function (assert) { + this.set("args", { + content: "content", + username: undefined, + }); + + await render( + hbs`` + ); + + const contentDiv = query(CONTENT_DIV_SELECTOR); + const usernameSpan = query("li a div span"); + assert.strictEqual(contentDiv.innerText, "content"); + assert.strictEqual(usernameSpan.innerText, ""); + }); } ); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/plugin-api-test.js b/app/assets/javascripts/discourse/tests/unit/lib/plugin-api-test.js index 7465a51bb2..516f326b0f 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/plugin-api-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/plugin-api-test.js @@ -1,4 +1,4 @@ -import { module, test } from "qunit"; +import { module, test, todo } from "qunit"; import EmberObject from "@ember/object"; import discourseComputed from "discourse-common/utils/decorators"; import { withPluginApi } from "discourse/lib/plugin-api"; @@ -87,4 +87,33 @@ module("Unit | Utility | plugin-api", function (hooks) { assert.strictEqual(thingy.keep, "hey!"); assert.strictEqual(thingy.prop, "g'day"); }); + + todo("modifyClass works with getters", function (assert) { + let Base = EmberObject.extend({ + get foo() { + throw new Error("base getter called"); + }, + }); + + getOwner(this).register("test-class:main", Base, { + instantiate: false, + }); + + // Performing this lookup triggers `factory._onLookup`. In DEBUG builds, that invokes injectedPropertyAssertion() + // https://github.com/emberjs/ember.js/blob/36505f1b42/packages/%40ember/-internals/runtime/lib/system/core_object.js#L1144-L1163 + // Which in turn invokes `factory.proto()`. + // This puts things in a state which will trigger https://github.com/emberjs/ember.js/issues/18860 when a native getter is overridden. + withPluginApi("1.1.0", (api) => { + api.modifyClass("test-class:main", { + get foo() { + return "modified getter"; + }, + }); + }); + + const obj = Base.create(); + assert.true(true, "no error thrown while merging mixin with getter"); + + assert.strictEqual(obj.foo, "modified getter", "returns correct result"); + }); }); diff --git a/app/assets/javascripts/polyfills.js b/app/assets/javascripts/polyfills.js index 381b769e39..c5c0caaa89 100644 --- a/app/assets/javascripts/polyfills.js +++ b/app/assets/javascripts/polyfills.js @@ -1,325 +1,5 @@ /* eslint-disable */ -// Needed for iOS <= 13.3 -if (!String.prototype.replaceAll) { - String.prototype.replaceAll = function ( - pattern, - replacementStringOrFunction - ) { - let regex; - - if ( - Object.prototype.toString.call(pattern).toLowerCase() === - "[object regexp]" - ) { - regex = pattern; - } else { - const escapedStr = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - regex = new RegExp(escapedStr, "g"); - } - - return this.replace(regex, replacementStringOrFunction); - }; -} - -// Needed for Safari 15.2 and below -// from: https://github.com/iamdustan/smoothscroll -(function () { - "use strict"; - - function e() { - var e = window; - var t = document; - if ( - "scrollBehavior" in t.documentElement.style && - e.__forceSmoothScrollPolyfill__ !== true - ) { - return; - } - var o = e.HTMLElement || e.Element; - var r = 1.8; - var l = { - scroll: e.scroll || e.scrollTo, - scrollBy: e.scrollBy, - elementScroll: o.prototype.scroll || s, - scrollIntoView: o.prototype.scrollIntoView, - }; - var n = - e.performance && e.performance.now - ? e.performance.now.bind(e.performance) - : Date.now; - - function i(e) { - var t = ["MSIE ", "Trident/", "Edge/"]; - return new RegExp(t.join("|")).test(e); - } - var f = i(e.navigator.userAgent) ? 1 : 0; - - function s(e, t) { - this.scrollLeft = e; - this.scrollTop = t; - } - - function c(e) { - return 0.5 * (1 - Math.cos(Math.PI * e)); - } - - function a(e) { - if ( - e === null || - typeof e !== "object" || - e.behavior === undefined || - e.behavior === "auto" || - e.behavior === "instant" - ) { - return true; - } - if (typeof e === "object" && e.behavior === "smooth") { - return false; - } - throw new TypeError( - "behavior member of ScrollOptions " + - e.behavior + - " is not a valid value for enumeration ScrollBehavior." - ); - } - - function u(e, t) { - if (t === "Y") { - return e.clientHeight + f < e.scrollHeight; - } - if (t === "X") { - return e.clientWidth + f < e.scrollWidth; - } - } - - function d(t, o) { - var r = e.getComputedStyle(t, null)["overflow" + o]; - return r === "auto" || r === "scroll"; - } - - function p(e) { - var t = u(e, "Y") && d(e, "Y"); - var o = u(e, "X") && d(e, "X"); - return t || o; - } - - function h(e) { - while (e !== t.body && p(e) === false) { - e = e.parentNode || e.host; - } - return e; - } - - function v(e, t) { - var o = r / t; - var l = Math.pow(1.16, Math.max(e / 80, 1)); - return o * e * (1 / l); - } - - function y(t) { - var o = n(); - var r = e.devicePixelRatio; - var l; - var i; - var f; - var s; - var a = v(Math.abs(t.x - t.startX), r); - var u = v(Math.abs(t.y - t.startY), r); - var d = (o - t.startTime) / a; - var p = (o - t.startTime) / u; - d = d > 1 ? 1 : d; - p = p > 1 ? 1 : p; - l = c(d); - i = c(p); - f = t.startX + (t.x - t.startX) * l; - s = t.startY + (t.y - t.startY) * i; - t.method.call(t.scrollable, f, s); - if (f !== t.x || s !== t.y) { - e.requestAnimationFrame(y.bind(e, t)); - } - } - - function m(o, r, i) { - var f; - var c; - var a; - var u; - var d = n(); - if (o === t.body) { - f = e; - c = e.scrollX || e.pageXOffset; - a = e.scrollY || e.pageYOffset; - u = l.scroll; - } else { - f = o; - c = o.scrollLeft; - a = o.scrollTop; - u = s; - } - y({ - scrollable: f, - method: u, - startTime: d, - startX: c, - startY: a, - x: r, - y: i, - }); - } - e.scroll = e.scrollTo = function () { - if (arguments[0] === undefined) { - return; - } - if (a(arguments[0]) === true) { - l.scroll.call( - e, - arguments[0].left !== undefined - ? arguments[0].left - : typeof arguments[0] !== "object" - ? arguments[0] - : e.scrollX || e.pageXOffset, - arguments[0].top !== undefined - ? arguments[0].top - : arguments[1] !== undefined - ? arguments[1] - : e.scrollY || e.pageYOffset - ); - return; - } - m.call( - e, - t.body, - arguments[0].left !== undefined - ? ~~arguments[0].left - : e.scrollX || e.pageXOffset, - arguments[0].top !== undefined - ? ~~arguments[0].top - : e.scrollY || e.pageYOffset - ); - }; - e.scrollBy = function () { - if (arguments[0] === undefined) { - return; - } - if (a(arguments[0])) { - l.scrollBy.call( - e, - arguments[0].left !== undefined - ? arguments[0].left - : typeof arguments[0] !== "object" - ? arguments[0] - : 0, - arguments[0].top !== undefined - ? arguments[0].top - : arguments[1] !== undefined - ? arguments[1] - : 0 - ); - return; - } - m.call( - e, - t.body, - ~~arguments[0].left + (e.scrollX || e.pageXOffset), - ~~arguments[0].top + (e.scrollY || e.pageYOffset) - ); - }; - o.prototype.scroll = o.prototype.scrollTo = function () { - if (arguments[0] === undefined) { - return; - } - if (a(arguments[0]) === true) { - if (typeof arguments[0] === "number" && arguments[1] === undefined) { - throw new SyntaxError("Value could not be converted"); - } - l.elementScroll.call( - this, - arguments[0].left !== undefined - ? ~~arguments[0].left - : typeof arguments[0] !== "object" - ? ~~arguments[0] - : this.scrollLeft, - arguments[0].top !== undefined - ? ~~arguments[0].top - : arguments[1] !== undefined - ? ~~arguments[1] - : this.scrollTop - ); - return; - } - var e = arguments[0].left; - var t = arguments[0].top; - m.call( - this, - this, - typeof e === "undefined" ? this.scrollLeft : ~~e, - typeof t === "undefined" ? this.scrollTop : ~~t - ); - }; - o.prototype.scrollBy = function () { - if (arguments[0] === undefined) { - return; - } - if (a(arguments[0]) === true) { - l.elementScroll.call( - this, - arguments[0].left !== undefined - ? ~~arguments[0].left + this.scrollLeft - : ~~arguments[0] + this.scrollLeft, - arguments[0].top !== undefined - ? ~~arguments[0].top + this.scrollTop - : ~~arguments[1] + this.scrollTop - ); - return; - } - this.scroll({ - left: ~~arguments[0].left + this.scrollLeft, - top: ~~arguments[0].top + this.scrollTop, - behavior: arguments[0].behavior, - }); - }; - o.prototype.scrollIntoView = function () { - if (a(arguments[0]) === true) { - l.scrollIntoView.call( - this, - arguments[0] === undefined ? true : arguments[0] - ); - return; - } - var o = h(this); - var r = o.getBoundingClientRect(); - var n = this.getBoundingClientRect(); - if (o !== t.body) { - m.call( - this, - o, - o.scrollLeft + n.left - r.left, - o.scrollTop + n.top - r.top - ); - if (e.getComputedStyle(o).position !== "fixed") { - e.scrollBy({ - left: r.left, - top: r.top, - behavior: "smooth", - }); - } - } else { - e.scrollBy({ - left: n.left, - top: n.top, - behavior: "smooth", - }); - } - }; - } - if (typeof exports === "object" && typeof module !== "undefined") { - module.exports = { - polyfill: e, - }; - } else { - e(); - } -})(); +// Polyfills for old browsers can be added here /* eslint-enable */ diff --git a/app/assets/javascripts/pretty-text/package.json b/app/assets/javascripts/pretty-text/package.json index 4d36c4aa5c..f535dcb3e5 100644 --- a/app/assets/javascripts/pretty-text/package.json +++ b/app/assets/javascripts/pretty-text/package.json @@ -15,7 +15,7 @@ "start": "ember serve" }, "dependencies": { - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "webpack": "^5.75.0", @@ -24,7 +24,7 @@ "devDependencies": { "@babel/core": "^7.20.12", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^2.0.2", + "@embroider/test-setup": "^2.1.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit.js b/app/assets/javascripts/select-kit/addon/components/select-kit.js index 304b6c3f5b..95f8d8cd55 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit.js @@ -188,6 +188,14 @@ export default Component.extend( this.handleDeprecations(); }, + didInsertElement() { + this._super(...arguments); + + if (this.selectKit.options.expandedOnInsert) { + this._open(); + } + }, + click(event) { event.preventDefault(); event.stopPropagation(); @@ -296,6 +304,7 @@ export default Component.extend( desktopPlacementStrategy: null, hiddenValues: null, disabled: false, + expandedOnInsert: false, }, autoFilterable: computed("content.[]", "selectKit.filter", function () { diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js index 26330d1128..126a9d80f5 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js @@ -18,6 +18,7 @@ export default Component.extend(UtilsMixin, { "title", "rowValue:data-value", "rowName:data-name", + "index:data-index", "role", "ariaChecked:aria-checked", "guid:data-guid", @@ -30,6 +31,7 @@ export default Component.extend(UtilsMixin, { "isNone:none", "item.classNames", ], + index: 0, role: "menuitemradio", diff --git a/app/assets/javascripts/select-kit/addon/templates/components/email-group-user-chooser-row.hbs b/app/assets/javascripts/select-kit/addon/templates/components/email-group-user-chooser-row.hbs index 3a293eaadc..edfe7ced3b 100644 --- a/app/assets/javascripts/select-kit/addon/templates/components/email-group-user-chooser-row.hbs +++ b/app/assets/javascripts/select-kit/addon/templates/components/email-group-user-chooser-row.hbs @@ -1,16 +1,20 @@ {{#if this.item.isUser}} {{avatar this.item imageSize="tiny"}} - {{format-username this.item.id}} - {{this.item.name}} +
    + {{format-username this.item.id}} + {{this.item.name}} +
    {{#if (and this.item.showUserStatus this.item.status)}} {{/if}} {{decorate-username-selector this.item.id}} {{else if this.item.isGroup}} {{d-icon "users"}} - {{this.item.id}} - {{this.item.full_name}} +
    + {{this.item.id}} + {{this.item.full_name}} +
    {{else}} {{d-icon "envelope"}} {{this.item.id}} -{{/if}} \ No newline at end of file +{{/if}} diff --git a/app/assets/javascripts/select-kit/addon/templates/components/select-kit/select-kit-collection.hbs b/app/assets/javascripts/select-kit/addon/templates/components/select-kit/select-kit-collection.hbs index f17c087bc3..cb9ac5ee1a 100644 --- a/app/assets/javascripts/select-kit/addon/templates/components/select-kit/select-kit-collection.hbs +++ b/app/assets/javascripts/select-kit/addon/templates/components/select-kit/select-kit-collection.hbs @@ -1,12 +1,13 @@ {{#if this.collection.content.length}} -{{/if}} \ No newline at end of file +{{/if}} diff --git a/app/assets/javascripts/select-kit/package.json b/app/assets/javascripts/select-kit/package.json index a226808c86..0f5a07d82a 100644 --- a/app/assets/javascripts/select-kit/package.json +++ b/app/assets/javascripts/select-kit/package.json @@ -15,7 +15,7 @@ "start": "ember serve" }, "dependencies": { - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "webpack": "^5.75.0" @@ -23,7 +23,7 @@ "devDependencies": { "@babel/core": "^7.20.12", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^2.0.2", + "@embroider/test-setup": "^2.1.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/truth-helpers/package.json b/app/assets/javascripts/truth-helpers/package.json index 5378b9fcb0..70c3bd58d0 100644 --- a/app/assets/javascripts/truth-helpers/package.json +++ b/app/assets/javascripts/truth-helpers/package.json @@ -15,7 +15,7 @@ "start": "ember serve" }, "dependencies": { - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "webpack": "^5.75.0" @@ -23,7 +23,7 @@ "devDependencies": { "@babel/core": "^7.20.12", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^2.0.2", + "@embroider/test-setup": "^2.1.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/wizard/package.json b/app/assets/javascripts/wizard/package.json index c582bef904..8aada3bdb5 100644 --- a/app/assets/javascripts/wizard/package.json +++ b/app/assets/javascripts/wizard/package.json @@ -15,7 +15,7 @@ "start": "ember serve" }, "dependencies": { - "ember-auto-import": "^2.5.0", + "ember-auto-import": "^2.6.0", "ember-cli-babel": "^7.26.10", "ember-cli-htmlbars": "^6.1.1", "webpack": "^5.75.0", @@ -24,7 +24,7 @@ "devDependencies": { "@babel/core": "^7.20.12", "@ember/optional-features": "^2.0.0", - "@embroider/test-setup": "^2.0.2", + "@embroider/test-setup": "^2.1.0", "@glimmer/component": "^1.1.2", "broccoli-asset-rev": "^3.0.0", "ember-cli": "~3.28.5", diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index 2b8a33d8d7..03a8c59aad 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -956,10 +956,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/standalone@^7.20.12": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.20.12.tgz#ae882b8642b4efb1ddd80c8a64a929e028095562" - integrity sha512-hK/X+m1il3w1tYS4H8LDaGCEdiT47SVqEXY8RiEAgou26BystipSU8ZL6EvBR6t5l7lTv0ilBiChXWblKJ5iUA== +"@babel/standalone@^7.20.13": + version "7.20.13" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.20.13.tgz#187d0cbacee5700eeedaff7583e7345a64aa7c3a" + integrity sha512-L13qadxX3yB4mU92iSiWKePm3hYfGaAXPMqGEPUDNzzsmNh0+1M7agMBF62UHM29kFWOWowGfRRDvfAU8uLovg== "@babel/template@^7.16.7", "@babel/template@^7.18.10", "@babel/template@^7.20.7": version "7.20.7" @@ -1054,10 +1054,10 @@ mkdirp "^1.0.4" silent-error "^1.1.1" -"@ember/render-modifiers@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@ember/render-modifiers/-/render-modifiers-2.0.4.tgz#0ac7af647cb736076dbfcd54ca71e090cd329d71" - integrity sha512-Zh/fo5VUmVzYHkHVvzWVjJ1RjFUxA2jH0zCp2+DQa80Bf3DUXauiEByxU22UkN4LFT55DBFttC0xCQSJG3WTsg== +"@ember/render-modifiers@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@ember/render-modifiers/-/render-modifiers-2.0.5.tgz#4b1d9496a82ca471aeaa3ecddd94ef089450f415" + integrity sha512-5cJ1niIdOJC6k6KtIn9HGbr1DATJQp4ZqMv1vbi6LKQWbVCQ3byvKONtUEi3H0wcewlrcaWCqXOgm0nACzCOQA== dependencies: "@embroider/macros" "^1.0.0" ember-cli-babel "^7.26.11" @@ -1124,10 +1124,10 @@ semver "^7.3.5" typescript-memoize "^1.0.1" -"@embroider/test-setup@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@embroider/test-setup/-/test-setup-2.0.2.tgz#fa9e2e0dbaaba481e889875114727dd78aa280fd" - integrity sha512-0rapCTr7T94TprLW11q60Lddeuom+bkBI35zyCsAp7A7idlTUekH9k7BTx9rbnPFuQJkwILGuZ05t03IAyQ4Ig== +"@embroider/test-setup@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@embroider/test-setup/-/test-setup-2.1.0.tgz#ad2c295efdee3d6270bceed4dfbd410cc56d0cd9" + integrity sha512-9cSrg5qzaf1mLpEBTMoQsjUhPCcWxJq7aRykLzjtD4YIk7y/IFalEUPS6W87c3jnnzus9hO2hTXV/1BfdYstzQ== dependencies: lodash "^4.17.21" resolve "^1.20.0" @@ -3668,10 +3668,10 @@ ember-auto-import-chunks-json-generator@^1.0.0: ember-cli-babel "^7.26.6" ember-cli-htmlbars "^5.7.1" -ember-auto-import@^2.2.3, ember-auto-import@^2.4.3, ember-auto-import@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/ember-auto-import/-/ember-auto-import-2.5.0.tgz#627607648e87d154f75cd3f70c435355ef7cced9" - integrity sha512-fKERUmpZLn4RJiCwTjS7D5zJxgnbF4E6GiSp1GYh53K96S+5UBs06r7ScDI52rq34z0+qdSrA6qiDJ3i/lWqKg== +ember-auto-import@^2.2.3, ember-auto-import@^2.4.3, ember-auto-import@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/ember-auto-import/-/ember-auto-import-2.6.0.tgz#c7a2f799c9b700d74648cb02e35cf7bc1b44ac02" + integrity sha512-xUyypxlaqWvrx2KSseLus0H8K7Dt+sXNCvcxtquT2EmIM6r67NuQUT9woiEMa9UBvqcaX2k9hNLeubDl78saig== dependencies: "@babel/core" "^7.16.7" "@babel/plugin-proposal-class-properties" "^7.16.7" @@ -4231,10 +4231,10 @@ ember-resolver@^8.0.3: ember-cli-version-checker "^5.1.2" resolve "^1.20.0" -ember-rfc176-data@^0.3.17: - version "0.3.17" - resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.17.tgz#d4fc6c33abd6ef7b3440c107a28e04417b49860a" - integrity sha512-EVzTTKqxv9FZbEh6Ktw56YyWRAA0MijKvl7H8C06wVF+8f/cRRz3dXxa4nkwjzyVwx4rzKGuIGq77hxJAQhWWw== +ember-rfc176-data@^0.3.17, ember-rfc176-data@^0.3.18: + version "0.3.18" + resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.18.tgz#bb6fdcef49999981317ea81b6cc9210fb4108d65" + integrity sha512-JtuLoYGSjay1W3MQAxt3eINWXNYYQliK90tLwtb8aeCuQK8zKGCRbBodVIrkcTqshULMnRuTOS6t1P7oQk3g6Q== ember-router-generator@^2.0.0: version "2.0.0" @@ -4516,10 +4516,10 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^8.31.0: - version "8.31.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.31.0.tgz#75028e77cbcff102a9feae1d718135931532d524" - integrity sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA== +eslint@^8.32.0: + version "8.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.32.0.tgz#d9690056bb6f1a302bd991e7090f5b68fbaea861" + integrity sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ== dependencies: "@eslint/eslintrc" "^1.4.1" "@humanwhocodes/config-array" "^0.11.8" @@ -6267,10 +6267,10 @@ js-yaml@^4.0.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsdom@^21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.0.0.tgz#33e22f2fc44286e50ac853c7b7656c8864a4ea45" - integrity sha512-AIw+3ZakSUtDYvhwPwWHiZsUi3zHugpMEKlNPaurviseYoBqo0zBd3zqoUi3LPCNtPFlEP8FiW9MqCZdjb2IYA== +jsdom@^21.1.0: + version "21.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.0.tgz#d56ba4a84ed478260d83bd53dc181775f2d8e6ef" + integrity sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg== dependencies: abab "^2.0.6" acorn "^8.8.1" @@ -6880,10 +6880,10 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -message-bus-client@^4.3.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/message-bus-client/-/message-bus-client-4.3.1.tgz#2107b569131b03d7277801cd3409059e48e9f25e" - integrity sha512-gPG8POalZrM6t9xZPIzER3uDCiAfdwMEjx6ulbYICqzJx0CpLSnZRXKuWvhds4dM3iZQZXpH37UCfYYNICKu5g== +message-bus-client@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/message-bus-client/-/message-bus-client-4.3.2.tgz#6c0db2f011e5e419d2bd47e668063f009cd65971" + integrity sha512-q0OardiBDTePbDqPciRDzwpzuleq1zGK4/jlBRjB9aVuqs5XrikUmrl7gRNJEiB0EyFNdl1ZYQzQR7V5L3hdhw== messageformat@0.1.5: version "0.1.5" @@ -7828,10 +7828,10 @@ qunit-dom@^2.0.0: ember-cli-babel "^7.23.0" ember-cli-version-checker "^5.1.1" -qunit@^2.19.3: - version "2.19.3" - resolved "https://registry.yarnpkg.com/qunit/-/qunit-2.19.3.tgz#bcf81a2e8d176dc19fe8dd358c4cbd08619af03a" - integrity sha512-vEnspSZ37u2oR01OA/IZ1Td5V7BvQYFECdKPv86JaBplDNa5IHg0v7jFSPoP5L5o78Dbi8sl7/ATtpRDAKlSdw== +qunit@^2.19.4: + version "2.19.4" + resolved "https://registry.yarnpkg.com/qunit/-/qunit-2.19.4.tgz#2d689bb1165edd4b812e3ed2ee06ff907e9f2ece" + integrity sha512-aqUzzUeCqlleWYKlpgfdHHw9C6KxkB9H3wNfiBg5yHqQMzy0xw/pbCRHYFkjl8MsP/t8qkTQE+JTYL71azgiew== dependencies: commander "7.2.0" node-watch "0.7.3" @@ -8213,10 +8213,10 @@ sane@^4.0.0, sane@^4.1.0: minimist "^1.1.1" walker "~1.0.5" -sass@^1.57.0: - version "1.57.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.0.tgz#64c4144ed4e1c0ccb96dc18aef2c424cdbc0c12b" - integrity sha512-IZNEJDTK1cF5B1cGA593TPAV/1S0ysUDxq9XHjX/+SMy0QfUny+nfUsq5ZP7wWSl4eEf7wDJcEZ8ABYFmh3m/w== +sass@^1.57.1: + version "1.57.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5" + integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" diff --git a/app/assets/stylesheets/common/admin/penalty.scss b/app/assets/stylesheets/common/admin/penalty.scss index 6e846748c7..4505a89a4c 100644 --- a/app/assets/stylesheets/common/admin/penalty.scss +++ b/app/assets/stylesheets/common/admin/penalty.scss @@ -1,5 +1,8 @@ .silence-user-modal, .suspend-user-modal { + .modal-body { + padding-bottom: 0em; + } .penalty-duration, .penalty-suspend-forever, .suspend-reason-title, @@ -87,4 +90,24 @@ } } } + .penalty-history { + position: sticky; + bottom: 0; + background-color: var(--secondary); + padding: 0.5em 0 1em 0; + } + .penalty-history::before { + position: absolute; + content: ""; + display: block; + height: 1.5em; + top: -1.5em; + width: 100%; + pointer-events: none; + background: linear-gradient( + to bottom, + rgba(var(--secondary-rgb), 0), + rgba(var(--secondary-rgb), 1) + ); + } } diff --git a/app/assets/stylesheets/common/base/category-list.scss b/app/assets/stylesheets/common/base/category-list.scss index d6c4154eb6..80eedde014 100644 --- a/app/assets/stylesheets/common/base/category-list.scss +++ b/app/assets/stylesheets/common/base/category-list.scss @@ -43,9 +43,8 @@ .category-boxes, .category-boxes-with-topics { - display: flex; - flex-wrap: wrap; - justify-content: flex-start; + display: grid; + gap: 1.5em; margin-top: 1em; margin-bottom: 1em; width: 100%; @@ -117,9 +116,8 @@ } .category-boxes { + grid-template-columns: repeat(auto-fit, minmax(15em, 1fr)); .category-box { - width: 23%; - margin: 0 1% 1.5em 1%; > a { width: 100%; padding: 0; @@ -253,9 +251,8 @@ } .category-boxes-with-topics { + grid-template-columns: repeat(auto-fit, minmax(18em, 1fr)); .category-box { - width: 31%; - margin: 0 1% 1.5em 1%; padding: 0; } diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 76e835deb7..ee5caad43d 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -305,12 +305,15 @@ html.composer-open { } } + .category-input + .mini-tag-chooser { + margin-left: 8px; + } + .mini-tag-chooser { flex-grow: 1; max-width: calc(50% - 4px); - margin: 0 0 8px 8px; + margin: 0 0 8px 0px; z-index: z("composer", "dropdown"); - .select-kit-header { color: var(--primary-high); } diff --git a/app/assets/stylesheets/common/base/edit-category.scss b/app/assets/stylesheets/common/base/edit-category.scss index f93326e2c9..0365781b45 100644 --- a/app/assets/stylesheets/common/base/edit-category.scss +++ b/app/assets/stylesheets/common/base/edit-category.scss @@ -102,7 +102,8 @@ div.edit-category { } } - .edit-category-tab-settings { + .edit-category-tab-settings, + .edit-category-tab-tags { > section { margin-bottom: 1.5em; } diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index c5249f07bc..be7821e32c 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -636,36 +636,21 @@ } .topic-bulk-actions-modal { + .modal-inner-container { + min-width: 0; + width: 100%; + } p { margin-top: 0; } - &.full .modal-body { - height: 400px; - max-height: 400px; - } - .bulk-buttons { - display: flex; - flex-wrap: wrap; - margin-right: -1%; - - .btn { - flex: 1 0 30%; - margin-bottom: 1em; - margin-right: 1%; - white-space: nowrap; - overflow: hidden; - max-width: 33%; - - @media screen and (max-width: 767px) { - flex: 1 0 48%; - max-width: 48%; - } - - @include breakpoint(mobile-extra-large) { - flex: 1 1 auto; - min-width: 49%; - } + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12em, 1fr)); + gap: 0.5em; + max-width: 100%; + min-width: 0; + .d-button-label { + @include ellipsis; } } } diff --git a/app/assets/stylesheets/common/base/new-user.scss b/app/assets/stylesheets/common/base/new-user.scss index b7afd61970..de505a50b9 100644 --- a/app/assets/stylesheets/common/base/new-user.scss +++ b/app/assets/stylesheets/common/base/new-user.scss @@ -1,5 +1,4 @@ .new-user-wrapper { - margin-top: -15px; // temp, can remove margin from sibling element after nav finalized .user-navigation { --user-navigation__border-width: 4px; border-bottom: 1px solid var(--primary-low); diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index b35ae31a21..94205c7419 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -738,7 +738,8 @@ aside.onebox.xkcd .onebox-body img { // allowlistedgeneric twitter labels .onebox.allowlistedgeneric, -.onebox.whitelistedgeneric { +.onebox.whitelistedgeneric, +.onebox.discoursetopic { .label1, .label2 { color: var(--primary-med-or-secondary-med); @@ -754,6 +755,7 @@ aside.onebox.xkcd .onebox-body img { .onebox { &.allowlistedgeneric, &.whitelistedgeneric, + &.discoursetopic, &.gfycat, &.githubfolder { .site-icon { @@ -769,6 +771,24 @@ aside.onebox.xkcd .onebox-body img { } } +.onebox.discoursetopic { + h3 { + width: 100%; + margin-bottom: 0.2rem !important; + } + + .discourse-tags { + vertical-align: bottom; + .d-icon-tag { + font-size: var(--font-down-1); + margin-right: 0.35em; + margin-top: 0.15em; + color: var(--primary-medium); + align-self: center; + } + } +} + .onebox.gfycat p { span.label1 a { white-space: nowrap; diff --git a/app/assets/stylesheets/common/base/search-menu.scss b/app/assets/stylesheets/common/base/search-menu.scss index 189cee2a63..1c3b6a5a40 100644 --- a/app/assets/stylesheets/common/base/search-menu.scss +++ b/app/assets/stylesheets/common/base/search-menu.scss @@ -39,6 +39,7 @@ $search-pad-horizontal: 0.5em; .btn.search-context { margin: 2px; margin-right: 0; + white-space: nowrap; } &:focus-within { @include default-focus; @@ -194,23 +195,31 @@ $search-pad-horizontal: 0.5em; .search-item-user img.avatar { width: 20px; height: 20px; - margin-right: 5px; + margin-right: 0.5em; } .label-suffix { color: var(--primary-medium); + margin-right: 0.33em; + } + + .search-item-tag { + color: var(--primary-high); } .extra-hint { color: var(--primary-low-mid); font-size: var(--font-down-1); - float: right; - margin-top: 2px; } - .search-item-slug .badge-wrapper { - font-size: var(--font-0); - margin-left: 2px; + .search-item-slug { + .keyword { + margin-right: 0.33em; + } + .badge-wrapper { + font-size: var(--font-0); + margin-left: 2px; + } } .search-menu-initial-options { @@ -225,10 +234,20 @@ $search-pad-horizontal: 0.5em; .search-menu-initial-options, .search-result-tag, .search-menu-assistant { + .search-item-prefix { + margin-right: 0.33em; + } + .badge-wrapper { + font-size: var(--font-0); + margin-right: 0.5em; + } .search-link { + display: flex; + flex-wrap: wrap; + align-items: baseline; @include ellipsis; .d-icon { - margin-right: 5px; + margin-right: 0.33em; vertical-align: middle; } .d-icon-tag { diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 8050ece7ca..1014899d57 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -131,7 +131,8 @@ .discourse-tags { display: inline-flex; flex-wrap: wrap; - a { + a, + span { margin-right: 0.25em; } } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 6e5c0c4288..3304523255 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -1,5 +1,3 @@ -$quote-share-maxwidth: 150px; - .button-count.has-pending { span { background-color: var(--danger); @@ -555,8 +553,9 @@ aside.quote { display: none; position: absolute; z-index: z("dropdown"); - opacity: 0.9; - background-color: var(--secondary-high); + background-color: var(--secondary); + border: 1px solid var(--primary-low); + box-shadow: shadow("card"); flex-direction: column; &.animated { @@ -567,10 +566,6 @@ aside.quote { display: flex; } - &.fast-editing { - opacity: 1; - } - .buttons { display: flex; } @@ -599,24 +594,21 @@ aside.quote { } } - .btn, - .btn:hover, - .d-icon, - .btn:hover .d-icon { - color: var(--secondary-or-primary); - } - - .btn-primary, - .btn-primary:hover, - .btn-primary .d-icon, - .btn-primary:hover .d-icon { - color: var(--secondary); - } - .insert-quote + .quote-sharing { border-left: 1px solid rgba(255, 255, 255, 0.3); } + .btn-flat { + .d-icon { + color: var(--primary-high); + } + .discourse-no-touch & { + &:hover { + background-color: var(--tertiary-low); + } + } + } + .quote-sharing { vertical-align: middle; display: inline-flex; @@ -629,16 +621,34 @@ aside.quote { .quote-share-label { opacity: 1; - max-width: $quote-share-maxwidth; - transition: opacity 0.3s ease-in-out, max-width 0.3s ease-in-out, - padding 0.3s ease-in-out; + transition: opacity 0.3s ease-in-out; } - &:hover .quote-share-label { - background: transparent; - opacity: 0; - max-width: 0px; - padding: 6px 0px; + &:hover { + .quote-share-label { + background: transparent; + opacity: 0; + max-width: 0; + padding: 0; + overflow: hidden; + } + .quote-share-label + .quote-share-buttons { + max-width: 10em; + opacity: 1; + transition: opacity 0.3s ease-in-out; + } + // this psuedo element creates a transition buffer zone + // without it, the width change on hover can cause transition jitter + // the width is roughly wide enough to cover long translations of "share" + &:after { + content: ""; + position: absolute; + display: block; + background: transparent; + height: 100%; + padding: 0.5em 4em; + z-index: -1; // below the buttons + } } .quote-share-label + .quote-share-buttons { @@ -646,12 +656,7 @@ aside.quote { overflow: hidden; max-width: 0; display: inline-flex; - transition: opacity 0.3s ease-in-out, max-width 0.3s ease-in-out; - } - - &:hover .quote-share-label + .quote-share-buttons { - max-width: $quote-share-maxwidth; - opacity: 1; + transition: opacity 0.3s ease-in-out; } } } diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 7efebd72e2..a14824e9eb 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -80,7 +80,7 @@ .user-main { .about { width: 100%; - margin-bottom: 15px; + margin-bottom: 0; &.has-background { .user-profile-image { @@ -104,7 +104,7 @@ dl { margin: 0; - padding: 5px 10px; + padding: 0.25em 0; div { display: inline-flex; align-items: baseline; @@ -148,28 +148,6 @@ background: rgba(var(--secondary-rgb), 0.8); border-bottom: 1px solid var(--primary-low); - h1 { - font-size: var(--font-up-5); - font-weight: normal; - .d-icon { - font-size: 0.8em; - } - } - - h2 { - font-weight: normal; - max-width: 100%; - @include ellipsis; - } - - h3 { - font-weight: normal; - margin-bottom: 0.5em; - .d-icon:not(:first-of-type) { - margin-left: 10px; - } - } - .groups { display: inline; } @@ -185,10 +163,7 @@ .primary { width: 100%; position: relative; - - h1 { - font-weight: bold; - } + display: flex; .bio { max-height: 300px; @@ -202,7 +177,7 @@ .user-profile-avatar { position: relative; - float: left; + align-self: flex-start; .avatar-flair { bottom: 8px; right: 16px; @@ -211,6 +186,7 @@ } .controls { + margin-left: auto; ul { list-style-type: none; margin-top: 0; @@ -235,15 +211,17 @@ height: 45px; } - h1 { + .user-profile-names__primary { font-size: var(--font-up-3); } - h2 { + .user-profile-names__secondary { font-size: var(--font-up-1); } - h3, - h3.location-and-website { + + .user-profile-names__title, + .user-profile__location-and-website, + .user-profile__featured-topic { display: none; } } @@ -778,11 +756,57 @@ } } +.primary-textual { + flex: 1 1 auto; + min-width: 0; +} + .primary-textual .staged, .user-card .staged { font-style: italic; } +.user-profile-names__primary, +.user-profile-names__secondary { + max-width: 100%; + margin: 0; + @include ellipsis; +} + +.user-profile-names__primary { + font-weight: bold; + font-size: var(--font-up-5); + line-height: var(--line-height-small); + .d-icon { + font-size: 0.8em; + vertical-align: baseline; + } +} + +.user-profile-names__secondary { + font-size: var(--font-up-3); +} + +.user-profile__featured-topic, +.user-profile__location-and-website { + font-size: var(--font-0); + margin-top: 0.5em; + @include ellipsis; + .d-icon { + font-size: var(--font-down-1); + color: var(--primary-high); + margin-right: 0.33em; + } +} + +.user-profile__location-and-website { + display: flex; + max-width: 100%; + .user-profile-location { + margin-right: 1em; + } +} + .selectable-avatars { max-height: 350px; margin-bottom: 1em; diff --git a/app/assets/stylesheets/common/components/user-card.scss b/app/assets/stylesheets/common/components/user-card.scss index 79074b863a..f0ae1b4019 100644 --- a/app/assets/stylesheets/common/components/user-card.scss +++ b/app/assets/stylesheets/common/components/user-card.scss @@ -307,6 +307,7 @@ h3.user-status { } .relative-date { + flex: 1 0 auto; text-align: left; font-size: var(--font-down-3); padding-top: 0.5em; diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index dcf6afb523..ea13ee5bc3 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -259,6 +259,7 @@ overflow: hidden; padding-left: 1em; position: absolute; // prevents text length from impacting width + max-height: 3em; // this hides the date when the count + date would wrap to more than 2 lines } .timeline-ago { diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 30d21f8ad1..ba540f199d 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -89,9 +89,8 @@ } .posters a:first-child .avatar.latest:not(.single) { box-shadow: 0 0 3px 1px rgba(var(--tertiary-rgb), 0.35); - border: 2px solid rgba(var(--tertiary-rgb), 0.5); + border: 1px solid rgba(var(--tertiary-rgb), 0.5); position: relative; - top: -2px; left: -2px; } diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 461e60a0a4..cb4d83b082 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -96,10 +96,6 @@ .btn.right { float: right; } - - h2 { - margin-bottom: 10px; - } } .pref-avatar { @@ -158,30 +154,12 @@ table.user-invite-list { margin: 0 20px 10px 0; } - .primary { - .primary-textual { - padding: 0 4px 4px; - h1 { - max-width: 100%; - @include ellipsis; - } - .location-and-website { - display: flex; - max-width: 100%; - @include ellipsis; - .user-profile-location { - margin-right: 1em; - } - } - } - - .bio { - max-width: 750px; - } + .primary-textual { + padding: 0 0 0.5em; } - .secondary { - margin-top: 16px; + .bio { + max-width: 750px; } } @@ -198,8 +176,6 @@ table.user-invite-list { } .controls { - padding: 0 0 12px 0; - float: right; max-width: 13.5em; li { @@ -283,11 +259,6 @@ table.user-invite-list { max-width: 100%; } - h3 { - color: var(--primary); - margin: 20px 0 10px 0; - } - .category-selector, .tag-chooser, textarea, diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss index fb69050d3d..d004c9309e 100644 --- a/app/assets/stylesheets/mobile/modal.scss +++ b/app/assets/stylesheets/mobile/modal.scss @@ -106,6 +106,14 @@ &.insert-hyperlink-modal .modal-inner-container { overflow: visible; } + + html.keyboard-visible:not(.ios-device) & { + height: calc(100% - env(keyboard-inset-height)); + + .modal-inner-container { + margin: auto; + } + } } .modal .modal-body.reorder-categories { diff --git a/app/assets/stylesheets/mobile/new-user.scss b/app/assets/stylesheets/mobile/new-user.scss index 5dbd5aae74..ad366f2e2a 100644 --- a/app/assets/stylesheets/mobile/new-user.scss +++ b/app/assets/stylesheets/mobile/new-user.scss @@ -120,6 +120,6 @@ .new-user-content-wrapper { .user-content { - margin-top: 2em; + margin-top: 1em; } } diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index f55a1ade01..d8135a4f69 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -5,6 +5,7 @@ grid-template-columns: 1fr 1fr; grid-template-rows: auto auto auto; grid-gap: 16px; + padding-top: 1em; .user-primary-navigation { grid-column-start: 1; grid-row-start: 1; @@ -102,14 +103,6 @@ .details { margin-bottom: 12px; - h1 { - line-height: var(--line-height-small); - } - - h2 { - line-height: var(--line-height-medium); - } - .user-profile-avatar { .avatar-flair { right: 2px; @@ -207,9 +200,7 @@ } .controls { - order: 3; flex: 1 1 25%; - margin-left: auto; .btn { margin-bottom: 16px; @@ -275,7 +266,7 @@ .user-main .collapsed-info.about .details { display: flex; - margin-bottom: 16px; + margin-bottom: 0; .user-profile-avatar { margin: 0; flex: 0 0 auto; diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb index 2660c9895e..fd429312e7 100644 --- a/app/controllers/admin/backups_controller.rb +++ b/app/controllers/admin/backups_controller.rb @@ -238,11 +238,11 @@ class Admin::BackupsController < Admin::AdminController end def valid_extension?(filename) - /\.(tar\.gz|t?gz)$/i =~ filename + /\.(tar\.gz|t?gz)\z/i =~ filename end def valid_filename?(filename) - !!(/^[a-zA-Z0-9\._\-]+$/ =~ filename) + !!(/\A[a-zA-Z0-9\._\-]+\z/ =~ filename) end def render_error(message_key) diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 4d9e581fd3..1af73960fe 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -44,35 +44,6 @@ class Admin::GroupsController < Admin::StaffController end end - def add_owners - group = Group.find_by(id: params.require(:id)) - raise Discourse::NotFound unless group - - return can_not_modify_automatic if group.automatic - guardian.ensure_can_edit_group!(group) - - users = User.where(username: group_params[:usernames].split(",")) - - users.each do |user| - group_action_logger = GroupActionLogger.new(current_user, group) - - if !group.users.include?(user) - group.add(user) - group_action_logger.log_add_user_to_group(user) - end - group.group_users.where(user_id: user.id).update_all(owner: true) - group_action_logger.log_make_user_group_owner(user) - - if group_params[:notify_users] == "true" || group_params[:notify_users] == true - group.notify_added_to_group(user, owner: true) - end - end - - group.restore_user_count! - - render json: success_json.merge!(usernames: users.pluck(:username)) - end - def remove_owner group = Group.find_by(id: params.require(:id)) raise Discourse::NotFound unless group diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index abc8b9e83e..ffa8463a9c 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -7,9 +7,9 @@ class Admin::ReportsController < Admin::StaffController ApplicationRequest .req_types .keys - .select { |r| r =~ /^page_view_/ && r !~ /mobile/ } + .select { |r| r =~ /\Apage_view_/ && r !~ /mobile/ } .map { |r| r + "_reqs" } + - Report.singleton_methods.grep(/^report_(?!about|storage_stats)/) + Report.singleton_methods.grep(/\Areport_(?!about|storage_stats)/) reports = reports_methods.map do |name| @@ -61,7 +61,7 @@ class Admin::ReportsController < Admin::StaffController def show report_type = params[:type] - raise Discourse::NotFound unless report_type =~ /^[a-z0-9\_]+$/ + raise Discourse::NotFound unless report_type =~ /\A[a-z0-9\_]+\z/ args = parse_params(params) diff --git a/app/controllers/admin/site_texts_controller.rb b/app/controllers/admin/site_texts_controller.rb index 041c59128c..a92065399f 100644 --- a/app/controllers/admin/site_texts_controller.rb +++ b/app/controllers/admin/site_texts_controller.rb @@ -160,7 +160,7 @@ class Admin::SiteTextsController < Admin::AdminController { id: key, value: value, locale: locale } end - PLURALIZED_REGEX = /(.*)\.(zero|one|two|few|many|other)$/ + PLURALIZED_REGEX = /(.*)\.(zero|one|two|few|many|other)\z/ def find_site_text(locale) if self.class.restricted_keys.include?(params[:id]) diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb index bf8ebb27d9..c25c2c1767 100644 --- a/app/controllers/admin/themes_controller.rb +++ b/app/controllers/admin/themes_controller.rb @@ -108,7 +108,7 @@ class Admin::ThemesController < Admin::AdminController render json: @theme, status: :created rescue RemoteTheme::ImportError => e if params[:force] - theme_name = params[:remote].gsub(/.git$/, "").split("/").last + theme_name = params[:remote].gsub(/.git\z/, "").split("/").last remote_theme = RemoteTheme.new remote_theme.private_key = private_key diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 41bd4889bc..770415b952 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -640,15 +640,25 @@ class ApplicationController < ActionController::Base def preload_current_user_data store_preloaded( "currentUser", - MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)), + MultiJson.dump( + CurrentUserSerializer.new( + current_user, + scope: guardian, + root: false, + enable_sidebar_param: params[:enable_sidebar], + ), + ), ) + report = TopicTrackingState.report(current_user) + serializer = ActiveModel::ArraySerializer.new( report, each_serializer: TopicTrackingStateSerializer, scope: guardian, ) + store_preloaded("topicTrackingStates", MultiJson.dump(serializer)) end @@ -669,7 +679,7 @@ class ApplicationController < ActionController::Base DiscoursePluginRegistry.html_builders.each do |name, _| if name.start_with?("client:") - data[name.sub(/^client:/, "")] = DiscoursePluginRegistry.build_html(name, self) + data[name.sub(/\Aclient:/, "")] = DiscoursePluginRegistry.build_html(name, self) end end diff --git a/app/controllers/drafts_controller.rb b/app/controllers/drafts_controller.rb index 78d1ea4fad..bf7e82881c 100644 --- a/app/controllers/drafts_controller.rb +++ b/app/controllers/drafts_controller.rb @@ -24,6 +24,16 @@ class DraftsController < ApplicationController def create raise Discourse::NotFound.new if params[:draft_key].blank? + if params[:data].size > SiteSetting.max_draft_length + raise Discourse::InvalidParameters.new(:data) + end + + begin + data = JSON.parse(params[:data]) + rescue JSON::ParserError + raise Discourse::InvalidParameters.new(:data) + end + sequence = begin Draft.set( @@ -59,12 +69,6 @@ class DraftsController < ApplicationController json = success_json.merge(draft_sequence: sequence) - begin - data = JSON.parse(params[:data]) - rescue JSON::ParserError - raise Discourse::InvalidParameters.new(:data) - end - if data.present? # this is a bit of a kludge we need to remove (all the parsing) too many special cases here # we need to catch action edit and action editSharedDraft diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index 8ff1f870c0..f793981220 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -28,11 +28,11 @@ class EmbedController < ApplicationController end if @embed_id = params[:discourse_embed_id] - raise Discourse::InvalidParameters.new(:embed_id) unless @embed_id =~ /^de\-[a-zA-Z0-9]+$/ + raise Discourse::InvalidParameters.new(:embed_id) unless @embed_id =~ /\Ade\-[a-zA-Z0-9]+\z/ end if @embed_class = params[:embed_class] - unless @embed_class =~ /^[a-zA-Z0-9\-_]+$/ + unless @embed_class =~ /\A[a-zA-Z0-9\-_]+\z/ raise Discourse::InvalidParameters.new(:embed_class) end end @@ -139,7 +139,7 @@ class EmbedController < ApplicationController by_url = {} if embed_urls.present? - urls = embed_urls.map { |u| u.sub(/#discourse-comments$/, "").sub(%r{/$}, "") } + urls = embed_urls.map { |u| u.sub(/#discourse-comments\z/, "").sub(%r{/\z}, "") } topic_embeds = TopicEmbed.where(embed_url: urls).includes(:topic).references(:topic) topic_embeds.each do |te| diff --git a/app/controllers/extra_locales_controller.rb b/app/controllers/extra_locales_controller.rb index 63fdb5d81c..a6c04bee71 100644 --- a/app/controllers/extra_locales_controller.rb +++ b/app/controllers/extra_locales_controller.rb @@ -71,6 +71,6 @@ class ExtraLocalesController < ApplicationController private def valid_bundle?(bundle) - bundle == OVERRIDES_BUNDLE || (bundle =~ /^(admin|wizard)$/ && current_user&.staff?) + bundle == OVERRIDES_BUNDLE || (bundle =~ /\A(admin|wizard)\z/ && current_user&.staff?) end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 99f3634ee6..4a772039f6 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -385,6 +385,32 @@ class GroupsController < ApplicationController end end + def add_owners + group = Group.find_by(id: params.require(:id)) + raise Discourse::NotFound unless group + + return can_not_modify_automatic if group.automatic + guardian.ensure_can_edit_group!(group) + + users = users_from_params + group_action_logger = GroupActionLogger.new(current_user, group) + + users.each do |user| + if !group.users.include?(user) + group.add(user) + group_action_logger.log_add_user_to_group(user) + end + group.group_users.where(user_id: user.id).update_all(owner: true) + group_action_logger.log_make_user_group_owner(user) + + group.notify_added_to_group(user, owner: true) if params[:notify_users].to_s == "true" + end + + group.restore_user_count! + + render json: success_json.merge!(usernames: users.pluck(:username)) + end + def join ensure_logged_in unless current_user.staff? @@ -667,6 +693,12 @@ class GroupsController < ApplicationController end end + protected + + def can_not_modify_automatic + render_json_error(I18n.t("groups.errors.can_not_modify_automatic")) + end + private def add_user_to_group(group, user, notify = false) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index b5c9ad2e0e..a9f061d426 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -466,6 +466,33 @@ class PostsController < ApplicationController render body: nil end + def permanently_delete_revisions + guardian.ensure_can_permanently_delete_post_revisions! + + post = find_post_from_params + raise Discourse::InvalidParameters.new(:post) if post.blank? + raise Discourse::NotFound unless post.revisions.present? + + RateLimiter.new( + current_user, + "admin_permanently_delete_post_revisions", + 20, + 1.minute, + apply_limit_to_staff: true, + ).performed! + + ActiveRecord::Base.transaction do + updated_at = Time.zone.now + post.revisions.destroy_all + post.update(version: 1, public_version: 1, last_version_at: updated_at) + StaffActionLogger.new(current_user).log_permanently_delete_post_revisions(post) + end + + post.rebake! + + render body: nil + end + def show_revision post_revision = find_post_revision_from_params guardian.ensure_can_show_post_revision!(post_revision) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 9c8216522c..586ed5dffd 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -203,7 +203,7 @@ class SessionController < ApplicationController end # If it's not a relative URL check the host - if return_path !~ %r{^/[^/]} + if return_path !~ %r{\A/[^/]} begin uri = URI(return_path) if (uri.hostname == Discourse.current_hostname) @@ -594,7 +594,7 @@ class SessionController < ApplicationController client_ip: request&.ip, user_agent: request&.user_agent, } - DiscourseEvent.trigger(:before_session_destroy, event_data) + DiscourseEvent.trigger(:before_session_destroy, event_data, **Discourse::Utils::EMPTY_KEYWORDS) redirect_url = event_data[:redirect_url] reset_session diff --git a/app/controllers/slugs_controller.rb b/app/controllers/slugs_controller.rb new file mode 100644 index 0000000000..531f8ff4fd --- /dev/null +++ b/app/controllers/slugs_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class SlugsController < ApplicationController + requires_login + + MAX_SLUG_GENERATIONS_PER_MINUTE = 20 + + def generate + params.require(:name) + + raise Discourse::InvalidAccess if !current_user.has_trust_level_or_staff?(TrustLevel[4]) + + RateLimiter.new( + current_user, + "max-slug-generations-per-minute", + MAX_SLUG_GENERATIONS_PER_MINUTE, + 1.minute, + ).performed! + + render json: success_json.merge(slug: Slug.for(params[:name], "")) + end +end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index fde841a46e..1eb75bd810 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -193,7 +193,11 @@ class StaticController < ApplicationController def brotli_asset is_asset_path - serve_asset(".br") { response.headers["Content-Encoding"] = "br" } + if params[:path].end_with?(".map") + serve_asset + else + serve_asset(".br") { response.headers["Content-Encoding"] = "br" } + end end def cdn_asset diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 9b27b69e6a..aa5037e2ea 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -5,12 +5,13 @@ class TagsController < ::ApplicationController include TopicQueryParams before_action :ensure_tags_enabled - before_action :ensure_visible, only: %i[show info] def self.show_methods Discourse.anonymous_filters.map { |f| :"show_#{f}" } end + before_action :ensure_visible, only: [:show, :info, *show_methods] + requires_login except: [:index, :show, :tag_feed, :search, :info, *show_methods] skip_before_action :check_xhr, only: [:tag_feed, :show, :index, *show_methods] @@ -40,29 +41,25 @@ class TagsController < ::ApplicationController if SiteSetting.tags_listed_by_group ungrouped_tags = Tag.where("tags.id NOT IN (SELECT tag_id FROM tag_group_memberships)") - ungrouped_tags = ungrouped_tags.where("tags.topic_count > 0") unless show_all_tags + ungrouped_tags = ungrouped_tags.used_tags_in_regular_topics(guardian) unless show_all_tags grouped_tag_counts = TagGroup .visible(guardian) .order("name ASC") - .includes(:tags) + .includes(:none_synonym_tags) .map do |tag_group| { id: tag_group.id, name: tag_group.name, - tags: - self.class.tag_counts_json( - tag_group.tags.where(target_tag_id: nil), - show_pm_tags: guardian.can_tag_pms?, - ), + tags: self.class.tag_counts_json(tag_group.none_synonym_tags, guardian), } end - @tags = self.class.tag_counts_json(ungrouped_tags, show_pm_tags: guardian.can_tag_pms?) + @tags = self.class.tag_counts_json(ungrouped_tags, guardian) @extras = { tag_groups: grouped_tag_counts } else - tags = show_all_tags ? Tag.all : Tag.where("tags.topic_count > 0") + tags = show_all_tags ? Tag.all : Tag.used_tags_in_regular_topics(guardian) unrestricted_tags = DiscourseTagging.filter_visible(tags.where(target_tag_id: nil), guardian) categories = @@ -77,13 +74,14 @@ class TagsController < ::ApplicationController category_tags = self.class.tag_counts_json( DiscourseTagging.filter_visible(c.tags.where(target_tag_id: nil), guardian), + guardian, ) next if category_tags.empty? { id: c.id, tags: category_tags } end .compact - @tags = self.class.tag_counts_json(unrestricted_tags, show_pm_tags: guardian.can_tag_pms?) + @tags = self.class.tag_counts_json(unrestricted_tags, guardian) @extras = { categories: category_tag_counts } end @@ -264,7 +262,7 @@ class TagsController < ::ApplicationController tags_with_counts, filter_result_context = DiscourseTagging.filter_allowed_tags(guardian, **filter_params, with_context: true) - tags = self.class.tag_counts_json(tags_with_counts, show_pm_tags: guardian.can_tag_pms?) + tags = self.class.tag_counts_json(tags_with_counts, guardian) json_response = { results: tags } @@ -388,18 +386,22 @@ class TagsController < ::ApplicationController end end - def self.tag_counts_json(tags, show_pm_tags: true) + def self.tag_counts_json(tags, guardian) + show_pm_tags = guardian.can_tag_pms? target_tags = Tag.where(id: tags.map(&:target_tag_id).compact.uniq).select(:id, :name) + tags .map do |t| - next if t.topic_count == 0 && t.pm_topic_count > 0 && !show_pm_tags + topic_count = t.public_send(Tag.topic_count_column(guardian)) + + next if topic_count == 0 && t.pm_topic_count > 0 && !show_pm_tags { id: t.name, text: t.name, name: t.name, description: t.description, - count: t.topic_count, + count: topic_count, pm_count: show_pm_tags ? t.pm_topic_count : 0, target_tag: t.target_tag_id ? target_tags.find { |x| x.id == t.target_tag_id }&.name : nil, diff --git a/app/controllers/theme_javascripts_controller.rb b/app/controllers/theme_javascripts_controller.rb index 63d6484d71..159402889f 100644 --- a/app/controllers/theme_javascripts_controller.rb +++ b/app/controllers/theme_javascripts_controller.rb @@ -47,7 +47,7 @@ class ThemeJavascriptsController < ApplicationController def show_tests digest = params[:digest] - raise Discourse::NotFound if !digest.match?(/^\h{40}$/) + raise Discourse::NotFound if !digest.match?(/\A\h{40}\z/) theme = Theme.find_by(id: params[:theme_id]) raise Discourse::NotFound if theme.blank? diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 4ad8b87911..7b01ff55e0 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -83,7 +83,7 @@ class TopicsController < ApplicationController # Special case: a slug with a number in front should look by slug first before looking # up that particular number - if params[:id] && params[:id] =~ /^\d+[^\d\\]+$/ + if params[:id] && params[:id] =~ /\A\d+[^\d\\]+\z/ topic = Topic.find_by_slug(params[:id]) return redirect_to_correct_topic(topic, opts[:post_number]) if topic end @@ -479,7 +479,12 @@ class TopicsController < ApplicationController enabled = params[:enabled] == "true" check_for_status_presence(:status, status) - @topic = Topic.find_by(id: topic_id) + @topic = + if params[:category_id] + Topic.find_by(id: topic_id, category_id: params[:category_id].to_i) + else + Topic.find_by(id: topic_id) + end case status when "closed" diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 218b5ae88d..b71240638e 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -306,7 +306,7 @@ class UploadsController < ApplicationController private - # We can pre-emptively check size for attachments, but not for images + # We can preemptively check size for attachments, but not for images # as they may be further reduced in size by UploadCreator (at this point # they may have already been reduced in size by preprocessors) def file_size_too_big?(file_name, file_size) diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index abf2408313..db47e4b701 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -39,7 +39,7 @@ class UserAvatarsController < ApplicationController def show_proxy_letter is_asset_path - if SiteSetting.external_system_avatars_url !~ %r{^/letter_avatar_proxy} + if SiteSetting.external_system_avatars_url !~ %r{\A/letter_avatar_proxy} raise Discourse::NotFound end @@ -192,7 +192,7 @@ class UserAvatarsController < ApplicationController end def redirect_s3_avatar(url) - immutable_for 1.hour + immutable_for 1.day redirect_to url, allow_other_host: true end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 23032ada60..efeadc161e 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -473,7 +473,7 @@ class UsersController < ApplicationController end def my_redirect - raise Discourse::NotFound if params[:path] !~ %r{^[a-z_\-/]+$} + raise Discourse::NotFound if params[:path] !~ %r{\A[a-z_\-/]+\z} if current_user.blank? cookies[:destination_url] = path("/my/#{params[:path]}") diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index b84d59c269..6044ef83bb 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -6,12 +6,20 @@ class WebhooksController < ActionController::Base skip_before_action :verify_authenticity_token def mailgun - return mailgun_failure if SiteSetting.mailgun_api_key.blank? + return signature_failure if SiteSetting.mailgun_api_key.blank? params["event-data"] ? handle_mailgun_new(params) : handle_mailgun_legacy(params) end def sendgrid + if SiteSetting.sendgrid_verification_key.present? + return signature_failure if !valid_sendgrid_signature? + else + Rails.logger.warn( + "Received a Sendgrid webhook, but no verification key has been configured. This is unsafe behaviour and will be disallowed in the future.", + ) + end + events = params["_json"] || [params] events.each do |event| message_id = Email::MessageIdService.message_id_clean((event["smtp-id"] || "")) @@ -32,6 +40,14 @@ class WebhooksController < ActionController::Base end def mailjet + if SiteSetting.mailjet_webhook_token.present? + return signature_failure if !valid_mailjet_token? + else + Rails.logger.warn( + "Received a Mailjet webhook, but no token has been configured. This is unsafe behaviour and will be disallowed in the future.", + ) + end + events = params["_json"] || [params] events.each do |event| message_id = event["CustomID"] @@ -49,20 +65,29 @@ class WebhooksController < ActionController::Base end def mandrill - events = JSON.parse(params["mandrill_events"]) - events.each do |event| - message_id = event.dig("msg", "metadata", "message_id") - to_address = event.dig("msg", "email") - error_code = event.dig("msg", "diag") - - case event["event"] - when "hard_bounce" - process_bounce(message_id, to_address, SiteSetting.hard_bounce_score, error_code) - when "soft_bounce" - process_bounce(message_id, to_address, SiteSetting.soft_bounce_score, error_code) - end + if SiteSetting.mandrill_authentication_key.present? + return signature_failure if !valid_mandrill_signature? + else + Rails.logger.warn( + "Received a Mandrill webhook, but no authentication key has been configured. This is unsafe behaviour and will be disallowed in the future.", + ) end + JSON + .parse(params["mandrill_events"]) + .each do |event| + message_id = event.dig("msg", "metadata", "message_id") + to_address = event.dig("msg", "email") + error_code = event.dig("msg", "diag") + + case event["event"] + when "hard_bounce" + process_bounce(message_id, to_address, SiteSetting.hard_bounce_score, error_code) + when "soft_bounce" + process_bounce(message_id, to_address, SiteSetting.soft_bounce_score, error_code) + end + end + success end @@ -73,6 +98,14 @@ class WebhooksController < ActionController::Base end def postmark + if SiteSetting.postmark_webhook_token.present? + return signature_failure if !valid_postmark_token? + else + Rails.logger.warn( + "Received a Postmark webhook, but no token has been configured. This is unsafe behaviour and will be disallowed in the future.", + ) + end + # see https://postmarkapp.com/developer/webhooks/bounce-webhook#bounce-webhook-data # and https://postmarkapp.com/developer/api/bounce-api#bounce-types @@ -90,6 +123,14 @@ class WebhooksController < ActionController::Base end def sparkpost + if SiteSetting.sparkpost_webhook_token.present? + return signature_failure if !valid_sparkpost_token? + else + Rails.logger.warn( + "Received a Sparkpost webhook, but no token has been configured. This is unsafe behaviour and will be disallowed in the future.", + ) + end + events = params["_json"] || [params] events.each do |event| message_event = event.dig("msys", "message_event") @@ -131,7 +172,7 @@ class WebhooksController < ActionController::Base private - def mailgun_failure + def signature_failure render body: nil, status: 406 end @@ -158,7 +199,7 @@ class WebhooksController < ActionController::Base def handle_mailgun_legacy(params) unless valid_mailgun_signature?(params["token"], params["timestamp"], params["signature"]) - return mailgun_failure + return signature_failure end event = params["event"] @@ -185,7 +226,7 @@ class WebhooksController < ActionController::Base signature["timestamp"], signature["signature"], ) - return mailgun_failure + return signature_failure end data = params["event-data"] @@ -205,6 +246,58 @@ class WebhooksController < ActionController::Base success end + def valid_sendgrid_signature? + signature = request.headers["X-Twilio-Email-Event-Webhook-Signature"] + timestamp = request.headers["X-Twilio-Email-Event-Webhook-Timestamp"] + request.body.rewind + payload = request.body.read + + hashed_payload = Digest::SHA256.digest("#{timestamp}#{payload}") + decoded_signature = Base64.decode64(signature) + + begin + public_key = OpenSSL::PKey::EC.new(Base64.decode64(SiteSetting.sendgrid_verification_key)) + rescue StandardError => err + Rails.logger.error("Invalid Sendgrid verification key") + return false + end + + public_key.dsa_verify_asn1(hashed_payload, decoded_signature) + end + + def valid_mailjet_token? + ActiveSupport::SecurityUtils.secure_compare(params[:t], SiteSetting.mailjet_webhook_token) + end + + def valid_mandrill_signature? + signature = request.headers["X-Mandrill-Signature"] + + payload = "#{Discourse.base_url}/webhooks/mandrill" + params + .permit(:mandrill_events) + .to_h + .sort_by(&:first) + .each do |key, value| + payload += key.to_s + payload += value + end + + payload_signature = + OpenSSL::HMAC.digest("sha1", SiteSetting.mandrill_authentication_key, payload) + ActiveSupport::SecurityUtils.secure_compare( + signature, + Base64.strict_encode64(payload_signature), + ) + end + + def valid_postmark_token? + ActiveSupport::SecurityUtils.secure_compare(params[:t], SiteSetting.postmark_webhook_token) + end + + def valid_sparkpost_token? + ActiveSupport::SecurityUtils.secure_compare(params[:t], SiteSetting.sparkpost_webhook_token) + end + def process_bounce(message_id, to_address, bounce_score, bounce_error_code = nil) return if message_id.blank? || to_address.blank? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2520cd69b5..3f7cafff49 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -50,7 +50,7 @@ module ApplicationHelper def google_universal_analytics_json(ua_domain_name = nil) result = {} - result[:cookieDomain] = ua_domain_name.gsub(%r{^http(s)?://}, "") if ua_domain_name + result[:cookieDomain] = ua_domain_name.gsub(%r{\Ahttp(s)?://}, "") if ua_domain_name result[:userId] = current_user.id if current_user.present? result[:allowLinker] = true if SiteSetting.ga_universal_auto_link_domains.present? result.to_json @@ -117,9 +117,9 @@ module ApplicationHelper # seconds. if !script.start_with?("discourse/tests/") if is_brotli_req? - path = path.gsub(/\.([^.]+)$/, '.br.\1') + path = path.gsub(/\.([^.]+)\z/, '.br.\1') elsif is_gzip_req? - path = path.gsub(/\.([^.]+)$/, '.gz.\1') + path = path.gsub(/\.([^.]+)\z/, '.gz.\1') end end elsif GlobalSetting.cdn_url&.start_with?("https") && is_brotli_req? && @@ -311,6 +311,15 @@ module ApplicationHelper result << tag(:meta, { name: "twitter:#{property}", content: content }, nil, true) end end + Array + .wrap(opts[:breadcrumbs]) + .each do |breadcrumb| + result << tag(:meta, property: "og:article:section", content: breadcrumb[:name]) + result << tag(:meta, property: "og:article:section:color", content: breadcrumb[:color]) + end + Array + .wrap(opts[:tags]) + .each { |tag_name| result << tag(:meta, property: "og:article:tag", content: tag_name) } if opts[:read_time] && opts[:read_time] > 0 && opts[:like_count] && opts[:like_count] > 0 result << tag(:meta, name: "twitter:label1", value: I18n.t("reading_time")) diff --git a/app/helpers/user_notifications_helper.rb b/app/helpers/user_notifications_helper.rb index e92dbeccd9..deb4042501 100644 --- a/app/helpers/user_notifications_helper.rb +++ b/app/helpers/user_notifications_helper.rb @@ -20,8 +20,8 @@ module UserNotificationsHelper def logo_url logo_url = SiteSetting.site_digest_logo_url - logo_url = SiteSetting.site_logo_url if logo_url.blank? || logo_url =~ /\.svg$/i - return nil if logo_url.blank? || logo_url =~ /\.svg$/i + logo_url = SiteSetting.site_logo_url if logo_url.blank? || logo_url =~ /\.svg\z/i + return nil if logo_url.blank? || logo_url =~ /\.svg\z/i logo_url end diff --git a/app/jobs/base.rb b/app/jobs/base.rb index 0f671c95a5..4d1d2529e5 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -29,7 +29,7 @@ module Jobs end def self.num_email_retry_jobs - Sidekiq::RetrySet.new.count { |job| job.klass =~ /Email$/ } + Sidekiq::RetrySet.new.count { |job| job.klass =~ /Email\z/ } end class Base diff --git a/app/jobs/onceoff/onceoff.rb b/app/jobs/onceoff/onceoff.rb index df9c94e54d..efbc0ae5f5 100644 --- a/app/jobs/onceoff/onceoff.rb +++ b/app/jobs/onceoff/onceoff.rb @@ -4,7 +4,7 @@ class Jobs::Onceoff < ::Jobs::Base sidekiq_options retry: false def self.name_for(klass) - klass.name.sub(/^Jobs\:\:/, "") + klass.name.sub(/\AJobs\:\:/, "") end def running_key_name diff --git a/app/jobs/regular/update_username.rb b/app/jobs/regular/update_username.rb index 3a0ed3194e..e9b56cdd69 100644 --- a/app/jobs/regular/update_username.rb +++ b/app/jobs/regular/update_username.rb @@ -29,9 +29,9 @@ module Jobs @raw_quote_regex = /(\[quote\s*=\s*["'']?)#{@old_username}(\,?[^\]]*\])/i cooked_username = PrettyText::Helpers.format_username(@old_username) - @cooked_mention_username_regex = /^@#{cooked_username}$/i + @cooked_mention_username_regex = /\A@#{cooked_username}\z/i @cooked_mention_user_path_regex = - %r{^/u(?:sers)?/#{UrlHelper.encode_component(cooked_username)}$}i + %r{\A/u(?:sers)?/#{UrlHelper.encode_component(cooked_username)}\z}i @cooked_quote_username_regex = /(?<=\s)#{cooked_username}(?=:)/i update_posts diff --git a/app/jobs/scheduled/grant_anniversary_badges.rb b/app/jobs/scheduled/grant_anniversary_badges.rb index c244a2e92e..06e35cb368 100644 --- a/app/jobs/scheduled/grant_anniversary_badges.rb +++ b/app/jobs/scheduled/grant_anniversary_badges.rb @@ -6,34 +6,13 @@ module Jobs def execute(args) return unless SiteSetting.enable_badges? - badge = Badge.find_by(id: Badge::Anniversary, enabled: true) - return unless badge + return unless badge = Badge.find_by(id: Badge::Anniversary, enabled: true) start_date = args[:start_date] || 1.year.ago end_date = start_date + 1.year - fmt_end_date = end_date.iso8601(6) - fmt_start_date = start_date.iso8601(6) - - user_ids = DB.query_single <<~SQL - SELECT u.id AS user_id - FROM users AS u - INNER JOIN posts AS p ON p.user_id = u.id - INNER JOIN topics AS t ON p.topic_id = t.id - LEFT OUTER JOIN user_badges AS ub ON ub.user_id = u.id AND - ub.badge_id = #{Badge::Anniversary} AND - ub.granted_at BETWEEN '#{fmt_start_date}' AND '#{fmt_end_date}' - WHERE u.active AND - u.silenced_till IS NULL AND - NOT p.hidden AND - p.deleted_at IS NULL AND - t.visible AND - t.archetype <> 'private_message' AND - p.created_at BETWEEN '#{fmt_start_date}' AND '#{fmt_end_date}' AND - u.created_at <= '#{fmt_start_date}' - GROUP BY u.id - HAVING COUNT(p.id) > 0 AND COUNT(ub.id) = 0 - SQL + sql = BadgeQueries.anniversaries(start_date, end_date) + user_ids = DB.query_single(sql) User .where(id: user_ids) diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 8c3245a803..1e845f1f15 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -377,7 +377,7 @@ class AdminDashboardData end def subfolder_ends_in_slash_check - I18n.t("dashboard.subfolder_ends_in_slash") if Discourse.base_path =~ %r{/$} + I18n.t("dashboard.subfolder_ends_in_slash") if Discourse.base_path =~ %r{/\z} end def email_polling_errored_recently diff --git a/app/models/api_key_scope.rb b/app/models/api_key_scope.rb index c0d072d0bc..eb1346f72e 100644 --- a/app/models/api_key_scope.rb +++ b/app/models/api_key_scope.rb @@ -28,8 +28,8 @@ class ApiKeyScope < ActiveRecord::Base params: %i[topic_id], }, update: { - actions: %w[topics#update], - params: %i[topic_id], + actions: %w[topics#update topics#status], + params: %i[topic_id category_id], }, read: { actions: %w[topics#show topics#feed topics#posts], @@ -101,6 +101,9 @@ class ApiKeyScope < ActiveRecord::Base anonymize: { actions: %w[admin/users#anonymize], }, + suspend: { + actions: %w[admin/users#suspend], + }, delete: { actions: %w[admin/users#destroy], }, @@ -121,6 +124,11 @@ class ApiKeyScope < ActiveRecord::Base actions: %w[admin/email#handle_mail admin/email#smtp_should_reject], }, }, + invites: { + create: { + actions: %w[invites#create], + }, + }, badges: { create: { actions: %w[admin/badges#create], @@ -146,6 +154,16 @@ class ApiKeyScope < ActiveRecord::Base actions: %w[user_badges#destroy], }, }, + search: { + show: { + actions: %w[search#show], + params: %i[q page], + }, + query: { + actions: %w[search#query], + params: %i[term], + }, + }, wordpress: { publishing: { actions: %w[site#site posts#create topics#update topics#status topics#show], diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index 3e1a6dab38..e69e67fd02 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true class Bookmark < ActiveRecord::Base - self.ignored_columns = [ - "post_id", # TODO (martin) (2022-08-01) remove - "for_topic", # TODO (martin) (2022-08-01) remove - ] - cattr_accessor :registered_bookmarkables self.registered_bookmarkables = [] diff --git a/app/models/category.rb b/app/models/category.rb index fc721f0940..a049414df0 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -130,6 +130,7 @@ class Category < ActiveRecord::Base has_many :category_required_tag_groups, -> { order(order: :asc) }, dependent: :destroy has_many :sidebar_section_links, as: :linkable, dependent: :delete_all + has_many :embeddable_hosts, dependent: :destroy belongs_to :reviewable_by_group, class_name: "Group" @@ -420,7 +421,7 @@ class Category < ActiveRecord::Base end # only allow to use category itself id. - match_id = /^(\d+)-category/.match(self.slug) + match_id = /\A(\d+)-category/.match(self.slug) if match_id.present? errors.add(:slug, :invalid) if new_record? || (match_id[1] != self.id.to_s) end @@ -896,7 +897,7 @@ class Category < ActiveRecord::Base slug_path.inject(nil) do |parent_id, slug| category = Category.where(slug: slug, parent_category_id: parent_id) - if match_id = /^(\d+)-category/.match(slug).presence + if match_id = /\A(\d+)-category/.match(slug).presence category = category.or(Category.where(id: match_id[1], parent_category_id: parent_id)) end diff --git a/app/models/concerns/category_hashtag.rb b/app/models/concerns/category_hashtag.rb index 52d4d01b5b..135a903aee 100644 --- a/app/models/concerns/category_hashtag.rb +++ b/app/models/concerns/category_hashtag.rb @@ -9,8 +9,8 @@ module CategoryHashtag # TODO (martin) Remove this when enable_experimental_hashtag_autocomplete # becomes the norm, it is reimplemented below for CategoryHashtagDataSourcee def query_from_hashtag_slug(category_slug) - slug_path = category_slug.split(SEPARATOR) - return nil if slug_path.empty? || slug_path.size > 2 + slug_path = split_slug_path(category_slug) + return if slug_path.blank? slug_path.map! { |slug| CGI.escape(slug) } if SiteSetting.slug_generation_method == "encoded" @@ -34,7 +34,9 @@ module CategoryHashtag category_slugs .map(&:downcase) .map do |slug| - slug_path = slug.split(":") + slug_path = split_slug_path(slug) + next if slug_path.blank? + if SiteSetting.slug_generation_method == "encoded" slug_path.map! { |slug| CGI.escape(slug) } end @@ -60,5 +62,11 @@ module CategoryHashtag end .compact end + + def split_slug_path(slug) + slug_path = slug.split(SEPARATOR) + return if slug_path.empty? || slug_path.size > 2 + slug_path + end end end diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index 2b2f64f08a..5330abdd69 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -20,7 +20,7 @@ module HasCustomFields sorted_types = types.keys.select { |k| k.end_with?("*") }.sort_by(&:length).reverse - sorted_types.each { |t| return types[t] if key =~ /^#{t}/i } + sorted_types.each { |t| return types[t] if key =~ /\A#{t}/i } types[key] end diff --git a/app/models/concerns/reports/top_uploads.rb b/app/models/concerns/reports/top_uploads.rb index d5e2955b7d..b9caaa63fa 100644 --- a/app/models/concerns/reports/top_uploads.rb +++ b/app/models/concerns/reports/top_uploads.rb @@ -66,7 +66,7 @@ module Reports::TopUploads builder.where("up.created_at < :end_date", end_date: report.end_date) if extension_filter - builder.where("up.extension = :extension", extension: extension_filter.sub(/^\./, "")) + builder.where("up.extension = :extension", extension: extension_filter.sub(/\A\./, "")) end builder.query.each do |row| diff --git a/app/models/discourse_connect.rb b/app/models/discourse_connect.rb index 850784173f..0d24cf63e7 100644 --- a/app/models/discourse_connect.rb +++ b/app/models/discourse_connect.rb @@ -328,7 +328,7 @@ class DiscourseConnect < DiscourseConnectBase def change_external_attributes_and_override(sso_record, user) @email_changed = false - if SiteSetting.auth_overrides_email && user.email != Email.downcase(email) + if SiteSetting.auth_overrides_email && email.present? && user.email != Email.downcase(email) user.email = email user.active = false if require_activation @email_changed = true diff --git a/app/models/embeddable_host.rb b/app/models/embeddable_host.rb index b9c63aff5b..285c6fd1b7 100644 --- a/app/models/embeddable_host.rb +++ b/app/models/embeddable_host.rb @@ -6,8 +6,8 @@ class EmbeddableHost < ActiveRecord::Base after_destroy :reset_embedding_settings before_validation do - self.host.sub!(%r{^https?://}, "") - self.host.sub!(%r{/.*$}, "") + self.host.sub!(%r{\Ahttps?://}, "") + self.host.sub!(%r{/.*\z}, "") end # TODO(2021-07-23): Remove @@ -45,9 +45,6 @@ class EmbeddableHost < ActiveRecord::Base def self.url_allowed?(url) return false if url.nil? - # Work around IFRAME reload on WebKit where the referer will be set to the Forum URL - return true if url&.starts_with?(Discourse.base_url) && EmbeddableHost.exists? - uri = begin URI(UrlHelper.normalized_encode(url)) diff --git a/app/models/emoji.rb b/app/models/emoji.rb index 94b85eb154..84a0f718dc 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -173,7 +173,7 @@ class Emoji emojis.each do |name, url| result << Emoji.new.tap do |e| e.name = name - url = (Discourse.base_path + url) if url[%r{^/[^/]}] + url = (Discourse.base_path + url) if url[%r{\A/[^/]}] e.url = url e.group = group || DEFAULT_GROUP end diff --git a/app/models/external_upload_stub.rb b/app/models/external_upload_stub.rb index 99ed046b97..10004f2906 100644 --- a/app/models/external_upload_stub.rb +++ b/app/models/external_upload_stub.rb @@ -43,9 +43,6 @@ class ExternalUploadStub < ActiveRecord::Base @statuses ||= Enum.new(created: 1, uploaded: 2, failed: 3) end - # TODO (martin): Lifecycle rule would be best to clean stuff up in the external - # systems, I don't think we really want to be calling out to the external systems - # here right? def self.cleanup! expired_created.delete_all expired_uploaded.delete_all diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index efd83403a0..51506e1c8c 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -5,7 +5,7 @@ class GlobalSetting define_singleton_method(key) { provider.lookup(key, default) } end - VALID_SECRET_KEY ||= /^[0-9a-f]{128}$/ + VALID_SECRET_KEY ||= /\A[0-9a-f]{128}\z/ # this is named SECRET_TOKEN as opposed to SECRET_KEY_BASE # for legacy reasons REDIS_SECRET_KEY ||= "SECRET_TOKEN" @@ -251,7 +251,7 @@ class GlobalSetting class BaseProvider def self.coerce(setting) return setting == "true" if setting == "true" || setting == "false" - return $1.to_i if setting.to_s.strip =~ /^([0-9]+)$/ + return $1.to_i if setting.to_s.strip =~ /\A([0-9]+)\z/ setting end @@ -283,7 +283,7 @@ class GlobalSetting .result() .split("\n") .each do |line| - if line =~ /^\s*([a-z_]+[a-z0-9_]*)\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|[^#]*)/ + if line =~ /\A\s*([a-z_]+[a-z0-9_]*)\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|[^#]*)/ @data[$1.strip.to_sym] = ($4 || $3 || $2).strip end end @@ -314,7 +314,7 @@ class GlobalSetting end def keys - ENV.keys.select { |k| k =~ /^DISCOURSE_/ }.map { |k| k[10..-1].downcase.to_sym } + ENV.keys.select { |k| k =~ /\ADISCOURSE_/ }.map { |k| k[10..-1].downcase.to_sym } end end diff --git a/app/models/group.rb b/app/models/group.rb index c31436815a..f6ca9fc0bc 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1005,7 +1005,7 @@ class Group < ActiveRecord::Base user = email_username_user domain = email_username_domain if user.present? && domain.present? - /^#{Regexp.escape(user)}(\+[^@]*)?@#{Regexp.escape(domain)}$/i + /\A#{Regexp.escape(user)}(\+[^@]*)?@#{Regexp.escape(domain)}\z/i end end @@ -1160,8 +1160,8 @@ class Group < ActiveRecord::Base value .split("|") .each do |domain| - domain.sub!(%r{^https?://}, "") - domain.sub!(%r{/.*$}, "") + domain.sub!(%r{\Ahttps?://}, "") + domain.sub!(%r{/.*\z}, "") if domain =~ Group::VALID_DOMAIN_REGEX valid_domains << domain diff --git a/app/models/group_request.rb b/app/models/group_request.rb index 706fcceab9..c923622aac 100644 --- a/app/models/group_request.rb +++ b/app/models/group_request.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true class GroupRequest < ActiveRecord::Base + REASON_CHARACTER_LIMIT = 280 + belongs_to :group belongs_to :user + + validates :reason, length: { maximum: REASON_CHARACTER_LIMIT } end # == Schema Information diff --git a/app/models/notification.rb b/app/models/notification.rb index f8aeab2b73..86ec8057ac 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -155,6 +155,7 @@ class Notification < ActiveRecord::Base following: 800, # Used by https://github.com/discourse/discourse-follow following_created_topic: 801, # Used by https://github.com/discourse/discourse-follow following_replied: 802, # Used by https://github.com/discourse/discourse-follow + circles_activity: 900, # Used by https://github.com/discourse/discourse-circles ) end diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index dbfa6d10c6..954686ca08 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -142,7 +142,7 @@ class OptimizedImage < ActiveRecord::Base end def local? - !(url =~ %r{^(https?:)?//}) + !(url =~ %r{\A(https?:)?//}) end def calculate_filesize @@ -337,7 +337,7 @@ class OptimizedImage < ActiveRecord::Base else error = +"Failed to optimize image:" - if e.message =~ /^convert:([^`]+)/ + if e.message =~ /\Aconvert:([^`]+)/ error << $1 else error << " unknown reason" diff --git a/app/models/post.rb b/app/models/post.rb index 7aa9155264..736295205c 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -61,6 +61,7 @@ class Post < ActiveRecord::Base belongs_to :image_upload, class_name: "Upload" has_many :post_hotlinked_media, dependent: :destroy, class_name: "PostHotlinkedMedia" + has_many :reviewables, as: :target, dependent: :destroy validates_with PostValidator, unless: :skip_validation diff --git a/app/models/post_action_type.rb b/app/models/post_action_type.rb index db785c307f..3f2f8d7e97 100644 --- a/app/models/post_action_type.rb +++ b/app/models/post_action_type.rb @@ -7,8 +7,8 @@ class PostActionType < ActiveRecord::Base include AnonCacheInvalidator def expire_cache - ApplicationSerializer.expire_cache_fragment!(/^post_action_types_/) - ApplicationSerializer.expire_cache_fragment!(/^post_action_flag_types_/) + ApplicationSerializer.expire_cache_fragment!(/\Apost_action_types_/) + ApplicationSerializer.expire_cache_fragment!(/\Apost_action_flag_types_/) end class << self diff --git a/app/models/published_page.rb b/app/models/published_page.rb index 937ba59310..5fd47bb923 100644 --- a/app/models/published_page.rb +++ b/app/models/published_page.rb @@ -8,7 +8,7 @@ class PublishedPage < ActiveRecord::Base validate :slug_format def slug_format - if slug !~ /^[a-zA-Z\-\_0-9]+$/ + if slug !~ /\A[a-zA-Z\-\_0-9]+\z/ errors.add(:slug, I18n.t("publish_page.slug_errors.invalid")) elsif %w[check-slug by-topic].include?(slug) errors.add(:slug, I18n.t("publish_page.slug_errors.unavailable")) diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index baed645654..4894a8a73c 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -15,8 +15,8 @@ class RemoteTheme < ActiveRecord::Base ALLOWED_FIELDS = %w[scss embedded_scss head_tag header after_header body_tag footer] - GITHUB_REGEXP = %r{^https?://github\.com/} - GITHUB_SSH_REGEXP = %r{^ssh://git@github\.com:} + GITHUB_REGEXP = %r{\Ahttps?://github\.com/} + GITHUB_SSH_REGEXP = %r{\Assh://git@github\.com:} has_one :theme, autosave: false scope :joined_remotes, @@ -329,7 +329,7 @@ class RemoteTheme < ActiveRecord::Base def github_diff_link if github_repo_url.present? && local_version != remote_version - "#{github_repo_url.gsub(/\.git$/, "")}/compare/#{local_version}...#{remote_version}" + "#{github_repo_url.gsub(/\.git\z/, "")}/compare/#{local_version}...#{remote_version}" end end diff --git a/app/models/report.rb b/app/models/report.rb index cd9b7623ac..1cf45d27cf 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -268,8 +268,8 @@ class Report wrap_slow_query do if respond_to?(report_method) public_send(report_method, report) - elsif type =~ /_reqs$/ - req_report(report, type.split(/_reqs$/)[0].to_sym) + elsif type =~ /_reqs\z/ + req_report(report, type.split(/_reqs\z/)[0].to_sym) else return nil end diff --git a/app/models/reviewable.rb b/app/models/reviewable.rb index 8f389cb670..02b1b47421 100644 --- a/app/models/reviewable.rb +++ b/app/models/reviewable.rb @@ -60,7 +60,7 @@ class Reviewable < ActiveRecord::Base end def self.valid_type?(type) - return false unless type =~ /^Reviewable[A-Za-z]+$/ + return false unless type =~ /\AReviewable[A-Za-z]+\z/ type.constantize <= Reviewable rescue NameError false diff --git a/app/models/screened_url.rb b/app/models/screened_url.rb index 0e827cf522..dcc19bc3e3 100644 --- a/app/models/screened_url.rb +++ b/app/models/screened_url.rb @@ -17,7 +17,7 @@ class ScreenedUrl < ActiveRecord::Base def normalize self.url = ScreenedUrl.normalize_url(self.url) if self.url - self.domain = self.domain.downcase.sub(/^www\./, "") if self.domain + self.domain = self.domain.downcase.sub(/\Awww\./, "") if self.domain end def self.watch(url, domain, opts = {}) @@ -30,8 +30,8 @@ class ScreenedUrl < ActiveRecord::Base def self.normalize_url(url) normalized = url.gsub(%r{http(s?)://}i, "") - normalized.gsub!(%r{(/)+$}, "") # trim trailing slashes - normalized.gsub!(%r{^([^/]+)(?:/)?}) { |m| m.downcase } # downcase the domain part of the url + normalized.gsub!(%r{(/)+\z}, "") # trim trailing slashes + normalized.gsub!(%r{\A([^/]+)(?:/)?}) { |m| m.downcase } # downcase the domain part of the url normalized end end diff --git a/app/models/tag.rb b/app/models/tag.rb index 64b932d02e..71435ef046 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -4,6 +4,10 @@ class Tag < ActiveRecord::Base include Searchable include HasDestroyedWebHook + self.ignored_columns = [ + "topic_count", # TODO(tgxworld): Remove on 1 July 2023 + ] + RESERVED_TAGS = [ "none", "constructor", # prevents issues with javascript's constructor of objects @@ -25,11 +29,14 @@ class Tag < ActiveRecord::Base # tags that have never been used and don't belong to a tag group scope :unused, -> { - where(topic_count: 0, pm_topic_count: 0).joins( + where(staff_topic_count: 0, pm_topic_count: 0).joins( "LEFT JOIN tag_group_memberships tgm ON tags.id = tgm.tag_id", ).where("tgm.tag_id IS NULL") } + scope :used_tags_in_regular_topics, + ->(guardian) { where("tags.#{Tag.topic_count_column(guardian)} > 0") } + scope :base_tags, -> { where(target_tag_id: nil) } has_many :tag_users, dependent: :destroy # notification settings @@ -62,7 +69,7 @@ class Tag < ActiveRecord::Base def self.update_topic_counts DB.exec <<~SQL UPDATE tags t - SET topic_count = x.topic_count + SET staff_topic_count = x.topic_count FROM ( SELECT COUNT(topics.id) AS topic_count, tags.id AS tag_id FROM tags @@ -73,7 +80,31 @@ class Tag < ActiveRecord::Base GROUP BY tags.id ) x WHERE x.tag_id = t.id - AND x.topic_count <> t.topic_count + AND x.topic_count <> t.staff_topic_count + SQL + + DB.exec <<~SQL + UPDATE tags t + SET public_topic_count = x.topic_count + FROM ( + WITH tags_with_public_topics AS ( + SELECT + COUNT(topics.id) AS topic_count, + tags.id AS tag_id + FROM tags + INNER JOIN topic_tags ON tags.id = topic_tags.tag_id + INNER JOIN topics ON topics.id = topic_tags.topic_id AND topics.deleted_at IS NULL AND topics.archetype != 'private_message' + INNER JOIN categories ON categories.id = topics.category_id AND NOT categories.read_restricted + GROUP BY tags.id + ) + SELECT + COALESCE(tags_with_public_topics.topic_count, 0 ) AS topic_count, + tags.id AS tag_id + FROM tags + LEFT JOIN tags_with_public_topics ON tags_with_public_topics.tag_id = tags.id + ) x + WHERE x.tag_id = t.id + AND x.topic_count <> t.public_topic_count; SQL DB.exec <<~SQL @@ -97,19 +128,18 @@ class Tag < ActiveRecord::Base self.find_by("lower(name) = ?", name.downcase) end - def self.top_tags(limit_arg: nil, category: nil, guardian: nil) + def self.top_tags(limit_arg: nil, category: nil, guardian: Guardian.new) # we add 1 to max_tags_in_filter_list to efficiently know we have more tags # than the limit. Frontend is responsible to enforce limit. limit = limit_arg || (SiteSetting.max_tags_in_filter_list + 1) - scope_category_ids = (guardian || Guardian.new).allowed_category_ids - + scope_category_ids = guardian.allowed_category_ids scope_category_ids &= ([category.id] + category.subcategories.pluck(:id)) if category return [] if scope_category_ids.empty? filter_sql = ( - if guardian&.is_staff? + if guardian.is_staff? "" else " AND tags.id IN (#{DiscourseTagging.visible_tags(guardian).select(:id).to_sql})" @@ -130,6 +160,14 @@ class Tag < ActiveRecord::Base tag_names_with_counts.map { |row| row.tag_name } end + def self.topic_count_column(guardian) + if guardian&.is_staff? || SiteSetting.include_secure_categories_in_tag_counts + "staff_topic_count" + else + "public_topic_count" + end + end + def self.pm_tags(limit: 1000, guardian: nil, allowed_user: nil) return [] if allowed_user.blank? || !(guardian || Guardian.new).can_tag_pms? user_id = allowed_user.id @@ -214,14 +252,15 @@ end # # Table name: tags # -# id :integer not null, primary key -# name :string not null -# topic_count :integer default(0), not null -# created_at :datetime not null -# updated_at :datetime not null -# pm_topic_count :integer default(0), not null -# target_tag_id :integer -# description :string +# id :integer not null, primary key +# name :string not null +# created_at :datetime not null +# updated_at :datetime not null +# pm_topic_count :integer default(0), not null +# target_tag_id :integer +# description :string +# public_topic_count :integer default(0), not null +# staff_topic_count :integer default(0), not null # # Indexes # diff --git a/app/models/tag_group.rb b/app/models/tag_group.rb index 54ee33bc1c..802ab651ac 100644 --- a/app/models/tag_group.rb +++ b/app/models/tag_group.rb @@ -5,6 +5,10 @@ class TagGroup < ActiveRecord::Base has_many :tag_group_memberships, dependent: :destroy has_many :tags, through: :tag_group_memberships + has_many :none_synonym_tags, + -> { where(target_tag_id: nil) }, + through: :tag_group_memberships, + source: "tag" has_many :category_tag_groups, dependent: :destroy has_many :category_required_tag_groups, dependent: :destroy has_many :categories, through: :category_tag_groups diff --git a/app/models/theme.rb b/app/models/theme.rb index 23701642e5..808a01a1aa 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -6,7 +6,7 @@ require "json_schemer" class Theme < ActiveRecord::Base include GlobalPath - BASE_COMPILER_VERSION = 69 + BASE_COMPILER_VERSION = 71 attr_accessor :child_components diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 780789c42b..a2404c7f83 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -94,7 +94,7 @@ class ThemeField < ActiveRecord::Base .css('script[type="text/x-handlebars"]') .each do |node| name = node["name"] || node["data-template-name"] || "broken" - is_raw = name =~ /\.(raw|hbr)$/ + is_raw = name =~ /\.(raw|hbr)\z/ hbs_template = node.inner_html begin @@ -523,63 +523,63 @@ class ThemeField < ActiveRecord::Base FILE_MATCHERS = [ ThemeFileMatcher.new( regex: - %r{^(?(?:mobile|desktop|common))/(?(?:head_tag|header|after_header|body_tag|footer))\.html$}, + %r{\A(?(?:mobile|desktop|common))/(?(?:head_tag|header|after_header|body_tag|footer))\.html\z}, targets: %i[mobile desktop common], names: %w[head_tag header after_header body_tag footer], types: :html, canonical: ->(h) { "#{h[:target]}/#{h[:name]}.html" }, ), ThemeFileMatcher.new( - regex: %r{^(?(?:mobile|desktop|common))/(?:\k)\.scss$}, + regex: %r{\A(?(?:mobile|desktop|common))/(?:\k)\.scss\z}, targets: %i[mobile desktop common], names: "scss", types: :scss, canonical: ->(h) { "#{h[:target]}/#{h[:target]}.scss" }, ), ThemeFileMatcher.new( - regex: %r{^common/embedded\.scss$}, + regex: %r{\Acommon/embedded\.scss\z}, targets: :common, names: "embedded_scss", types: :scss, canonical: ->(h) { "common/embedded.scss" }, ), ThemeFileMatcher.new( - regex: %r{^common/color_definitions\.scss$}, + regex: %r{\Acommon/color_definitions\.scss\z}, targets: :common, names: "color_definitions", types: :scss, canonical: ->(h) { "common/color_definitions.scss" }, ), ThemeFileMatcher.new( - regex: %r{^(?:scss|stylesheets)/(?.+)\.scss$}, + regex: %r{\A(?:scss|stylesheets)/(?.+)\.scss\z}, targets: :extra_scss, names: nil, types: :scss, canonical: ->(h) { "stylesheets/#{h[:name]}.scss" }, ), ThemeFileMatcher.new( - regex: %r{^javascripts/(?.+)$}, + regex: %r{\Ajavascripts/(?.+)\z}, targets: :extra_js, names: nil, types: :js, canonical: ->(h) { "javascripts/#{h[:name]}" }, ), ThemeFileMatcher.new( - regex: %r{^test/(?.+)$}, + regex: %r{\Atest/(?.+)\z}, targets: :tests_js, names: nil, types: :js, canonical: ->(h) { "test/#{h[:name]}" }, ), ThemeFileMatcher.new( - regex: /^settings\.ya?ml$/, + regex: /\Asettings\.ya?ml\z/, names: "yaml", types: :yaml, targets: :settings, canonical: ->(h) { "settings.yml" }, ), ThemeFileMatcher.new( - regex: %r{^locales/(?(?:#{I18n.available_locales.join("|")}))\.yml$}, + regex: %r{\Alocales/(?(?:#{I18n.available_locales.join("|")}))\.yml\z}, names: I18n.available_locales.map(&:to_s), types: :yaml, targets: :translations, diff --git a/app/models/top_topic.rb b/app/models/top_topic.rb index 975adc8ec7..31fbf4efd5 100644 --- a/app/models/top_topic.rb +++ b/app/models/top_topic.rb @@ -5,21 +5,25 @@ class TopTopic < ActiveRecord::Base # The top topics we want to refresh often def self.refresh_daily! - transaction do - remove_invisible_topics - add_new_visible_topics + DistributedMutex.synchronize("update_top_topics", validity: 5.minutes) do + transaction do + remove_invisible_topics + add_new_visible_topics - update_counts_and_compute_scores_for(:daily) + update_counts_and_compute_scores_for(:daily) + end end end # We don't have to refresh these as often def self.refresh_older! - older_periods = periods - %i[daily all] + DistributedMutex.synchronize("update_top_topics", validity: 5.minutes) do + older_periods = periods - %i[daily all] - transaction { older_periods.each { |period| update_counts_and_compute_scores_for(period) } } + transaction { older_periods.each { |period| update_counts_and_compute_scores_for(period) } } - compute_top_score_for(:all) + compute_top_score_for(:all) + end end def self.refresh! diff --git a/app/models/topic.rb b/app/models/topic.rb index b15bbc519f..2fe7f11446 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -958,6 +958,7 @@ class Topic < ActiveRecord::Base def changed_to_category(new_category) return true if new_category.blank? || Category.exists?(topic_id: id) + if new_category.id == SiteSetting.uncategorized_category_id && !SiteSetting.allow_uncategorized_topics return false @@ -971,6 +972,15 @@ class Topic < ActiveRecord::Base if old_category Category.where(id: old_category.id).update_all("topic_count = topic_count - 1") + + count = + if old_category.read_restricted && !new_category.read_restricted + 1 + elsif !old_category.read_restricted && new_category.read_restricted + -1 + end + + Tag.update_counters(self.tags, { public_topic_count: count }) if count end # when a topic changes category we may have to start watching it @@ -1190,7 +1200,7 @@ class Topic < ActiveRecord::Base else !!invite_to_topic(invited_by, target_user, group_ids, guardian) end - elsif username_or_email =~ /^.+@.+$/ && guardian.can_invite_via_email?(self) + elsif username_or_email =~ /\A.+@.+\z/ && guardian.can_invite_via_email?(self) !!Invite.generate( invited_by, email: username_or_email, @@ -1781,12 +1791,15 @@ class Topic < ActiveRecord::Base def convert_to_public_topic(user, category_id: nil) public_topic = TopicConverter.new(self, user).convert_to_public_topic(category_id) + Tag.update_counters(public_topic.tags, { public_topic_count: 1 }) if !category.read_restricted add_small_action(user, "public_topic") if public_topic public_topic end def convert_to_private_message(user) + read_restricted = category.read_restricted private_topic = TopicConverter.new(self, user).convert_to_private_message + Tag.update_counters(private_topic.tags, { public_topic_count: -1 }) if !read_restricted add_small_action(user, "private_topic") if private_topic private_topic end @@ -1879,46 +1892,6 @@ class Topic < ActiveRecord::Base .first end - def incoming_email_addresses(group: nil, received_before: Time.zone.now) - email_addresses = Set.new - - # TODO(martin) Look at improving this N1, it will just get slower the - # more replies/incoming emails there are for the topic. - self - .incoming_email - .where("created_at <= ?", received_before) - .each do |incoming_email| - to_addresses = incoming_email.to_addresses_split - cc_addresses = incoming_email.cc_addresses_split - combined_addresses = [to_addresses, cc_addresses].flatten - - # We only care about the emails addressed to the group or CC'd to the - # group if the group is present. If combined addresses is empty we do - # not need to do this check, and instead can proceed on to adding the - # from address. - # - # Will not include test1@gmail.com if the only IncomingEmail - # is: - # - # from: test1@gmail.com - # to: test+support@discoursemail.com - # - # Because we don't care about the from addresses and also the to address - # is not the email_username, which will be something like test1@gmail.com. - if group.present? && combined_addresses.any? - next if combined_addresses.none? { |address| address =~ group.email_username_regex } - end - - email_addresses.add(incoming_email.from_address) - email_addresses.merge(combined_addresses) - end - - email_addresses.subtract([nil, ""]) - email_addresses.delete(group.email_username) if group.present? - - email_addresses.to_a - end - def create_invite_notification!(target_user, notification_type, invited_by, post_number: 1) if UserCommScreener.new( acting_user: invited_by, diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb index 3b9545bf81..8f9bb37c22 100644 --- a/app/models/topic_embed.rb +++ b/app/models/topic_embed.rb @@ -25,7 +25,7 @@ class TopicEmbed < ActiveRecord::Base end def self.normalize_url(url) - url.downcase.sub(%r{/$}, "").sub(/\-+/, "-").strip + url.downcase.sub(%r{/\z}, "").sub(/\-+/, "-").strip end def self.imported_from_html(url) @@ -36,7 +36,7 @@ class TopicEmbed < ActiveRecord::Base # Import an article from a source (RSS/Atom/Other) def self.import(user, url, title, contents, category_id: nil, cook_method: nil, tags: nil) - return unless url =~ %r{^https?\://} + return unless url =~ %r{\Ahttps?\://} contents = first_paragraph_from(contents) if SiteSetting.embed_truncate && cook_method.nil? contents ||= "" @@ -253,7 +253,7 @@ class TopicEmbed < ActiveRecord::Base end def self.topic_id_for_embed(embed_url) - embed_url = normalize_url(embed_url).sub(%r{^https?\://}, "") + embed_url = normalize_url(embed_url).sub(%r{\Ahttps?\://}, "") TopicEmbed.where("embed_url ~* ?", "^https?://#{Regexp.escape(embed_url)}$").pluck_first( :topic_id, ) diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb index 03bc3587b4..05d35f55ae 100644 --- a/app/models/topic_link.rb +++ b/app/models/topic_link.rb @@ -175,7 +175,7 @@ class TopicLink < ActiveRecord::Base lookup = {} results.each do |tl| - normalized = tl.url.downcase.sub(%r{^https?://}, "").sub(%r{/$}, "") + normalized = tl.url.downcase.sub(%r{\Ahttps?://}, "").sub(%r{/\z}, "") lookup[normalized] = { domain: tl.domain, username: tl.user.username_lower, diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb index a1f121ee84..751493dbd5 100644 --- a/app/models/topic_link_click.rb +++ b/app/models/topic_link_click.rb @@ -21,9 +21,9 @@ class TopicLinkClick < ActiveRecord::Base uri = UrlHelper.relaxed_parse(url) urls = Set.new urls << url - if url =~ /^http/ - urls << url.sub(/^https/, "http") - urls << url.sub(/^http:/, "https:") + if url =~ /\Ahttp/ + urls << url.sub(/\Ahttps/, "http") + urls << url.sub(/\Ahttp:/, "https:") urls << UrlHelper.schemaless(url) end urls << UrlHelper.absolute_without_cdn(url) @@ -90,7 +90,7 @@ class TopicLinkClick < ActiveRecord::Base # If no link is found... unless link.present? # ... return the url for relative links or when using the same host - return url if url =~ %r{^/[^/]} || uri.try(:host) == Discourse.current_hostname + return url if url =~ %r{\A/[^/]} || uri.try(:host) == Discourse.current_hostname # If we have it somewhere else on the site, just allow the redirect. # This is likely due to a onebox of another topic. diff --git a/app/models/topic_tag.rb b/app/models/topic_tag.rb index 67a8218d94..b2ed69791a 100644 --- a/app/models/topic_tag.rb +++ b/app/models/topic_tag.rb @@ -9,14 +9,18 @@ class TopicTag < ActiveRecord::Base if topic.archetype == Archetype.private_message tag.increment!(:pm_topic_count) else - tag.increment!(:topic_count) + counters_to_update = { staff_topic_count: 1 } - if topic.category_id - if stat = CategoryTagStat.find_by(tag_id: tag_id, category_id: topic.category_id) - stat.increment!(:topic_count) - else - CategoryTagStat.create(tag_id: tag_id, category_id: topic.category_id, topic_count: 1) - end + if Category.exists?(id: topic.category_id, read_restricted: false) + counters_to_update[:public_topic_count] = 1 + end + + Tag.update_counters(tag.id, counters_to_update) + + if stat = CategoryTagStat.find_by(tag_id: tag_id, category_id: topic.category_id) + stat.increment!(:topic_count) + else + CategoryTagStat.create!(tag_id: tag_id, category_id: topic.category_id, topic_count: 1) end end end @@ -27,12 +31,17 @@ class TopicTag < ActiveRecord::Base if topic.archetype == Archetype.private_message tag.decrement!(:pm_topic_count) else - if topic.category_id && - stat = CategoryTagStat.find_by(tag_id: tag_id, category: topic.category_id) - stat.topic_count == 1 ? stat.destroy : stat.decrement!(:topic_count) + if stat = CategoryTagStat.find_by(tag_id: tag_id, category: topic.category_id) + stat.topic_count == 1 ? stat.destroy! : stat.decrement!(:topic_count) end - tag.decrement!(:topic_count) + counters_to_update = { staff_topic_count: -1 } + + if Category.exists?(id: topic.category_id, read_restricted: false) + counters_to_update[:public_topic_count] = -1 + end + + Tag.update_counters(tag.id, counters_to_update) end end end diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index 10fa727450..490bf9a514 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -95,6 +95,8 @@ class TopicTrackingState end def self.publish_muted(topic) + return unless topic.regular? + user_ids = topic .topic_users @@ -110,6 +112,8 @@ class TopicTrackingState end def self.publish_unmuted(topic) + return unless topic.regular? + user_ids = User .watching_topic(topic) @@ -172,6 +176,8 @@ class TopicTrackingState end def self.publish_recover(topic) + return unless topic.regular? + group_ids = secure_category_group_ids(topic) message = { topic_id: topic.id, message_type: RECOVER_MESSAGE_TYPE } @@ -180,6 +186,8 @@ class TopicTrackingState end def self.publish_delete(topic) + return unless topic.regular? + group_ids = secure_category_group_ids(topic) message = { topic_id: topic.id, message_type: DELETE_MESSAGE_TYPE } @@ -188,6 +196,8 @@ class TopicTrackingState end def self.publish_destroy(topic) + return unless topic.regular? + group_ids = secure_category_group_ids(topic) message = { topic_id: topic.id, message_type: DESTROY_MESSAGE_TYPE } @@ -549,12 +559,14 @@ class TopicTrackingState end def self.secure_category_group_ids(topic) - ids = topic&.category&.secure_group_ids + category = topic.category - if ids.blank? - [Group::AUTO_GROUPS[:admin]] + if category.read_restricted + ids = [Group::AUTO_GROUPS[:admins]] + ids.push(*category.secure_group_ids) + ids.uniq else - ids + nil end end private_class_method :secure_category_group_ids diff --git a/app/models/translation_override.rb b/app/models/translation_override.rb index 73137243ed..eddaac8983 100644 --- a/app/models/translation_override.rb +++ b/app/models/translation_override.rb @@ -147,7 +147,7 @@ class TranslationOverride < ActiveRecord::Base end def transform_pluralized_key(key) - match = key.match(/(.*)\.(zero|two|few|many)$/) + match = key.match(/(.*)\.(zero|two|few|many)\z/) match ? match.to_a.second + ".other" : key end end diff --git a/app/models/upload.rb b/app/models/upload.rb index 964ef52d1d..249c50f4be 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -100,6 +100,10 @@ class Upload < ActiveRecord::Base self.url end + def to_markdown + UploadMarkdown.new(self).to_markdown + end + def thumbnail(width = self.thumbnail_width, height = self.thumbnail_height) optimized_images.find_by(width: width, height: height) end @@ -263,7 +267,7 @@ class Upload < ActiveRecord::Base end def local? - !(url =~ %r{^(https?:)?//}) + !(url =~ %r{\A(https?:)?//}) end def fix_dimensions! @@ -526,7 +530,7 @@ class Upload < ActiveRecord::Base # keep track of the url previous_url = upload.url.dup # where is the file currently stored? - external = previous_url =~ %r{^//} + external = previous_url =~ %r{\A//} # download if external if external url = SiteSetting.scheme + ":" + previous_url diff --git a/app/models/upload_reference.rb b/app/models/upload_reference.rb index 73de830f54..ea5401e123 100644 --- a/app/models/upload_reference.rb +++ b/app/models/upload_reference.rb @@ -4,6 +4,8 @@ class UploadReference < ActiveRecord::Base belongs_to :upload belongs_to :target, polymorphic: true + delegate :to_markdown, to: :upload + def self.ensure_exist!(upload_ids: [], target: nil, target_type: nil, target_id: nil) if !target && !(target_type && target_id) raise "target OR target_type and target_id are required" diff --git a/app/models/user.rb b/app/models/user.rb index d0d9846a43..5bc5304b2f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -396,7 +396,7 @@ class User < ActiveRecord::Base .reserved_usernames .unicode_normalize .split("|") - .any? { |reserved| username.match?(/^#{Regexp.escape(reserved).gsub('\*', ".*")}$/) } + .any? { |reserved| username.match?(/\A#{Regexp.escape(reserved).gsub('\*', ".*")}\z/) } end def self.editable_user_custom_fields(by_staff: false) @@ -1117,7 +1117,7 @@ class User < ActiveRecord::Base # TODO it may be worth caching this in a distributed cache, should be benched if SiteSetting.external_system_avatars_enabled url = SiteSetting.external_system_avatars_url.dup - url = +"#{Discourse.base_path}#{url}" unless url =~ %r{^https?://} + url = +"#{Discourse.base_path}#{url}" unless url =~ %r{\Ahttps?://} url.gsub! "{color}", letter_avatar_color(normalized_username) url.gsub! "{username}", UrlHelper.encode_component(username) url.gsub! "{first_letter}", @@ -1805,7 +1805,7 @@ class User < ActiveRecord::Base end def redesigned_user_menu_enabled? - !SiteSetting.legacy_navigation_menu? + !SiteSetting.legacy_navigation_menu? || SiteSetting.enable_new_notifications_menu end protected diff --git a/app/models/user_history.rb b/app/models/user_history.rb index e3a98fe9b2..53cae31249 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -119,6 +119,7 @@ class UserHistory < ActiveRecord::Base watched_word_create: 97, watched_word_destroy: 98, delete_group: 99, + permanently_delete_post_revisions: 100, ) end @@ -213,6 +214,7 @@ class UserHistory < ActiveRecord::Base watched_word_create watched_word_destroy delete_group + permanently_delete_post_revisions ] end diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index 2ed6d1b027..edd8ade93b 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -45,7 +45,7 @@ class UserProfile < ActiveRecord::Base def bio_excerpt(length = 350, opts = {}) return nil if bio_cooked.blank? - excerpt = PrettyText.excerpt(bio_cooked, length, opts).sub(/
    $/, "") + excerpt = PrettyText.excerpt(bio_cooked, length, opts).sub(/
    \z/, "") return excerpt if excerpt.blank? || (user.has_trust_level?(TrustLevel[1]) && !user.suspended?) PrettyText.strip_links(excerpt) end diff --git a/app/models/username_validator.rb b/app/models/username_validator.rb index 8c82653b20..842b676356 100644 --- a/app/models/username_validator.rb +++ b/app/models/username_validator.rb @@ -40,13 +40,13 @@ class UsernameValidator errors.empty? end - CONFUSING_EXTENSIONS ||= /\.(js|json|css|htm|html|xml|jpg|jpeg|png|gif|bmp|ico|tif|tiff|woff)$/i + CONFUSING_EXTENSIONS ||= /\.(js|json|css|htm|html|xml|jpg|jpeg|png|gif|bmp|ico|tif|tiff|woff)\z/i MAX_CHARS ||= 60 ASCII_INVALID_CHAR_PATTERN ||= /[^\w.-]/ UNICODE_INVALID_CHAR_PATTERN ||= /[^\p{Alnum}\p{M}._-]/ - INVALID_LEADING_CHAR_PATTERN ||= /^[^\p{Alnum}\p{M}_]+/ - INVALID_TRAILING_CHAR_PATTERN ||= /[^\p{Alnum}\p{M}]+$/ + INVALID_LEADING_CHAR_PATTERN ||= /\A[^\p{Alnum}\p{M}_]+/ + INVALID_TRAILING_CHAR_PATTERN ||= /[^\p{Alnum}\p{M}]+\z/ REPEATED_SPECIAL_CHAR_PATTERN ||= /[-_.]{2,}/ private diff --git a/app/models/watched_word.rb b/app/models/watched_word.rb index e493f50e41..3e9b4d5b57 100644 --- a/app/models/watched_word.rb +++ b/app/models/watched_word.rb @@ -19,7 +19,7 @@ class WatchedWord < ActiveRecord::Base before_validation do self.word = self.class.normalize_word(self.word) - if self.action == WatchedWord.actions[:link] && !(self.replacement =~ %r{^https?://}) + if self.action == WatchedWord.actions[:link] && !(self.replacement =~ %r{\Ahttps?://}) self.replacement = "#{Discourse.base_url}#{self.replacement&.starts_with?("/") ? "" : "/"}#{self.replacement}" end diff --git a/app/serializers/basic_group_serializer.rb b/app/serializers/basic_group_serializer.rb index ab64df800a..f5d4c80d36 100644 --- a/app/serializers/basic_group_serializer.rb +++ b/app/serializers/basic_group_serializer.rb @@ -31,6 +31,7 @@ class BasicGroupSerializer < ApplicationSerializer :members_visibility_level, :can_see_members, :can_admin_group, + :can_edit_group, :publish_read_state def include_display_name? @@ -73,6 +74,14 @@ class BasicGroupSerializer < ApplicationSerializer owner_group_ids.present? end + def can_edit_group + scope.can_edit_group?(object) + end + + def include_can_edit_group? + scope.can_edit_group?(object) + end + def can_admin_group scope.can_admin_group?(object) end diff --git a/app/serializers/concerns/topic_tags_mixin.rb b/app/serializers/concerns/topic_tags_mixin.rb index 2704ba3361..0c001d4633 100644 --- a/app/serializers/concerns/topic_tags_mixin.rb +++ b/app/serializers/concerns/topic_tags_mixin.rb @@ -32,7 +32,8 @@ module TopicTagsMixin if SiteSetting.tags_sort_alphabetically topic.tags.sort_by(&:name) else - topic.tags.sort_by(&:topic_count).reverse + topic_count_column = Tag.topic_count_column(scope) + topic.tags.sort_by { |tag| tag.public_send(topic_count_column) }.reverse end ) tags = tags.reject { |tag| scope.hidden_tag_names.include?(tag[:name]) } if !scope.is_staff? diff --git a/app/serializers/concerns/user_sidebar_mixin.rb b/app/serializers/concerns/user_sidebar_mixin.rb index cd87219585..5f8c5add0a 100644 --- a/app/serializers/concerns/user_sidebar_mixin.rb +++ b/app/serializers/concerns/user_sidebar_mixin.rb @@ -2,9 +2,11 @@ module UserSidebarMixin def sidebar_tags + topic_count_column = Tag.topic_count_column(scope) + object .visible_sidebar_tags(scope) - .pluck(:name, :topic_count, :pm_topic_count) + .pluck(:name, topic_count_column, :pm_topic_count) .reduce([]) do |tags, sidebar_tag| tags.push(name: sidebar_tag[0], pm_only: sidebar_tag[1] == 0 && sidebar_tag[2] > 0) end @@ -45,6 +47,6 @@ module UserSidebarMixin private def sidebar_navigation_menu? - !SiteSetting.legacy_navigation_menu? + !SiteSetting.legacy_navigation_menu? || options[:enable_sidebar_param] == "1" end end diff --git a/app/serializers/detailed_tag_serializer.rb b/app/serializers/detailed_tag_serializer.rb index 83570c3d5e..7fd7a3943b 100644 --- a/app/serializers/detailed_tag_serializer.rb +++ b/app/serializers/detailed_tag_serializer.rb @@ -6,7 +6,7 @@ class DetailedTagSerializer < TagSerializer has_many :categories, serializer: BasicCategorySerializer def synonyms - TagsController.tag_counts_json(object.synonyms) + TagsController.tag_counts_json(object.synonyms, scope) end def categories diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 78e559c031..0b35974241 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -561,15 +561,20 @@ class PostSerializer < BasicPostSerializer end def mentioned_users - if @topic_view && (mentions = @topic_view.mentions[object.id]) - users = mentions.map { |username| @topic_view.mentioned_users[username] }.compact - else - users = User.where(username: object.mentions) - end + users = + if @topic_view && (mentioned_users = @topic_view.mentioned_users[object.id]) + mentioned_users + else + User.where(username: object.mentions) + end users.map { |user| BasicUserWithStatusSerializer.new(user, root: false) } end + def include_mentioned_users? + SiteSetting.enable_user_status + end + private def can_review_topic? diff --git a/app/serializers/tag_serializer.rb b/app/serializers/tag_serializer.rb index 6fa46a80bd..81e5515696 100644 --- a/app/serializers/tag_serializer.rb +++ b/app/serializers/tag_serializer.rb @@ -3,6 +3,10 @@ class TagSerializer < ApplicationSerializer attributes :id, :name, :topic_count, :staff, :description + def topic_count + object.public_send(Tag.topic_count_column(scope)) + end + def staff DiscourseTagging.staff_tag_names.include?(name) end diff --git a/app/serializers/user_card_serializer.rb b/app/serializers/user_card_serializer.rb index d2c32facdc..301082cab6 100644 --- a/app/serializers/user_card_serializer.rb +++ b/app/serializers/user_card_serializer.rb @@ -113,7 +113,7 @@ class UserCardSerializer < BasicUserSerializer end return if uri.nil? || uri.host.nil? - uri.host.sub(/^www\./, "") + uri.path + uri.host.sub(/\Awww\./, "") + uri.path end def ignored diff --git a/app/services/external_upload_manager.rb b/app/services/external_upload_manager.rb index 23523ab4bc..58823158ff 100644 --- a/app/services/external_upload_manager.rb +++ b/app/services/external_upload_manager.rb @@ -149,8 +149,6 @@ class ExternalUploadManager raise ChecksumMismatchError if external_sha1 && external_sha1 != actual_sha1 end - # TODO (martin): See if these additional opts will be needed - # - check if retain_hours is needed opts = { type: external_upload_stub.upload_type, existing_external_upload_key: external_upload_stub.key, diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 8b8de6aa82..b792a6e701 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -23,7 +23,14 @@ class PostAlerter topic_title: post.topic.title, topic_id: post.topic.id, excerpt: - excerpt || post.excerpt(400, text_entities: true, strip_links: true, remap_emoji: true), + excerpt || + post.excerpt( + 400, + text_entities: true, + strip_links: true, + remap_emoji: true, + plain_hashtags: true, + ), username: username || post.username, post_url: post_url, } diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index a17ac7b34b..ea1c1b8e89 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -50,7 +50,7 @@ class SearchIndexer .reduce(additional_lexemes) do |array, (lexeme, _, positions)| count = 0 - if lexeme !~ /^(\d+\.)?(\d+\.)*(\*|\d+)$/ + if lexeme !~ /\A(\d+\.)?(\d+\.)*(\*|\d+)\z/ loop do count += 1 break if count >= 10 # Safeguard here to prevent infinite loop when a term has many dots diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 70c937e561..9de363d855 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -954,6 +954,16 @@ class StaffActionLogger ) end + def log_permanently_delete_post_revisions(post) + raise Discourse::InvalidParameters.new(:post) if post.nil? + + UserHistory.create!( + action: UserHistory.actions[:permanently_delete_post_revisions], + acting_user_id: @admin.id, + post_id: post.id, + ) + end + private def get_changes(changes) diff --git a/app/services/tag_hashtag_data_source.rb b/app/services/tag_hashtag_data_source.rb index 11b089c84f..b186ca12c4 100644 --- a/app/services/tag_hashtag_data_source.rb +++ b/app/services/tag_hashtag_data_source.rb @@ -12,26 +12,30 @@ class TagHashtagDataSource "tag" end - def self.tag_to_hashtag_item(tag) - tag = Tag.new(tag.slice(:id, :name, :description).merge(topic_count: tag[:count])) if tag.is_a?( - Hash, - ) + def self.tag_to_hashtag_item(tag, guardian) + topic_count_column = Tag.topic_count_column(guardian) + + tag = + Tag.new( + tag.slice(:id, :name, :description).merge(topic_count_column => tag[:count]), + ) if tag.is_a?(Hash) HashtagAutocompleteService::HashtagItem.new.tap do |item| item.text = tag.name - item.secondary_text = "x#{tag.topic_count}" + item.secondary_text = "x#{tag.public_send(topic_count_column)}" item.description = tag.description item.slug = tag.name item.relative_url = tag.url item.icon = icon end end + private_class_method :tag_to_hashtag_item def self.lookup(guardian, slugs) return [] if !SiteSetting.tagging_enabled DiscourseTagging .filter_visible(Tag.where_name(slugs), guardian) - .map { |tag| tag_to_hashtag_item(tag) } + .map { |tag| tag_to_hashtag_item(tag, guardian) } end def self.search( @@ -60,9 +64,9 @@ class TagHashtagDataSource ) TagsController - .tag_counts_json(tags_with_counts) + .tag_counts_json(tags_with_counts, guardian) .take(limit) - .map { |tag| tag_to_hashtag_item(tag) } + .map { |tag| tag_to_hashtag_item(tag, guardian) } end def self.search_sort(search_results, _) @@ -82,8 +86,8 @@ class TagHashtagDataSource ) TagsController - .tag_counts_json(tags_with_counts) + .tag_counts_json(tags_with_counts, guardian) .take(limit) - .map { |tag| tag_to_hashtag_item(tag) } + .map { |tag| tag_to_hashtag_item(tag, guardian) } end end diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 511755628f..6a569b6a53 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -347,6 +347,6 @@ class UserUpdater def format_url(website) return nil if website.blank? - website =~ /^http/ ? website : "http://#{website}" + website =~ /\Ahttp/ ? website : "http://#{website}" end end diff --git a/app/views/topics/show.html.erb b/app/views/topics/show.html.erb index d949ebb2b0..65c12933c9 100644 --- a/app/views/topics/show.html.erb +++ b/app/views/topics/show.html.erb @@ -132,7 +132,7 @@ <% content_for :head do %> <%= auto_discovery_link_tag(@topic_view, {action: :feed, slug: @topic_view.topic.slug, topic_id: @topic_view.topic.id}, rel: 'alternate nofollow', title: t('rss_posts_in_topic', topic: @topic_view.title), type: 'application/rss+xml') %> - <%= raw crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary(strip_images: true), image: @topic_view.image_url, read_time: @topic_view.read_time, like_count: @topic_view.like_count, ignore_canonical: true, published_time: @topic_view.published_time) %> + <%= raw crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary(strip_images: true), image: @topic_view.image_url, read_time: @topic_view.read_time, like_count: @topic_view.like_count, ignore_canonical: true, published_time: @topic_view.published_time, breadcrumbs: @breadcrumbs, tags: @topic_view.tags) %> <% if @topic_view.prev_page || @topic_view.next_page %> <% if @topic_view.prev_page %> diff --git a/config/application.rb b/config/application.rb index 0c31d3a327..466fe3fcc9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.7.0") - STDERR.puts "Discourse requires Ruby 2.7 or above" +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.1.0") + STDERR.puts "Discourse requires Ruby 3.1 or above" exit 1 end diff --git a/config/discourse_defaults.conf b/config/discourse_defaults.conf index 74b202d064..b5cc812af9 100644 --- a/config/discourse_defaults.conf +++ b/config/discourse_defaults.conf @@ -365,3 +365,6 @@ preload_link_header = false # When using an external upload store, redirect `user_avatar` requests instead of proxying redirect_avatar_requests = false + +# Force the entire cluster into postgres readonly mode. Equivalent to running `Discourse.enable_pg_force_readonly_mode` +pg_force_readonly_mode = false diff --git a/config/initializers/002-rails_failover.rb b/config/initializers/002-rails_failover.rb index 276e6d951b..d5ee5c2da2 100644 --- a/config/initializers/002-rails_failover.rb +++ b/config/initializers/002-rails_failover.rb @@ -69,7 +69,11 @@ if defined?(RailsFailover::ActiveRecord) end RailsFailover::ActiveRecord.register_force_reading_role_callback do - Discourse.redis.exists?(Discourse::PG_READONLY_MODE_KEY, Discourse::PG_FORCE_READONLY_MODE_KEY) + GlobalSetting.pg_force_readonly_mode || + Discourse.redis.exists?( + Discourse::PG_READONLY_MODE_KEY, + Discourse::PG_FORCE_READONLY_MODE_KEY, + ) rescue => e if !e.is_a?(Redis::CannotConnectError) Rails.logger.warn "#{e.class} #{e.message}: #{e.backtrace.join("\n")}" diff --git a/config/initializers/004-message_bus.rb b/config/initializers/004-message_bus.rb index 2fa74be763..383d8a0113 100644 --- a/config/initializers/004-message_bus.rb +++ b/config/initializers/004-message_bus.rb @@ -62,6 +62,11 @@ def setup_message_bus_env(env) extra_headers["Discourse-Logged-Out"] = "1" if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] + if Rails.env.development? + # Adding no-transform prevents the expressjs ember-cli proxy buffering/compressing the response + extra_headers["Cache-Control"] = "no-transform, must-revalidate, private, max-age=0" + end + hash = { extra_headers: extra_headers, user_id: user_id, diff --git a/config/initializers/100-logster.rb b/config/initializers/100-logster.rb index 8bbbc2a7a1..5c227b9983 100644 --- a/config/initializers/100-logster.rb +++ b/config/initializers/100-logster.rb @@ -7,15 +7,6 @@ if GlobalSetting.skip_redis? return end -if Rails.env.development? && RUBY_VERSION.match?(/^2\.5\.[23]/) - STDERR.puts "WARNING: Discourse development environment runs slower on Ruby 2.5.3 or below" - STDERR.puts "We recommend you upgrade to the latest Ruby 2.x for the optimal development performance" - - # we have to used to older and slower version of the logger cause the new one exposes a Ruby bug in - # the Queue class which causes segmentation faults - Logster::Scheduler.disable -end - if Rails.env.development? && !Sidekiq.server? && ENV["RAILS_LOGS_STDOUT"] == "1" Rails.application.config.after_initialize do console = ActiveSupport::Logger.new(STDOUT) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index a6f421687c..10075d2c70 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -288,6 +288,7 @@ ar: banner: enabled: "حوَّل هذا الموضوع إلى بانر في %{when}. سيظهر أعلى كل صفحة حتى يُزيله المستخدم." disabled: "أزال هذا البانر في %{when}. ولن يظهر بعد الآن في أعلى كل صفحة." + forwarded: "أعاد توجيه رسالة البريد الإلكتروني أعلاه" topic_admin_menu: "إجراءات الموضوع" skip_to_main_content: "تخطي إلى المحتوى الرئيسي" emails_are_disabled: "أوقف أحد المسؤولين البريد الصادر بشكلٍ عام. ولن يتم إرسال إشعارات عبر البريد الإلكتروني أيًا كان نوعها." @@ -441,13 +442,13 @@ ar: edit: "تعديل الإشارة المرجعية" not_bookmarked: "وضع إشارة مرجعية على هذا المنشور" remove_reminder_keep_bookmark: "إزالة التذكير والاحتفاظ بالإشارة المرجعية" - created_with_reminder: "لقد وضعت إشارة مرجعية على هذا المنشور وضبطت تذكيرًا في %{date}. ‏%{name}" - created_with_reminder_generic: "لقد وضعت إشارة مرجعية على هذا المنشور وضبطت تذكيرًا في %{date}. ‏%{name}" + created_with_reminder: "لقد وضعت إشارة مرجعية على هذا المنشور وعيَّنت تذكيرًا في %{date}. ‏%{name}" + created_with_reminder_generic: "لقد وضعت إشارة مرجعية على هذا المنشور وعيَّنت تذكيرًا في %{date}. ‏%{name}" delete: "حذف الإشارة المرجعية" confirm_delete: "هل تريد بالتأكيد حذف هذه الإشارة المرجعية؟ سيتم حذف التذكير أيضًا." confirm_clear: "هل تريد بالتأكيد مسح كل إشاراتك المرجعية من هذا الموضوع؟" save: "حفظ" - no_timezone: 'لم تحدد منطقتك الزمنية بعد. ولن تتمكن من ضبط التذكيرات. حدِّد منطقة زمنية في ملفك الشخصي.' + no_timezone: 'لم تحدد منطقتك الزمنية بعد. ولن تتمكن من تعيين التذكيرات. حدِّد منطقة زمنية في ملفك الشخصي.' invalid_custom_datetime: "التاريخ والوقت الذين أدخلتهما غير صالحين، يُرجى إعادة المحاولة." list_permission_denied: "ليس لديك إذن بعرض الإشارات المرجعية لهذا المستخدم." no_user_bookmarks: "ليس لديك منشورات موضوع عليها إشارة مرجعية. تتيح لك الإشارات المرجعية الرجوع إلى المنشورات التي تريدها بسرعة." @@ -463,7 +464,7 @@ ar: today_with_time: "اليوم في الساعة %{time}" tomorrow_with_time: "غدًا في الساعة %{time}" at_time: "بتاريخ %{date_time}" - existing_reminder: "لقد ضبطت تذكيرًا لهذه الإشارة المرجعية، وسيتم إرساله إليك في %{at_date_time}" + existing_reminder: "لقد عيَّنت تذكيرًا لهذه الإشارة المرجعية، وسيتم إرساله إليك في %{at_date_time}" copy_codeblock: copied: "تم النسخ!" copy: "نسخ الرمز إلى الحافظة" @@ -853,7 +854,7 @@ ar: edit_columns: title: "تعديل أعمدة الدليل" save: "حفظ" - reset_to_default: "إعادة الضبط على الافتراضي" + reset_to_default: "إعادة التعيين إلى الإعداد الافتراضي" group: all: "كل المجموعات" sort: @@ -916,8 +917,8 @@ ar: imap_instructions: 'عند تمكين IMAP للمجموعة، تتم مزامنة الرسائل الإلكترونية بين صندوق الوارد للمجموعة وخادم IMAP وصندوق البريد المقدَّمين. يجب تفعيل SMTP باستخدام بيانات اعتماد صالحة ومُختبَرة قبل تفعيل IMAP. سيتم استخدام اسم مستخدم البريد الإلكتروني وكلمة المرور المستخدمين لخادم SMTP في خادم IMAP. لمزيد من المعلومات، راجع إعلان الميزة في Discourse Meta.' imap_alpha_warning: "تحذير: هذه الميزة في مرحلة الإصدار الأولي. ويتم دعم Gmail فقط بشكلٍ رسمي. استخدمها على مسؤوليتك الخاصة!" imap_settings_valid: "إعدادات IMAP صالحة." - smtp_disable_confirm: "إذا أوقفت SMTP، فستتم إعادة ضبط جميع إعدادات SMTP وIMAP وإيقاف الوظائف المرتبطة. هل تريد بالتأكيد الاستمرار؟" - imap_disable_confirm: "إذا أوقفت IMAP، فستتم إعادة ضبط جميع إعدادات IMAP وإيقاف الوظائف المرتبطة. هل تريد بالتأكيد الاستمرار؟" + smtp_disable_confirm: "إذا أوقفت SMTP، فستتم إعادة تعيين جميع إعدادات SMTP وIMAP وإيقاف الوظائف المرتبطة. هل تريد بالتأكيد الاستمرار؟" + imap_disable_confirm: "إذا أوقفت IMAP، فستتم إعادة تعيين جميع إعدادات IMAP وإيقاف الوظائف المرتبطة. هل تريد بالتأكيد الاستمرار؟" imap_mailbox_not_selected: "يجب تحديد صندوق بريد لإعداد خادم IMAP وإلا فلن تتم مزامنة أي صناديق بريد!" prefill: title: "الملء المسبق بإعدادات:" @@ -948,7 +949,7 @@ ar: categories: title: الفئات long_title: "الإشعارات الافتراضية للفئة" - description: "عند إضافة مستخدمين إلى هذه المجموعة، سيتم ضبط إعدادات إشعارات الفئة لديهم على تلك القيم. ويمكنهم تغييرها بعد ذلك." + description: "عند إضافة مستخدمين إلى هذه المجموعة، سيتم تعيين إعدادات إشعارات الفئة لديهم على تلك القيم. ويمكنهم تغييرها بعد ذلك." watched_categories_instructions: "يمكنك مراقبة كل الموضوعات في هذه الفئات تلقائيًا. سيتلقى أعضاء المجموعة إشعارات بالمنشورات والموضوعات الجديدة، وسيظهر أيضًا عدد المنشورات الجديدة بجانب الموضوع." tracked_categories_instructions: "يمكنك تتبُّع كل الموضوعات في هذه الفئات تلقائيًا. وسيظهر عدد المنشورات الجديدة بجانب الموضوع." watching_first_post_categories_instructions: "سيتلقى المستخدمون إشعارًا بأول منشور في كل موضوع جديد في هذه الفئات." @@ -1042,7 +1043,7 @@ ar: make_owner_description: "جعل %{username} أحد مالكي هذه المجموعة" remove_owner: "إزالة كمالك" remove_owner_description: "إزالة %{username} كمالك هذه المجموعة" - make_primary: "الضبط كمجموعة أساسية" + make_primary: "التعيين كمجموعة أساسية" make_primary_description: "جعل هذه المجموعة هي المجموعة الأساسية للمستخدم %{username}" remove_primary: "الإزالة كمجموعة أساسية" remove_primary_description: "إزالة هذه المجموعة كمجموعة أساسية للمستخدم %{username}" @@ -1052,7 +1053,7 @@ ar: make_owners_description: "تعيين المستخدمين المحدَّدين كمالكين لهذه المجموعة" remove_owners: "إزالة المالكين" remove_owners_description: "إزالة المستخدمين المحدَّدين كمالكين لهذه المجموعة" - make_all_primary: "الضبط كمجموعة أساسية للجميع" + make_all_primary: "التعيين كمجموعة أساسية للجميع" make_all_primary_description: "جعل هذه المجموعة هي المجموعة الأساسية للمستخدمين المحدَّدين" remove_all_primary: "الإزالة كمجموعة أساسية" remove_all_primary_description: "إزالة هذه المجموعة كمجموعة أساسية" @@ -1084,10 +1085,10 @@ ar: description: "ستتقلى إشعارات بالرسائل الجديدة في هذه المجموعة ولكن ليس الردود على الرسائل." tracking: title: "التتبُّع" - description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك، وسترى عدد الردود الجديدة." + description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك، وسترى عدد الردود الجديدة." regular: title: "عادية" - description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك." + description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك." muted: title: "الكتم" description: "لن تتلقى أي إشعارات أبدًا بخصوص الرسائل من هذه المجموعة." @@ -1235,6 +1236,7 @@ ar: notification_schedule: title: "جدول الإشعارات" label: "تفعيل جدول الإشعارات المخصَّص" + tip: "سيتم تعطيل إشعاراتك مؤقتًا خارج هذه الساعات." midnight: "منتصف الليل" none: "لا يوجد" monday: "الاثنين" @@ -1278,12 +1280,16 @@ ar: perm_denied_expl: "لقد رفضت منح الإذن بإرسال الإشعارات. يمكنك السماح بالإشعارات في إعدادات المتصفح." disable: "إيقاف الإشعارات" enable: "تفعيل الإشعارات" + each_browser_note: "ملاحظة: عليك تغيير هذا الإعداد في كل متصفح تستخدمه. سيتم تعطيل جميع الإشعارات إذا أوقفت الإشعارات من قائمة المستخدم، بغض النظر عن هذا الإعداد." consent_prompt: "هل تريد تلقي إشعارات فورية عند رد الأشخاص على منشوراتك؟" dismiss: "تجاهل" dismiss_notifications: "تجاهل الكل" dismiss_notifications_tooltip: "وضع علامة مقروءة على كل الإشعارات غير المقروءة" dismiss_bookmarks_tooltip: "وضع علامة \"مقروءة\" على كل تذكيرات الإشارات المرجعية غير المقروءة" dismiss_messages_tooltip: "وضع علامة \"مقروءة\" على كل إشعارات الرسائل الشخصية غير المقروءة" + no_likes_title: "لم تتلقَّ أي تسجيلات إعجاب بعد" + no_likes_body: > + ستتلقى إشعارًا هنا في أي وقت يسجِّل فيه شخص ما إعجابه بأحد منشوراتك؛ حتى تتمكن من رؤية ما ينال إعجاب الآخرين. سيرى الآخرون الشيء نفسه عندما تسجِّل إعجابك بمنشوراتهم أيضًا!

    لا يتم إرسال إشعارات بتسجيلات الإعجاب إلى بريدك الإلكتروني أبدًا، لكن يمكنك ضبط طريقة تلقي الإشعارات بشأن تسجيلات الإعجاب على الموقع في تفضيلات الإشعارات. no_messages_title: "ليس لديك أي رسائل" no_messages_body: > هل تحتاج إلى إجراء محادثة شخصية مباشرة مع شخص ما خارج مسار إجراء المحادثات التقليدي؟ راسله عن طريق تحديد صورته الرمزية واستخدام زر الرسالة %{icon}.

    إذا كنت بحاجة إلى مساعدة، يمكنك مراسلة عضو في فريق العمل. @@ -1305,13 +1311,13 @@ ar: description: "تخطي نصائح وشارات تهيئة المستخدم الجديد" reset_seen_user_tips: "عرض نصائح المستخدم مرة أخرى" theme_default_on_all_devices: "جعل هذه السمة الافتراضية على كل أجهزتي" - color_scheme_default_on_all_devices: "ضبط نظام (أنظمة) الألوان الافتراضي على جميع أجهزتي" + color_scheme_default_on_all_devices: "تعيين نظام (أنظمة) الألوان الافتراضي على جميع أجهزتي" color_scheme: "نظام الألوان" color_schemes: default_description: "السمة الافتراضية" disable_dark_scheme: "مثل العادي" dark_instructions: "يمكنك معاينة نظام ألوان الوضع الداكن عن طريق تفعيل الوضع الداكن لجهازك." - undo: "إعادة الضبط" + undo: "إعادة التعيين" regular: "العادي" dark: "الوضع الداكن" default_dark_scheme: "(الوضع الافتراضي للموقع)" @@ -1441,6 +1447,86 @@ ar: warnings: "تحذيرات رسمية" read_more_in_group: "هل ترغب في قراءة المزيد؟ تصفح الرسائل الأخرى في %{groupLink}." read_more: "هل ترغب في قراءة المزيد؟ تصفح الرسائل الأخرى في الرسائل الشخصية." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + zero {}=0 {} + one {هناك رسالة غير مقروءة} + two {هناك رسالتان غير مقروءتين} + few {هناك # رسائل غير مقروءة} + many {هناك # رسالة غير مقروءة} + other {هناك # رسالة غير مقروءة} + } + { NEW, plural, + zero {}=0 {} + one { ورسالة جديدة متبقية، أو استعرض الرسائل الأخرى في {groupLink}} + two { ورسالتان جديدتان متبقيتان، أو استعرض الرسائل الأخرى في {groupLink}} + few { و# رسائل جديدة متبقية، أو استعرض الرسائل الأخرى في {groupLink}} + many { و# رسالة جديدة متبقية، أو استعرض الرسائل الجديدة في {groupLink}} + other { و# رسالة جديدة متبقية، أو استعرض الرسائل الجديدة في {groupLink}} + } + } + false { + { UNREAD, plural, + zero {}=0 {} + one {هناك رسالة غير مقروءة متبقية، أو استعرض الرسائل الأخرى في {groupLink}} + two {هناك رسالتان غير مقروءتين متبقيتان، أو استعرض الرسائل الأخرى في {groupLink}} + few {هناك # رسائل غير مقروءة متبقية، أو استعرض الرسائل الأخرى في {groupLink}} + many {هناك # رسالة غير مقروءة متبقية، أو استعرض الرسائل الأخرى في {groupLink}} + other {هناك # رسالة غير مقروءة متبقية، أو استعرض الرسائل الأخرى في {groupLink}} + } + { NEW, plural, + zero {}=0 {} + one {هناك رسالة جديدة متبقية، أو استعرض الرسائل الأخرى في {groupLink}} + two {هناك رسالتان جديدتان متبقيتان، أو استعرض الرسائل الأخرى في {groupLink}} + few {هناك # رسائل جديدة متبقية، أو استعرض الرسائل الأخرى في {groupLink}} + many {هناك # رسالة جديدة متبقية، أو استعرض الرسائل الجديدة في {groupLink}} + other {هناك # رسالة جديدة متبقية، أو استعرض الرسائل الجديدة في {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + zero {}=0 {} + one {هناك رسالة غير مقروءة} + two {هناك رسالتان غير مقروءتين} + few {هناك # رسائل غير مقروءة} + many {هناك # رسالة غير مقروءة} + other {هناك # رسالة غير مقروءة} + } + { NEW, plural, + zero {}=0 {} + one { ورسالة جديدة متبقية، أو استعرض الرسائل الشخصية الأخرى} + two { ورسالتان جديدتان متبقيتان، أو استعرض الرسائل الشخصية الأخرى} + few { و# رسائل جديدة متبقية، أو استعرض الرسائل الشخصية الأخرى} + many { و# رسالة جديدة متبقية، أو استعرض الرسائل الشخصية الأخرى} + other { و# رسالة جديدة متبقية، أو استعرض الرسائل الشخصية الأخرى} + } + } + false { + { UNREAD, plural, + zero {}=0 {} + one {هناك رسالة غير مقروءة متبقية، أو استعرض الرسائل الشخصية الأخرى} + two {هناك رسالتان غير مقروءتين متبقيتان، أو استعرض browse other الرسائل الشخصية الأخرى} + few {هناك # رسائل غير مقروءة متبقية، أو استعرض الرسائل الشخصية الأخرى} + many {هناك # رسالة غير مقروءة متبقية، أو استعرض الرسائل الشخصية الأخرى} + other {هناك # رسالة غير مقروءة متبقية، أو استعرض الرسائل الشخصية الأخرى} + } + { NEW, plural, + zero {}=0 {} + one {هناك رسالة جديدة متبقية، أو استعرض الرسائل الشخصية الأخرى} + two {هناك رسالتان جديدتان متبقيتان، أو استعرض الرسائل الشخصية الأخرى} + few {هناك # رسائل جديدة متبقية، أو استعرض الرسائل الشخصية الأخرى} + many {هناك # رسالة جديدة متبقية، أو استعرض الرسائل الشخصية الأخرى} + other {هناك # رسالة جديدة متبقية، أو استعرض الرسائل الشخصية الأخرى} + } + } + other {} + } preferences_nav: account: "الحساب" security: "الأمان" @@ -1459,8 +1545,8 @@ ar: in_progress: "(جارٍ إرسال رسالة البريد الإلكتروني)" error: "(خطأ)" emoji: "رمز قفل" - action: "إرسال رسالة إلكترونية لإعادة ضبط كلمة المرور" - set_password: "ضبط كلمة المرور" + action: "إرسال رسالة إلكترونية لإعادة تعيين كلمة المرور" + set_password: "تعيين كلمة المرور" choose_new: "اختيار كلمة مرور جديدة" choose: "اختيار كلمة مرور" second_factor_backup: @@ -1513,7 +1599,14 @@ ar: use: "استخدام تطبيق المصادقة" enforced_notice: "يلزم تفعيل المصادقة الثنائية قبل الوصول إلى هذا الموقع." disable: "إيقاف" + disable_confirm: "هل تريد بالتأكيد تعطيل المصادقة الثنائية؟" delete: "حذف" + delete_confirm_header: "سيتم حذف أدوات المصادقة المستندة إلى الرموز ومفاتيح الأمان المادية هذه:" + delete_confirm_instruction: "للتأكيد، اكتب %{confirm} في المربع أدناه." + delete_single_confirm_title: "حذف تطبيق مصادقة" + delete_single_confirm_message: "أنت على وشك حذف %{name}. لا يمكنك التراجع عن هذا الإجراء. إذا غيَّرت رأيك، فعليك تسجيل أداة المصادقة هذه مجددًا." + delete_backup_codes_confirm_title: "حذف رموز النسخ الاحتياطي" + delete_backup_codes_confirm_message: "أنت على وشك حذف رموز النسخ الاحتياطي. لا يمكنك التراجع عن هذا الإجراء. إذا غيَّرت رأيك، فعليك إعادة إنشاء رموز النسخ الاحتياطي." save: "حفظ" edit: "تعديل" edit_title: "تعديل تطبيق المصادقة" @@ -1588,13 +1681,13 @@ ar: resending_label: "جارٍ الإرسال..." resent_label: "تم إرسال الرسالة الإلكترونية" update_email: "تغيير البريد الإلكتروني" - set_primary: "ضبط عنوان البريد الإلكتروني الرئيسي" + set_primary: "تعيين عنوان البريد الإلكتروني الرئيسي" destroy: "إزالة عنوان البريد الإلكتروني" add_email: "إضافة بريد إلكتروني بديل" auth_override_instructions: "يمكن تحديث البريد الإلكتروني من موفِّر المصادقة." no_secondary: "لا توجد عناوين بريد إلكتروني ثانوية" instructions: "لا يظهر للعامة أبدًا." - admin_note: "ملاحظة: يشير تغيير المستخدم المسؤول لعنوان البريد الإلكتروني لمستخدم آخر غير مسؤول إلى أن المستخدم قد فقد الوصول إلى حساب البريد الإلكتروني الأصلي؛ لذلك ستتم مراسلته عبر البريد الإلكتروني لإعادة ضبط كلمة المرور إلى عنوانه الجديد. ولن يتغير عنوان البريد الإلكتروني للمستخدم حتى يكمل عملية إعادة ضبط كلمة المرور." + admin_note: "ملاحظة: يشير تغيير المستخدم المسؤول لعنوان البريد الإلكتروني لمستخدم آخر غير مسؤول إلى أن المستخدم قد فقد الوصول إلى حساب البريد الإلكتروني الأصلي؛ لذلك ستتم مراسلته عبر البريد الإلكتروني لإعادة تعيين كلمة المرور إلى عنوانه الجديد. ولن يتغير عنوان البريد الإلكتروني للمستخدم حتى يكمل عملية إعادة تعيين كلمة المرور." ok: "سنُرسل إليك رسالة إلكترونية للتأكيد" required: "يُرجى إدخال عنوان بريد إلكتروني" invalid: "يُرجى إدخال عنوان بريد إلكتروني صالح" @@ -1684,6 +1777,8 @@ ar: title: "يعرض عنوان صفحة الخلفية عدد:" notifications: "الإشعارات الجديدة" contextual: "محتوى الصفحة الجديد" + bookmark_after_notification: + title: "بعد إرسال إشعار التذكير بإشارة مرجعية:" like_notification_frequency: title: "إرسال إشعار عند تسجيل الإعجاب" always: "دائمًا" @@ -1704,7 +1799,7 @@ ar: every_month: "كل شهر" every_six_months: "كل ستة أشهر" email_level: - title: "مراسلتي عبر البريد الإلكتروني عند اقتباس كلامي أو الرد عليَّ أو عند الإشارة إلى @username الخاص بي، أو عندما يكون هناك نشاط جديد على فئاتي أو وسومي أو موضوعاتي المراقبة" + title: "مراسلتي عبر البريد الإلكتروني عند اقتباس كلامي أو الرد عليَّ أو عند الإشارة إلى @اسم المستخدم الخاص بي، أو عندما يكون هناك نشاط جديد على فئاتي أو وسومي أو موضوعاتي المراقبة" always: "دائمًا" only_when_away: "عندما أكون بعيدًا فقط" never: "أبدًا" @@ -1733,7 +1828,7 @@ ar: after_4_minutes: "بعد 4 دقائق" after_5_minutes: "بعد 5 دقائق" after_10_minutes: "بعد 10 دقائق" - notification_level_when_replying: "عندما أنشر في موضوع، ضبط ذلك الموضوع على" + notification_level_when_replying: "عندما أنشر في موضوع، تعيين ذلك الموضوع على" invited: title: "الدعوات" pending_tab: "قيد الانتظار" @@ -1945,6 +2040,7 @@ ar: save: "حفظ" set_custom_status: "تعيين حالة مخصَّصة" what_are_you_doing: "ماذا تفعل؟" + pause_notifications: "إيقاف الإشعارات مؤقتًا" remove_status: "إزالة الحالة" user_tips: primary: "فهمت!" @@ -1998,6 +2094,60 @@ ar: logout_disabled: "يتم إيقاف تسجيل الخروج عندما يكون الموقع في وضع القراءة فقط." staff_writes_only_mode: enabled: "هذا الموقع في وضع القراءة فقط. يُرجى المتابعة للتصفح، لكن سيقتصر التصفح والرد وتسجيل الإعجاب والإجراءات الأخرى على الأعضاء في طاقم العمل فقط." + too_few_topics_and_posts_notice_MF: | + لنبدأ المناقشة! هناك { currentTopics, plural, + zero {}one {موضوع واحد} + two {موضوعان} + few {# موضوعات} + many {# موضوعًا} + other {# موضوع} + } و{ currentPosts, plural, + zero {}one {منشور واحد} + two {منشوران} + few {# منشورات} + many {# منشورًا} + other {# منشور} + }. يحتاج الزوار إلى المزيد لقراءته والرد عليه – نحن نقترح على الأقل { requiredTopics, plural, + zero {}one {موضوعًا واحدًا} + two {موضوعين} + few {# موضوعات} + many {# موضوعًا} + other {# موضوع} + } و{ requiredPosts, plural, + zero {}one {منشورًا واحدًا} + two {منشورين} + few {# منشورات} + many {# منشورًا} + other {# منشور} + }. لا يمكن إلا لفريق العمل رؤية هذه الرسالة. + too_few_topics_notice_MF: | + لنبدأ المناقشة! هناك { currentTopics, plural, + zero {}one {موضوع واحد} + two {موضوعان} + few {# موضوعات} + many {# موضوعًا} + other {# موضوع} + }. يحتاج الزوار إلى المزيد لقراءته والرد عليه – نحن نقترح على الأقل { requiredTopics, plural, + zero {}one {موضوعًا واحدًا} + two {موضوعين} + few {# موضوعات} + many {# موضوعًا} + other {# موضوع} + }. لا يمكن إلا لفريق العمل رؤية هذه الرسالة. + too_few_posts_notice_MF: | + لنبدأ المناقشة! هناك { currentPosts, plural, + zero {}one {منشور واحد} + two {منشوران} + few {# منشورات} + many {# منشورًا} + other {# منشور} + }. يحتاج الزوار إلى المزيد لقراءته والرد عليه – نحن نقترح على الأقل { requiredPosts, plural, + zero {}one {منشورًا واحدًا} + two {منشورين} + few {# منشورات} + many {# منشورًا} + other {# منشور} + }. لا يمكن إلا لفريق العمل رؤية هذه الرسالة. logs_error_rate_notice: reached_hour_MF: | {relativeAge} – لقد بلغ معدل الخطأ {rate, plural, zero {# خطأ/الساعة} one {خطأ واحد (#)/الساعة} two {خطآن (#)/الساعة} few {# أخطاء/الساعة} many {# خطأ/الساعة} other {# خطأ/الساعة}} حد إعدادات الموقع البالغ {limit, plural, zero {# خطأ/الساعة} one {خطأ واحد (#)/الساعة} two {خطآن (#)/الساعة} few {# أخطاء/الساعة} many {# خطأ/الساعة} other {# خطأ/الساعة}}. @@ -2078,15 +2228,15 @@ ar: failed: "حدث خطأ ما. قد يكون هذا البريد الإلكتروني مسجلًا بالفعل. جرِّب رابط نسيان كلمة المرور" associate: "لديك حساب بالفعل؟ سجِّل الدخول لربط حسابك من %{provider}." forgot_password: - title: "إعادة ضبط كلمة المرور" + title: "إعادة تعيين كلمة المرور" action: "نسيت كلمة مروري" - invite: "أدخِل اسم المستخدم أو عنوان البريد الإلكتروني، وسنُرسل إليك رسالة إلكترونية لإعادة ضبط كلمة المرور." + invite: "أدخِل اسم المستخدم أو عنوان البريد الإلكتروني، وسنُرسل إليك رسالة إلكترونية لإعادة تعيين كلمة المرور." invite_no_username: "أدخل عنوان بريدك الإلكتروني، وسنُرسل إليك رسالة لإعادة تعيين كلمة المرور." - reset: "إعادة ضبط كلمة المرور" - complete_username: "إذا تطابق أحد الحسابات مع اسم المستخدم %{username}، فستتلقى سريعًا رسالة إلكترونية تتضمَّن تعليمات عن كيفية إعادة ضبط كلمة المرور." - complete_email: "إذا تطابق أحد الحسابات مع %{email}، فستتلقى سريعًا رسالة إلكترونية تتضمَّن تعليمات عن كيفية إعادة ضبط كلمة المرور." - complete_username_found: "لقد عثرنا على حساب مطابق لاسم المستخدم %{username}. ومن المفترض أن تتلقى رسالة إلكترونية بإرشادات إعادة ضبط كلمة المرور قريبًا." - complete_email_found: "لقد عثرنا على حساب مطابق لعنوان البريد الإلكتروني %{email}. ومن المفترض أن تتلقى رسالة إلكترونية بإرشادات إعادة ضبط كلمة المرور قريبًا." + reset: "إعادة تعيين كلمة المرور" + complete_username: "إذا تطابق أحد الحسابات مع اسم المستخدم %{username}، فستتلقى سريعًا رسالة إلكترونية تتضمَّن تعليمات عن كيفية إعادة تعيين كلمة المرور." + complete_email: "إذا تطابق أحد الحسابات مع %{email}، فستتلقى سريعًا رسالة إلكترونية تتضمَّن تعليمات عن كيفية إعادة تعيين كلمة المرور." + complete_username_found: "لقد عثرنا على حساب مطابق لاسم المستخدم %{username}. ومن المفترض أن تتلقى رسالة إلكترونية بإرشادات إعادة تعيين كلمة المرور قريبًا." + complete_email_found: "لقد عثرنا على حساب مطابق لعنوان البريد الإلكتروني %{email}. ومن المفترض أن تتلقى رسالة إلكترونية بإرشادات إعادة تعيين كلمة المرور قريبًا." complete_username_not_found: "لا يوجد حساب مطابق لاسم المستخدم %{username}" complete_email_not_found: "لا يوجد حساب مطابق لعنوان البريد الإلكتروني %{email}" help: "لم تصلك الرسالة الإلكترونية بعد؟ احرص على التحقُّق من مجلد البريد غير المرغوب فيه أولًا.

    لست متأكدًا من عنوان البريد الإلكتروني الذي استخدمته؟ أدخِل عنوان البريد الإلكتروني وسنخبرك إذا كان موجودًا هنا.

    إذا فقدت الوصول إلى عنوان البريد الإلكتروني المرتبط بحسابك، يُرجى التواصل مع فريق عملنا لمساعدتك.

    " @@ -2113,6 +2263,9 @@ ar: username: "المستخدم" password: "كلمة المرور" show_password: "إظهار" + hide_password: "إخفاء" + show_password_title: "إظهار كلمة المرور" + hide_password_title: "إخفاء كلمة المرور" second_factor_title: "المصادقة الثنائية" second_factor_description: "يُرجى إدخال رمز المصادقة الثنائية من تطبيقك:" second_factor_backup: "تسجيل الدخول باستخدام رمز احتياطي" @@ -2132,8 +2285,9 @@ ar: rate_limit: "يُرجى الانتظار قبل محاولة تسجيل الدخول مرة أخرى." blank_username: "يُرجى إدخال بريدك الإلكتروني أو اسم المستخدم." blank_username_or_password: "يُرجى إدخال بريدك الإلكتروني أو اسم المستخدم، وكلمة المرور." - reset_password: "إعادة ضبط كلمة المرور" + reset_password: "إعادة تعيين كلمة المرور" logging_in: "جارٍ تسجيل الدخول..." + previous_sign_up: "هل لديك حساب بالفعل؟" or: "أو" authenticating: "جارٍ المصادقة..." awaiting_activation: "ما زال حسابك بانتظار التفعيل، استخدم رابط \"نسيت كلمة المرور\" لإرسال رسالة إلكترونية أخرى للتفعيل." @@ -2340,6 +2494,16 @@ ar: private: "لقد أشرت إلى @%{username}، لكنه لن يتلقى إشعارًا لأنه لا يمكنه رؤية هذه الرسالة الخاصة. عليك دعوته إلى هذه الرسالة الخاصة." muted_topic: "لقد أشرت إلى @%{username}، لكنه لن يتلقى إشعارًا لأنه كتم هذا الموضوع." not_allowed: "لقد أشرت إلى @%{username}، لكنه لن يتلقى إشعارًا لأنه لم تتم دعوته إلى هذا الموضوع." + cannot_see_group_mention: + not_mentionable: "لا يمكنك الإشارة إلى المجموعة @%{group}." + some_not_allowed: + zero: "لقد أشرت إلى المجموعة @%{group}، لكن سيتلقى %{count} عضو فقط الإشعار لأن الأعضاء الآخرين لا يمكنهم رؤية هذه الرسالة الشخصية. سيتعيَّن عليك دعوتهم إلى هذه الرسالة الشخصية." + one: "لقد أشرت إلى المجموعة @%{group}، لكن سيتلقى عضو واحد (%{count}) فقط الإشعار لأن الأعضاء الآخرين لا يمكنهم رؤية هذه الرسالة الشخصية. سيتعيَّن عليك دعوتهم إلى هذه الرسالة الشخصية." + two: "لقد أشرت إلى المجموعة @%{group}، لكن سيتلقى عضوان (%{count}) فقط الإشعار لأن الأعضاء الآخرين لا يمكنهم رؤية هذه الرسالة الشخصية. سيتعيَّن عليك دعوتهم إلى هذه الرسالة الشخصية." + few: "لقد أشرت إلى المجموعة @%{group}، لكن سيتلقى %{count} أعضاء فقط الإشعار لأن الأعضاء الآخرين لا يمكنهم رؤية هذه الرسالة الشخصية. سيتعيَّن عليك دعوتهم إلى هذه الرسالة الشخصية." + many: "لقد أشرت إلى المجموعة @%{group}، لكن سيتلقى %{count} عضوًا فقط الإشعار لأن الأعضاء الآخرين لا يمكنهم رؤية هذه الرسالة الشخصية. سيتعيَّن عليك دعوتهم إلى هذه الرسالة الشخصية." + other: "لقد أشرت إلى المجموعة @%{group}، لكن سيتلقى %{count} عضو فقط الإشعار لأن الأعضاء الآخرين لا يمكنهم رؤية هذه الرسالة الشخصية. سيتعيَّن عليك دعوتهم إلى هذه الرسالة الشخصية." + not_allowed: "لقد أشرت إلى @%{group}، لكن لن يتلقى أي عضوٍ من أعضائها إشعارًا لأنه لا يمكنهم رؤية هذه الرسالة الشخصية. سيتعيَّن عليك دعوتهم إلى هذه الرسالة الشخصية." here_mention: zero: "بالإشارة إلى @%{here}، أنت على وشك إرسال إشعار إلى %{count} مستخدم - هل أنت متأكد من ذلك؟" one: "بالإشارة إلى @%{here}، أنت على وشك إرسال إشعار إلى مستخدم واحد (%{count}) - هل أنت متأكد من ذلك؟" @@ -2348,6 +2512,7 @@ ar: many: "بالإشارة إلى @%{here}، أنت على وشك إرسال إشعار إلى %{count} مستخدمًا - هل أنت متأكد من ذلك؟" other: "بالإشارة إلى @%{here}، أنت على وشك إرسال إشعار إلى %{count} مستخدم - هل أنت متأكد من ذلك؟" duplicate_link: "يبدو أن رابطك إلى %{domain} قدم تم نشره في الموضوع بواسطة @%{username} في رد بتاريخ %{ago}. هل تريد بالتأكيد نشره مرة أخرى؟" + duplicate_link_same_user: "يبدو أنك قد نشرت بالفعل رابطًا إلى %{domain} في هذا الموضوع في ردٍ في %{ago} - هل تريد بالتأكيد نشره مجددًا؟" reference_topic_title: "بخصوص: %{title}" error: title_missing: "العنوان مطلوب" @@ -2511,7 +2676,21 @@ ar: few: "%{count} إشعارات عالية الأولوية غير مقروءة" many: "%{count} إشعارًا عالي الأولوية غير مقروء" other: "%{count} إشعار عالي الأولوية غير مقروء" - title: "إشعارات الإشارة إلى اسمك باستخدام الرمز @، والردود على منشوراتك وموضوعاتك ورسائلك، وغيرها" + new_message_notification: + zero: "%{count} إشعار برسالة جديدة" + one: "إشعار واحد (%{count}) برسالة جديدة" + two: "إشعاران (%{count}) برسالتين جديدتين" + few: "%{count} إشعارات برسائل جديدة" + many: "%{count} إشعارًا برسائل جديدة" + other: "%{count} إشعار برسائل جديدة" + new_reviewable: + zero: "%{count} عنصر قابل للمراجعة" + one: "عنصر واحد (%{count}) قابل للمراجعة" + two: "عنصران (%{count}) قابلان للمراجعة" + few: "%{count} عناصر قابلة للمراجعة" + many: "%{count} عنصرًا قابلًا للمراجعة" + other: "%{count} عنصر قابل للمراجعة" + title: "إشعارات الإشارة إلى @اسمك، والردود على منشوراتك وموضوعاتك ورسائلك، وغيرها" none: "يتعذَّر تحميل الإشعارات في الوقت الحالي." empty: "لم يتم العثور على إشعارات." post_approved: "تمت الموافقة على منشورك" @@ -2572,6 +2751,7 @@ ar: reaction: "‏⁨%{username}⁩ ‏%{description}" reaction_2: "%{username} و%{username2} %{description}" votes_released: "%{description} - اكتمل" + new_features: "تتوفر ميزات جديدة!" dismiss_confirmation: body: default: @@ -2642,6 +2822,7 @@ ar: membership_request_consolidated: "طلبات العضوية الجديدة" reaction: "تفاعل جديد" votes_released: "تم تحرير التصويت" + new_features: "تم إطلاق ميزات جديدة في Discourse!" upload_selector: uploading: "جارٍ التحميل" processing: "التحميل قيد المعالجة" @@ -2714,6 +2895,7 @@ ar: status: "يقوم بالتصفية حسب حالة الموضوع" full_search: "يشغِّل البحث في الصفحة بأكملها" full_search_key: "%{modifier} + Enter" + me: "يعرض منشوراتك فقط" advanced: title: عوامل تصفية متقدمة posted_by: @@ -2904,7 +3086,7 @@ ar: other: "تجاهل الجديدة (%{count})" toggle: "تفعيل التحديد الجماعي للموضوعات" actions: "الإجراءات الجماعية" - change_category: "ضبط الفئة..." + change_category: "تعيين الفئة..." close_topics: "إغلاق الموضوعات" archive_topics: "أرشفة الموضوعات" move_messages_to_inbox: "النقل إلى صندوق الوارد" @@ -3030,7 +3212,60 @@ ar: show_links: "إظهار الروابط في هذا الموضوع" collapse_details: "طي تفاصيل الموضوع" expand_details: "توسيع تفاصيل الموضوع" + read_more_in_category: "هل تريد قراءة المزيد؟ استعرض موضوعات أخرى في %{categoryLink} أو شاهد آخر الموضوعات." + read_more: "هل تريد قراءة المزيد؟ استعرض كل الفئات أو شاهد آخر الموضوعات." unread_indicator: "لم يقرأ أي عضو آخر منشور في هذا الموضوع بعد." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + zero {}=0 {} + one {هناك رسالة غير مقروءة} + two {هناك رسالتان غير مقروءتين} + few {هناك # رسائل غير مقروءة} + many {هناك # رسالة غير مقروءة} + other {هناك # رسالة غير مقروءة} + } + { NEW, plural, + zero {}=0 {} + one { وموضوع جديد متبقٍ،} + two { وموضوعان جديدان متبقيان،} + few { و# موضوعات جديدة متبقية،} + many { و# موضوعًا جديدًا متبقيًا،} + other { و# موضوع جديد متبقٍ،} + } + } + false { + { UNREAD, plural, + zero {}=0 {} + one {هناك موضوع غير مقروء متبقٍ،} + two {هناك موضوعان غير مقروءين متبقيان،} + few {هناك # موضوعات غير مقروءة متبقية،} + many {هناك # موضوعًا غير مقروء متبقيًا،} + other {هناك # موضوع غير مقروءمتبقٍ،} + } + { NEW, plural, + zero {}=0 {} + one {هناك موضوع جديد متبقٍ،} + two {هناك موضوعان جديدان متبقيان،} + few {هناك # موضوعات جديدة متبقية،} + many {هناك # موضوعًا جديدًا متبقيًا،} + other {هناك # موضوع جديد متبقٍ،} + } + } + other {} + } + { HAS_CATEGORY, select, + true { أو استعرض الموضوعات الأخرى في {categoryLink}} + false { أو شاهد آخر الموضوعات} + other {} + } + bumped_at_title: | + أول منشور: %{createdAtDate} + تاريخ النشر: %{bumpedAtDate} + browse_all_categories_latest: "استعرض كل الفئات أو شاهد آخر الموضوعات." + browse_all_categories_latest_or_top: "استعرض كل الفئات أو شاهد آخر الموضوعات، أو انظر أبرز:" + browse_all_tags_or_latest: "استعرض كل الوسوم أو شاهد آخر الموضوعات." suggest_create_topic: هل أنت مستعد لبدء محادثة جديدة؟ jump_reply_up: الانتقال إلى الرد السابق jump_reply_down: الانتقال إلى الرد التالي @@ -3062,7 +3297,7 @@ ar: duration: "يُرجى الانتظار لمدة %{duration} بين المنشورات في هذا الموضوع" topic_status_update: title: "مؤقِّت الموضوع" - save: "ضبط المؤقِّت" + save: "تعيين المؤقِّت" num_of_hours: "عدد الساعات:" num_of_days: "عدد الأيام:" remove: "إزالة المؤقِّت" @@ -3153,8 +3388,8 @@ ar: "2_4": "سترى عدد الردود الجديدة لأنك نشرت ردًا في هذا الموضوع." "2_2": "سترى عدد الردود الجديدة لأنك تتتبَّع هذا الموضوع." "2": 'سترى عدد الردود الجديدة لأنك قرأت هذا الموضوع.' - "1_2": "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك." - "1": "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك." + "1_2": "سنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك." + "1": "سنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك." "0_7": "أنت تتجاهل جميع الإشعارات في هذه الفئة." "0_2": "أنت تتجاهل جميع الإشعارات التي تخص هذا الموضوع." "0": "أنت تتجاهل جميع الإشعارات التي تخص هذا الموضوع." @@ -3166,16 +3401,16 @@ ar: description: "سنُرسل إليك إشعارًا بكل ردٍ جديد في هذا الموضوع، وسترى عدد الردود الجديدة." tracking_pm: title: "التتبُّع" - description: "سيظهر عدد الردود الجديدة في هذه الرسالة. وسنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك." + description: "سيظهر عدد الردود الجديدة في هذه الرسالة. وسنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك." tracking: title: "التتبُّع" - description: "سيظهر عدد الردود الجديدة في هذا الموضوع. وسنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك." + description: "سيظهر عدد الردود الجديدة في هذا الموضوع. وسنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك." regular: title: "عادية" - description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك." + description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك." regular_pm: title: "عادية" - description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك." + description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك." muted_pm: title: "الكتم" description: "لن نُرسل إليك أي إشعارات أبدًا بخصوص هذه الرسالة." @@ -3189,18 +3424,18 @@ ar: open: "فتح الموضوع" close: "إغلاق الموضوع" multi_select: "تحديد المنشورات..." - slow_mode: "ضبط الوضع البطيء..." - timed_update: "ضبط مؤقِّت للموضوع..." + slow_mode: "تعيين الوضع البطيء..." + timed_update: "تعيين مؤقِّت للموضوع..." pin: "تثبيت الموضوع..." unpin: "إلغاء تثبيت الموضوع" unarchive: "إلغاء أرشفة الموضوع" archive: "أرشفة الموضوع" invisible: "إلغاء إدراج الموضوع" visible: "إدارج الموضوع" - reset_read: "إعادة ضبط بيانات القراءة" + reset_read: "إعادة تعيين بيانات القراءة" make_public: "التحويل إلى موضوع عام..." make_private: "التحويل إلى رسالة خاصة" - reset_bump_date: "إعادة ضبط تاريخ الرفع" + reset_bump_date: "إعادة تعيين تاريخ الرفع" feature: pin: "تثبيت الموضوع" unpin: "إلغاء تثبيت الموضوع" @@ -3551,6 +3786,7 @@ ar: many: "عذرًا، يمكنك تحميل %{count} ملفًا فقط في الوقت نفسه." other: "عذرًا، يمكنك تحميل %{count} ملف فقط في الوقت نفسه." upload_not_authorized: "عذرًا، الملف الذي تحاول تحميله غير مسموح به (الامتدادات المسموح بها: %{authorized_extensions})." + no_uploads_authorized: "عذرًا، لا توجد ملفات مصرَّح بتحميلها." image_upload_not_allowed_for_new_user: "عذرًا، لا يمكن للمستخدمين الجُدد تحميل الصور." attachment_upload_not_allowed_for_new_user: "عذرًا، لا يمكن للمستخدمين الجُدد تحميل المرفقات." attachment_download_requires_login: "عذرًا، عليك تسجيل الدخول لتنزيل المرفقات." @@ -3562,6 +3798,7 @@ ar: via_email: "لقد وصل هذا المنشور عبر البريد الإلكتروني" via_auto_generated_email: "لقد وصل هذا المنشور عبر رسالة إلكترونية تم إنشاؤها تلقائيًا" whisper: "هذا المنشور عبارة عن همسة خاصة للمشرفين" + whisper_groups: "هذا المنشور هو همسة خاصة لا يراها سوى %{groupNames}" wiki: about: "هذا المنشور عبارة عن Wiki" few_likes_left: "نشكرك على نشر المحبة في المجتمع! و لكن للأسف لقد اقتربت من الحد اليومي المسموح به لمرات تسجيل الإعجاب." @@ -3815,11 +4052,12 @@ ar: toggle_full: "إذن تفعيل الإنشاء" inherited: 'هذا الإذن مكتسب من "الجميع"' special_warning: "تحذير: هذه الفئة مصنَّفة مسبقًا ولا يمكن تعديل إعدادات الأمان لها. إذا كنت لا ترغب في استخدام هذه الفئة، فعليك حذفها بدلًا من إعادة توظيفها." - uncategorized_security_warning: "هذه الفئة خاصة. وتهدف إلى جمع الموضوعات التي لا تنتمي إلى أي فئة، ولا يمكنك ضبط إعدادات حماية لها." + uncategorized_security_warning: "هذه الفئة خاصة. وتهدف إلى جمع الموضوعات التي لا تنتمي إلى أي فئة، ولا يمكنك تعيين إعدادات حماية لها." uncategorized_general_warning: 'هذه الفئة خاصة. ويتم استخدامها كفئة افتراضية للموضوعات الجديدة التي لا تنتمي إلى أي فئة. إذا أردت منع هذا السلوك وفرض تحديد الفئة، يُرجى إيقاف هذا الإعداد من هنا. إذا أردت تغيير اسم الفئة أو وصفها، فانتقل إلى التخصيص/المحتوى النصي.' pending_permission_change_alert: "لم تُضف المجموعة %{group} إلى هذه الفئة، انقر على هذا الزر لإضافتها." images: "الصور" email_in: "العنوان المخصَّص للبريد الوارد:" + email_in_tooltip: "يمكنك فصل عدة عناوين بريد إلكتروني باستخدام الرمز |." email_in_allow_strangers: "قبول الرسائل الإلكترونية من المستخدمين المجهولين الذين لا يملكون حسابات" email_in_disabled: "تم إيقاف نشر الموضوعات الجديدة عبر البريد الإلكتروني في إعدادات الموقع. لتفعيل نشر الموضوعات الجديدة عبر البريد الإلكتروني، " email_in_disabled_click: 'فعليك تفعيل خيار "email in" في الإعدادات.' @@ -3844,7 +4082,7 @@ ar: this_year: "هذا العام" position: "الترتيب في صفحة الفئات:" default_position: "الترتيب الافتراضي" - position_disabled: "سيتم عرض الفئات بترتيب النشاط. للتحكم في ترتيب الفئات في القوائم، فعِّل الإعداد \"المواضع الثابتة للفئات\"." + position_disabled: "سيتم عرض الفئات بترتيب النشاط. للتحكم في ترتيب الفئات في القوائم، قم بتمكين الإعداد \"المواضع الثابتة للفئات\"." minimum_required_tags: "الحد الأدنى من عدد الوسوم المطلوبة في الموضوع:" default_slow_mode: 'تفعيل "الوضع البطيء" للموضوعات الجديدة في هذه الفئة.' parent: "الفئة الرئيسية" @@ -3863,7 +4101,7 @@ ar: description: "ستتتبَّع تلقائيًا كل الموضوعات في هذه الفئة. وسيتم إرسال إشعار إليك إذا أشار إليك شخص ما @name أو ردَّ عليك، وسيتم عرض عدد الردود الجديدة." regular: title: "عادية" - description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك." + description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ عليك." muted: title: "الكتم" description: "لن يتم إرسال إشعار إليك أبدًا بأي شيء يتعلَّق بالموضوعات الجديدة في هذه الفئة، ولن تظهر في الموضوعات الحديثة." @@ -4160,7 +4398,6 @@ ar: this_week: "الأسبوع" today: "اليوم" browser_update: 'عذرًا، متصفحك غير مدعوم. يُرجى التبديل إلى متصفح مدعوم لعرض المحتوى الغني، وتسجيل الدخول والرد.' - safari_13_warning: سيزيل هذا الموقع الدعم لإصدارات 13 من iOS وSafari، والإصدارات الأقدم. ستظل هناك نسخة مبسَّطة متاحة للقراءة فقط. (المزيد من المعلومات) permission_types: full: "الإنشاء/الرد/العرض" create_post: "الرد/العرض" @@ -4443,7 +4680,7 @@ ar: description: "ستتتبَّع تلقائيًا كل الموضوعات التي تحمل هذا الوسم. وسيظهر أيضًا عدد المنشورات غير المقروءة والجديدة بجانب الموضوع." regular: title: "عادية" - description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ على منشورك." + description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى @اسمك أو ردَّ على منشورك." muted: title: "الكتم" description: "لن تتلقى أي إشعارات أبدًا بخصوص الموضوعات الجديدة التي تحمل هذا الوسم، ولن تظهر في علامة تبويب الموضوعات غير المقروءة." @@ -4500,6 +4737,8 @@ ar: enabled: "تم تفعيل الوضع الآمن، أغلق نافذة المتصفح هذه للخروج منه" image_removed: "(تمت إزالة الصورة)" pause_notifications: + title: "إيقاف الإشعارات مؤقتًا لمدة..." + label: "إيقاف الإشعارات مؤقتًا" remaining: "بقي %{remaining}..." options: half_hour: "30 دقيقة" @@ -4507,7 +4746,7 @@ ar: two_hours: "ساعتان" tomorrow: "حتى الغد" custom: "مخصَّصة" - set_schedule: "ضبط جدول الإشعارات" + set_schedule: "تعيين جدول الإشعارات" trust_levels: names: newuser: "مستخدم جديد" @@ -4543,6 +4782,8 @@ ar: second_factor_auth: redirect_after_success: "نجحت المصادقة الثنائية. جارٍ إعادة التوجيه إلى الصفحة السابقة…" sidebar: + show_sidebar: "إظهار الشريط الجانبي" + hide_sidebar: "إخفاء الشريط الجانبي" unread_count: zero: "%{count} غير مقروء" one: "%{count} غير مقروء" @@ -4557,6 +4798,7 @@ ar: few: "%{count} جديدة" many: "%{count} جديدًا" other: "%{count} جديدة" + toggle_section: "تبديل القسم" more: "المزيد" all_categories: "كل الفئات" all_tags: "كل الوسوم" @@ -4565,6 +4807,7 @@ ar: header_link_text: "نبذة" messages: header_link_text: "الرسائل" + header_action_title: "إنشاء رسالة شخصية" links: inbox: "صندوق الوارد" sent: "المُرسَلة" @@ -4581,7 +4824,8 @@ ar: none: "لم تضف أي وسوم." click_to_get_started: "انقر هنا للبدء." header_link_text: "الوسوم" - configure_defaults: "ضبط الإعدادات الافتراضية" + header_action_title: "تعديل وسوم الشريط الجانبي" + configure_defaults: "تعيين الإعدادات الافتراضية" categories: links: add_categories: @@ -4590,27 +4834,37 @@ ar: none: "لم تضف أي فئات." click_to_get_started: "انقر هنا للبدء." header_link_text: "الفئات" - configure_defaults: "ضبط الإعدادات الافتراضية" + header_action_title: "تعديل فئات الشريط الجانبي" + configure_defaults: "تعيين الإعدادات الافتراضية" community: header_link_text: "المجتمع" + header_action_title: "إنشاء موضوع" links: about: content: "نبذة" + title: "المزيد من التفاصيل بشأن هذا الموقع" admin: content: "مسؤول" + title: "إعدادات وتقارير الموقع" badges: content: "الشارات" + title: "كل الشارات الممكن منحها لك" everything: content: "كل شيء" title: "كل الموضوعات" faq: content: "الأسئلة الشائعة" + title: "إرشادات لاستخدام هذا الموقع" groups: content: "المجموعات" + title: "قائمة بمجموعات المستخدمين المتاحة" users: content: "المستخدمون" + title: "قائمة بكل المستخدمين" my_posts: content: "منشوراتي" + title: "نشاطي الأخير في الموضوع" + title_drafts: "مسوداتي غير المنشورة" draft_count: zero: "%{count} مسودة" one: "مسودة واحدة (%{count})" @@ -4620,6 +4874,7 @@ ar: other: "%{count} مسودة" review: content: "المراجعة" + title: "المنشورات المُبلَغ عنها والعناصر الأخرى في قائمة الانتظار" pending_count: "بقي %{count}" welcome_topic_banner: title: "إنشاء موضوعك الترحيبي" @@ -4725,7 +4980,7 @@ ar: no_data: "لا توجد بيانات لعرضها." trending_search: more: 'سجلات البحث' - disabled: 'تقرير عبارات البحث الرائجة متوقف. فعِّل تسجيل استعلامات البحث لجمع البيانات.' + disabled: 'تقرير عبارات البحث الرائجة متوقف. قم بتمكين تسجيل استعلامات البحث لجمع البيانات.' average_chart_label: المتوسط filters: file_extension: @@ -4779,7 +5034,10 @@ ar: many: "يوجد %{count} مستخدمًا لديهم نطاقات البريد الإلكتروني الجديدة وستتم إضافته إلى المجموعة." other: "يوجد %{count} مستخدم لديه نطاقات البريد الإلكتروني الجديدة وستتم إضافته إلى المجموعة." automatic_membership_associated_groups: "ستتم إضافة المستخدمين الأعضاء في مجموعة لإحدى الخدمات المُدرَجة هنا تلقائيًا إلى هذه المجموعة عند تسجيل الدخول باستخدام الخدمة." - primary_group: "الضبط تلقائيًا كمجموعة أساسية" + primary_group: "التعيين تلقائيًا كمجموعة أساسية" + alert: + primary_group: "نظرًا لأن هذه مجموعة أساسية، فإن الاسم '%{group_name}' سيتم استخدامه في فئات CSS، والتي يمكن للجميع عرضها." + flair_group: "نظرًا لأن هذه المجموعة لديها طابع خاص لأعضائها، فسيكون الاسم '%{group_name}' مرئيًا لأي شخص." name_placeholder: "اسم المجموعة بلا مسافات، على غرار قاعدة اسم المستخدم" primary: "المجموعة الأساسية" no_primary: "(لا توجد مجموعة أساسية)" @@ -4789,6 +5047,14 @@ ar: about: "يمكنك تعديل عضوية المجموعة والأسماء من هنا" group_members: "أعضاء المجموعة" delete: "حذف" + delete_confirm: "هل تريد بالتأكيد حذف هذه المجموعة؟" + delete_with_messages_confirm: + zero: "سيؤدي حذف هذه المجموعة إلى عزل %{count} رسالة، وسيفقد أعضاء المجموعة الوصول إليها." + one: "سيؤدي حذف هذه المجموعة إلى عزل رسالة واحدة (%{count})، وسيفقد أعضاء المجموعة الوصول إليها." + two: "سيؤدي حذف هذه المجموعة إلى عزل رسالتين (%{count})، وسيفقد أعضاء المجموعة الوصول إليها." + few: "سيؤدي حذف هذه المجموعة إلى عزل %{count} رسائل، وسيفقد أعضاء المجموعة الوصول إليهما." + many: "سيؤدي حذف هذه المجموعة إلى عزل %{count} رسالةً، وسيفقد أعضاء المجموعة الوصول إليها." + other: "سيؤدي حذف هذه المجموعة إلى عزل %{count} رسالة، وسيفقد أعضاء المجموعة الوصول إليها." delete_failed: "يتعذَّر حذف المجموعة. إذا كانت هذه مجموعة تلقائية، فلا يمكن تدميرها." delete_automatic_group: هذه مجموعة تلقائية ولا يمكن حذفها. delete_owner_confirm: "هل تريد إزالة صلاحيات المالك من \"%{username}\"؟" @@ -4854,7 +5120,7 @@ ar: topics: read: قراءة موضوع أو منشور محدَّد فيه. يتم دعم RSS أيضًا. write: إنشاء موضوع جديد أو النشر في موضوع موجود - update: تحديث الموضوع. غيِّر العنوان والفئة والوسوم، إلى آخره. + update: تحديث موضوع. يمكنك تغيير العنوان، والفئة، والوسوم، والحالة، والنموذج البدائي، والرابط المميز، وما إلى ذلك. read_lists: قراءة قوائم الموضوعات مثل الأكثر نشاطًا، والجديدة، والحديثة، وما إلى ذلك. يتم دعم RSS أيضًا. posts: edit: تعديل أي منشور أو منشور معيَّن. @@ -4873,6 +5139,9 @@ ar: anonymize: إخفاء هوية حسابات المستخدمين delete: حذف حسابات المستخدمين list: احصل على قائمة بالمستخدمين. + user_status: + read: اقرأ حالة المستخدم. + update: قم بتحديث حالة المستخدم. email: receive_emails: ادمج هذا النطاق مع مستقبل البريد لمعالجة الرسائل الإلكترونية الواردة. badges: @@ -4897,6 +5166,7 @@ ar: create: "إنشاء" edit: "تعديل" save: "حفظ" + description_label: "مشغِّلات الأحداث" controls: "عناصر التحكم" go_back: "العودة إلي القائمة" payload_url: "عنوان URL للحمولة" @@ -5007,6 +5277,8 @@ ar: broken_route: "تتعذَّر تهيئة الرابط إلى \"%{name}\". تأكَّد من إيقاف كل أدوات حجب الإعلانات وحاول إعادة تحميل الصفحة." navigation_menu: sidebar: "الشريط الجانبي" + header_dropdown: "القائمة المنسدلة للرأس" + legacy: "القائمة القديمة" backups: title: "النسخ الاحتياطية" menu: @@ -5293,7 +5565,7 @@ ar: undo: "تراجع" undo_title: "يمكنك التراجع عن التغييرات التي أجريتها على هذا اللون منذ آخر مرة تم حفظه." revert: "تراجع" - revert_title: "إعادة ضبط هذا اللون إلى لوحة ألوان Discourse الافتراضية" + revert_title: "إعادة تعيين هذا اللون إلى لوحة ألوان Discourse الافتراضية" primary: name: "الأساسي" description: "أغلب النصوص، والأيقونات، والحدود." @@ -5343,8 +5615,8 @@ ar: heading: "تخصيص نمط الرسالة الإلكترونية" html: "نموذج HTML" css: "CSS" - reset: "إعادة الضبط على الافتراضي" - reset_confirm: "هل تريد بالتأكيد إعادة الضبط إلى قيمة %{fieldName} الافتراضية وفقدان جميع التغييرات؟" + reset: "إعادة التعيين إلى الإعداد الافتراضي" + reset_confirm: "هل تريد بالتأكيد إعادة التعيين إلى قيمة %{fieldName} الافتراضية وفقدان جميع التغييرات؟" save_error_with_reason: "لم يتم حفظ تغييراتك. %{error}" instructions: "خصِّص النموذج الذي يتم فيه عرض جميع الرسائل الإلكترونية بتنسيق HTML، والنمط باستخدام CSS." email: @@ -5699,6 +5971,7 @@ ar: user: suspend_failed: "حدث خطأ في أثناء تعليق هذا المستخدم %{error}" unsuspend_failed: "حدث خطأ في أثناء إلغاء تعليق هذا المستخدم %{error}" + suspend_duration: "تعليق المستخدم حتى:" suspend_reason_label: "ما سبب التعليق؟ سيكون هذا النص مرئيًا للجميع على صفحة الملف الشخصي لهذا المستخدم، وسيظهر للمستخدم عند محاولته تسجيل الدخول. احرص على أن يكون موجزًا." suspend_reason_hidden_label: "ما سبب التعليق؟ سيظهر هذا النص للمستخدم عند محاولته تسجيل الدخول. احرص على أن يكون موجزًا." suspend_reason: "السبب" @@ -5722,7 +5995,9 @@ ar: silence_message: "رسالة إلكترونية" silence_message_placeholder: "(اتركه فارغا لإرسال الرسالة الافتراضية)" suspended_until: "(حتى %{until})" + suspend_forever: "التعليق للأبد" cant_suspend: "لا يمكن تعليق هذا المستخدم." + cant_silence: "لا يمكن كتم هذا المستخدم." delete_posts_failed: "حدثت مشكلة في أثناء حذف المنشورات." post_edits: "تعديلات المنشور" view_edits: "عرض التعديلات" @@ -5732,6 +6007,8 @@ ar: penalty_post_edit: "تعديل المنشور" penalty_post_none: "عدم اتخاذ أي إجراء" penalty_count: "عدد العقوبات" + penalty_history_MF: >- + هذا المستخدم تم تعليقه { SUSPENDED, plural, zero {}one {مرة واحدة} two {مرتين} few {# مرات} many {# مرةً} other {# مرة} } وكتمه { SILENCED, plural, zero {}one {مرة واحدة} two {مرتين} few {# مرات} many {# مرةً} other {# مرة} } في الأشهر الستة الأخيرة. clear_penalty_history: title: "مسح سجل العقوبات" description: "لا يمكن للمستخدمين الذين حصلوا على عقوبات الوصول إلى مستوى الثقة 3" @@ -5786,7 +6063,7 @@ ar: time_read: "وقت القراءة" post_edits_count: "تعديلات المنشور" anonymize: "إخفاء هوية المستخدم" - anonymize_confirm: "هل تريد بالتأكيد إخفاء هوية هذا الحساب؟ سيؤدي ذلك إلى تغيير اسم المستخدم والبريد الإلكتروني، وإعادة ضبط جميع معلومات الملف الشخصي." + anonymize_confirm: "هل تريد بالتأكيد إخفاء هوية هذا الحساب؟ سيؤدي ذلك إلى تغيير اسم المستخدم والبريد الإلكتروني، وإعادة تعيين جميع معلومات الملف الشخصي." anonymize_yes: "نعم، إخفاء هوية هذا الحساب" anonymize_failed: "حدثت مشكلة في أثناء إخفاء هوية الحساب." delete: "حذف المستخدم" @@ -5875,8 +6152,8 @@ ar: silence_accept: "نعم، كتم هذا المستخدم" bounce_score: "نقاط الارتداد" reset_bounce_score: - label: "إعادة الضبط" - title: "إعادة ضبط نقاط الارتداد على 0" + label: "إعادة التعيين" + title: "إعادة تعيين نقاط الارتداد على 0" visit_profile: "قم بزيارة صفحة تفضيلات هذا المستخدم لتعديل ملفه الشخصي" deactivate_explanation: "يجب على المستخدم غير النشط إعادة التحقُّق من صحة بريده الإلكتروني" suspended_explanation: "لا يمكن للمستخدم المعلَّق تسجيل الدخول." @@ -5898,10 +6175,18 @@ ar: silenced_count: "مكتوم" suspended_count: "معلَّق" last_six_months: "آخر 6 أشهر" + other_matches: + zero: "هناك %{count} مستخدم آخر بعنوان IP نفسه. راجع المستخدمين المشبوهين وحدِّدهم لمعاقبتهم مع %{username}." + one: "هناك مستخدم واحد (%{count}) آخر بعنوان IP نفسه. راجع المستخدمين المشبوهين وحدِّدهم لمعاقبتهم مع %{username}." + two: "هناك مستخدمان (%{count}) آخران بعنوان IP نفسه. راجع المستخدمين المشبوهين وحدِّدهم لمعاقبتهم مع %{username}." + few: "هناك %{count} مستخدمين آخرين بعنوان IP نفسه. راجع المستخدمين المشبوهين وحدِّدهم لمعاقبتهم مع %{username}." + many: "هناك %{count} مستخدمًا آخر بعنوان IP نفسه. راجع المستخدمين المشبوهين وحدِّدهم لمعاقبتهم مع %{username}." + other: "هناك %{count} مستخدم آخر بعنوان IP نفسه. راجع المستخدمين المشبوهين وحدِّدهم لمعاقبتهم مع %{username}." other_matches_list: username: "اسم المستخدم" trust_level: "مستوى الثقة" read_time: "وقت القراءة" + topics_entered: "الموضوعات المُدخَلة" posts: "المنشورات" tl3_requirements: title: "المتطلبات لمستوى الثقة 3." @@ -6000,7 +6285,7 @@ ar: settings: show_overriden: "إظهار النصوص المخصَّصة فقط" history: "عرض سجل التغييرات" - reset: "إعادة الضبط" + reset: "إعادة التعيين" none: "لا يوجد" site_settings: emoji_list: @@ -6101,7 +6386,7 @@ ar: image: الصورة graphic: رمز الشارة icon_help: "إدخال اسم أيقونة Font Awesome (استخدم البادئة \"far-\" للأيقونات العادية والبادئة \"fab-‎\" لأيقونات العلامات التجارية)" - image_help: "يؤدي تحميل صورة إلى تجاوز حقل الرمز إذا تم ضبط كليهما." + image_help: "يؤدي تحميل صورة إلى تجاوز حقل الرمز إذا تم تعيين كليهما." select_an_icon: "تحديد أيقونة" upload_an_image: "تحميل صورة" read_only_setting_help: "تخصيص النص" @@ -6226,8 +6511,10 @@ ar: finish: "الخروج من الإعداد" back: "الرجوع" next: "التالي" + configure_more: "تكوين المزيد..." step-text: "الخطوة" step: "%{current} من %{total}" + upload: "تحميل ملف" uploading: "جارٍ التحميل..." upload_error: "عذرًا، حدث خطأ في أثناء تحميل هذا الملف. يُرجى إعادة المحاولة." staff_count: diff --git a/config/locales/client.be.yml b/config/locales/client.be.yml index ac4f163b92..2ab131def1 100644 --- a/config/locales/client.be.yml +++ b/config/locales/client.be.yml @@ -77,6 +77,8 @@ be: close: "схаваць" emails_are_disabled: "Адпраўка паведамленняў па электроннай пошце было глабальна адключана адміністратарам. Ні адно апавяшчэнне па электроннай пошце не будзе даслана." bootstrap_invite_button_title: "Адправіць запрашэнні" + themes: + default_description: "Default" s3: regions: ap_northeast_1: "Азія (Токіа)" @@ -545,6 +547,7 @@ be: email: title: "Электронная пошта" primary_label: "асноўнай" + resent_label: "Ліст адпраўлены" update_email: "Змяненне адрасы электроннай пошты" invalid: "Калі ласка, увядзіце верны email" associated_accounts: @@ -930,6 +933,7 @@ be: close_topics: "Закрыць тэму" archive_topics: "Заархіваваныя тэмы" move_messages_to_inbox: "Перамясціць у тэчцы Уваходныя" + notification_level: "Натыфікацыі..." none: unread: "У вас няма непрачытаных тэм." new: "У вас няма новых тэм." @@ -1080,6 +1084,7 @@ be: like: "упадабаць гэты пост" edit_action: "Рэдагаваць" more: "Болей" + grant_badge: "даць Значок..." delete_topic: "выдаліць тэму" actions: people: @@ -1173,6 +1178,7 @@ be: email: "Электронная пошта" flagging: action: "Пазначыць запіс" + take_action: "прыняць меры..." take_action_options: default: title: "прыняць меры" @@ -1459,6 +1465,7 @@ be: user: "Карыстальнік" title: "API" created: створаны + never_used: (ніколі) generate: "згенераваць" revoke: "Адклікнуць" all_users: "Усе карыстальнікі" @@ -1655,6 +1662,9 @@ be: user_placeholder: "username" address_placeholder: "name@example.com" type_placeholder: "digest, signup ..." + moderation_history: + actions: + delete_topic: "Тэма выдаленая" logs: title: "Логі" action: "дзеянне" diff --git a/config/locales/client.bg.yml b/config/locales/client.bg.yml index 4ef80402aa..1856e88b6b 100644 --- a/config/locales/client.bg.yml +++ b/config/locales/client.bg.yml @@ -225,6 +225,9 @@ bg: now: "преди малко" read_more: "прочетете повече" more: "Повече" + x_more: + one: "Още %{count}" + other: "Още %{count}" never: "никога" every_30_minutes: "на всеки 30 минути" every_hour: "на всеки час" @@ -1905,6 +1908,7 @@ bg: desc: Шепотът се вижда само от служителите create_topic: label: "Нова тема" + ignore: "Игнорирай" notifications: title: "уведомления за @name споменавания, отговори на вашите публикации и теми, лични съобщения, и т.н." none: "В момента не могат да бъдат заредени уведомленията." @@ -2071,6 +2075,7 @@ bg: close_topics: "Затвори темите" archive_topics: "Архивирай темите" move_messages_to_inbox: "Премести във входящи" + notification_level: "Известия..." choose_new_category: "Избери нова категория за тези теми:" selected: one: "Вие сте избрали %{count} тема." @@ -2214,6 +2219,7 @@ bg: title: прогрес на темата jump_prompt: "отиди на..." jump_prompt_long: "Отиди на..." + jump_prompt_to_date: "до дата" jump_prompt_or: "или" notifications: reasons: @@ -2265,6 +2271,7 @@ bg: unarchive: "Разархивирай темата" archive: "Архивирай темата" reset_read: "Изчисти прочетените данни " + make_public: "Направи темата публична..." feature: pin: "Закови темата" unpin: "Отгови темата" @@ -2515,6 +2522,8 @@ bg: rebake: "Прегенерирай HTML " publish_page: "Публикуване на страници" unhide: "Покажи " + change_owner: "Смени Правомощията..." + grant_badge: "Присъдени значка..." lock_post: "Заключване на публикацията" lock_post_description: "не позволявайте на публикувалия да редактира тази публикация" unlock_post: "Отключи публикацията" @@ -2652,6 +2661,8 @@ bg: search_priority: options: normal: "Нормален" + ignore: "Игнорирай" + low: "Ниска" high: "Висок" sort_options: default: "по подразбиране" @@ -2709,6 +2720,10 @@ bg: clicks: one: "%{count} кликване" other: "%{count} кликвания" + post_links: + title: + one: "още %{count}" + other: "още %{count}" topic_statuses: warning: help: "Това е официално предупреждение." @@ -2780,6 +2795,9 @@ bg: lower_title_with_count: one: "%{count} непрочетен" other: "%{count} непрочетени" + unseen: + title: "Непрегледани" + lower_title: "непрегледани" new: lower_title_with_count: one: "%{count} нов" @@ -2889,6 +2907,9 @@ bg: others_count: "Други са такава значка (%{count})" title: Значки allow_title: "Може да използвате тази значка като титла." + more_badges: + one: "+Още %{count}" + other: "+Още %{count}" select_badge_for_title: Изберете значка за титла? none: "(никой)" successfully_granted: "Успешно дадохте %{badge}на %{username}" @@ -2943,6 +2964,7 @@ bg: save: "Запази" delete: "Изтрий" everyone_can_use: "Етикетите могат да се използват от всички" + parent_tag_placeholder: "По избор" topics: none: unread: "Нямате непрочетени теми." @@ -3007,6 +3029,8 @@ bg: content: "Администратор" badges: content: "Значки" + everything: + content: "Всичко" faq: content: "FAQ" groups: @@ -3040,6 +3064,7 @@ bg: latest_version: "Последни" new_features: dismiss: "Отмени" + learn_more: "Научете повече" last_checked: "Последна проверка" refresh_problems: "Обнови" no_problems: "Не бяха открити проблеми." @@ -3055,6 +3080,8 @@ bg: show_traffic_report: "Покажи детайлен репорт на трафика" general_tab: "Основни" security_tab: "Сигурност" + report_filter_any: "всеки" + disabled: Деактивирани reports: today: "Днес" yesterday: "Вчера" @@ -3112,6 +3139,7 @@ bg: user: "Потребител" title: "API" created: Създадени + never_used: (никога) generate: "Генраирай" revoke: "Анулирай" all_users: "Всички потребители" @@ -3129,6 +3157,7 @@ bg: details: "Когато има нов отговор, редакция, изтриване или възстановяване." delivery_status: failed: "Провалени" + disabled: "Деактивирани" events: request: "Заявка" headers: "Хедъри" @@ -3220,6 +3249,9 @@ bg: delete: "Изтрий" color: "Цвят" opacity: "Прозрачност" + copy_to_clipboard: "Копиране в клипборда" + copied_to_clipboard: "Копирано в клипборда" + copy_to_clipboard_error: "Грешка при копирането на данни в клипборда" email_templates: title: "Имейл" subject: "Тема" @@ -3228,6 +3260,7 @@ bg: revert: "Върни промените" revert_confirm: "Сигурен ли си, че искаш да върнеш промените?" theme: + theme: "Тема" customize_desc: "Персонализация:" create_type: "Тип" create_name: "Име" @@ -3236,6 +3269,7 @@ bg: settings: "Настройки" collapse: Намали upload: "Качване" + discard: "Отхвърляне" installed: "Инсталирани" install_popular: "Популярни" about_theme: "Относно" @@ -3291,6 +3325,7 @@ bg: description: "Цвят на бутона харесвам. " email_style: css: "CSS" + reset: "Обновявам до първоначалното" email: title: "Имейли" settings: "Настройки" @@ -3350,6 +3385,9 @@ bg: address_placeholder: "name@example.com" type_placeholder: "дайджест, регистрация" reply_key_placeholder: "reply key " + moderation_history: + actions: + delete_topic: "Тема е изтрита" logs: title: "Логове" action: "Действия" @@ -3473,6 +3511,8 @@ bg: flag: "Сигнализиране" form: add: "Добави" + test: + no_matches: "Няма намерени съвпадения" impersonate: title: "Представи" help: "Използвайте този инструмент, за да предоставите потребителски профили за отстраняване на грешките. Трябва да излезете когато приключите." @@ -3588,6 +3628,8 @@ bg: activate_failed: "Възникна проблем с активирането на потребителя. " deactivate_account: "Деактивирай профила" deactivate_failed: "Възникна проблем при деактивирането на потребителя. " + reset_bounce_score: + label: "Нулиране" deactivate_explanation: "Деактивирания потребител трябва повторно да валидира неговия имейл. " suspended_explanation: "Отстраненитят потребител не може да се логва . " staged_explanation: "Поставените потребители могат да пишат теми само чрез писма." @@ -3721,7 +3763,9 @@ bg: dashboard: "Работен плот" navigation: "Навигация" default_categories: + modal_description: "Искате ли да приложите тази промяна исторически? Това ще промени предпочитанията за %{count} съществуващи потребители." modal_yes: "Да" + modal_no: "Не, промяната се прилага само занапред" badges: title: Значки new_badge: Нова значка diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index a7536d0ad9..27490c8e24 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -1957,9 +1957,11 @@ bs_BA: dismiss_new: "Odpusti Nove" toggle: "preklopi masovno označavanje tema" actions: "Masovno odrađene akcije" + change_category: "Postavi kategoriju..." close_topics: "Zatvori teme" archive_topics: "Arhiviraj teme" move_messages_to_inbox: "Idi u inbox" + notification_level: "Obavijest..." choose_new_category: "Izaberi novu kategoriju za temu:" selected: one: "Označili ste %{count} temu." @@ -2149,6 +2151,7 @@ bs_BA: unarchive: "Vrati temu iz arhiva" archive: "Arhiviraj temu" reset_read: "Reset Read Data" + make_public: "Napiši temu javno..." make_private: "Pretvori u privatnu poruku" reset_bump_date: "Resetuj datum bumpa" feature: @@ -2405,6 +2408,8 @@ bs_BA: revert_to_regular: "Ukloni boje osoblja" rebake: "Popravi HTML" unhide: "Unhide" + change_owner: "Izmijeni vlasništvo..." + grant_badge: "Grant Badge..." lock_post: "Zaključaj objavu" lock_post_description: "spriječi objavljivača ove objave da izmijeni objavu" unlock_post: "Odključaj objavu" @@ -2412,6 +2417,7 @@ bs_BA: delete_topic_disallowed_modal: "Nemate dozvolu za brisanje ove teme. Ako zaista želite da bude obrisan, pošaljite kaznu za pažnju moderatora zajedno s obrazloženjem." delete_topic_disallowed: "nemate dozvolu za brisanje ove teme" delete_topic: "obriši temu" + add_post_notice: "Dodaj obavještenje moderatora..." remove_timer: "ukloni tajmer" actions: people: @@ -2603,6 +2609,7 @@ bs_BA: flagging: title: "Zašto prijavljujete ovaj post?" action: "Prijavi objavu" + take_action: "Poduzmi Akciju..." take_action_options: default: title: "Poduzmi Akciju" @@ -3013,6 +3020,7 @@ bs_BA: delete: "Delete" confirm_delete: "Jeste li sigurni da želite izbrisati ovu grupu oznaka?" everyone_can_use: "Oznake mogu koristiti svi" + parent_tag_placeholder: "Opciono" topics: none: unread: "Nemate više nepročitanih tema." @@ -3084,6 +3092,8 @@ bs_BA: content: "Admin" badges: content: "Bedž" + everything: + content: "Sve" faq: content: "Česta pitanja" groups: @@ -3094,6 +3104,7 @@ bs_BA: content: "Moje objave" review: content: "Pregled" + until: "Sve do:" admin_js: type_to_filter: "kucaj da sortiraš..." admin: @@ -3123,6 +3134,7 @@ bs_BA: problems_found: "Neki saveti na osnovu vaših trenutnih postavki sajta" new_features: dismiss: "Odpusti" + learn_more: "Saznaj više" last_checked: "Zadnje pogledani" refresh_problems: "Osvježi" no_problems: "No problems were found." diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index 7ce685788c..197c155291 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -1890,9 +1890,11 @@ ca: dismiss_new: "Descarta'n els nous" toggle: "commuta la selecció massiva de temes" actions: "Accions massives" + change_category: "Defineix categoria..." close_topics: "Tanca temes" archive_topics: "Arxiva temes" move_messages_to_inbox: "Mou a la safata d'entrada" + notification_level: "Notificacions..." choose_new_category: "Seleccioneu la categoria nova per als temes:" selected: one: "He seleccionat %{count} tema." @@ -2076,6 +2078,7 @@ ca: unarchive: "Desarxiva tema" archive: "Arxiva tema" reset_read: "Restableix data de lectura" + make_public: "Fes públic el tema..." make_private: "Converteix en missatge personal" reset_bump_date: "Restableix la data d'elevació" feature: @@ -2318,6 +2321,8 @@ ca: revert_to_regular: "Elimina el color de l'equip responsable" rebake: "Refés HTML" unhide: "Desfés amagar" + change_owner: "Canvia la propietat..." + grant_badge: "Atorga insígnia..." lock_post: "Bloca la publicació" lock_post_description: "impedeix que l'autor editi aquesta publicació" unlock_post: "Desbloca la publicació" @@ -2325,6 +2330,7 @@ ca: delete_topic_disallowed_modal: "No teniu permís per a suprimir aquest tema. Si realment voleu que se suprimeixi, envieu una bandera perquè un moderador hi pari atenció juntament amb una argumentació." delete_topic_disallowed: "no teniu permís per a suprimir aquest tema" delete_topic: "suprimeix el tema" + add_post_notice: "Afegeix un avís a l'equip responsable..." remove_timer: "elimina el temporitzador" actions: people: @@ -2377,9 +2383,14 @@ ca: title: "Mostra la part HTML del correu electrònic" button: "HTML" bookmarks: + create: "Crea un marcador" + edit: "Edita el marcador" updated: "Actualitzat" name: "Nom" options: "Opcions" + actions: + edit_bookmark: + name: "Edita el marcador" category: none: "(sense categoria)" all: "Totes les categories" @@ -2505,6 +2516,7 @@ ca: flagging: title: "Gràcies per ajudar a mantenir endreçada la comunitat!" action: "Marca la publicació amb bandera " + take_action: "Actua..." take_action_options: default: title: "Actua" @@ -2849,6 +2861,7 @@ ca: delete: "Suprimeix" confirm_delete: "Esteu segur que voleu suprimir aquest grup d'etiquetes?" everyone_can_use: "Etiquetes que poden ser utilitzades per tothom" + parent_tag_placeholder: "Opcional" topics: none: unread: "No teniu temes no llegits." @@ -2916,6 +2929,8 @@ ca: content: "Administració" badges: content: "Insígnies" + everything: + content: "Qualsevol cosa" faq: content: "PMF" groups: @@ -2926,6 +2941,7 @@ ca: content: "Les meves publicacions" review: content: "Revisa" + until: "Fins a:" admin_js: type_to_filter: "escriu per a filtrar..." admin: @@ -2955,6 +2971,7 @@ ca: problems_found: "Uns quants consells basats en la configuració actual del lloc web." new_features: dismiss: "Descarta-ho" + learn_more: "Per a saber-ne més" last_checked: "Comprovat per darrera vegada" refresh_problems: "Actualitza" no_problems: "No s'han trobat problemes." diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 43fcbfd6c9..0a0de0bf29 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -565,11 +565,31 @@ cs: title: "Proč odmítáte tohoto uživatele?" send_email: "Odeslat e-mail o odmítnutí" relative_time_picker: + minutes: + one: "minuta" + few: "minut" + many: "minut" + other: "minut" + hours: + one: "hodina" + few: "hodin" + many: "hodin" + other: "hodin" days: one: "den" few: "dní" many: "dní" other: "dnů" + months: + one: "měsíc" + few: "měsíců" + many: "měsíců" + other: "měsíců" + years: + one: "rok" + few: "let" + many: "let" + other: "let" relative: "Relativní" time_shortcut: later_today: "Později dnes" @@ -841,9 +861,19 @@ cs: few: "%{count} témata" many: "%{count} témat" other: "%{count} témat" + topic_stat: + one: "%{number} / %{unit}" + few: "%{number} / %{unit}" + many: "%{number} / %{unit}" + other: "%{number} / %{unit}" topic_stat_unit: week: "týden" month: "měsíc" + topic_stat_all_time: + one: "Celkem %{number}" + few: "Celkem %{number}" + many: "Celkem %{number}" + other: "Celkem %{number}" n_more: "Kategorie (%{count} dalších)..." ip_lookup: title: Vyhledávání podle IP adresy @@ -916,6 +946,7 @@ cs: collapse_profile: "Sbalit" bookmarks: "Záložky" bio: "O mně" + timezone: "Časové pásmo" invited_by: "Pozvánka od" trust_level: "Důvěryhodnost" notifications: "Oznámení" @@ -1020,6 +1051,11 @@ cs: many: "Nepřečtená (%{count})" other: "Nepřečtená (%{count})" new: "Nová" + new_with_count: + one: "Nové" + few: "Nová (%{count})" + many: "Nové" + other: "Nové" archive: "Archív" groups: "Moje skupiny" move_to_inbox: "Přesunout do doručených" @@ -1878,9 +1914,11 @@ cs: dismiss_new: "Označit jako přečtené vše nové" toggle: "hromadný výběr témat" actions: "Hromadné akce" + change_category: "Nastavit kategorii..." close_topics: "Zavřít téma" archive_topics: "Archivovat témata" move_messages_to_inbox: "Přesunout do doručených" + notification_level: "Upozornění..." choose_new_category: "Zvolte novou kategorii pro témata:" selected: one: "Vybrali jste %{count} téma." @@ -2073,6 +2111,7 @@ cs: unarchive: "Navrátit z archivu" archive: "Archivovat téma" reset_read: "Vynulovat počet čtení" + make_public: "Vytvořit Veřejné Téma..." make_private: "Vytvořit soukromou zprávu " reset_bump_date: "Resetovat datum zvýraznění" feature: @@ -2309,6 +2348,8 @@ cs: revert_to_regular: "Zrušit zvýraznění" rebake: "Obnovit HTML" unhide: "Odkrýt" + change_owner: "Změnit autora..." + grant_badge: "Udělit odznak..." lock_post: "Uzamknout příspěvek " lock_post_description: "Zabránit přispěvatelům v úpravách tohoto příspěvku" unlock_post: "Odemknout příspěvek" @@ -2372,8 +2413,15 @@ cs: title: "Zobrazit html část emailu" button: "HTML" bookmarks: + create: "Vytvořit záložku" + edit: "Upravit záložku" name: "Jméno" options: "Možnosti" + actions: + delete_bookmark: + name: "Smazat záložku" + edit_bookmark: + name: "Upravit záložku" category: none: "(bez kategorie)" all: "Všechny kategorie" @@ -2484,6 +2532,7 @@ cs: flagging: title: "Děkujeme, že pomáháte udržovat komunitu zdvořilou!" action: "Nahlásit příspěvek" + take_action: "Zakročit..." take_action_options: default: title: "Zakročit" @@ -2843,6 +2892,7 @@ cs: delete: "Smazat" confirm_delete: "Jste si jistí, že chcete smazat tuto skupinu štítků?" everyone_can_use: "Tagy mohou být použity kýmkoliv" + parent_tag_placeholder: "Volitelné" topics: none: unread: "Nemáte žádná nepřečtená témata." @@ -2913,6 +2963,8 @@ cs: content: "Administrace" badges: content: "Odznaky" + everything: + content: "Vše" faq: content: "FAQ" groups: @@ -2921,6 +2973,7 @@ cs: content: "Uživatelé" my_posts: content: "Mé příspěvky" + until: "dokud:" admin_js: type_to_filter: "text pro filtrování..." admin: @@ -2946,6 +2999,7 @@ cs: latest_version: "Poslední verze" new_features: dismiss: "Označit jako přečtené" + learn_more: "Více informací" last_checked: "Naposledy zkontrolováno" refresh_problems: "Obnovit" no_problems: "Nenalezeny žádné problémy." @@ -3039,6 +3093,7 @@ cs: user: "Uživatel" title: "API" created: Vytvořený + never_used: (nikdy) generate: "Vygenerovat API klíč" revoke: "zrušit" all_users: "Všichni uživatelé" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index e6a5058f5d..3215e6df16 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -2532,6 +2532,7 @@ da: open: "Åbn emne" close: "Luk emne" multi_select: "Vælg indlæg..." + slow_mode: "Indstil langsom tilstand..." timed_update: "Indstil timerfunktion for emne..." pin: "Fastgør Emne..." unpin: "Fjern fastgøring af Emne..." @@ -2848,6 +2849,8 @@ da: delete_topic_confirm_modal_no: "Nej, behold dette emne" delete_topic_error: "Der opstod en fejl under sletning af dette emne" delete_topic: "slet emne" + add_post_notice: "Tilføj Hjælperteam Notits..." + change_post_notice: "Ændre Personale Meddelelse..." delete_post_notice: "Slet Personale Meddelelse" remove_timer: "fjern timer" edit_timer: "rediger timer" @@ -3617,7 +3620,9 @@ da: header_link_text: "Tags" configure_defaults: "Konfigurer standardindstillinger" categories: + click_to_get_started: "Klik her for at komme i gang." header_link_text: "Kategorier" + configure_defaults: "Konfigurer standardindstillinger" community: header_link_text: "Fællesskab" links: @@ -3627,6 +3632,8 @@ da: content: "Admin" badges: content: "Emblemer" + everything: + content: "Alting" faq: content: "OSS" groups: diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index a72bdc2647..04c906987a 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -176,6 +176,7 @@ de: banner: enabled: "Hat dieses Banner erstellt, %{when}. Es wird oberhalb jeder Seite angezeigt, bis es vom Benutzer weggeklickt wird." disabled: "Hat dieses Banner entfernt, %{when}. Es wird nicht mehr oberhalb jeder Seite angezeigt." + forwarded: "Hat die obige E-Mail weitergeleitet" topic_admin_menu: "Themen-Administration" skip_to_main_content: "Zum Hauptinhalt springen" emails_are_disabled: "Die ausgehende E-Mail-Kommunikation wurde von einem Administrator global deaktiviert. Es werden keinerlei Benachrichtigungen per E-Mail verschickt." @@ -1051,12 +1052,16 @@ de: perm_denied_expl: "Du hast das Anzeigen von Benachrichtigungen verboten. Aktiviere die Benachrichtigungen über deine Browser-Einstellungen." disable: "Benachrichtigungen deaktivieren" enable: "Benachrichtigungen aktivieren" + each_browser_note: "Hinweis: Du musst diese Einstellung in jedem Browser, den du verwendest, ändern. Alle Benachrichtigungen werden unabhängig von dieser Einstellung deaktiviert, wenn du die Benachrichtigungen im Benutzermenü pausierst." consent_prompt: "Möchtest du Live-Benachrichtigungen erhalten, wenn jemand auf deine Beiträge antwortet?" dismiss: "Alles gelesen" dismiss_notifications: "Alles gelesen" dismiss_notifications_tooltip: "Alle ungelesenen Benachrichtigungen als gelesen markieren" dismiss_bookmarks_tooltip: "Markiere alle ungelesenen Lesezeichen-Erinnerungen als gelesen" dismiss_messages_tooltip: "Markiere alle ungelesenen persönlichen Nachrichten als gelesen" + no_likes_title: "Du hast noch keine „Gefällt mir“-Angaben erhalten" + no_likes_body: > + Du wirst hier jedes Mal benachrichtigt, wenn jemand einem deiner Beiträge ein „Gefällt mir“ gibt, damit du sehen kannst, was andere für nützlich halten. Andere sehen das auch, wenn du ihren Beiträgen ein „Gefällt mir“ gibst!

    Benachrichtigungen über „Gefällt mir“-Angaben werden dir nie per E-Mail zugeschickt, aber du kannst in deinen Benachrichtigungseinstellungen festlegen, wie du Benachrichtigungen über „Gefällt mir“-Angaben auf der Website erhältst. no_messages_title: "Du hast keine Nachrichten" no_messages_body: > Benötigst du ein direktes persönliches Gespräch mit jemandem außerhalb des normalen Gesprächsflusses? Sende der Person eine Nachricht, indem du ihren Avatar auswählst und die Schaltfläche %{icon} verwendest.

    Wenn du Hilfe benötigst, kannst du eine Nachricht an ein Team-Mitglied senden. @@ -1206,6 +1211,62 @@ de: warnings: "Offizielle Warnungen" read_more_in_group: "Möchtest du mehr lesen? Entdecke andere Nachrichten in %{groupLink}." read_more: "Möchtest du mehr lesen? Durchsuche andere Nachrichten in persönliche Nachrichten." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {# ungelesene Nachricht} + other {# ungelesene Nachrichten} + } + { NEW, plural, + =0 {} + one { und # neue Nachricht verbleibend. Oder durchstöbere andere Nachrichten in {groupLink}} + other { und # neue Nachrichten verbleibend. Oder durchstöbere andere Nachrichten in {groupLink}} + } + } + false { + { UNREAD, plural, + =0 {} + one {# ungelesene Nachricht verbleibend. Oder durchstöbere andere Nachrichten in {groupLink}} + other {# ungelesene Nachrichten verbleibend. Oder durchstöbere andere Nachrichten in {groupLink}} + } + { NEW, plural, + =0 {} + one {# neue Nachricht verbleibend. Oder durchstöbere andere Nachrichten in {groupLink}} + other {# neue Nachrichten verbleibend. Oder durchstöbere andere Nachrichten in {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {# ungelesene Nachricht} + other {# ungelesene Nachricht} + } + { NEW, plural, + =0 {} + one { und # neue Nachricht verbleibend. Oder durchstöbere andere persönliche Nachrichten} + other { und # neue Nachrichten verbleibend. Oder durchstöbere andere persönliche Nachrichten} + } + } + false { + { UNREAD, plural, + =0 {} + one {# ungelesene Nachricht verbleibend. Oder durchstöbere andere persönliche Nachrichten} + other {# ungelesene Nachrichten verbleibend. Oder durchstöbere andere persönliche Nachrichten} + } + { NEW, plural, + =0 {} + one {# neue Nachricht verbleibend. Oder durchstöbere andere persönliche Nachrichten} + other {# neue Nachrichten verbleibend. Oder durchstöbere andere persönliche Nachrichten} + } + } + other {} + } preferences_nav: account: "Konto" security: "Sicherheit" @@ -1270,7 +1331,14 @@ de: use: "Benutze die Authentifizierungs-App" enforced_notice: "Du musst die Zwei-Faktor-Authentifizierung aktivieren, um auf die Website zugreifen zu können." disable: "Deaktivieren" + disable_confirm: "Bist du sicher, dass du die Zwei-Faktor-Authentifizierung deaktivieren willst?" delete: "Löschen" + delete_confirm_header: "Diese tokenbasierten Authentifikatoren und physischen Sicherheitsschlüssel werden gelöscht:" + delete_confirm_instruction: "Gib zur Bestätigung %{confirm} in das Feld unten ein." + delete_single_confirm_title: "Löschen eines Authentifikators" + delete_single_confirm_message: "Du löschst %{name}. Du kannst diese Aktion nicht rückgängig machen. Wenn du deine Meinung änderst, musst du den Authentifikator erneut registrieren." + delete_backup_codes_confirm_title: "Wiederherstellungscodes löschen" + delete_backup_codes_confirm_message: "Du löschst Wiederherstellungscodes. Du kannst diese Aktion nicht rückgängig machen. Wenn du deine Meinung änderst, musst du die Wiederherstellungscodes neu erstellen." save: "Speichern" edit: "Bearbeiten" edit_title: "Authentifikator bearbeiten" @@ -1437,6 +1505,8 @@ de: title: "Hintergrund-Seitentitel zeigt Anzahl von:" notifications: "Neue Benachrichtigungen" contextual: "Neuer Seiteninhalt" + bookmark_after_notification: + title: "Nachdem eine Lesezeichen-Erinnerungsbenachrichtigung gesendet wurde:" like_notification_frequency: title: "Benachrichtigung für erhaltene „Gefällt mir“ anzeigen" always: "Immer" @@ -1716,6 +1786,36 @@ de: logout_disabled: "Die Abmeldung ist deaktiviert, während sich die Website im Nur-Lesen-Modus befindet." staff_writes_only_mode: enabled: "Diese Website befindet sich im Nur-Team-Modus. Du kannst weiterhin Inhalte lesen, aber das Antworten, Vergeben von „Gefällt mir“ und Durchführen einiger weiterer Aktionen ist derzeit nur für Team-Mitglieder möglich." + too_few_topics_and_posts_notice_MF: | + Beginnen wir die Diskussion! Es { currentTopics, plural, + one {gibt # Thema} + other {gibt # Themen} + } und { currentPosts, plural, + one {# Beitrag} + other {# Beiträge} + }. Besucher brauchen mehr zum Lesen und Beantworten – wir empfehlen mindestens { requiredTopics, plural, + one {# Thema} + other {# Themen} + } und { requiredPosts, plural, + one {# Beitrag} + other {# Beiträge} + }. Nur das Team kann diese Nachricht sehen. + too_few_topics_notice_MF: | + Beginnen wir die Diskussion! Es { currentTopics, plural, + one {gibt # Thema} + other {gibt # Themen} + }. Besucher brauchen mehr zum Lesen und Beantworten – wir empfehlen mindestens { requiredTopics, plural, + one {# Thema} + other {# Themen} + }. Nur das Team kann diese Nachricht sehen. + too_few_posts_notice_MF: | + Beginnen wir die Diskussion! Es { currentPosts, plural, + one {gibt # Beitrag} + other {gibt # Beiträge} + }. Besucher brauchen mehr zum Lesen und Beantworten – wir empfehlen mindestens { requiredPosts, plural, + one {# Beitrag} + other {# Beiträge} + }. Nur das Team kann diese Nachricht sehen. logs_error_rate_notice: reached_hour_MF: | {relativeAge}{rate, plural, one {# Fehler/Stunde} other {# Fehler/Stunde}} hat die Grenze der Website-Einstellung von {limit, plural, one {# Fehler/Stunde} other {# Fehlern/Stunde}} erreicht. @@ -1823,6 +1923,9 @@ de: username: "Benutzername" password: "Passwort" show_password: "Anzeigen" + hide_password: "Ausblenden" + show_password_title: "Passwort anzeigen" + hide_password_title: "Passwort ausblenden" second_factor_title: "Zwei-Faktor-Authentifizierung" second_factor_description: "Bitte gib den Authentifizierungscode aus deiner App ein:" second_factor_backup: "Anmeldung mit einem Wiederherstellungscode" @@ -1844,6 +1947,7 @@ de: blank_username_or_password: "Bitte gib deine E-Mail-Adresse oder deinen Benutzernamen und dein Passwort ein." reset_password: "Passwort zurücksetzen" logging_in: "Anmeldung läuft …" + previous_sign_up: "Du hast bereits ein Konto?" or: "Oder" authenticating: "Authentifiziere …" awaiting_activation: "Dein Konto ist noch nicht aktiviert. Verwende den „Passwort vergessen“-Link, um eine weitere E-Mail mit Anweisungen zur Aktivierung zu erhalten." @@ -2018,10 +2122,17 @@ de: private: "Du hast @%{username} erwähnt, aber diese Person wird nicht benachrichtigt, da sie diese persönliche Nachricht nicht sehen kann. Lade sie zu dieser persönlichen Nachricht ein." muted_topic: "Du hast @%{username} erwähnt, aber diese Person wird nicht benachrichtigt, weil sie dieses Thema stummgeschaltet hat." not_allowed: "Du hast @%{username} erwähnt, aber diese Person wird nicht benachrichtigt, weil sie nicht zu diesem Thema eingeladen wurde." + cannot_see_group_mention: + not_mentionable: "Du kannst die Gruppe @%{group} nicht erwähnen." + some_not_allowed: + one: "Du hast @%{group} erwähnt, aber nur %{count} Mitglied wird benachrichtigt, weil die anderen Mitglieder diese persönliche Nachricht nicht sehen können. Du musst sie zu dieser persönlichen Nachricht einladen." + other: "Du hast @%{group} erwähnt, aber nur %{count} Mitglieder werden benachrichtigt, weil die anderen Mitglieder diese persönliche Nachricht nicht sehen können. Du musst sie zu dieser persönlichen Nachricht einladen." + not_allowed: "Du hast @%{group} erwähnt, aber keines der Mitglieder wird benachrichtigt, weil sie diese persönliche Nachricht nicht sehen können. Du musst sie zu dieser persönlichen Nachricht einladen." here_mention: one: "Indem du @%{here} erwähnst, bist du im Begriff, %{count} Benutzer zu benachrichtigen – bist du sicher?" other: "Indem du @%{here} erwähnst, bist du im Begriff, %{count} Benutzer zu benachrichtigen – bist du sicher?" duplicate_link: "Es sieht so aus, als ob dein Link zu %{domain} bereits im Thema von @%{username} in einer Antwort vor %{ago} geteilt wurde – bist du sicher, dass du ihn noch einmal teilen möchtest?" + duplicate_link_same_user: "Es sieht so aus, als hättest du bereits einen Link zu %{domain} in diesem Thema in einer Antwort am %{ago} gepostet – bist du sicher, dass du ihn erneut posten willst?" reference_topic_title: "AW: %{title}" error: title_missing: "Titel ist erforderlich" @@ -2157,6 +2268,12 @@ de: high_priority: one: "%{count} ungelesene Benachrichtigung hoher Priorität" other: "%{count} ungelesene Benachrichtigungen hoher Priorität" + new_message_notification: + one: "%{count} neue Nachrichtenbenachrichtigung" + other: "%{count} neue Nachrichtenbenachrichtigungen" + new_reviewable: + one: "%{count} neu zur Überprüfung" + other: "%{count} neu zur Überprüfung" title: "Benachrichtigungen über namentliche Erwähnungen mit @, Antworten auf deine Beiträge und Themen, Nachrichten usw." none: "Die Benachrichtigungen können derzeit nicht geladen werden." empty: "Keine Benachrichtigungen gefunden." @@ -2202,6 +2319,7 @@ de: reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} – abgeschlossen" + new_features: "Neue Funktionen verfügbar!" dismiss_confirmation: body: default: @@ -2256,6 +2374,7 @@ de: membership_request_consolidated: "Neue Mitgliedschaftsanfragen" reaction: "neue Reaktion" votes_released: "Abstimmung wurde freigegeben" + new_features: "neue Discourse-Funktionen wurden veröffentlicht!" upload_selector: uploading: "Wird hochgeladen" processing: "Upload wird verarbeitet" @@ -2324,6 +2443,7 @@ de: status: "filtern nach Themenstatus" full_search: "startet die Ganzseitensuche" full_search_key: "%{modifier} + Eingabetaste" + me: "zeigt nur deine Beiträge" advanced: title: Erweiterte Filter posted_by: @@ -2564,7 +2684,48 @@ de: show_links: "zeige Links innerhalb dieses Themas" collapse_details: "Themendetails einklappen" expand_details: "Themendetails erweitern" + read_more_in_category: "Willst du mehr lesen? Durchstöbere andere Themen in %{categoryLink} oder sieh dir die aktuellen Themen an." + read_more: "Willst du mehr lesen? Durchstöbere alle Kategorien oder sieh dir die aktuellen Themen an." unread_indicator: "Kein Mitglied hat den letzten Beitrag dieses Themas bisher gelesen." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {# ungelesenes Thema} + other {# ungelesene Themen} + } + { NEW, plural, + =0 {} + one { und # neues Thema verbleibend.} + other { und # neue Themen verbleibend.} + } + } + false { + { UNREAD, plural, + =0 {} + one {# ungelesenes Thema verbleibend.} + other {# ungelesene Themen verbleibend.} + } + { NEW, plural, + =0 {} + one {# neues Thema verbleibend.} + other {# neue Themen verbleibend.} + } + } + other {} + } + { HAS_CATEGORY, select, + true { Oder durchstöbere andere Themen in {categoryLink}} + false { Oder sieh dir die aktuellen Themen an} + other {} + } + bumped_at_title: | + Erster Beitrag: %{createdAtDate} + Gepostet: %{bumpedAtDate} + browse_all_categories_latest: "Durchstöbere alle Kategorien oder sieh dir die aktuellen Themen an." + browse_all_categories_latest_or_top: "Durchstöbere alle Kategorien, sieh dir die aktuellen Themen oder die Top-Themen an:" + browse_all_tags_or_latest: "Durchstöbere alle Schlagwörter oder sieh dir die aktuellen Themen an." suggest_create_topic: Bereit, eine neue Unterhaltung zu beginnen? jump_reply_up: zur vorherigen Antwort springen jump_reply_down: zur nachfolgenden Antwort springen @@ -2985,6 +3146,7 @@ de: one: "Es kann leider nur %{count} Datei gleichzeitig hochgeladen werden." other: "Es können leider nur %{count} Dateien gleichzeitig hochgeladen werden." upload_not_authorized: "Entschuldige, die Datei, die du hochladen möchtest, ist nicht erlaubt (erlaubte Dateiendungen sind: %{authorized_extensions})." + no_uploads_authorized: "Es dürfen keine Dateien hochgeladen werden." image_upload_not_allowed_for_new_user: "Entschuldige, neue Benutzer dürfen keine Bilder hochladen." attachment_upload_not_allowed_for_new_user: "Entschuldige, neue Benutzer dürfen keine Anhänge hochladen." attachment_download_requires_login: "Entschuldige, du musst angemeldet sein, um Anhänge herunterladen zu können." @@ -2996,6 +3158,7 @@ de: via_email: "dieser Beitrag ist per E-Mail eingetroffen" via_auto_generated_email: "dieser Beitrag ist per automatisch generierter E-Mail eingegangen" whisper: "dies ist ein privater geflüsterter Beitrag für Moderatoren" + whisper_groups: "dieser Beitrag ist ein privater geflüsterter Beitrag, der nur für %{groupNames} sichtbar ist" wiki: about: "dieser Beitrag ist ein Wiki" few_likes_left: "Danke fürs Teilen der Liebe! Du hast für heute nur noch wenige „Gefällt mir“ übrig." @@ -3218,6 +3381,7 @@ de: pending_permission_change_alert: "Du hast %{group} nicht zu dieser Kategorie hinzugefügt; klicke diese Schaltfläche, um sie zuzufügen." images: "Bilder" email_in: "Benutzerdefinierte Adresse für eingehende E-Mails:" + email_in_tooltip: "Du kannst mehrere E-Mail-Adressen mit dem Zeichen | trennen." email_in_allow_strangers: "Akzeptiere E-Mails von anonymen Benutzern." email_in_disabled: "Das Erstellen von neuen Themen per E-Mail ist in den Website-Einstellungen deaktiviert. Um das Erstellen von neuen Themen per E-Mail zu erlauben, " email_in_disabled_click: 'Aktiviere die Einstellung „email in“.' @@ -3498,7 +3662,6 @@ de: this_week: "Woche" today: "Heute" browser_update: 'Leider wird dein Browser nicht unterstützt. Bitte wechsle zu einem unterstützten Browser, um die Inhalte in vollem Umfang zu sehen, dich anzumelden und zu antworten.' - safari_13_warning: Diese Website wird bald die Unterstützung für iOS und Safari Version 13 und darunter entfernen. Eine vereinfachte Nur-Lesen-Version wird weiterhin verfügbar sein. (Mehr Informationen) permission_types: full: "Erstellen/Antworten/Ansehen" create_post: "Antworten/Ansehen" @@ -3831,12 +3994,15 @@ de: second_factor_auth: redirect_after_success: "Die Zwei-Faktor-Authentifizierung war erfolgreich. Weiterleitung zur vorherigen Seite …" sidebar: + show_sidebar: "Seitenleiste anzeigen" + hide_sidebar: "Seitenleiste ausblenden" unread_count: one: "%{count} ungelesen" other: "%{count} ungelesen" new_count: one: "%{count} neu" other: "%{count} neu" + toggle_section: "Abschnitt umschalten" more: "Mehr" all_categories: "Alle Kategorien" all_tags: "Alle Schlagwörter" @@ -3845,6 +4011,7 @@ de: header_link_text: "Über uns" messages: header_link_text: "Nachrichten" + header_action_title: "Eine persönliche Nachricht erstellen" links: inbox: "Posteingang" sent: "Gesendet" @@ -3861,6 +4028,7 @@ de: none: "Du hast keine Schlagwörter hinzugefügt." click_to_get_started: "Klicke hier, um zu beginnen." header_link_text: "Schlagwörter" + header_action_title: "Bearbeite die Schlagwörter in deiner Seitenleiste" configure_defaults: "Standardeinstellungen konfigurieren" categories: links: @@ -3870,32 +4038,43 @@ de: none: "Du hast keine Kategorien hinzugefügt." click_to_get_started: "Klicke hier, um zu beginnen." header_link_text: "Kategorien" + header_action_title: "Bearbeite die Kategorien in deiner Seitenleiste" configure_defaults: "Standardeinstellungen konfigurieren" community: header_link_text: "Community" + header_action_title: "Thema erstellen" links: about: content: "Über uns" + title: "Weitere Details zu dieser Website" admin: content: "Admin-Panel" + title: "Website-Einstellungen und -Berichte" badges: content: "Abzeichen" + title: "Alle Abzeichen, die es zu verdienen gibt" everything: content: "Alles" title: "Alle Themen" faq: content: "FAQ" + title: "Richtlinien für die Nutzung dieser Website" groups: content: "Gruppen" + title: "Liste der verfügbaren Benutzergruppen" users: content: "Benutzer" + title: "Liste aller Benutzer" my_posts: content: "Meine Beiträge" + title: "Meine aktuelle Themenaktivität" + title_drafts: "Meine nicht geposteten Entwürfe" draft_count: one: "%{count} Entwurf" other: "%{count} Entwürfe" review: content: "Überprüfen" + title: "Gemeldete Beiträge und andere Elemente in der Warteschlange" pending_count: "%{count} ausstehend" welcome_topic_banner: title: "Erstelle dein Willkommensthema" @@ -4048,6 +4227,9 @@ de: other: "%{count} Benutzer haben die neuen E-Mail-Domains und werden der Gruppe hinzugefügt." automatic_membership_associated_groups: "Benutzer, die Mitglied einer Gruppe eines hier aufgeführten Dienstes sind, werden dieser Gruppe automatisch hinzugefügt, wenn sie sich mittels dieses Dienstes anmelden." primary_group: "Automatisch als primäre Gruppe festlegen" + alert: + primary_group: "Da es sich um eine primäre Gruppe handelt, wird der Name „%{group_name}“ in CSS-Klassen verwendet, die von jedem eingesehen werden können." + flair_group: "Da diese Gruppe Flair für ihre Mitglieder hat, wird der Name „%{group_name}“ für jeden sichtbar sein." name_placeholder: "Gruppenname, keine Leerzeichen, gleiche Regel wie beim Benutzernamen" primary: "Primäre Gruppe" no_primary: "(keine primäre Gruppe)" @@ -4057,6 +4239,10 @@ de: about: "Hier kannst du Gruppenzugehörigkeiten und Gruppennamen bearbeiten." group_members: "Gruppenmitglieder" delete: "Löschen" + delete_confirm: "Bist du sicher, dass du diese Gruppe löschen willst?" + delete_with_messages_confirm: + one: "Wenn du diese Gruppe löschst, wird %{count} Nachricht verwaist sein, die Gruppenmitglieder haben keinen Zugriff mehr darauf." + other: "Wenn du diese Gruppe löschst, werden %{count} Nachrichten verwaist sein, die Gruppenmitglieder haben keinen Zugriff mehr darauf." delete_failed: "Gruppe konnte nicht gelöscht werden. Wenn dies eine automatische Gruppe ist, kann sie nicht gelöscht werden." delete_automatic_group: Dies ist eine automatische Gruppe und sie kann nicht gelöscht werden. delete_owner_confirm: "Eigentümerrechte für „%{username}“ entfernen?" @@ -4122,7 +4308,6 @@ de: topics: read: Lies ein Thema oder einen bestimmten Beitrag darin. RSS wird ebenfalls unterstützt. write: Erstelle ein neues Thema oder schreibe einen Beitrag zu einem bestehenden. - update: Aktualisiere ein Thema. Ändere den Titel, die Kategorie, Schlagwörter usw. read_lists: Lies Themenlisten wie „Angesagt“, „Neu“, „Aktuell“ usw. RSS wird ebenfalls unterstützt. posts: edit: Bearbeite jeden Beitrag oder einen bestimmten. @@ -4141,6 +4326,9 @@ de: anonymize: Benutzerkonten anonymisieren. delete: Benutzerkonten löschen. list: Benutzerliste abrufen. + user_status: + read: Benutzerstatus lesen. + update: Benutzerstatus aktualisieren. email: receive_emails: Kombiniere diesen Bereich mit dem E-Mail-Empfänger, um eingehende E-Mails zu verarbeiten. badges: @@ -4165,6 +4353,7 @@ de: create: "Erstellen" edit: "Bearbeiten" save: "Speichern" + description_label: "Ereignisauslöser" controls: "Kontrollelemente" go_back: "Zurück zur Liste" payload_url: "Payload-URL" @@ -4267,6 +4456,8 @@ de: broken_route: "Link zu „%{name}“ kann nicht konfiguriert werden. Vergewissere dich, dass Werbeblocker deaktiviert sind, und versuche, die Seite neu zu laden." navigation_menu: sidebar: "Seitenleiste" + header_dropdown: "Kopfzeilen-Drop-down" + legacy: "Legacy" backups: title: "Backups" menu: @@ -4951,6 +5142,7 @@ de: user: suspend_failed: "Beim Sperren dieses Benutzers ist etwas schiefgegangen %{error}" unsuspend_failed: "Beim Entsperren dieses Benutzers ist etwas schiefgegangen %{error}" + suspend_duration: "Benutzer sperren bis:" suspend_reason_label: "Was ist der Grund für die Sperre? Dieser Text ist auf der Profilseite des Benutzers für jeden sichtbar und wird dem Benutzer angezeigt, wenn sich dieser anmelden will. Fasse dich kurz." suspend_reason_hidden_label: "Was ist der Grund für die Sperre? Dieser Text wird dem Benutzer angezeigt, wenn er versucht, sich anzumelden. Fasse dich kurz." suspend_reason: "Grund" @@ -4974,7 +5166,9 @@ de: silence_message: "E-Mail-Nachricht" silence_message_placeholder: "(leer lassen, um Standardnachricht zu schicken)" suspended_until: "(bis %{until})" + suspend_forever: "Für immer sperren" cant_suspend: "Der Benutzer kann nicht gesperrt werden." + cant_silence: "Dieser Benutzer kann nicht stummgeschaltet werden." delete_posts_failed: "Es gab ein Problem beim Löschen der Beiträge." post_edits: "Beitragsbearbeitungen" view_edits: "Bearbeitungen anzeigen" @@ -4984,6 +5178,8 @@ de: penalty_post_edit: "Beitrag bearbeiten" penalty_post_none: "Nichts tun" penalty_count: "Anzahl der Strafen" + penalty_history_MF: >- + In den letzten 6 Monaten wurde dieser Benutzer { SUSPENDED, plural, one {# Mal} other {# Mal} } gesperrt und { SILENCED, plural, one {# Mal} other {# Mal} } stummgeschaltet. clear_penalty_history: title: "Lösche die Strafenhistorie" description: "Benutzer mit Strafen können VS3 nicht erreichen" @@ -5138,10 +5334,14 @@ de: silenced_count: "Stummgeschaltet" suspended_count: "Gesperrt" last_six_months: "Letzte 6 Monate" + other_matches: + one: "Es gibt %{count} anderen Benutzer mit der gleichen IP-Adresse. Überprüfe ihn und wähle einen verdächtigen Benutzer aus, um ihn zusammen mit %{username} zu bestrafen." + other: "Es gibt %{count} andere Benutzer mit der gleichen IP-Adresse. Überprüfe sie und wähle verdächtige Benutzer aus, um sie zusammen mit %{username} zu bestrafen." other_matches_list: username: "Benutzername" trust_level: "Vertrauensstufe" read_time: "Lesezeit" + topics_entered: "Besuchte Themen" posts: "Beiträge" tl3_requirements: title: "Anforderungen für Vertrauensstufe 3" @@ -5458,8 +5658,10 @@ de: finish: "Installation verlassen" back: "Zurück" next: "Nächstes" + configure_more: "Mehr konfigurieren …" step-text: "Schritt" step: "%{current} von %{total}" + upload: "Datei hochladen" uploading: "Wird hochgeladen …" upload_error: "Entschuldige, es gab einen Fehler beim Hochladen der Datei. Bitte versuche es noch einmal." staff_count: diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 4384c94a80..66a81097db 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -1941,9 +1941,11 @@ el: dismiss_new: "Αγνόησε τα νέα" toggle: "εναλλαγή μαζικής επιλογής νημάτων" actions: "Μαζικές Ενέργειες" + change_category: "Θέσε Κατηγορία..." close_topics: "Κλείσιμο Νημάτων" archive_topics: "Αρχειοθέτηση Νημάτων" move_messages_to_inbox: "Μετακίνηση στα Εισερχόμενα" + notification_level: "Ειδοποιήσεις..." choose_new_category: "Διάλεξε νέα κατηγορία για τα νήματα:" selected: one: "Έχεις διαλέξει %{count} νήμα." @@ -2025,6 +2027,7 @@ el: enable: "Ενεργοποίηση" remove: "Απενεργοποίηση" hours: "ώρες:" + minutes: "Λεπτά:" durations: 10_minutes: "10 Λεπτά" 15_minutes: "15 Λεπτά" @@ -2143,6 +2146,7 @@ el: unarchive: "Επαναφορά Νήματος από Αρχείο" archive: "Αρχειοθέτηση Νήματος" reset_read: "Μηδενισμός Διαβασμένων" + make_public: "Κάνε Δημόσιο το Νήμα..." make_private: "Κάνε προσωπικό μήνυμα" reset_bump_date: "Επαναφορά ημερομηνίας ώθησης" feature: @@ -2403,6 +2407,8 @@ el: rebake: "Ανανέωση HTML" publish_page: "Δημοσίευση σελίδας" unhide: "Επανεμφάνιση" + change_owner: "Αλλαγή Ιδιοκτησίας..." + grant_badge: "Απονομή Παράσημου..." lock_post: "Κλείδωμα ανάρτησης" lock_post_description: "αποτρέψτε τον συντάκτη από την επεξεργασία αυτής της ανάρτησης" unlock_post: "Ξεκλείδωμα ανάρτησης" @@ -2410,6 +2416,7 @@ el: delete_topic_disallowed_modal: "Δεν έχετε άδεια διαγραφής αυτού του θέματος. Εάν θέλετε πραγματικά να διαγραφεί, υποβάλετε μια επισήμανση για προσοχή από συντονιστή μαζί με το σκεπτικό." delete_topic_disallowed: "δεν έχετε άδεια να διαγράψετε αυτό το θέμα" delete_topic: "διαγραφή νήματος" + add_post_notice: "Προσθήκη ειδοποίησης προσωπικού..." remove_timer: "αφαίρεση χρονοδιακόπτη" actions: people: @@ -2614,6 +2621,7 @@ el: flagging: title: "Ευχαριστούμε για τη συνεισφορά σου!" action: "Επισήμανση Ανάρτησης" + take_action: "Λάβε Δράση..." take_action_options: default: title: "Λάβε Δράση" @@ -3000,6 +3008,7 @@ el: delete: "Διαγραφή" confirm_delete: "Είσαι βέβαιος πως θέλεις να διαγράψεις αυτή την ομάδα ετικετών;" everyone_can_use: "Οι ετικέτες μπορούν να χρησιμοποιηθούν από όλους" + parent_tag_placeholder: "Προεραιτικό" topics: none: unread: "Δεν έχεις αδιάβαστα νήματα." @@ -3073,6 +3082,8 @@ el: content: "Διαχείριση" badges: content: "Παράσημα" + everything: + content: "Τα πάνΤα" faq: content: "Συχνές ερωτήσεις" groups: @@ -3083,6 +3094,7 @@ el: content: "Οι αναρτήσεις μου" review: content: "Ανασκόπηση" + until: "Μέχρι:" admin_js: type_to_filter: "γράψε εδώ για φιλτράρισμα..." admin: @@ -3112,6 +3124,7 @@ el: problems_found: "Μερικές συμβουλές με βάση τις τρέχουσες ρυθμίσεις του ιστότοπού σας" new_features: dismiss: "Απόρριψη" + learn_more: "Μάθε περισσότερα" last_checked: "Τελευταίος έλεγχος" refresh_problems: "Ανανέωση" no_problems: "Δεν βρέθηκε κανένα πρόβλημα." diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a807ded6f4..37b561e5a5 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -197,6 +197,7 @@ en: topic_admin_menu: "topic actions" skip_to_main_content: "Skip to main content" + skip_user_nav: "Skip to profile content" emails_are_disabled: "All outgoing email has been globally disabled by an administrator. No email notifications of any kind will be sent." emails_are_disabled_non_staff: "Outgoing email has been disabled for non-staff users." @@ -1121,7 +1122,7 @@ en: perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings." disable: "Disable Notifications" enable: "Enable Notifications" - each_browser_note: 'Note: You have to change this setting on every browser you use. All notifications will be disabled if you pause notifications from user menu, regardless of this setting.' + each_browser_note: "Note: You have to change this setting on every browser you use. All notifications will be disabled if you pause notifications from user menu, regardless of this setting." consent_prompt: "Do you want live notifications when people reply to your posts?" dismiss: "Dismiss" dismiss_notifications: "Dismiss All" @@ -3548,6 +3549,8 @@ en: last: "Last revision" hide: "Hide revision" show: "Show revision" + destroy: "Delete revisions" + destroy_confirm: "Are you sure you want to delete all of the revisions on this post? This action is permanent." revert: "Revert to revision %{revision}" edit_wiki: "Edit Wiki" edit_post: "Edit Post" @@ -3806,7 +3809,9 @@ en: inappropriate: "It's Inappropriate" spam: "It's Spam" custom_placeholder_notify_user: "Be specific, be constructive, and always be kind." + notify_user_textarea_label: "Message for the user" custom_placeholder_notify_moderators: "Let us know specifically what you are concerned about, and provide relevant links and examples where possible." + notify_moderators_textarea_label: "Message for the moderators" custom_message: at_least: one: "enter at least %{count} character" @@ -3987,8 +3992,6 @@ en: browser_update: 'Unfortunately, your browser is unsupported. Please switch to a supported browser to view rich content, log in and reply.' - safari_13_warning: This site will soon remove support for iOS and Safari versions 13 and below. A simplified read-only version will remain available. (more information) - permission_types: full: "Create / Reply / See" create_post: "Reply / See" @@ -4670,7 +4673,7 @@ en: topics: read: Read a topic or a specific post in it. RSS is also supported. write: Create a new topic or post to an existing one. - update: Update a topic. Change the title, category, tags, etc. + update: Update a topic. Change the title, category, tags, status, archetype, featured_link etc. read_lists: Read topic lists like top, new, latest, etc. RSS is also supported. posts: edit: Edit any post or a specific one. @@ -4687,6 +4690,7 @@ en: update: Update user profile information. log_out: Log out all sessions for a user. anonymize: Anonymize user accounts. + suspend: Suspend user accounts. delete: Delete user accounts. list: Get a list of users. user_status: @@ -4694,6 +4698,8 @@ en: update: Update user status. email: receive_emails: Combine this scope with the mail-receiver to process incoming emails. + invites: + create: Send email invites or generate invite links. badges: create: Create a new badge. show: Obtain information about a badge. @@ -4702,6 +4708,9 @@ en: list_user_badges: List user badges. assign_badge_to_user: Assign a badge to a user. revoke_badge_from_user: Revoke a badge from a user. + search: + show: Search using the `/search.json?q=term` endpoint. + query: Search using the /search/query?term=term` endpoint. wordpress: publishing: Necessary for the WP Discourse plugin publishing features (required). commenting: Necessary for the WP Discourse plugin commenting features. diff --git a/config/locales/client.en_GB.yml b/config/locales/client.en_GB.yml index 2cdd3aa67f..f6bd76d107 100644 --- a/config/locales/client.en_GB.yml +++ b/config/locales/client.en_GB.yml @@ -93,6 +93,8 @@ en_GB: admin: dashboard: exception_error: Sorry, an error occurred whilst executing the query + reports: + no_data: "No data to display." api: scopes: descriptions: diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 445470f110..b10cde7e8a 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -176,6 +176,7 @@ es: banner: enabled: "Hizo esto un banner %{when}. Aparecerá en la parte superior de cada página hasta que el usuario lo descarte." disabled: "Quitó este banner %{when}. Ya no aparecerá en la parte superior de cada página." + forwarded: "Reenvió el correo electrónico de arriba" topic_admin_menu: "acciones del tema" skip_to_main_content: "Saltar al contenido principal" emails_are_disabled: "Todos los correos electrónicos salientes han sido desactivados globalmente por un administrador. No se enviarán notificaciones por correo electrónico de ningún tipo." @@ -1007,6 +1008,7 @@ es: notification_schedule: title: "Horario de notificaciones" label: "Activar horario personalizado de notificaciones" + tip: "Fuera de este horario, tus notificaciones se pondrán en pausa." midnight: "Medianoche" none: "Ninguno" monday: "Lunes" @@ -1050,12 +1052,16 @@ es: perm_denied_expl: "Has denegado el permiso para las notificaciones. Configura tu navegador para permitir notificaciones. " disable: "Desactivar notificaciones" enable: "Activar notificaciones" + each_browser_note: "Nota: Hay que cambiar esta configuración en cada navegador que utilices. Todas las notificaciones se desactivarán si pausas las notificaciones desde el menú de usuario, independientemente de este ajuste." consent_prompt: "¿Quieres recibir notificaciones en tiempo real cuando alguien responda a tus mensajes?" dismiss: "Descartar" dismiss_notifications: "Descartar todo" dismiss_notifications_tooltip: "Marcar todas las notificaciones como leídas" dismiss_bookmarks_tooltip: "Marcar todos los recordatorios de marcadores como leídos" dismiss_messages_tooltip: "Marcar todos los mensajes personales como leídos" + no_likes_title: "Todavía no has recibido ningún «Me gusta»" + no_likes_body: > + Se te notificará aquí cada vez que a alguien le guste una de tus publicaciones, para que puedas ver lo que otros encuentran valioso. Los demás también verán lo mismo cuando te gusten sus publicaciones.

    Las notificaciones de «Me gusta» nunca se te envían por correo electrónico, pero puedes ajustar la forma en que recibes las notificaciones de «Me gusta» en tus preferencias de notificaciones. no_messages_title: "No tienes ningún mensaje" no_messages_body: > ¿Necesitas tener una conversación personal directamente con una persona fuera de la conversación actual? Mándale un mensaje seleccionando su foto y usando el botón de %{icon} mensaje.

    Si necesitas ayuda, puedes enviar un mensaje a alguien del equipo. @@ -1205,6 +1211,62 @@ es: warnings: "Advertencias oficiales" read_more_in_group: "¿Quieres leer más? Consulta otros mensajes de %{groupLink}." read_more: "¿Quieres seguir leyendo? Echa un vistazo a otros mensajes en tus mensajes personales." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Hay # sin leer} + other {Hay # sin leer} + } + { NEW, plural, + =0 {} + one { y # mensaje nuevo restante, o explora otros mensajes en {groupLink}} + other { y # mensajes nuevos restantes, o explora otros mensajes en {groupLink}} + } + } + false { + { UNREAD, plural, + =0 {} + one {Hay # mensaje sin leer restante, o explora otros mensajes en {groupLink}} + other {Hay # mensajes sin leer restantes, o explora otros mensajes en {groupLink}} + } + { NEW, plural, + =0 {} + one {Hay # mensaje nuevo restante, o explora otros mensajes en {groupLink}} + other {Hay # mensajes nuevos restante, o explora otros mensajes en {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Hay # sin leer} + other {Hay # sin leer} + } + { NEW, plural, + =0 {} + one { y # mensaje nuevo restante, o explora otros mensajes personales} + other { y # mensajes nuevos restantes, o explora otros mensajes personales} + } + } + false { + { UNREAD, plural, + =0 {} + one {Hay # mensaje sin leer restante, o explora otros mensajes personales} + other {Hay # mensajes sin leer restantes, o explora otros mensajes personales} + } + { NEW, plural, + =0 {} + one {Hay # mensaje nuevo restante, o explora otros mensajes personales} + other {Hay # mensajes nuevos restantes, o explora otros mensajes personales} + } + } + other {} + } preferences_nav: account: "Cuenta" security: "Seguridad" @@ -1269,7 +1331,14 @@ es: use: "Utilizar la aplicación de autenticación" enforced_notice: "Es obligatorio que actives la autenticación de dos factores antes de acceder al sitio web." disable: "Desactivar" + disable_confirm: "¿Seguro que quieres desactivar todos la autenticación de dos factores?" delete: "Eliminar" + delete_confirm_header: "Estos autentificadores basados en tokens y claves de seguridad física se eliminarán:" + delete_confirm_instruction: "Para confirmar, escribe %{confirm} en la casilla de abajo." + delete_single_confirm_title: "Eliminar un autentificador" + delete_single_confirm_message: "Estás eliminando %{name}. Esta acción no se puede deshacer. Si cambias de opinión, debes volver a registrar este autentificador." + delete_backup_codes_confirm_title: "Eliminar códigos de copia de seguridad" + delete_backup_codes_confirm_message: "Estás eliminando código de copia de seguridad. Esta acción no se puede deshacer. Si cambias de opinión, debes volver a generar estos códigos de copia de seguridad." save: "Guardar" edit: "Editar" edit_title: "Editar autenticador" @@ -1436,6 +1505,8 @@ es: title: "El título de la página muestra un recuento de:" notifications: "Notificaciones nuevas" contextual: "Contenido nuevo en la página" + bookmark_after_notification: + title: "Después de enviar una notificación de recordatorio de marcador:" like_notification_frequency: title: "Notificar cuando me dan me gusta" always: "Siempre" @@ -1661,6 +1732,7 @@ es: save: "Guardar" set_custom_status: "Escribir estado personalizado" what_are_you_doing: "¿Qué estás haciendo?" + pause_notifications: "Pausar notificaciones" remove_status: "Quitar estado" user_tips: primary: "¡Entendido!" @@ -1714,6 +1786,23 @@ es: logout_disabled: "No se puede cerrar sesión mientras el sitio se encuentre en modo de solo lectura." staff_writes_only_mode: enabled: "Este sitio está en modo solo para el personal. Sigue navegando, pero las respuestas, los «me gusta» y otras acciones están limitadas solo a los miembros del personal." + too_few_topics_and_posts_notice_MF: | + ¡Comencemos la discusión! Hay { currentTopics, plural, + one {# tema} + other {# temas} + } y { currentPosts, plural, + one {# publicación} + other {# publicaciones} + }. Los visitantes necesitan más contenido para leer y responder: nosotros recomendamos al menos { requiredTopics, plural, + one {# tema} + other {# temas} + } y { requiredPosts, plural, + one {# publicación} + other {# publicaciones} + }. Solo los miembros del equipo pueden ver este mensaje. + too_few_topics_notice_MF: "¡Comencemos la discusión! Hay { currentTopics, plural, \n one {# tema}\n other {# temas}}.\nLos visitantes necesitan más contenido para leer y responder: nosotros recomendamos al menos { requiredTopics, plural, \n one {# tema} \n other {# temas}}. \nSolo los miembros del equipo pueden ver este mensaje.\n" + too_few_posts_notice_MF: | + ¡Comencemos la discusión! Hay { currentPosts, plural, one {# publicación} other {# publicaciones}}. Los visitantes necesitan más contenido para leer y responder – nosotros recomendamos al menos { requiredPosts, plural, one {# publicación} other {# publicaciones}}. Solo los miembros del equipo pueden ver este mensaje. logs_error_rate_notice: reached_hour_MF: | {relativeAge}{rate, plural, one {# error/hora} other {# errores/hora}} alcanzó el límite de la configuración del sitio de {limit, plural, one {# error/hora} other {# errores/hora}}. @@ -1821,6 +1910,9 @@ es: username: "Usuario" password: "Contraseña" show_password: "Mostrar" + hide_password: "Ocultar" + show_password_title: "Mostrar contraseña" + hide_password_title: "Ocultar contraseña" second_factor_title: "Autenticación de dos factores" second_factor_description: "Introduce el código de autenticación desde tu aplicación:" second_factor_backup: "Iniciar sesión utilizando un código de copia de seguridad" @@ -1842,6 +1934,7 @@ es: blank_username_or_password: "Introduce tu correo electrónico o nombre de usuario y tu contraseña." reset_password: "Restablecer contraseña" logging_in: "Iniciando sesión..." + previous_sign_up: "¿Ya tienes una cuenta?" or: "O" authenticating: "Autenticando..." awaiting_activation: "Tu cuenta está pendiente de activación, usa el enlace de «olvidé contraseña» para recibir otro correo electrónico de activación." @@ -2016,10 +2109,17 @@ es: private: "Has mencionado a @%{username}, pero no le va a llegar una notificación porque no puede ver este mensaje. Tendrás que invitarle al mensaje." muted_topic: "Has mencionado a @%{username}, pero no le va a llegar una notificación porque ha silenciado este tema." not_allowed: "Has mencionado a @%{username}, pero no le va a llegar una notificación porque no está invitado a este tema." + cannot_see_group_mention: + not_mentionable: "No puedes mencionar el grupo @%{group}." + some_not_allowed: + one: "Has mencionado a @%{grupo} pero solo se notificará a %{count} miembro porque los demás miembros no pueden ver este mensaje personal. Tendrás que invitarles a este mensaje personal." + other: "Has mencionado a @%{grupo} pero solo se notificará a %{count} miembros porque los demás miembros no pueden ver este mensaje personal. Tendrás que invitarles a este mensaje personal." + not_allowed: "Has mencionado a @%{group}, pero no se notificará a ninguno de sus miembros porque no puede ver este mensaje personal. Tendrás que invitarle al mensaje personal." here_mention: one: "Al mencionar a @%{here} mencionarás a %{count} usuario – ¿quieres continuar?" other: "Al mencionar a @%{here} mencionarás a %{count} usuarios – ¿quieres continuar?" duplicate_link: "Parece que tu enlace a %{domain} ya fue publicado en el tema por @%{username} en una respuesta el %{ago}. ¿Seguro que deseas volver a publicarlo?" + duplicate_link_same_user: "Parece que ya has publicado un enlace a %{domain} en este tema en una respuesta el %{ago}. ¿Seguro que quieres volver a publicarlo?" reference_topic_title: "RE: %{title}" error: title_missing: "Es necesario un título" @@ -2155,6 +2255,12 @@ es: high_priority: one: "%{count} notificación de alta prioridad sin leer" other: "%{count} notificaciones de alta prioridad sin leer" + new_message_notification: + one: "%{count} notificación de mensaje nuevo" + other: "%{count} notificaciones de mensajes nuevos" + new_reviewable: + one: "%{count} nuevo revisable" + other: "%{count} nuevos revisables" title: "notificaciones por menciones a tu @nombre, respuestas a tus publicaciones y temas, mensajes, etc" none: "No se pudieron cargar las notificaciones en este momento." empty: "No se encontraron notificaciones." @@ -2200,6 +2306,7 @@ es: reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} - completado" + new_features: "¡Nuevas características disponibles!" dismiss_confirmation: body: default: @@ -2254,6 +2361,7 @@ es: membership_request_consolidated: "nuevas solicitudes de membresía" reaction: "nueva reacción" votes_released: "Voto liberado" + new_features: "¡Se han lanzado nuevas funciones de Discourse!" upload_selector: uploading: "Subiendo" processing: "Procesando subida" @@ -2322,6 +2430,7 @@ es: status: "filtra por el estado del tema" full_search: "lanza la página de búsqueda" full_search_key: "%{modifier} + Intro" + me: "muestra solo tus publicaciones" advanced: title: Filtros avanzados posted_by: @@ -2562,7 +2671,48 @@ es: show_links: "mostrar enlaces dentro de este tema" collapse_details: "ocultar detalles del tema" expand_details: "expandir detalles del tema" + read_more_in_category: "¿Quieres leer más? Explora otros temas en %{categoryLink} o consulta los últimos temas." + read_more: "¿Quieres leer más? Examina todas las categorías o consulta los últimos temas." unread_indicator: "Ningún miembro ha leído todavía la última publicación de este tema." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Hay # sin leer} + other {Hay # sin leer} + } + { NEW, plural, + =0 {} + one { y # tema nuevo restante,} + other { y # temas nuevos restantes,} + } + } + false { + { UNREAD, plural, + =0 {} + one {Hay # tema nuevo restante,} + other {Hay # temas nuevos restantes,} + } + { NEW, plural, + =0 {} + one {Hay # nuevo restante,} + other {Hay # nuevos restantes,} + } + } + other {} + } + { HAS_CATEGORY, select, + true { o explora otros temas en {categoryLink}} + false { o consulta otros temas} + other {} + } + bumped_at_title: | + Primera publicación: %{createdAtDate} + Publicado: %{bumpedAtDate} + browse_all_categories_latest: "Explorar todas las categorías o ver los temas más recientes." + browse_all_categories_latest_or_top: "Explorar todas las categorías, ver los temas más recientes o ver la parte superior:" + browse_all_tags_or_latest: "Explorar todas las etiquetas o ver los temas más recientes." suggest_create_topic: '¿Listo para iniciar una nueva conversación?' jump_reply_up: saltar a la primera respuesta jump_reply_down: saltar a la última respuesta @@ -2983,6 +3133,7 @@ es: one: "Solo se pueden subir archivos de %{count} en 1." other: "Solo se pueden subir %{count} archivos al mismo tiempo." upload_not_authorized: "Lo sentimos, el archivo que estás intentando subir no está permitido (extensiones autorizadas: %{authorized_extensions})." + no_uploads_authorized: "Lo sentimos, no hay archivos autorizados para subir." image_upload_not_allowed_for_new_user: "Lo sentimos, los usuarios nuevos no pueden subir imágenes." attachment_upload_not_allowed_for_new_user: "Lo sentimos, los usuarios nuevos no pueden subir archivos adjuntos." attachment_download_requires_login: "Lo sentimos, necesitas haber iniciado sesión para descargar archivos adjuntos." @@ -2994,6 +3145,7 @@ es: via_email: "esta publicación llegó por correo electrónico" via_auto_generated_email: "esta publicación llegó a través de un correo electrónico generado automáticamente" whisper: "esta publicación es un susurro privado para los moderadores" + whisper_groups: "esta publicación es un susurro privado solo visible para %{groupNames}" wiki: about: "esta publicación tiene formato wiki" few_likes_left: "¡Gracias por compartir amor! Solo te quedan unos pocos me gusta por hoy." @@ -3216,6 +3368,7 @@ es: pending_permission_change_alert: "No has añadido a %{group} a esta categoría. Haz clic en este botón para añadirlos." images: "Imágenes" email_in: "Dirección de correo electrónico personalizada para el correo entrante:" + email_in_tooltip: "Puedes separar varias direcciones de correo electrónico con el carácter |." email_in_allow_strangers: "Aceptar correo electrónicos de usuarios anónimos sin cuenta" email_in_disabled: "La posibilidad de publicar temas nuevos por correo electrónico está desactivada en los ajustes del sitio. Para activar la publicación de temas nuevos por correo electrónico," email_in_disabled_click: 'activa la opción «correo electrónico»' @@ -3496,7 +3649,6 @@ es: this_week: "Semana" today: "Hoy" browser_update: 'Por desgracia, tu navegador no es compatible. Por favor, usa otro navegador para ver el contenido completo, iniciar sesión y responder.' - safari_13_warning: Este sitio pronto dejará de ser compatible con las versiones 13 y anteriores de iOS y Safari. En su lugar, verás una versión simplificada y de solo lectura. (más información) permission_types: full: "Crear / Responder / Ver" create_post: "Responder / Ver" @@ -3784,6 +3936,8 @@ es: enabled: "El modo seguro está activado, para salir del modo seguro cierra esta ventana del navegador" image_removed: "(imagen eliminada)" pause_notifications: + title: "Pausar notificaciones para..." + label: "Pausar notificaciones" remaining: "%{remaining} restante" options: half_hour: "30 minutos" @@ -3827,12 +3981,15 @@ es: second_factor_auth: redirect_after_success: "La autenticación de segundo factor se ha realizado con éxito. Redirigiendo a la página anterior…" sidebar: + show_sidebar: "Mostrar barra lateral" + hide_sidebar: "Ocultar barra lateral" unread_count: one: "%{count} sin leer" other: "%{count} sin leer" new_count: one: "%{count} nuevo" other: "%{count} nuevos" + toggle_section: "Mostrar/ocultar sección" more: "Más" all_categories: "Todas las categorías" all_tags: "Todas las etiquetas" @@ -3841,6 +3998,7 @@ es: header_link_text: "Acerca de" messages: header_link_text: "Mensajes" + header_action_title: "Crear un mensaje personal" links: inbox: "Bandeja de entrada" sent: "Enviados" @@ -3857,6 +4015,7 @@ es: none: "No has añadido ninguna etiqueta." click_to_get_started: "Haz clic aquí para empezar." header_link_text: "Etiquetas" + header_action_title: "Editar las etiquetas de tu barra lateral" configure_defaults: "Configurar valores predeterminados" categories: links: @@ -3866,32 +4025,43 @@ es: none: "No has añadido ninguna categoría." click_to_get_started: "Haz clic aquí para empezar." header_link_text: "Categorías" + header_action_title: "Editar las categorías de tu barra lateral" configure_defaults: "Configurar valores predeterminados" community: header_link_text: "Comunidad" + header_action_title: "Crear un tema" links: about: content: "Acerca de" + title: "Más detalles sobre este sitio" admin: content: "Administrador" + title: "Ajustes del sitio e informes" badges: content: "Insignias" + title: "Todas las insignias disponibles para ganar" everything: content: "Todo" title: "Todos los temas" faq: content: "Preguntas frecuentes" + title: "Normas de uso de este sitio" groups: content: "Grupos" + title: "Lista de grupos de usuarios disponibles" users: content: "Usuarios" + title: "Lista de todos los usuarios" my_posts: content: "Mis publicaciones" + title: "Mi actividad de tema reciente" + title_drafts: "Mis borradores no publicados" draft_count: one: "%{count} borrador" other: "%{count} borradores" review: content: "Revisión" + title: "Publicaciones denunciadas y otros elementos en cola" pending_count: "%{count} pendiente(s)" welcome_topic_banner: title: "Crea tu tema de bienvenida" @@ -4044,6 +4214,9 @@ es: other: "%{count} usuarios tienen el nuevo dominio de correo electrónico y serán añadidos al grupo" automatic_membership_associated_groups: "Los usuarios que sean miembros de un grupo en uno de los proveedores listados aquí serán añadidos automáticamente a este grupo cuando inicien sesión con el proveedor." primary_group: "Establecer automáticamente como grupo principal" + alert: + primary_group: "Dado que se trata de un grupo primario, el nombre «%{group_name}» se utilizará en las clases CSS que puede ver cualquiera." + flair_group: "Dado que este grupo tiene estilo para sus miembros, el nombre «%{group_name}» será visible para cualquiera." name_placeholder: "Nombre del grupo, sin espacios, al igual que los nombres de usuarios" primary: "Grupo principal" no_primary: "(ningún grupo principal)" @@ -4053,6 +4226,10 @@ es: about: "Edita la membresía de tu grupo y sus nombres aquí" group_members: "Miembros del grupo" delete: "Eliminar" + delete_confirm: "¿Seguro que quieres eliminar este grupo?" + delete_with_messages_confirm: + one: "Al eliminar este grupo, %{count} mensaje se quedará huérfano, y los miembros del grupo dejarán de tener acceso a él." + other: "Al eliminar este grupo, %{count} mensajes se quedará huérfanos, y los miembros del grupo dejarán de tener acceso a ellos." delete_failed: "No se pudo eliminar el grupo. Si este es un grupo automático, no se puede destruir." delete_automatic_group: Este grupo se ha creado automáticamente y no se puede eliminar. delete_owner_confirm: "¿Quitar los privilegios de propietario a «%{username}»?" @@ -4117,7 +4294,6 @@ es: topics: read: Leer un tema o una publicación específica del mismo. También usando RSS. write: Crear un nuevo tema o publicar en uno existente. - update: Actualizar un tema. Cambiar el título, categoría, etiquetas, etc. read_lists: Lea listas de temas como destacados, nuevos, recientes, etc. También se admite RSS. posts: edit: Edita cualquier publicación o una específica. @@ -4136,6 +4312,9 @@ es: anonymize: Anonimizar cuentas de usuario. delete: Eliminar cuentas de usuario. list: Obtener una lista de usuarios. + user_status: + read: Lee el estado del usuario. + update: Actualizar estado de usuario. email: receive_emails: Combinar este ámbito con el ámbito mail-receiver para procesar correos entrantes. badges: @@ -4160,6 +4339,7 @@ es: create: "Crear" edit: "Editar" save: "Guardar" + description_label: "Activadores de eventos" controls: "Controles" go_back: "Volver a la lista" payload_url: "URL a la que enviar el webhook" @@ -4262,6 +4442,8 @@ es: broken_route: "No se puede configurar el enlace a «%{name}». Asegúrate de que los bloqueadores de publicidad están desactivados e intenta recargar la página." navigation_menu: sidebar: "Barra lateral" + header_dropdown: "Menú desplegable de encabezado" + legacy: "Heredado" backups: title: "Copias de seguridad" menu: @@ -4946,6 +5128,7 @@ es: user: suspend_failed: "Algo salió mal al suspender a este usuario %{error}" unsuspend_failed: "Algo salió mal al desbloquear a este usuario %{error}" + suspend_duration: "Suspender al usuario hasta:" suspend_reason_label: "¿Por qué se le suspende? Este texto será visible para todos en la página de perfil del usuario y se mostrará al usuario cuando intente iniciar sesión. Sé conciso." suspend_reason_hidden_label: "¿Por qué se le suspende? Este texto se mostrará al usuario cuando trate de iniciar sesión. Sé conciso." suspend_reason: "Motivo" @@ -4969,7 +5152,9 @@ es: silence_message: "Mensaje por correo electrónico" silence_message_placeholder: "(dejar en blanco para enviar mensaje por defecto)" suspended_until: "(hasta %{until})" + suspend_forever: "Suspender para siempre" cant_suspend: "Este usuario no puede ser suspendido." + cant_silence: "Este usuario no puede ser silenciado." delete_posts_failed: "Hubo un problema al eliminar los mensajes." post_edits: "Ediciones de la publicación" view_edits: "Ver ediciones" @@ -4979,6 +5164,8 @@ es: penalty_post_edit: "Editar la publicación" penalty_post_none: "No hacer nada" penalty_count: "Contador de faltas" + penalty_history_MF: >- + ¡En los últimos 6 meses este usuario ha sido suspendido { SUSPENDED, plural, one {# vez} other {# veces} } y silenciado { SILENCED, plural, one {# vez} other {# veces} }. clear_penalty_history: title: "Eliminado historial de faltas" description: "usuarios con faltas no pueden alcanzar NC3" @@ -5133,10 +5320,14 @@ es: silenced_count: "Silenciado" suspended_count: "Suspendido" last_six_months: "Últimos 6 meses" + other_matches: + one: "Hay %{count} otro usuario con la misma dirección IP. Revisa y selecciona los sospechosos para penalizar junto con %{username}." + other: "Hay %{count} otros usuarios con la misma dirección IP. Revisa y selecciona los sospechosos para penalizar junto con %{username}." other_matches_list: username: "Nombre de usuario" trust_level: "Nivel de confianza" read_time: "Tiempo de lectura" + topics_entered: "Temas visitados" posts: "Publicaciones" tl3_requirements: title: "Requisitos para el nivel de confianza 3" @@ -5453,8 +5644,10 @@ es: finish: "Salir del configurador" back: "Atrás" next: "Siguiente" + configure_more: "Configurar más..." step-text: "Intervalo" step: "%{current} de %{total}" + upload: "Subir archivo" uploading: "Subiendo..." upload_error: "Lo sentimos, se ha producido un error al cargar este archivo. Inténtalo de nuevo." staff_count: diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 2107eb1783..57ea330fe9 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -949,6 +949,7 @@ et: primary: "Peamine e-post" secondary: "Teine e-post" primary_label: "primaarne" + resent_label: "meil saadetud" update_email: "Muuda meiliaadressi" no_secondary: "Teist e-posti pole" instructions: "Ei näidata avalikult." @@ -1572,9 +1573,11 @@ et: dismiss_new: "Ignoreeri uusi" toggle: "lülita teemade massiline ära märkimine ümber" actions: "Masstoimingud" + change_category: "Määra kategooria..." close_topics: "Sulge Teemad" archive_topics: "Arhiveeri Teemad" move_messages_to_inbox: "Liiguta sisendkausta" + notification_level: "Teavitus..." change_notification_level: "Muuda teavituste taset" choose_new_category: "Vali teemadele uus foorum:" selected: @@ -1772,6 +1775,7 @@ et: unarchive: "Taasta teema arhiivist" archive: "Arhiveeri teema" reset_read: "Nulli andmed teema lugemise kohta" + make_public: "Loo avalik teema..." make_private: "Loo isiklik sõnum" feature: pin: "Tõsta teema esile" @@ -2010,6 +2014,8 @@ et: revert_to_regular: "Eemalda meeskonna värv" rebake: "Rekonstrueeri HTML" unhide: "Too nähtavale" + change_owner: "Omanikuvahetus..." + grant_badge: "Anna märgis..." lock_post: "Lukusta postitus" delete_topic: "kustuta teema" actions: @@ -2186,6 +2192,7 @@ et: flagging: title: "Täname, et aitad meie kogukonna viisakust säilitada!" action: "Tähista postitus" + take_action: "Tegutse..." take_action_options: default: title: "Tegutse" @@ -2485,6 +2492,7 @@ et: save: "Salvesta" delete: "Kustuta" confirm_delete: "Oled kindel, et soovid selle siltide grupi kustutada?" + parent_tag_placeholder: "Valikuline" topics: none: unread: "Sul ei ole lugemata teemasid." @@ -2587,6 +2595,7 @@ et: latest_version: "Viimased" new_features: dismiss: "Ignoreeri" + learn_more: "Uuri veel" last_checked: "Viimati kontrollitud" refresh_problems: "Värskenda" no_problems: "Probleeme ei tuvastatud." @@ -2683,6 +2692,7 @@ et: user: "Kasutaja" title: "API" created: Loodud + never_used: (mitte kunagi) generate: "Genereeri" revoke: "Tühista " all_users: "Kõik kasutajad" diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index b11a153e6c..cc21103d75 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -990,7 +990,7 @@ fa_IR: perm_denied_expl: "شما دسترسی دریافت اعلان را بسته‌اید. در تنظیمات مرورگر خود آن‌را فعال کنید." disable: "غیرفعال کردن آگاه‌سازی" enable: "فعال کردن آگاه‌سازی" - each_browser_note: 'توجه: شما باید این تنظیمات را در هر مرورگری که استفاده می‌کنید، تغییر دهید. اگر آگاه‌سازی‌ها را از منوی کاربر متوقف کنید، صرف‌نظر از این تنظیم، همه آگاه‌سازی‌ها غیرفعال می‌شوند.' + each_browser_note: "توجه: شما باید این تنظیمات را در هر مرورگری که استفاده می‌کنید، تغییر دهید. اگر آگاه‌سازی‌ها را از منوی کاربر متوقف کنید، صرف‌نظر از این تنظیم، همه آگاه‌سازی‌ها غیرفعال می‌شوند." consent_prompt: "آیا مایلید وقتی دیگران به شما پاسخ می‌دهند، آگاه‌سازی زنده دریافت کنید؟" dismiss: "نخواستیم" dismiss_notifications: "پنهان کردن همه" @@ -1432,6 +1432,7 @@ fa_IR: edit_title: "ویرایش دعوت‌نامه" instructions: "این لینک را برای ارائه دسترسی فوری به سایت٬ به اشتراک بگذارید:" copy_link: "کپی پیوند" + expires_in_time: "در %{time} منقضی می شود" expired_at_time: "منقضی شده در %{time}" show_advanced: "نمایش تنظیمات پیشرفته" hide_advanced: "پنهان کردن تنظیمات پیشرفته" @@ -2254,6 +2255,7 @@ fa_IR: dismiss_new: "بستن جدید" toggle: "تغییر وضعیت انتخاب گروهی موضوعات" actions: "عملیات گروهی" + change_category: "تنظیم دسته‌بندی..." close_topics: "بستن موضوعات" archive_topics: "بایگانی موضوعات" move_messages_to_inbox: "انتقال به صندوق دریافت" @@ -2476,6 +2478,7 @@ fa_IR: unarchive: "موضوع بایگانی نشده" archive: "بایگانی کردن موضوع" reset_read: "تنظیم مجدد خواندن داده ها" + make_public: "ایجاد موضوع عمومی..." make_private: "به پیام تبدیل کن" reset_bump_date: "تنظیم مجدد خواندن داده ها" feature: @@ -2714,6 +2717,8 @@ fa_IR: revert_to_regular: "حذف رنگ همکاران" rebake: "ساخت مجدد HTML" unhide: "آشکار کردن" + change_owner: "تغییر مالکیت..." + grant_badge: "اعطای نشان..." lock_post: "قفل کردن پست" lock_post_description: "ممانعت از ویرایش پست توسط فرستنده" unlock_post: "بازکردن پست" @@ -2724,6 +2729,8 @@ fa_IR: delete_topic_confirm_modal_no: "خیر، این موضوع را نگه دار" delete_topic_error: "خطایی در هنگام حذف این موضوع رخ داده است." delete_topic: "حذف موضوع" + add_post_notice: "اطلاعیه کارکنان را اضافه کنید..." + change_post_notice: "تغییر اطلاعیه همکاران..." delete_post_notice: "حذف اطلاعیه همکاران" remove_timer: "حذف زمان‌سنج" actions: @@ -2922,9 +2929,12 @@ fa_IR: moderation: "معتدل" appearance: "ظاهر" email: "ایمیل" + list_filters: + all: "همه موضوعات" flagging: title: "تشکر برای کمک به نگه داشتن جامعه ما بصورت مدنی !" action: "پرچم‌گذاری نوشته" + take_action: "اقدام..." take_action_options: default: title: "اقدام" @@ -3051,6 +3061,9 @@ fa_IR: lower_title_with_count: one: "%{count} خوانده نشده" other: "%{count} خوانده نشده" + unseen: + title: "دیده نشده" + lower_title: "دیده نشده" new: lower_title_with_count: one: "%{count} تازه" @@ -3094,7 +3107,6 @@ fa_IR: this_month: "ماه" this_week: "هفته" today: "امروز" - safari_13_warning: این سایت به زودی از iOS و Safari، نسخه ۱۳ به بالا پشتیبانی نخواهد کرد. یک نسخه فقط خواندنی ساده در دسترس باقی خواهد ماند. (اطلاعات بیشتر) permission_types: full: "ساختن / پاسخ دادن / دیدن" create_post: "پاسخ دادن / دیدن" @@ -3293,9 +3305,11 @@ fa_IR: everyone_can_use: "برچسب‌ها می‌توانندتوسط همه استفاده شوند" usable_only_by_groups: "برچسب ها برای همه قابل مشاهده است، اما فقط گروه های زیر می توانند از آنها استفاده کنند" visible_only_to_groups: "برچسب ها فقط برای گروه های زیر قابل مشاهده است" + parent_tag_placeholder: "اختیاری" topics: none: unread: "شما موضوع خوانده نشده‌ای ندارید." + unseen: "شما هیچ موضوع دیده نشده‌ای ندارید." new: "شما موضوع جدیدی ندارید." read: "شما هیچ موضوعی را نخوانده‌اید." posted: "شما هیچ نوشته‌ای در موضوعات ندارید." @@ -3436,6 +3450,7 @@ fa_IR: problems_found: "چند توصیه براثاث تنظیمات فعلی سایت شما" new_features: dismiss: "نخواستیم" + learn_more: "بیشتر بدانید" last_checked: " آخرین بررسی" refresh_problems: "تازه کردن" no_problems: "هیچ مشکلات پیدا نشد." @@ -3554,6 +3569,7 @@ fa_IR: title: "API" created: ساخته شده updated: به روز شده + never_used: (هرگز) generate: "تولید کردن" revoke: "ابطال" all_users: "همه کاربران" @@ -3566,8 +3582,6 @@ fa_IR: global: جهانی action: اقدام descriptions: - topics: - update: به‌روزرسانی موضوع. تغییر عنوان، دسته‌بندی، برچسب‌ها و غیره. user_status: read: خواندن وضعیت کاربر. update: به‌روزرسانی وضعیت کاربر. @@ -3979,6 +3993,9 @@ fa_IR: address_placeholder: "name@example.com" type_placeholder: "خلاصه، ثبت نام..." reply_key_placeholder: "کلید پاسخ" + moderation_history: + actions: + delete_topic: "مبحث حذف شده" logs: title: "گزارش‌ها" action: "عمل" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index f98d01409f..d327e6ce21 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -176,6 +176,7 @@ fi: banner: enabled: "Asetettu banneriksi %{when}. Se näytetään jokaisen sivun ylälaidassa, kunnes käyttäjä kuittaa sen nähdyksi." disabled: "Poistettu banneri näkyvistä %{when}. Sitä ei enää näytetä jokaisen sivun ylälaidassa." + forwarded: "Välitetty yllä oleva sähköposti" topic_admin_menu: "ketjun toiminnot" skip_to_main_content: "Siirry pääsisältöön" emails_are_disabled: "Ylläpitäjä on estänyt kaiken lähtevän sähköpostiliikenteen. Mitään sähköposti-ilmoituksia ei lähetetä." @@ -322,7 +323,7 @@ fi: no_timezone: 'Et ole valinnut aikavyöhykettä, joten et voi asettaa muistutuksia. Aseta se profiilisivullasi.' invalid_custom_datetime: "Antamasi päivämäärä ja kellonaika ei kelpaa, yritä uudelleen." list_permission_denied: "Et voi nähdä tämän käyttäjän kirjanmerkkejä." - no_user_bookmarks: "Kirjanmerkeissäsi ei ole mitään. Kirjanmerkitsemällä viestin löydät sen myöhemmin helposti." + no_user_bookmarks: "Kirjanmerkeissäsi ei ole mitään. Kirjanmerkitsemällä viestejä löydät ne myöhemmin helposti." auto_delete_preference: label: "Kun sinulle on ilmoitettu" never: "Säilytä kirjanmerkki" @@ -455,7 +456,7 @@ fi: topic: "Ketju:" filtered_topic: "Olet suodattanut käsiteltävän sisällön yhdestä ketjusta." filtered_user: "Käyttäjä" - filtered_reviewed_by: "Käsittelijä:" + filtered_reviewed_by: "Käsittelijä" show_all_topics: "näytä kaikki ketjut" deleted_post: "(viesti poistettu)" deleted_user: "(käyttäjä poistettu)" @@ -1007,6 +1008,7 @@ fi: notification_schedule: title: "Ilmoitusten aikataulu" label: "Aseta mukautettu ilmoitusten aikataulu" + tip: "Näiden aikojen ulkopuolella ilmoituksesi keskeytetään." midnight: "Keskiyö" none: "Ei mitään" monday: "Maanantai" @@ -1050,12 +1052,16 @@ fi: perm_denied_expl: "Olet kieltänyt ilmoitukset. Salli ilmoitukset selaimesi asetuksista." disable: "Poista ilmoitukset käytöstä" enable: "Näytä ilmoituksia" + each_browser_note: "Huomautus: Tätä asetusta on muutettava jokaisessa käyttämässäsi selaimessa. Kaikki ilmoitukset poistetaan käytöstä, kun keskeytät ilmoitukset käyttäjävalikosta riippumatta tästä asetuksesta." consent_prompt: "Haluatko reaaliaikaisia ilmoituksia, kun ihmiset vastaavat viesteihisi?" dismiss: "Kuittaa" dismiss_notifications: "Kuittaa kaikki" dismiss_notifications_tooltip: "Merkitse kaikki lukemattomat ilmoitukset luetuiksi" dismiss_bookmarks_tooltip: "Merkitse kaikki lukemattomat kirjanmerkkimuistutukset luetuiksi" dismiss_messages_tooltip: "Merkitse kaikki lukemattomien yksityisviestien ilmoitukset luetuiksi" + no_likes_title: "Et ole vielä saanut tykkäyksiä" + no_likes_body: > + Saat ilmoituksen täällä aina, kun joku tykkää viestistäsi, jotta näet, mitä muut pitävät arvokkaana. Muut näkevät myös saman, kun tykkäät heidän viesteistään!

    Ilmoituksia tykkäyksistä ei koskaan lähetetä sinulle sähköpostitse, mutta voit säätää, miten saat ilmoituksia tykkäyksistä sivustolla ilmoitusasetuksissasi. no_messages_title: "Sinulla ei ole viestejä" no_messages_body: > Haluatko suoraa henkilökohtaista keskustelua jonkun kanssa normaalin keskusteluvirran ulkopuolella? Lähetä hänelle viesti klikkaamalla hänen profiilikuvaansa ja käyttämällä viestipainiketta %{icon}.

    Jos tarvitset apua, voit lähettää viestin henkilökunnan jäsenelle. @@ -1205,6 +1211,62 @@ fi: warnings: "Viralliset varoitukset" read_more_in_group: "Haluatko lukea lisää? Selaile muita ryhmän %{groupLink} viestejä." read_more: "Haluatko lukea lisää? Selaa muita viestejä yksityisviesteissä." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {# lukematon} + other {# lukematonta} + } + { NEW, plural, + =0 {} + one { ja # uusi viesti jäljellä, tai selaa muista viestejä ryhmässä {groupLink}} + other { ja # uutta viestiä jäljellä, tai selaa muista viestejä ryhmässä {groupLink}} + } + } + false { + { UNREAD, plural, + =0 {} + one {# lukematon viesti jäljellä, tai selaa muista viestejä ryhmässä {groupLink}} + other {# lukematonta viestiä jäljellä, tai selaa muista viestejä ryhmässä {groupLink}} + } + { NEW, plural, + =0 {} + one {# uusi viesti jäljellä, tai selaa muista viestejä ryhmässä {groupLink}} + other {# uutta viestiä jäljellä, tai selaa muista viestejä ryhmässä {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {# lukematon} + other {# lukematonta} + } + { NEW, plural, + =0 {} + one { ja # uusi viesti jäljellä, tai selaa muita yksityisviestejä} + other { ja # uutta viestiä jäljellä, tai selaa muita yksityisviestejä} + } + } + false { + { UNREAD, plural, + =0 {} + one {There is # lukematon viesti jäljellä, tai selaa muita yksityisviestejä} + other {There are # lukematonta viestiä jäljellä, tai selaa muita yksityisviestejä} + } + { NEW, plural, + =0 {} + one {There is # uusi viesti jäljellä, tai selaa muita yksityisviestejä} + other {There are # uutta viestiä jäljellä, tai selaa muita yksityisviestejä} + } + } + other {} + } preferences_nav: account: "Tili" security: "Tietoturva" @@ -1269,7 +1331,14 @@ fi: use: "Käytä Authenticator-sovellusta" enforced_notice: "Kaksivaiheinen tunnistus täytyy ottaa käyttöön, jotta voit käyttää sivustoa." disable: "Poista käytöstä" + disable_confirm: "Oletko varma, että haluat poistaa kaksivaiheisen tunnistuksen käytöstä?" delete: "Poista" + delete_confirm_header: "Nämä tunnistepohjaiset todentajat ja tunnistautumislaitteet poistetaan:" + delete_confirm_instruction: "Vahvista kirjoittamalla %{confirm} alla olevaan ruutuun." + delete_single_confirm_title: "Todentajan poistaminen" + delete_single_confirm_message: "Olet poistamassa todentajaa %{name}. Et voi kumota tätä toimintoa. Jos muutat mielesi, sinun täytyy rekisteröidä tämän todentaja uudelleen." + delete_backup_codes_confirm_title: "Varakoodeja poistetaan" + delete_backup_codes_confirm_message: "Olet poistamassa varakoodeja. Et voi kumota tätä toimintoa. Jos muutat mielesi, sinun täytyy luoda varakoodit uudelleen." save: "Tallenna" edit: "Muokkaa" edit_title: "Muokkaa todentajaa" @@ -1436,6 +1505,8 @@ fi: title: "Taustasivun otsikon näyttäjen määrä:" notifications: "Uusia ilmoituksia" contextual: "Uusi sivusisältö" + bookmark_after_notification: + title: "Kun kirjanmerkkimuistutusilmoitus on lähetetty:" like_notification_frequency: title: "Ilmoita, kun viestistäni tykätään" always: "Aina" @@ -1661,6 +1732,7 @@ fi: save: "Tallenna" set_custom_status: "Aseta mukautettu tila" what_are_you_doing: "Mitä teet?" + pause_notifications: "Keskeytä ilmoitukset" remove_status: "Poista tila" user_tips: primary: "Selvä!" @@ -1714,6 +1786,36 @@ fi: logout_disabled: "Et voi kirjautua ulos, kun sivusto on vain luku -tilassa." staff_writes_only_mode: enabled: "Sivusto on vain henkilökunta -tilassa. Voit jatkaa selailua, mutta vastaaminen, tykkääminen ja muita toimintoja on rajoitettu vain henkilökunnan jäsenille." + too_few_topics_and_posts_notice_MF: | + Aloitetaan keskustelu! Sivustolla on { currentTopics, plural, + one {# ketju} + other {# ketjua} + } ja { currentPosts, plural, + one {# viesti} + other {# viestiä} + }. Vierailijat tarvitsevat enemmän luettavaa ja vastattavaa – suosittelemme, että sivustolla olisi vähintään { requiredTopics, plural, + one {# ketju} + other {# ketjua} + } ja { requiredPosts, plural, + one {# viesti} + other {# viestiä} + }. Vain henkilökunta voi nähdä tämän viestin. + too_few_topics_notice_MF: | + Aloitetaan keskustelu! Sivustolla on { currentTopics, plural, + one {# ketju} + other {# ketjua} + }. Vierailijat tarvitset enemmän luettavaa ja vastattavaa – suosittelemme, että sivustolla olisi vähintään { requiredTopics, plural, + one {# ketju} + other {# ketjua} + }. Vain henkilökunta voi nähdä tämän viestin. + too_few_posts_notice_MF: | + Aloitetaan keskustelu! Sivustolla on { currentPosts, plural, + one {# viesti} + other {# viestiä} + }. Vierailijat tarvitset enemmän luettavaa ja vastattavaa – suosittelemme, että sivustolla olisi vähintään { requiredPosts, plural, + one {# viesti} + other {# viestiä} + }. Vain henkilökunta voi nähdä tämän viestin. logs_error_rate_notice: reached_hour_MF: | {relativeAge}{rate, plural, one {# virhe/tunti} other {# virhettä/tunti}} saavutti sivustoasetusrajan {limit, plural, one {# virhe/tunti} other {# virhettä/tunti}}. @@ -1821,6 +1923,9 @@ fi: username: "Käyttäjä" password: "Salasana" show_password: "Näytä" + hide_password: "Piilota" + show_password_title: "Näytä salasana" + hide_password_title: "Piilota salasana" second_factor_title: "Kaksivaiheinen tunnistus" second_factor_description: "Syötä sovelluksen antama todennuskoodi:" second_factor_backup: "Kirjaudu käyttäen varakoodia" @@ -1842,6 +1947,7 @@ fi: blank_username_or_password: "Kirjoita sähköpostiosoite tai käyttäjätunnus ja salasana." reset_password: "Uusi salasana" logging_in: "Kirjaudutaan..." + previous_sign_up: "Onko sinulla jo tili?" or: "Tai" authenticating: "Todennetaan..." awaiting_activation: "Tilisi odottaa aktivointia; saat uuden aktivointisähköpostin unohdin salasanani -linkin kautta." @@ -2016,10 +2122,17 @@ fi: private: "Mainitsit käyttäjän @%{username}, mutta hän ei saa ilmoitusta, koska hän ei näe tätä yksityiskeskustelua. Hänet täytyy kutsua tähän yksityiskeskusteluun." muted_topic: "Mainitsit käyttäjän @%{username}, mutta hän ei saa ilmoitusta, koska hän on vaimentanut tämän ketjun." not_allowed: "Mainitsit käyttäjän @%{username}, mutta hän ei saa ilmoitusta, koska häntä ei ole kutsuttu tähän ketjuun." + cannot_see_group_mention: + not_mentionable: "Et voi mainita ryhmää @%{group}." + some_not_allowed: + one: "Mainitsit ryhmän @%{group}, mutta vain %{count} jäsen saa ilmoituksen, koska muut jäsenet eivät näe tätä yksityiskeskustelua. Heidät täytyy kutsua tähän yksityiskeskusteluun." + other: "Mainitsit ryhmän @%{group}, mutta vain %{count} jäsentä saa ilmoituksen, koska muut jäsenet eivät näe tätä yksityiskeskustelua. Heidät täytyy kutsua tähän yksityiskeskusteluun." + not_allowed: "Mainitsit ryhmän @%{group}, mutta yksikään sen jäsenistä ei saa ilmoitusta, koska he eivät näe tätä yksityiskeskustelua. Heidät täytyy kutsua tähän yksityiskeskusteluun." here_mention: one: "Maininta @%{here} lähettää ilmoituksen %{count} käyttäjälle – oletko varma?" other: "Maininta @%{here} lähettää ilmoituksen %{count} käyttäjälle – oletko varma?" duplicate_link: "Näyttää siltä, että @%{username} linkitti jo samaan kohteeseen %{domain} ketjun aiemmassa vastauksessa %{ago} – oletko varma, että haluat lähettää sen uudestaan?" + duplicate_link_same_user: "Näyttää siltä, että olet jo lähettänyt linkin %{domain} tähän ketjuun vastauksessa %{ago} – oletko varma, että haluat lähettää sen uudelleen?" reference_topic_title: "RE: %{title}" error: title_missing: "Otsikko on pakollinen" @@ -2155,6 +2268,12 @@ fi: high_priority: one: "%{count} lukematon korkean prioriteetin ilmoitus" other: "%{count} lukematonta korkean prioriteetin ilmoitusta" + new_message_notification: + one: "%{count} ilmoitus uudesta viestistä" + other: "%{count} ilmoitusta uusista viesteistä" + new_reviewable: + one: "%{count} uusi käsiteltävä kohde" + other: "%{count} uutta käsiteltävää kohdetta" title: "ilmoitukset @nimi-maininnoista, vastauksista omiin viesteihin ja ketjuihin, viesteistä ym." none: "Ilmoitusten lataaminen ei onnistunut." empty: "Ilmoituksia ei löytynyt." @@ -2200,6 +2319,7 @@ fi: reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} – valmis" + new_features: "Uusia ominaisuuksia saatavilla!" dismiss_confirmation: body: default: @@ -2254,6 +2374,7 @@ fi: membership_request_consolidated: "uusia jäsenhakemuksia" reaction: "uusi reaktio" votes_released: "Ääni vapautui" + new_features: "uusia Discoursen ominaisuuksia on julkaistu!" upload_selector: uploading: "Ladataan" processing: "Latausta käsitellään" @@ -2563,7 +2684,48 @@ fi: show_links: "näytä tämän ketjun linkit" collapse_details: "kutista ketjun tiedot" expand_details: "laajenna ketjun tiedot" + read_more_in_category: "Haluatko lukea lisää? Selaa muita ketjuja alueelta %{categoryLink} tai katso viimeisimmät ketjut." + read_more: "Haluatko lukea lisää? Selaa kaikkia alueita tai katso viimeisimmät ketjut." unread_indicator: "Kukaan jäsen ei ole lukenut ketjun viimeistä viestiä vielä." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Jäljellä on # lukematon} + other {Jäljellä on # lukematonta} + } + { NEW, plural, + =0 {} + one { ja # uusi ketju,} + other { ja # uutta ketjua,} + } + } + false { + { UNREAD, plural, + =0 {} + one {Jäljellä on # lukematon ketju,} + other {Jäljellä on # lukematonta ketjua,} + } + { NEW, plural, + =0 {} + one {Jäljellä on # uusi ketju,} + other {Jäljellä on # uutta ketjua,} + } + } + other {} + } + { HAS_CATEGORY, select, + true { tai selaa muita ketjuja alueella {categoryLink}} + false { tai katso viimeisimmät ketjut} + other {} + } + bumped_at_title: | + Ensimmäinen viesti: %{createdAtDate} + Lähetetty: %{bumpedAtDate} + browse_all_categories_latest: "Selaa kaikkia alueita tai katso viimeisimmät ketjut." + browse_all_categories_latest_or_top: "Selaa kaikkia alueita, katso viimeisimmät ketjut tai katso suosituimmat:" + browse_all_tags_or_latest: "Selaa kaikkia tunnisteita tai katso viimeisimmät ketjut." suggest_create_topic: Valmiina aloittamaan uuden keskustelun? jump_reply_up: siirry aiempaan vastaukseen jump_reply_down: siirry myöhempään vastaukseen @@ -2984,6 +3146,7 @@ fi: one: "Voit ladata vain %{count} tiedoston kerrallaan." other: "Voit ladata vain %{count} tiedostoa kerrallaan." upload_not_authorized: "Tiedosto. jota yrität ladata, ei ole sallittu (sallitut päätteet: %{authorized_extensions})." + no_uploads_authorized: "Tiedostojen lataamista verkkoon ei ole sallittu." image_upload_not_allowed_for_new_user: "Uudet käyttäjät eivät saa ladata kuvia." attachment_upload_not_allowed_for_new_user: "Uudet käyttäjät eivät saa ladata liitteitä." attachment_download_requires_login: "Sinun täytyy kirjautua sisään, jotta voit ladata liitetiedostoja." @@ -2995,6 +3158,7 @@ fi: via_email: "tämä viesti lähetettiin sähköpostitse" via_auto_generated_email: "tämä viesti saapui automaattisesti luotuna sähköpostina" whisper: "tämä viesti on yksityinen kuiskaus valvojille" + whisper_groups: "tämä viesti on yksityinen kuiskaus, joka näkyy vain ryhmille" wiki: about: "tämä viesti on wiki-viesti" few_likes_left: "Kiitos hyvän mielen levittämisestä! Sinulla on enää muutama tykkäys jäljellä tälle päivälle." @@ -3217,6 +3381,7 @@ fi: pending_permission_change_alert: "Et ole lisännyt ryhmää %{group} tälle alueelle; lisää klikkaamalla tätä painiketta." images: "Kuvat" email_in: "Mukautettu saapuvan postin sähköpostiosoite:" + email_in_tooltip: "Voit erottaa useita sähköpostiosoitteita |-merkillä." email_in_allow_strangers: "Hyväksy sähköpostit anonyymeiltä käyttäjiltä, joilla ei ole tiliä" email_in_disabled: "Uusien ketjujen aloittaminen sähköpostitse on poistettu käytöstä sivuston asetuksissa. Salliaksesi uusien ketjujen luomisen sähköpostilla, " email_in_disabled_click: 'ota käyttöön saapuvien sähköpostien asetus.' @@ -3497,7 +3662,6 @@ fi: this_week: "Viikko" today: "Tänään" browser_update: 'Valitettavasti selaintasi ei tueta. Vaihda tuettuun selaimeen, niin voit katsella monipuolista sisältöä, kirjautua sisään ja vastata.' - safari_13_warning: Tämä sivusto poistaa pian iOS:n ja Safarin version 13 ja sitä vanhempien versioiden tuen. Yksinkertaistettu vain luku -versio on edelleen käytettävissä. (Lisätietoja) permission_types: full: "Luoda / Vastata / Nähdä" create_post: "Vastata / Nähdä" @@ -3785,6 +3949,8 @@ fi: enabled: "Vikasietotila on käytössä, poistu vikasietotilasta sulkemalla selainikkuna" image_removed: "(kuva poistettu)" pause_notifications: + title: "Keskeytä ilmoitukset..." + label: "Keskeytä ilmoitukset" remaining: "%{remaining} jäljellä" options: half_hour: "30 minuuttiin" @@ -3828,12 +3994,15 @@ fi: second_factor_auth: redirect_after_success: "Kaksiosainen todennus onnistui. Sinut ohjataan edelliselle sivulle…" sidebar: + show_sidebar: "Näytä sivupalkki" + hide_sidebar: "Piilota sivupalkki" unread_count: one: "%{count} lukematta" other: "%{count} lukematta" new_count: one: "%{count} uusi" other: "%{count} uutta" + toggle_section: "Vaihda osa" more: "Lisää" all_categories: "Kaikki alueet" all_tags: "Kaikki tunnisteet" @@ -3842,6 +4011,7 @@ fi: header_link_text: "Tietoa" messages: header_link_text: "Viestit" + header_action_title: "Luo yksityisviesti" links: inbox: "Postilaatikko" sent: "Lähetetyt" @@ -3858,6 +4028,7 @@ fi: none: "Et ole lisännyt tunnisteita." click_to_get_started: "Aloita klikkaamalla tätä." header_link_text: "Tunnisteet" + header_action_title: "Muokkaa sivupalkin tunnisteitasi" configure_defaults: "Määritä oletukset" categories: links: @@ -3867,32 +4038,43 @@ fi: none: "Et ole lisännyt alueita." click_to_get_started: "Aloita klikkaamalla tätä." header_link_text: "Alueet" + header_action_title: "Muokkaa sivupalkin alueitasi" configure_defaults: "Määritä oletukset" community: header_link_text: "Yhteisö" + header_action_title: "Aloita ketju" links: about: content: "Tietoa" + title: "Lisätietoja tästä sivustosta" admin: content: "Ylläpitäjä" + title: "Sivuston asetukset ja raportit" badges: content: "Kunniamerkit" + title: "Kaikki ansaittavissa olevat kunniamerkit" everything: content: "Kaikki" title: "Kaikki ketjut" faq: content: "UKK" + title: "Ohjeet tämän sivuston käyttöön" groups: content: "Ryhmät" + title: "Luettelo käytettävissä olevista käyttäjäryhmistä" users: content: "Käyttäjät" + title: "Luettelo kaikista käyttäjistä" my_posts: content: "Viestini" + title: "Viimeaikainen ketjutoimintani" + title_drafts: "Julkaisemattomat luonnokseni" draft_count: one: "%{count} luonnos" other: "%{count} luonnosta" review: content: "Käsittele" + title: "Liputetut viestit ja muut jonossa olevat kohteet" pending_count: "%{count} odottaa" welcome_topic_banner: title: "Luo tervetuloketjusi" @@ -4045,6 +4227,9 @@ fi: other: "%{count} käyttäjällä on jokin uusista sähköpostiverkkotunnuksista, ja heidät lisätään ryhmään." automatic_membership_associated_groups: "Käyttäjät, jotka ovat ryhmän jäseniä tässä luetellussa palvelussa, lisätään automaattisesti tähän ryhmään, kun he kirjautuvat sisään palvelulla." primary_group: "Aseta automaattisesti ensisijaiseksi ryhmäksi" + alert: + primary_group: "Koska tämä on ensisijainen ryhmä, nimeä \"%{group_name}\" käytetään CSS-luokissa, joita kuka tahansa voi tarkastella." + flair_group: "Koska tällä ryhmällä on flairia jäsenilleen, nimi \"%{group_name}\" näkyy kaikille." name_placeholder: "Ryhmän nimi, ei välilyöntejä, samat säännöt kuin käyttäjätunnuksilla" primary: "Ensisijainen ryhmä" no_primary: "(ei ensisijaista ryhmää)" @@ -4054,6 +4239,10 @@ fi: about: "Muokkaa ryhmien jäsenyyksiä ja nimiä täällä" group_members: "Ryhmän jäsenet" delete: "Poista" + delete_confirm: "Oletko varma, että haluat poistaa tämän ryhmän?" + delete_with_messages_confirm: + one: "Tämän ryhmän poistaminen aiheuttaa %{count} viestin orpoutumisen, ja ryhmän jäsenillä ei ole tämän jälkeen pääsyä siihen." + other: "Tämän ryhmän poistaminen aiheuttaa %{count} viestin orpoutumisen, ja ryhmän jäsenillä ei ole tämän jälkeen pääsyä siihen." delete_failed: "Ryhmän poistaminen ei onnistu. Jos tämä on automaattinen ryhmä, sitä ei voi poistaa." delete_automatic_group: Tämä on automaattisesti luotu ryhmä, eikä sitä voi poistaa. delete_owner_confirm: "Perutaanko käyttäjältä '%{username}' isännyys?" @@ -4118,7 +4307,6 @@ fi: topics: read: Lue ketjua tai tiettyä sen viestiä. RSS on myös tuettu. write: Aloita uusi ketju tai kirjoita olemassa olevaan. - update: Päivitä ketju. Muuta otsikkoa, aluetta, tunnisteita jne. read_lists: Lue ketjuluetteloita, kuten suositut, uudet, tuoreimmat jne. RSS on myös tuettu. posts: edit: Muokkaa mitä tahansa viestiä tai tiettyä viestiä. @@ -4137,6 +4325,9 @@ fi: anonymize: Anonymisoi käyttäjätilejä. delete: Poista käyttäjätilejä. list: Hae luettelo käyttäjistä. + user_status: + read: Käyttäjän tilan lukeminen. + update: Käyttäjän tilan päivittäminen. email: receive_emails: Yhdistä tämä näkyvyysalue sähköpostivastaanottimen kanssa käsitelläksesi saapuvia sähköpostiviestejä. badges: @@ -4161,6 +4352,7 @@ fi: create: "Luo" edit: "Muokkaa" save: "Tallenna" + description_label: "Tapahtuman triggerit" controls: "Hallinta" go_back: "Takaisin luetteloon" payload_url: "Tietosisällön URL" @@ -4263,6 +4455,8 @@ fi: broken_route: "Linkin määrittäminen kohteeseen %{name} ei onnistu. Varmista, että mainostenesto-ohjelmat on poistettu käytöstä, ja kokeile ladata sivu uudelleen." navigation_menu: sidebar: "Sivupalkki" + header_dropdown: "Yläpalkin avattava valikko" + legacy: "Vanha" backups: title: "Varmuuskopiot" menu: @@ -4718,7 +4912,7 @@ fi: actions: delete_user: "poista käyttäjä" change_trust_level: "muuta luottamustasoa" - change_username: "muuta käyttäjätunnusta" + change_username: "vaihda käyttäjätunnus" change_site_setting: "muuta sivuston asetusta" change_theme: "vaihda teema" delete_theme: "poista teema" @@ -4947,6 +5141,7 @@ fi: user: suspend_failed: "Jokin meni vikaan hyllytettäessä tätä käyttäjää: %{error}" unsuspend_failed: "Jokin meni vikaan peruttaessa tämän käyttäjän hyllytystä: %{error}" + suspend_duration: "Hyllytä käyttäjä asti:" suspend_reason_label: "Miksi hyllytät käyttäjän? Tämä teksti näkyy kaikille käyttäjän profiilisivulla ja näytetään käyttäjälle, kun hän kirjautuu sisään. Pidä siis viesti lyhyenä." suspend_reason_hidden_label: "Miksi hyllytät käyttäjän? Tämä teksti näytetään käyttäjälle, kun hän yrittää kirjautua sisään. Pidä se lyhyenä." suspend_reason: "Syy" @@ -4970,7 +5165,9 @@ fi: silence_message: "Sähköpostiviesti" silence_message_placeholder: "(jätä tyhjäksi, jos haluat lähettää oletusviestin)" suspended_until: "(%{until} asti)" + suspend_forever: "Hyllytä ikuisesti" cant_suspend: "Käyttäjää ei voi hyllyttää." + cant_silence: "Tätä käyttäjää ei voi hiljentää." delete_posts_failed: "Viestien poistaminen epäonnistui." post_edits: "Viestimuokkaukset" view_edits: "Näytä muokkaukset" @@ -4980,6 +5177,8 @@ fi: penalty_post_edit: "Muokkaa viestiä" penalty_post_none: "Älä tee mitään" penalty_count: "Rangaistusten määrä" + penalty_history_MF: >- + Viimeisten kuuden kuukauden aikana tämä käyttäjä on hyllytetty { SUSPENDED, plural, one {kerran} other {# kertaa} } ja hiljennetty { SILENCED, plural, one {kerran} other {# kertaa} }. clear_penalty_history: title: "Pyyhi rangaistushistoria" description: "käyttäjä, jota on rangaistu, ei voi nousta LT3:lle" @@ -5134,10 +5333,14 @@ fi: silenced_count: "Hiljennetty" suspended_count: "Hyllytetyt" last_six_months: "Viimeiset 6 kuukautta" + other_matches: + one: "%{count} muulla käyttäjällä on sama IP-osoite. Tarkista ja valitse epäilyttävät käyttäjät rangaistavaksi yhdessä käyttäjän %{username} kanssa." + other: "%{count} muulla käyttäjällä on sama IP-osoite. Tarkista ja valitse epäilyttävät käyttäjät rangaistavaksi yhdessä käyttäjän %{username} kanssa." other_matches_list: username: "Käyttäjätunnus" trust_level: "Luottamustaso" read_time: "Lukuaika" + topics_entered: "Katseltuja ketjuja" posts: "Viestit" tl3_requirements: title: "Luottamustason 3 vaatimukset" @@ -5454,8 +5657,10 @@ fi: finish: "Poistu määrityksestä" back: "Takaisin" next: "Seuraava" + configure_more: "Määritä lisää..." step-text: "Askelväli" step: "%{current}/%{total}" + upload: "Lataa tiedosto" uploading: "Ladataan..." upload_error: "Tiedoston lataaminen epäonnistui. Yritä uudelleen." staff_count: diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 515ad29fdc..2221871ef7 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -176,6 +176,7 @@ fr: banner: enabled: "A mis à lA une %{when}. Il serA Affiché en hAut de chAque pAge jusqu'à ce qu'il soit ignoré pAr un utilisAteur." disabled: "A supprimé de lA une %{when}. Il ne serA plus Affiché en hAut de chAque pAge." + forwarded: "A transmis l'e-mail ci-dessus" topic_admin_menu: "actions du sujet" skip_to_main_content: "Passer au contenu principal" emails_are_disabled: "L'e-mail sortant a été désactivé par un administrateur. Aucune notification par e-mail ne sera envoyée." @@ -1007,6 +1008,7 @@ fr: notification_schedule: title: "Planification des notifications" label: "Activer la planification personnalisée des notifications" + tip: "En dehors de ces heures, vos notifications seront suspendues." midnight: "Minuit" none: "Jamais" monday: "Lundi" @@ -1050,12 +1052,16 @@ fr: perm_denied_expl: "Vous n'avez pas autorisé les notifications. Autorisez-les à partir des paramètres de votre navigateur." disable: "Désactiver les notifications" enable: "Activer les notifications" + each_browser_note: "Remarque : vous devez modifier ce paramètre sur chaque navigateur utilisé. Toutes les notifications seront désactivées si vous suspendez les notifications à partir du menu utilisateur, quel que soit ce paramètre." consent_prompt: "Souhaitez-vous recevoir des notifications en temps réel en cas de réponse à vos messages ?" dismiss: "Vu" dismiss_notifications: "Tout vu" dismiss_notifications_tooltip: "Marquer les notifications comme lues" dismiss_bookmarks_tooltip: "Marquer tous les rappels de signets non lus comme lus" dismiss_messages_tooltip: "Marquer toutes les notifications de messages directs non lues comme lues" + no_likes_title: "Vous n'avez reçu aucun like pour le moment" + no_likes_body: > + Vous serez averti(e) ici chaque fois que quelqu'un aimera l'un de vos messages. Vous pourrez ainsi savoir ce qui intéresse les autres utilisateurs. Les autres utilisateurs verront la même chose lorsque vous aimerez leurs publications !

    Les notifications relatives aux likes ne vous sont jamais envoyées par e-mail, mais vous pouvez modifier la façon dont vous recevez les notifications concernant les likes sur le site dans vos préférences de notification. no_messages_title: "Vous n'avez aucun message" no_messages_body: > Besoin d'échanger directement avec une personne en-dehors de la conversation principale ? Sélectionnez son avatar et utilisez le bouton %{icon} pour lui écrire un message.

    Pour obtenir de l'aide, adressez-vous à l'un des responsables du site. @@ -1205,6 +1211,62 @@ fr: warnings: "Avertissements officiels" read_more_in_group: "Vous voulez en savoir plus ? Parcourez les autres messages dans %{groupLink}." read_more: "Vous voulez en savoir plus ? Parcourez les autres messages dans les messages personnels." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Il y a # non lu} + other {Il y a # non lus} + } + { NEW, plural, + =0 {} + one { et il reste # nouveau message, ou parcourez d'autres messages dans {groupLink}} + other { et il reste # nouveaux messages, ou parcourez d'autres messages dans {groupLink}} + } + } + false { + { UNREAD, plural, + =0 {} + one {Il reste # message non lu, ou parcourez d'autres messages dans {groupLink}} + other {Il reste # messages non lus, ou parcourez d'autres messages dans {groupLink}} + } + { NEW, plural, + =0 {} + one {Il reste # nouveau message, ou parcourez d'autres messages dans {groupLink}} + other {Il reste # nouveaux messages, ou parcourez d'autres messages dans {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Il y a # non lu} + other {Il y a # non lus} + } + { NEW, plural, + =0 {} + one { et il reste # nouveau message, ou parcourez les autres messages personnels} + other { et il reste # nouveaux messages, ou parcourez les autres messages personnels} + } + } + false { + { UNREAD, plural, + =0 {} + one {Il reste # message non lu, ou parcourez les autres messages personnels} + other {Il reste # messages non lus, ou parcourez les autres messages personnels} + } + { NEW, plural, + =0 {} + one {Il reste # nouveau message, ou parcourez les autres messages personnels} + other {Il reste # nouveaux messages, ou parcourez les autres messages personnels} + } + } + other {} + } preferences_nav: account: "Compte" security: "Sécurité" @@ -1269,7 +1331,14 @@ fr: use: "Utiliser l'application Authenticator" enforced_notice: "Vous devez activer l'authentification à deux facteurs avant d'accéder à ce site." disable: "Désactiver" + disable_confirm: "Voulez-vous vraiment désactiver l'authentification à deux facteurs ?" delete: "Supprimer" + delete_confirm_header: "Ces authentificateurs basés sur des jetons et ces clés de sécurité physiques seront supprimés :" + delete_confirm_instruction: "Pour confirmer, saisissez %{confirm} dans la case ci-dessous." + delete_single_confirm_title: "Suppression d'un authentificateur" + delete_single_confirm_message: "Vous supprimez %{name}. Vous ne pouvez pas annuler cette action. Si vous changez d'avis, vous devrez réenregistrer cet authentificateur." + delete_backup_codes_confirm_title: "Suppression des codes de sauvegarde" + delete_backup_codes_confirm_message: "Vous supprimez des codes de sauvegarde. Vous ne pouvez pas annuler cette action. Si vous changez d'avis, vous devrez générer à nouveau les codes de sauvegarde." save: "Enregistrer" edit: "Modifier" edit_title: "Gérer l'application d'authentification" @@ -1436,6 +1505,8 @@ fr: title: "Le titre de la page en arrière-plan affiche le nombre de :" notifications: "Nouvelles notifications" contextual: "Nouveau contenu de la page" + bookmark_after_notification: + title: "Après l'envoi d'une notification de rappel de signet :" like_notification_frequency: title: "Envoyer une notification si un « J'aime » est attribué" always: "Toujours" @@ -1661,6 +1732,7 @@ fr: save: "Enregistrer" set_custom_status: "Définir un statut personnalisé" what_are_you_doing: "Que faites-vous ?" + pause_notifications: "Suspendre les notifications" remove_status: "Supprimer le statut" user_tips: primary: "J'ai compris !" @@ -1714,6 +1786,36 @@ fr: logout_disabled: "La déconnexion est désactivée quand le site est en lecture seule." staff_writes_only_mode: enabled: "Ce site est en mode réservé aux responsables. Vous pouvez continuer à naviguer, mais les réponses, les j'aime et les autres actions sont limitées aux responsables uniquement." + too_few_topics_and_posts_notice_MF: | + Commençons la discussion ! Il y a { currentTopics, plural, + one {# sujet} + other {# sujets} + } et { currentPosts, plural, + one {# message} + other {# messages} + }. Il en faudrait davantage pour les visiteurs. Nous recommandons de proposer au moins { requiredTopics, plural, + one {# sujet} + other {# sujets} + } et { requiredPosts, plural, + one {# message} + other {# messages} + }. Seul les responsables peuvent voir ce message. + too_few_topics_notice_MF: | + Commençons la discussion ! Il y a { currentTopics, plural, + one {# sujet} + other {# sujets} + }. Il en faudrait davantage pour les visiteurs. Nous recommandons de proposer au moins { requiredTopics, plural, + one {# sujet} + other {# sujets} + }. Seuls les responsables peuvent voir ce message. + too_few_posts_notice_MF: | + Commençons la discussion ! Il y a { currentPosts, plural, + one {# message} + other {# messages} + }. Il en faudrait davantage pour les visiteurs. Nous recommandons de proposer au moins { requiredPosts, plural, + one {# message} + other {# messages} + }. Seuls les responsables peuvent voir ce message. logs_error_rate_notice: reached_hour_MF: | {relativeAge}{rate, plural, one {# erreur/heure} other {# erreurs/heure}} arrive à la limite paramétrée de {limit, plural, one {# erreur/heure} other {# erreurs/heure}}. @@ -1821,6 +1923,9 @@ fr: username: "Utilisateur" password: "Mot de passe" show_password: "Afficher" + hide_password: "Masquer" + show_password_title: "Afficher le mot de passe" + hide_password_title: "Masquer le mot de passe" second_factor_title: "Authentification à deux facteurs" second_factor_description: "Veuillez saisir le code d'authentification généré par votre application :" second_factor_backup: "Se connecter avec un code de secours" @@ -1842,6 +1947,7 @@ fr: blank_username_or_password: "Veuillez saisir votre adresse e-mail ou votre nom d'utilisateur et votre mot de passe." reset_password: "Réinitialiser le mot de passe" logging_in: "Connexion en cours…" + previous_sign_up: "Vous avez déjà un compte ?" or: "ou" authenticating: "Authentification…" awaiting_activation: "Votre compte est en attente d'activation, utilisez le lien de mot de passe oublié pour envoyer un autre e-mail d'activation." @@ -2016,10 +2122,17 @@ fr: private: "Vous avez mentionné @%{username}, mais aucune notification ne lui sera envoyée, car ce message direct ne lui est pas visible. Vous devez l'inviter à rejoindre ce message direct." muted_topic: "Vous avez mentionné @%{username}, mais aucune notification ne lui sera envoyée, car il ou elle a désactivé ce sujet." not_allowed: "Vous avez mentionné @%{username}, mais aucune notification ne lui sera envoyée, car il ou elle n'a pas été invité(e) à rejoindre ce sujet." + cannot_see_group_mention: + not_mentionable: "Vous ne pouvez pas mentionner le groupe @%{group}." + some_not_allowed: + one: "Vous avez mentionné @%{group}, mais seul %{count} membre sera notifié, car les autres membres ne peuvent pas voir ce message direct. Vous devrez les inviter pour qu'ils accèdent à ce message direct." + other: "Vous avez mentionné @%{group}, mais seuls %{count} membres seront notifiés, car les autres membres ne peuvent pas voir ce message direct. Vous devrez les inviter pour qu'ils accèdent à ce message direct." + not_allowed: "Vous avez mentionné @%{group}, mais aucun de ses membres ne sera notifié, car ils ne peuvent pas voir ce message direct. Vous devrez les inviter pour qu'ils accèdent à ce message direct." here_mention: one: "En mentionnant @%{here}, vous êtes sur le point de notifier %{count} utilisateur. Voulez-vous continuer ?" other: "En mentionnant @%{here}, vous êtes sur le point de notifier %{count} utilisateurs. Voulez-vous continuer ?" duplicate_link: "Il semblerait que votre lien vers %{domain} a déjà été publié dans le sujet par @%{username} dans une réponse de %{ago}. Voulez-vous vraiment le publier à nouveau ?" + duplicate_link_same_user: "Il semble que vous avez déjà publié un lien vers %{domain} dans ce sujet dans une réponse le %{ago}. Voulez-vous vraiment le publier à nouveau ?" reference_topic_title: "RE : %{title}" error: title_missing: "Le titre est requis" @@ -2155,6 +2268,12 @@ fr: high_priority: one: "%{count} notification prioritaire non lue" other: "%{count} notifications prioritaires non lues" + new_message_notification: + one: "%{count} notification de nouveau message" + other: "%{count} notifications de nouveaux messages" + new_reviewable: + one: "%{count} nouveau message à examiner" + other: "%{count} nouveaux messages à examiner" title: "notifications des mentions de votre nom d'utilisateur, des réponses à vos messages et sujets, etc." none: "Impossible de charger les notifications pour le moment." empty: "Aucune notification trouvée." @@ -2200,6 +2319,7 @@ fr: reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} - terminé" + new_features: "Nouvelles fonctionnalités disponibles !" dismiss_confirmation: body: default: @@ -2254,6 +2374,7 @@ fr: membership_request_consolidated: "nouvelles demandes d'adhésion" reaction: "nouvelle réaction" votes_released: "Le vote a été publié" + new_features: "De nouvelles fonctionnalités de Discourse ont été publiées !" upload_selector: uploading: "En cours d'envoi" processing: "Traitement du téléversement" @@ -2322,6 +2443,7 @@ fr: status: "filtres par statut de sujet" full_search: "lance la recherche en pleine page" full_search_key: "%{modifier} + Entrée" + me: "affiche uniquement vos messages" advanced: title: Filtres avancés posted_by: @@ -2562,7 +2684,48 @@ fr: show_links: "afficher les liens dans ce sujet" collapse_details: "masquer les détails de ce sujet" expand_details: "afficher les détails de ce sujet" + read_more_in_category: "Vous voulez en savoir plus ? Parcourez les autres sujets de %{categoryLink} ou affichez les derniers sujets." + read_more: "Vous voulez en savoir plus ? Parcourez toutes les catégories ou affichez les derniers sujets." unread_indicator: "Aucun membre n'a encore lu le dernier message de ce sujet." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Il y a # message non lu} + other {Il y a # messages non lus} + } + { NEW, plural, + =0 {} + one { et il reste # nouveau sujet,} + other { et il reste # nouveaux sujets,} + } + } + false { + { UNREAD, plural, + =0 {} + one {Il reste # sujet non lu,} + other {Il reste # sujets non lus,} + } + { NEW, plural, + =0 {} + one {Il reste # nouveau sujet,} + other {Il reste # nouveaux sujets,} + } + } + other {} + } + { HAS_CATEGORY, select, + true { ou parcourez d'autres sujets dans {categoryLink}} + false { ou consultez les derniers sujets} + other {} + } + bumped_at_title: | + Premier message : %{createdAtDate} + Publié : %{bumpedAtDate} + browse_all_categories_latest: "Parcourez toutes les catégories ou consultez les derniers sujets." + browse_all_categories_latest_or_top: "Parcourez toutes les catégories, consultez les derniers sujets ou consultez les principaux sujets :" + browse_all_tags_or_latest: "Parcourez toutes les étiquettes ou consultez les derniers sujets." suggest_create_topic: Prêt à démarrer une nouvelle conversation ? jump_reply_up: accéder aux réponses précédentes jump_reply_down: accéder aux réponses ultérieures @@ -2983,6 +3146,7 @@ fr: one: "Nous sommes désolés, vous ne pouvez envoyer que %{count} fichier à la fois." other: "Nous sommes désolés, vous ne pouvez envoyer que %{count} fichiers à la fois." upload_not_authorized: "Nous sommes désolés, le fichier que vous essayez d'envoyer n'est pas autorisé (extensions autorisées : %{authorized_extensions})." + no_uploads_authorized: "Nous sommes désolés, aucun fichier ne peut être téléversé." image_upload_not_allowed_for_new_user: "Nous sommes désolés, les nouveaux utilisateurs ne peuvent pas envoyer d'images." attachment_upload_not_allowed_for_new_user: "Nous sommes désolés, les nouveaux utilisateurs ne peuvent pas envoyer de fichiers." attachment_download_requires_login: "Nous sommes désolés, vous devez être connecté(e) pour télécharger une pièce jointe." @@ -2994,6 +3158,7 @@ fr: via_email: "ce message est arrivé par e-mail" via_auto_generated_email: "ce message est arrivé via un e-mail généré automatiquement" whisper: "ce message est un murmure privé pour les modérateurs" + whisper_groups: "ce message est un murmure privé visible uniquement par %{groupNames}" wiki: about: "ce message est un wiki" few_likes_left: "Merci de partager votre amour ! Vous n'avez plus que quelques « J'aime » à attribuer aujourd'hui." @@ -3216,6 +3381,7 @@ fr: pending_permission_change_alert: "Vous n'avez pas ajouté %{group} à cette catégorie ; cliquez sur ce bouton pour l'ajouter." images: "Images" email_in: "Adresse e-mail entrant personnalisée :" + email_in_tooltip: "Vous pouvez séparer plusieurs adresses e-mail avec le caractère | ." email_in_allow_strangers: "Accepter les e-mails d'utilisateurs anonymes sans compte" email_in_disabled: "La création de nouveaux sujets par e-mail est désactivée dans les paramètres. Pour activer la création de nouveaux sujets par e-mail, " email_in_disabled_click: 'activez le paramètre « e-mail entrant ».' @@ -3496,7 +3662,6 @@ fr: this_week: "Semaine" today: "Aujourd'hui" browser_update: 'Malheureusement, votre navigateur n''est pas pris en charge. Merci de mettre à jour votre navigateur pour afficher le contenu enrichi, vous connecter et répondre.' - safari_13_warning: Ce site va bientôt supprimer la prise en charge des versions 13 et inférieures d'iOS et de Safari. Une version simplifiée en lecture seule restera disponible. (plus d'informations) permission_types: full: "Créer/Répondre/Voir" create_post: "Répondre/Voir" @@ -3784,6 +3949,8 @@ fr: enabled: "Le mode sans échec est activé ; fermez cette fenêtre de navigateur pour le quitter" image_removed: "(image supprimée)" pause_notifications: + title: "Suspendre les notifications pour…" + label: "Suspendre les notifications" remaining: "Encore %{remaining}" options: half_hour: "30 minutes" @@ -3827,12 +3994,15 @@ fr: second_factor_auth: redirect_after_success: "L'authentification à deux facteurs est réussie. Redirection vers la page précédente…" sidebar: + show_sidebar: "Afficher la barre latérale" + hide_sidebar: "Masquer la barre latérale" unread_count: one: "%{count} non lu" other: "%{count} non lus" new_count: one: "%{count} nouveau" other: "%{count} nouveaux" + toggle_section: "Basculer la section" more: "Plus" all_categories: "Toutes les catégories" all_tags: "Toutes les étiquettes" @@ -3841,6 +4011,7 @@ fr: header_link_text: "À propos" messages: header_link_text: "Messages" + header_action_title: "Créer un message direct" links: inbox: "Boîte de réception" sent: "Envoyés" @@ -3857,6 +4028,7 @@ fr: none: "Vous n'avez ajouté aucune étiquette." click_to_get_started: "Cliquez ici pour commencer." header_link_text: "Étiquettes" + header_action_title: "Modifier les étiquettes de votre barre latérale" configure_defaults: "Configurer les valeurs par défaut" categories: links: @@ -3866,32 +4038,43 @@ fr: none: "Vous n'avez ajouté aucune catégorie." click_to_get_started: "Cliquez ici pour commencer." header_link_text: "Catégories" + header_action_title: "Modifier les catégories de votre barre latérale" configure_defaults: "Configurer les valeurs par défaut" community: header_link_text: "Communauté" + header_action_title: "Créer un sujet" links: about: content: "À propos" + title: "Plus de détails sur ce site" admin: content: "Administrateur" + title: "Paramètres du site et rapports" badges: content: "Badges" + title: "Tous les badges disponibles à gagner" everything: content: "Tout" title: "Tous les sujets" faq: content: "FAQ" + title: "Lignes directrices pour l'utilisation de ce site" groups: content: "Groupes" + title: "Liste des groupes d'utilisateurs disponibles" users: content: "Utilisateurs" + title: "Liste de tous les utilisateurs" my_posts: content: "Mes messages" + title: "Mon activité récente sur le sujet" + title_drafts: "Mes brouillons non publiés" draft_count: one: "%{count} brouillon" other: "%{count} brouillons" review: content: "À examiner" + title: "Publications signalées et autres éléments en file d'attente" pending_count: "%{count} en attente" welcome_topic_banner: title: "Créez votre sujet de bienvenue" @@ -4044,6 +4227,9 @@ fr: other: "%{count} utilisateurs ont les nouveaux domaines de messagerie et seront ajoutés au groupe." automatic_membership_associated_groups: "Les utilisateurs qui sont membres d'un groupe d'un fournisseur d'identité repris ici seront automatiquement ajoutés à ce groupe lorsqu'ils s'authentifieront avec ce fournisseur." primary_group: "Définir comme groupe principal automatiquement" + alert: + primary_group: "Puisqu'il s'agit d'un groupe principal, le nom « %{group_name} » sera utilisé dans les classes CSS qui peuvent être consultées par n'importe qui." + flair_group: "Puisque ce groupe a du flair pour ses membres, le nom « %{group_name} » sera visible par tous." name_placeholder: "Nom du groupe (sans espaces, mêmes règles que pour les noms d'utilisateurs)" primary: "Groupe principal" no_primary: "(pas de groupe principal)" @@ -4053,6 +4239,10 @@ fr: about: "Modifier votre adhésion et les noms ici" group_members: "Membres du groupe" delete: "Supprimer" + delete_confirm: "Voulez-vous vraiment supprimer ce groupe ?" + delete_with_messages_confirm: + one: "La suppression de ce groupe rendra %{count} message orphelin, les membres du groupe n'y auront plus accès." + other: "La suppression de ce groupe rendra %{count} messages orphelins, les membres du groupe n'y auront plus accès." delete_failed: "Impossible de supprimer le groupe. S'il s'agit d'un groupe automatique, il ne peut pas être détruit." delete_automatic_group: Ce groupe ne peut pas être supprimé car il est constitué de façon automatique. delete_owner_confirm: "Retirer les privilèges de propriétaire pour %{username} ?" @@ -4118,7 +4308,7 @@ fr: topics: read: Lire un sujet ou un message spécifique qu'il contient. Le RSS est aussi accepté. write: Créer un nouveau sujet ou publier sur un sujet existant. - update: Mettre à jour un sujet. Modifiez le titre, la catégorie, les étiquettes, etc. + update: Mettre à jour un sujet. Modifiez le titre, la catégorie, les étiquettes, le statut, l'archétype, le lien en vedette, etc. read_lists: Lire les listes de sujets comme top, nouveaux, récents, etc. Le RSS est aussi accepté. posts: edit: Modifiez n'importe quel message ou un message spécifique. @@ -4137,6 +4327,9 @@ fr: anonymize: Anonymiser les comptes utilisateurs. delete: Supprimer les comptes utilisateurs. list: Obtenir une liste d'utilisateurs. + user_status: + read: Lire le statut de l'utilisateur. + update: Mettre à jour le statut de l'utilisateur. email: receive_emails: Combiner ces permissions au service de réception d'e-mail pour traiter les e-mails entrants. badges: @@ -4161,6 +4354,7 @@ fr: create: "Créer" edit: "Modifier" save: "Enregistrer" + description_label: "Déclencheurs d'événements" controls: "Contrôles" go_back: "Retour à la liste" payload_url: "URL cible" @@ -4263,6 +4457,8 @@ fr: broken_route: "Impossible de configurer le lien vers « %{name} ». Assurez-vous que les bloqueurs de publicités sont désactivés et essayez de recharger la page." navigation_menu: sidebar: "Barre latérale" + header_dropdown: "Liste déroulante de l'en-tête" + legacy: "Héritage" backups: title: "Sauvegardes" menu: @@ -4765,7 +4961,7 @@ fr: post_edit: "message modifié" post_unlocked: "message déverrouillé" check_personal_message: "vérifier un message direct" - disabled_second_factor: "désactiver la validation en deux étapes" + disabled_second_factor: "désactiver l'authentification à deux facteurs" topic_published: "sujet publié" post_approved: "message approuvé" post_rejected: "message rejeté" @@ -4947,6 +5143,7 @@ fr: user: suspend_failed: "Une erreur s'est produite lors de la suspension de cet utilisateur %{error}" unsuspend_failed: "Une erreur s'est produite lors de la réactivation de cet utilisateur %{error}" + suspend_duration: "Suspendre l'utilisateur jusqu'à :" suspend_reason_label: "Pourquoi suspendez-vous cet utilisateur ? Ce texte sera visible par tout le monde sur la page du profil de cet utilisateur et sera affiché à l'utilisateur quand il essaiera de se connecter. Tâchez d'être concis(e)." suspend_reason_hidden_label: "Pourquoi suspendez-vous cet utilisateur ? Ce texte sera affiché à l'utilisateur quand il essaiera de se connecter. Tâchez d'être concis(e)." suspend_reason: "Raison" @@ -4970,7 +5167,9 @@ fr: silence_message: "E-mail" silence_message_placeholder: "(laissez vide pour envoyer un message par défaut)" suspended_until: "(jusqu'à %{until})" + suspend_forever: "Suspendre pour toujours" cant_suspend: "Cet utilisateur ne peut pas être suspendu." + cant_silence: "Cet utilisateur ne peut pas être mis en sourdine." delete_posts_failed: "Un problème est survenu lors de la suppression des messages." post_edits: "Modifications de message" view_edits: "Afficher les modifications" @@ -4980,6 +5179,8 @@ fr: penalty_post_edit: "Modifier le message" penalty_post_none: "Ne rien faire" penalty_count: "Nombre de pénalités" + penalty_history_MF: >- + Au cours des 6 derniers mois, cet utilisateur a été suspendu { SUSPENDED, plural, one {# fois} other {# fois} } et mis en sourdine { SILENCED, plural, one {# fois} other {# fois} }. clear_penalty_history: title: "Effacer l'historique des pénalités" description: "les utilisateurs ayant des pénalités ne peuvent pas atteindre le niveau de confiance 3" @@ -5019,7 +5220,7 @@ fr: private_topics_count: Sujets privés posts_read_count: Messages lus post_count: Messages créés - second_factor_enabled: Validation en deux étapes activée + second_factor_enabled: Authentification à deux facteurs activée topics_entered: Sujets vus flags_given_count: Signalements effectués flags_received_count: Signalements reçus @@ -5134,10 +5335,14 @@ fr: silenced_count: "En sourdine" suspended_count: "Suspendus" last_six_months: "6 derniers mois" + other_matches: + one: "Il y a %{count} autre utilisateur avec la même adresse IP. vérifiez et sélectionnez l'utilisateur suspect à pénaliser avec %{username}." + other: "Il y a %{count} autres utilisateurs avec la même adresse IP. vérifiez et sélectionnez les utilisateurs suspects à pénaliser avec %{username}." other_matches_list: username: "Nom d'utilisateur" trust_level: "Niveau de confiance" read_time: "Temps de lecture" + topics_entered: "Sujets entrés" posts: "Messages" tl3_requirements: title: "Prérequis pour le niveau de confiance 3" @@ -5454,8 +5659,10 @@ fr: finish: "Quitter la configuration" back: "Retour" next: "Suivant" + configure_more: "Configurer plus…" step-text: "Étape" step: "%{current} sur %{total}" + upload: "Téléverser un fichier" uploading: "Envoi en cours…" upload_error: "Nous sommes désolés, une erreur est survenue lors de l'envoi de ce fichier. Veuillez réessayer." staff_count: diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index de173728ab..fdc2b9dbdd 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -1399,6 +1399,7 @@ gl: edit_title: "Editar convite" instructions: "Compartir esta ligazón para conceder acceso a este sitio:" copy_link: "copiar a ligazón" + expired_at_time: "Caduca o %{time}" show_advanced: "Amosar opcións avanzadas" hide_advanced: "Agochar opcións avanzadas" add_to_groups: "Engadir aos grupos" @@ -2127,9 +2128,11 @@ gl: dismiss_new: "Desbotar novas" toggle: "cambiar a selección en bloque dos temas" actions: "Accións en bloque" + change_category: "Estabelecer categoría..." close_topics: "Pechar temas" archive_topics: "Arquivar temas" move_messages_to_inbox: "Mover á caixa de entrada" + notification_level: "Notificacións..." change_notification_level: "Cambiar o nivel de notificacións" choose_new_category: "Seleccionar a nova categoría dos temas:" selected: @@ -2353,12 +2356,14 @@ gl: open: "Abrir tema" close: "Pechar tema" multi_select: "Seleccionar publicacións…" + slow_mode: "Estabelecer modo lento..." timed_update: "Estabelecer temporizador do tema..." pin: "Fixar tema…" unpin: "Desprender tema…" unarchive: "Desarquivar tema" archive: "Arquivar tema" reset_read: "Restabelecer datos de lecturas" + make_public: "Facer público o tema..." make_private: "Facer privada a mensaxe" reset_bump_date: "Restabelecer data de promoción" feature: @@ -2641,6 +2646,8 @@ gl: rebake: "Reconstruír HTML" publish_page: "Publicación da páxina" unhide: "Non agochar" + change_owner: "Cambiar propietario..." + grant_badge: "Conceder insignia..." lock_post: "Bloquear publicación" lock_post_description: "evitar que o autor edite esta publicación" unlock_post: "Desbloquear a publicación" @@ -2654,6 +2661,8 @@ gl: delete_topic_confirm_modal_no: "Non, manter este tema" delete_topic_error: "Produciuse un erro ao eliminar este tema" delete_topic: "eliminar tema" + add_post_notice: "Engadir aviso do equipo..." + change_post_notice: "Cambiar o aviso do equipo..." delete_post_notice: "Eliminar o aviso do equipo" remove_timer: "retirar o temporizador" edit_timer: "editar temporizador" @@ -3285,6 +3294,7 @@ gl: everyone_can_use: "Todos poden usar as etiquetas" usable_only_by_groups: "As etiquetas son visíbeis para todos, pero só os seguintes grupos poden usalas" visible_only_to_groups: "As etiquetas só son visíbeis para os seguintes grupos" + parent_tag_placeholder: "Opcional" topics: none: unread: "Non ten temas sen ler." @@ -3361,6 +3371,8 @@ gl: content: "Administración" badges: content: "Insignias" + everything: + content: "Todo" faq: content: "PMF" groups: @@ -3371,6 +3383,7 @@ gl: content: "As miñas publicacións" review: content: "Revisar" + until: "Ata:" admin_js: type_to_filter: "escriba para filtrar..." admin: diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index acae120830..4265f51e28 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -1033,7 +1033,7 @@ he: posts: "פוסטים" topics: "נושאים" latest: "לאחרונה" - subcategories: "תתי קטגוריות" + subcategories: "תת־קטגוריות" muted: "קטגוריות מושתקות" topic_sentence: one: "נושא אחד" @@ -1166,7 +1166,7 @@ he: perm_denied_expl: "דחית הרשאה לקבלת התראות. יש לאפשר התראות בהגדרות הדפדפן שלך." disable: "השבתת התראות" enable: "הפעלת התראות" - each_browser_note: 'הערה: עליך לשנות הגדרה זו בכל דפדפן שמשמש אותך. כל ההודעות יושבתו אם בחרת להשהות התראות מתפריט המשתמש, ללא קשר להגדרה זו.' + each_browser_note: "הערה: עליך לשנות הגדרה זו בכל דפדפן שמשמש אותך. כל ההודעות יושבתו אם בחרת להשהות התראות מתפריט המשתמש, ללא קשר להגדרה זו." consent_prompt: "לקבל התראות חיות כשמתקבלות תגובות לפוסטים שלך?" dismiss: "דחה" dismiss_notifications: "להתעלם מהכול" @@ -3848,7 +3848,6 @@ he: this_week: "שבוע" today: "היום" browser_update: 'אתרע מזלך וכי דפדפנך אינו נתמך עוד. נא לעבור לדפדפן נתמך כדי לצפות בתוכן עשיר, להיכנס ולהגיב.' - safari_13_warning: אתר זה יסיר בקרוב את התמיכה בגרסאות 13 ומטה של Safari ושל iOS. גרסה מופשאת לקריאה בלבד תישאר זמינה עבורם. (מידע נוסף) permission_types: full: "יצירה / תגובה / צפייה" create_post: "תגובה / צפייה" @@ -4533,7 +4532,7 @@ he: topics: read: קריאת נושא או פוסט מסוים בתוכו. יש גם תמיכה ב־RSS. write: יצירת נושא חדש או פרסום לאחד קיים. - update: עדכון נושא. החלפת הכותרת, הקטגוריה, התגיות וכו׳. + update: עדכון נושא. אפשר לערוך את הכותרת, הקטגוריה, התגיות, המצב, סוג שימוש, קישור מובילים וכו׳. read_lists: לקרוא רשימת נושאים כמו מובילים, חדשים, אחרונים וכו׳. יש גם תמיכה ב־RSS. posts: edit: לערוך כל פוסט שהוא או אחד מסוים. @@ -4685,6 +4684,7 @@ he: broken_route: "לא ניתן להגדיר את הקישור אל ‚%{name}’. כדאי לוודא שחוסמי פרסומות מושבתים ואז לנסות לטעון את הדף מחדש." navigation_menu: sidebar: "סרגל צד" + legacy: "מיושן" backups: title: "גיבויים" menu: diff --git a/config/locales/client.hr.yml b/config/locales/client.hr.yml index 1b13ea2b8c..8c95ac6ab9 100644 --- a/config/locales/client.hr.yml +++ b/config/locales/client.hr.yml @@ -204,6 +204,7 @@ hr: banner: enabled: "Pretvoreno u banner %{when}. Banner će biti prikazan na vrhu svake stranice dok ga korisnik ne isključi." disabled: "Maknuo banner %{when}. Banner se više neće prikazivati na vrhu svake stranice." + forwarded: "Prosljeđeno na gore navedenu adresu e-pošte." topic_admin_menu: "mogućnosti teme" skip_to_main_content: "Preskoči na glavni sadržaj" emails_are_disabled: "Svi emailovi prema van su blokirani od strane administratora. Ni jedna vrsta obavijesti putem emaila neće biti poslana." @@ -217,6 +218,7 @@ hr: other: "Kako bi vam olakšali pokretanje novog foruma, nalazite se u bootstrap načinu rada. Svim novim korisnicima biti će dodijeljena razina povjerenja 1 i omogućeni dnevni e-mailovi sa sažecima. Bootstrap će se automatski isključiti čim se učlani minimalno %{count} korisnika." bootstrap_mode_disabled: "Bootstrap mod biti će isključen u sljedećih 24 sata." bootstrap_invite_button_title: "Pošalji pozivnice" + bootstrap_wizard_link_title: "Završi postavljanje" themes: default_description: "Zadano" broken_theme_alert: "Vaša stranica možda neće raditi jer tema/komponenta ima pogreške." @@ -259,6 +261,7 @@ hr: delete: "Pobriši" generic_error: "Dogodila se greška, ispričavamo se." generic_error_with_reason: "Dogodila se greška: %{error}" + multiple_errors: "Došlo je do više grešaka: %{errors}" sign_up: "Učlani se" log_in: "Prijavi se" age: "Dob" @@ -322,6 +325,7 @@ hr: like_count: "Likeovi" topic_count: "Teme" post_count: "Objave" + user_count: "Učlani se" active_user_count: "Aktivni korisnici" contact: "Kontaktirajte nas" contact_info: "U slučaju kritičnog problema ili hitnosti koje utječu na rad stranice, kontaktirajte nas na %{contact_info}." @@ -631,6 +635,8 @@ hr: relative: "Relativno" time_shortcut: now: "Sada" + in_one_hour: "Za jedan sat" + in_two_hours: "Za dva sata" later_today: "Kasnije danas" two_days: "Dva dana" next_business_day: "Sljedeći radni dan" @@ -698,6 +704,8 @@ hr: reset_to_default: "Vrati na zadano" group: all: "sve grupe" + sort: + label: "Sortiraj po %{criteria}" group_histories: actions: change_group_setting: "Promijenite postavke grupe" @@ -863,6 +871,7 @@ hr: group_type: "Vrsta grupe" is_group_user: "Član" is_group_owner: "Vlasnik" + search_results: "Rezultati pretraživanja pojavit će se ispod." title: one: "Grupa" few: "Grupe" @@ -898,6 +907,7 @@ hr: no_filter_matches: "Nijedan član ne odgovara toj potrazi." topics: "Teme" posts: "Objave" + aria_post_number: "%{title} - članak #%{postNumber}" mentions: "Spomeni" messages: "Poruke" notification_level: "Zadani nivo obavijesti za grupne poruke" @@ -1017,6 +1027,7 @@ hr: user_fields: none: "(odaberi opciju)" required: 'Unesi vrijednost za "%{name}"' + same_as_password: "Vaša lozinka ne smije se ponavljati u drugim poljima." user: said: "%{username}:" profile: "Profil" @@ -1054,6 +1065,7 @@ hr: notification_schedule: title: "Raspored obavijesti (notifikacije)" label: "Omogući prilagođeni raspored obavijesti" + tip: "Izvan ovih sati vaše obavijesti biti će zaustavljene." midnight: "Ponoć" none: "Ništa" monday: "ponedjeljak" @@ -1097,10 +1109,16 @@ hr: perm_denied_expl: "Odbili ste dopuštenje za obavijesti. Dopusti obavijesti putem postavki preglednika." disable: "Isključi obavijesti" enable: "Uključi obavijesti" + each_browser_note: "Napomena: ovu postavku morate promijeniti na svakom pregledniku koji koristite. Sve će obavijesti biti onemogućene ako pauzirate obavijesti iz korisničkog izbornika, bez obzira na ovu postavku." consent_prompt: "Želite li obavijesti (uživo) kada ljudi odgovaraju na vaše postove?" dismiss: "Skloni" dismiss_notifications: "Skloni sve" dismiss_notifications_tooltip: "Označi sve nepročitane obavijesti kao pročitane" + dismiss_bookmarks_tooltip: "Označite sve nepročitane podsjetnike kao pročitane" + dismiss_messages_tooltip: "Označi sve nepročitane obavijesti o privatnim porukama kao pročitane" + no_likes_title: "Još niste dobili nijedan lajk" + no_likes_body: > + Biti ćete obaviješteni svaki put kada netko lajka neku od vaših objava kako biste mogli vidjeti što drugi smatraju vrijednim. Drugi će vidjeti isto kada i vi lajkate njihove objave!

    Obavijesti o lajkovima nikada vam se ne šalju e-poštom, ali možete podesiti kako ćete primati obavijesti o lajkovima na web mjestu u svojim postavkama obavijesti. no_messages_title: "Nemate nijednu poruku" no_messages_body: > Trebate voditi izravan osobni razgovor s nekim, izvan uobičajenog toka razgovora? Pošaljite im poruku odabirom avatara i %{icon} poruke.

    Ako trebate pomoć, možete poslati poruku osoblju. @@ -1111,12 +1129,16 @@ hr: no_notifications_title: "Još nemate nijednu obavijest" no_notifications_body: > Bit ćete obaviješteni u ovom panelu o aktivnosti izravno relevantne za vas, uključujući i odgovorima na vaše teme i postove, kada netko @označi Vas ili citira vas, i odgovori na temu koju gledate. Obavijesti će se slati i na vašu e-poštu kada se neko vrijeme niste prijavili.

    Potražite %{icon} da biste odlučili o kojim određenim temama, kategorijama i oznakama želite biti obaviješteni. Više informacija potražite u postavki obavijesti. + no_other_notifications_title: "Još nemate nijednu obavijest" + no_other_notifications_body: > + Na ovom ćete panelu biti obaviješteni o drugim vrstama aktivnosti koje mogu biti relevantne za vas - na primjer, kada netko linka vaš post ili uredi neki od vaših postova. no_notifications_page_title: "Još nemate nijednu obavijest" no_notifications_page_body: > Bit ćete obaviješteni o aktivnostima izravno relevantne za vas, uključujući i odgovorima na vaše teme i postove, kada netko @mentions Vi ili citati vas, i odgovori na temu koju gledate. Obavijesti će se također slati na vašu e -poštu ako se neko vrijeme niste prijavili.

    Potražite %{icon} da biste odlučili o kojim temama, kategorijama i oznakama želite biti obaviješteni. Za više informacija pogledajte postavki obavijesti. dynamic_favicon: "Prikaži brojeve na ikoni preglednika" skip_new_user_tips: description: "Preskoči savjete i značke novog korisnika" + reset_seen_user_tips: "Ponovno prikaži savjete za korisnike" theme_default_on_all_devices: "Postavi ovu temu kao zadanu na svim mojim uređajima" color_scheme_default_on_all_devices: "Postavljanje zadane sheme boja na svim mojim uređajima" color_scheme: "Shema boja" @@ -1136,8 +1158,16 @@ hr: enable_quoting: "Omogući citirani odgovor označenoj teksta" enable_defer: "Uključi odgodu označavanja nepročitanih tema" experimental_sidebar: + enable: "Omogući bočnu traku" options: "Mogućnosti" + categories_section: "Odjeljak s kategorijama" + categories_section_instruction: "Odabrane kategorije bit će prikazane u odjeljku kategorija na bočnoj traci." + tags_section: "Odjeljak s oznakama" + tags_section_instruction: "Odabrane oznake bit će prikazane u odjeljku oznaka na bočnoj traci." navigation_section: "Navigacija" + list_destination_instruction: "Kada se pojavi novi sadržaj na bočnoj traci..." + list_destination_default: "koristite zadanu vezu i prikažite značku za nove stavke" + list_destination_unread_new: "poveži na nepročitano/novo i prikaži broj novih stavki" change: "promijeni" featured_topic: "Istaknuta tema" moderator: "%{user} je moderator" @@ -1240,6 +1270,70 @@ hr: warnings: "Službena upozorenja" read_more_in_group: "Želite li pročitati više? Pregledajte ostale poruke u %{groupLink}." read_more: "Želite li pročitati više? Pregledajte ostale poruke u osobnim porukama." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Postoji # nepročitano} + few {Ima ih # nepročitanih} + other {Ima ih # nepročitanih} + } + { NEW, plural, + =0 {} + one { i # nova preostala poruka ili pregledajte ostale poruke u {groupLink}} + few { i # novih preostalih poruka ili pregledajte ostale poruke u {groupLink}} + other { i # novih preostalih poruka ili pregledajte ostale poruke u {groupLink}} + } + } + false { + { UNREAD, plural, + =0 {} + one {Preostale su # nepročitane poruke ili pregledajte ostale poruke u {groupLink}} + few {Preostalo je # nepročitanih poruka ili pregledajte ostale poruke u {groupLink}} + other {Preostalo je # nepročitanih poruka ili pregledajte ostale poruke u {groupLink}} + } + { NEW, plural, + =0 {} + one {Preostalo je # novih poruka ili pregledajte ostale poruke u {groupLink}} + few {Preostalo je # novih poruka ili pregledajte ostale poruke u {groupLink}} + other {Preostalo je # novih poruka ili pregledajte ostale poruke u {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Postoji # nepročitanih} + few {Postoji # nepročitano} + other {Postoji # nepročitano} + } + { NEW, plural, + =0 {} + one { i # nova preostala poruka ili pregledajte ostale osobne poruke} + few { i # novih preostalih poruka ili pregledajte ostalih osobnih poruka} + other { i # novih preostalih poruka ili pregledajte ostalih osobnih poruka} + } + } + false { + { UNREAD, plural, + =0 {} + one {Preostalo je # nepročitanih poruka ili pregledajte ostalih osobnih poruka} + few {Preostalo je # nepročitanih poruka ili pregledajte ostalih osobnih poruka} + other {Preostalo je # nepročitanih poruka ili pregledajte ostalih osobnih poruka} + } + { NEW, plural, + =0 {} + one {Preostalo je # novih poruka ili pregledajte ostalih osobnih poruka} + few {Preostalo je # novih poruka ili pregledajte ostalih osobnih poruka} + other {Preostalo je # novih poruka ili pregledajte ostalih osobnih poruka} + } + } + other {} + } preferences_nav: account: "Račun" security: "Sigurnost" @@ -1252,6 +1346,7 @@ hr: tags: "Oznake" interface: "Sučelje" apps: "Aplikacije" + sidebar: "Bočna traka" change_password: success: "(email je poslan)" in_progress: "(email se šalje)" @@ -1307,12 +1402,18 @@ hr: disable: "Onemogući" disable_confirm: "Jeste li sigurni da želite onemogućiti dvofaktorsku provjeru autentičnosti?" delete: "Izbriši" + delete_confirm_header: "Ovi autentifikatori temeljeni na tokenima i fizički sigurnosni ključevi bit će izbrisani:" delete_confirm_instruction: "Za potvrdu unesite %{confirm} u okvir ispod." delete_single_confirm_title: "Brisanje autentifikatora" + delete_single_confirm_message: "Brišete %{name}. Ne možete poništiti ovu radnju. Ako se predomislite, morate ponovno registrirati ovaj autentifikator." + delete_backup_codes_confirm_title: "Brisanje pričuvnih kodova" + delete_backup_codes_confirm_message: "Brišete pričuvne kodove. Ne možete poništiti ovu radnju. Ako se predomislite, morate ponovno generirati pričuvne kodove." save: "Spremi" edit: "Izmijeni" edit_title: "Uredi autentifikator" edit_description: "Ime autentifikatora" + enable_security_key_description: | + Kada pripremite svoj hardverski sigurnosni ključ ili kompatibilni mobilni uređaj, pritisnite gumb za registraciju u nastavku. totp: title: "Autentifikatori temeljeni na Tokenu" add: "Dodaj provjeru autentičnosti" @@ -1320,12 +1421,16 @@ hr: name_and_code_required_error: "Morate navesti ime i kôd iz aplikacije za provjeru autentičnosti." security_key: register: "Registracija" + title: "Fizički sigurnosni ključevi" + add: "Dodajte fizički sigurnosni ključ" default_name: "Glavni sigurnosni ključ" iphone_default_name: "iPhone" android_default_name: "Android" not_allowed_error: "Postupak registracije sigurnosnog ključa ili je vremenski ograničen ili je otkazan." already_added_error: "Već ste registrirali ovaj sigurnosni ključ. Ne morate ga ponovno registrirati." + edit: "Uredi fizički sigurnosni ključ" save: "Spremi" + edit_description: "Naziv ključa fizičke sigurnosti" name_required_error: "Morate navesti ime za svoj sigurnosni ključ." change_about: title: "Promijeni O meni" @@ -1470,6 +1575,8 @@ hr: title: "Naslov pozadinske stranice prikazuje broj:" notifications: "Nove obavijesti" contextual: "Sadržaj nove stranice" + bookmark_after_notification: + title: "Nakon slanja obavijesti o podsjetniku oznake:" like_notification_frequency: title: "Obavijesti ako se sviđa" always: "Uvijek" @@ -1687,6 +1794,9 @@ hr: title: "Njuh" none: "(ništa)" instructions: "ikona prikazana uz sliku vašeg profila" + status: + title: "Prilagođeni status" + not_set: "Nije postavljeno" primary_group: title: "Primarna grupa" none: "(ništa)" @@ -1702,6 +1812,25 @@ hr: set_custom_status: "Postavi prilagođeni status" what_are_you_doing: "Što radiš?" pause_notifications: "Pauziraj obavijesti" + remove_status: "Ukloni status" + user_tips: + primary: "Kužim!" + secondary: "ne pokazuj mi ove savjete" + first_notification: + title: "Vaša prva obavijest!" + content: "Obavijesti se koriste kako biste bili u tijeku s onim što se događa u zajednici." + topic_timeline: + title: "Vremenski okvir teme" + content: "Brzo listajte kroz objavu pomoću vremenske trake teme." + post_menu: + title: "Izbornik za objave" + content: "Pogledajte kako još možete komunicirati s objavom klikom na tri točkice!" + topic_notification_levels: + title: "Sada pratite ovu temu" + content: "Potražite ovo zvono da prilagodite svoje postavke obavijesti za određene teme ili cijele kategorije." + suggested_topics: + title: "Nastavi čitati!" + content: "Evo nekih tema za koje mislimo da biste ih željeli pročitati sljedeće." loading: "Očitavanje..." errors: prev_page: "pri pokušaju očitanja" @@ -1734,6 +1863,36 @@ hr: enabled: "Ova stranica je u načinu rada samo za čitanje. Nastavite pregledavati, ali odgovaranje, sviđanja i druge radnje su za sada onemogućene." login_disabled: "Prijava je onemogućena dok je stranica u modelu \"samo čitanje\"," logout_disabled: "Odjava je onemogućena dok je forum u načinu rada samo za čitanje." + staff_writes_only_mode: + enabled: "Ova stranica je u načinu rada samo za osoblje. Nastavite s pregledavanjem, ali odgovaranje, sviđanje i druge radnje ograničeni su samo na članove osoblja." + too_few_topics_and_posts_notice_MF: | + raspravu! Tamo { currentTopics, plural, + one {je # tema} + few {su # tema} + other {su # tema} + } i { currentPosts, plural, + one {# post} + few {# postovi} + other {# postovi} + }. Posjetitelji trebaju više za čitanje i odgovaranje – preporučujemo barem { requiredTopics, plural, + one {# tema} + few {# tema} + other {# tema} + } i { requiredPosts, plural, + one {# post} + few {# postova} + other {# postova} + }. Samo osoblje može vidjeti ovu poruku. + too_few_topics_notice_MF: | + raspravu! Tamo { currentTopics, plural, + one {je # tema} + few {su # tema} + other {su # tema} + }. Posjetitelji trebaju više za čitanje i odgovaranje – preporučujemo barem { requiredTopics, plural, + one {# tema} + few {# tema} + other {# tema} + }. Samo osoblje može vidjeti ovu poruku. too_few_posts_notice_MF: | Hajde da započnemo diskusiju! Tamo {currentPosts, plural, one {je # tema} few {su # teme} other {su # teme}}. Posjetiteljima je potrebno više čitati i odgovoriti na — preporučujemo barem {requiredPosts, plural, one {# tema} few {# teme} other {# teme}}. Samo osoblje može vidjeti ovu poruku. logs_error_rate_notice: @@ -1761,9 +1920,11 @@ hr: other: odgovora signup_cta: sign_up: "Učlani se" + hide_session: "Možda kasnije" hide_forever: "ne hvala" hidden_for_session: "OK, pitat ćemo te sutra. Uvijek možete upotrijebiti 'Prijava' i za stvaranje računa." intro: "Zdravo! Čini se da uživate u raspravi, ali još se niste prijavili za račun." + value_prop: "Umorni ste od listanja istih postova? Kada kreirate račun uvijek ćete se vraćati tamo gdje ste stali. S računom također možete primati obavijesti o novim odgovorima, spremati oznake i koristiti lajkove da zahvalite drugima. Svi možemo raditi zajedno kako bismo ovu zajednicu učinili sjajnom. :heart:" summary: enabled_description: "Gledate pregled ove teme: najzanimljivije članke odabire zajednica." description: @@ -1791,6 +1952,7 @@ hr: remove_allowed_user: "Želite li zaista ukloniti %{name} iz ove poruke?" remove_allowed_group: "Želite li zaista ukloniti %{name} iz ove poruke?" leave: "Napustiti" + remove_group: "Ukloni grupu" remove_user: "Ukloni korisnika" email: "E-mail" username: "Korisničko ime" @@ -1842,12 +2004,16 @@ hr: username: "Korisnik" password: "Zaporka" show_password: "Pokaži" + hide_password: "Sakrij" + show_password_title: "Pokaži lozinku" + hide_password_title: "Sakrij lozinku" second_factor_title: "Dvofaktorska autentifikacija" second_factor_description: "Unesite kod za provjeru autentičnosti iz svoje aplikacije:" second_factor_backup: "Prijavite se pomoću rezervnog koda" second_factor_backup_title: "Sigurnosna kopija od dva faktora" second_factor_backup_description: "Unesite jedan od vaših rezervnih kodova:" second_factor: "Prijavite se pomoću aplikacije Authenticator" + security_key_description: "Kada pripremite fizički sigurnosni ključ ili kompatibilni mobilni uređaj, pritisnite gumb Autentifikacija pomoću sigurnosnog ključa u nastavku." security_key_alternative: "Pokušaj na drugi način" security_key_authenticate: "Autentifikacija sa sigurnosnim ključem" security_key_not_allowed_error: "Proces provjere autentičnosti sigurnosnog ključa je istekao ili je otkazan." @@ -1862,6 +2028,7 @@ hr: blank_username_or_password: "Molimo unesite e-mail ili korisničko ime i zaporku." reset_password: "Ponovno postavi zaporku" logging_in: "Prijavljivanje..." + previous_sign_up: "Već imate račun?" or: "Ili" authenticating: "Autentifikaciranje..." awaiting_activation: "Vaš račun čeka aktivaciju, poslužite se poveznicom za zaboravljenu zaporku da pošaljemo drugi aktivacijski e-mail." @@ -1920,6 +2087,7 @@ hr: success: "Vaš račun je kreiran i sada ste prijavljeni." name_label: "Ime" password_label: "Lozinka" + existing_user_can_redeem: "Iskoristite svoju pozivnicu za temu ili grupu." password_reset: continue: "Nastavite na %{site_name}" emoji_set: @@ -1933,6 +2101,7 @@ hr: categories_only: "Samo kategorije" categories_with_featured_topics: "Kategorije s istaknutim temama" categories_and_latest_topics: "Kategorije i najnovije teme" + categories_and_latest_topics_created_date: "Kategorije i najnovije teme (poredaj po datumu kreiranja teme)" categories_and_top_topics: "Kategorije i glavne teme" categories_boxes: "Kutije s potkategorijama" categories_boxes_with_topics: "Kutije s istaknutim temama" @@ -2026,6 +2195,9 @@ hr: similar_topics: "Vaša tema je slična..." drafts_offline: "nacrti offline" edit_conflict: "uredi sukob" + esc: "esc" + esc_label: "Kliknite ili pritisnite Esc za odbacivanje" + ok_proceed: "Ok, nastavi" group_mentioned_limit: one: "Upozorenje! Spomenuli ste %{group}, međutim ova grupa ima više članova od administratora konfiguriranog ograničenja spominjanja od %{count} korisnika. Nitko neće biti obaviješten." few: "Upozorenje! Spomenuli ste %{group}, međutim ova grupa ima više članova od administratora konfiguriranog ograničenja spominjanja od %{count} korisnika. Nitko neće biti obaviješten." @@ -2039,11 +2211,19 @@ hr: private: "Spomenuli ste @%{username} , ali oni neće biti obaviješteni jer ne mogu vidjeti ovu osobnu poruku. Morat ćete ih pozvati u ovu osobnu poruku." muted_topic: "Spomenuli ste @%{username} , ali oni neće biti obaviješteni jer su isključili ovu temu." not_allowed: "Spomenuli ste @%{username} , ali oni neće biti obaviješteni jer nisu pozvani na ovu temu." + cannot_see_group_mention: + not_mentionable: "Ne možete spomenuti grupu @%{group}." + some_not_allowed: + one: "Spomenuli ste @%{group} , ali samo %{count} član će biti obaviješten jer ostali članovi ne mogu vidjeti ovu osobnu poruku. Morat ćete ih pozvati u ovu osobnu poruku." + few: "Spomenuli ste @%{group} , ali samo %{count} članovi će biti obaviješteni jer ostali članovi ne mogu vidjeti ovu osobnu poruku. Morat ćete ih pozvati u ovu osobnu poruku." + other: "Spomenuli ste @%{group} , ali samo %{count} članovi će biti obaviješteni jer ostali članovi ne mogu vidjeti ovu osobnu poruku. Morat ćete ih pozvati na ovu osobnu poruku." + not_allowed: "Spomenuli ste @%{group} , ali nitko od njegovih članova neće biti obaviješten jer ne mogu vidjeti ovu osobnu poruku. Morat ćete ih pozvati u ovu osobnu poruku." here_mention: one: "Spominjanjem @%{here}obavijestit ćete %{count} korisnika – jeste li sigurni?" few: "Spominjanjem @%{here}obavijestit ćete %{count} korisnika – jeste li sigurni?" other: "Spominjanjem @%{here}obavijestit ćete %{count} korisnika – jeste li sigurni?" duplicate_link: "Čini se da je vašu vezu na %{domain} već objavio u temi @%{username} u odgovor na %{ago} - jeste li sigurni da ga želite ponovo objaviti?" + duplicate_link_same_user: "Čini se da ste već objavili vezu na %{domain} u ovoj temi u odgovoru na %{ago} - jeste li sigurni da je želite ponovno objaviti?" reference_topic_title: "RE: %{title}" error: title_missing: "Naslov je obavezan" @@ -2079,6 +2259,7 @@ hr: create_shared_draft: "Stvori zajedničku skicu" edit_shared_draft: "Uredi zajedničku skicu" title: "Ili pritisnite %{modifier}Enter" + users_placeholder: "Dodajte korisnike ili grupe" title_placeholder: "O čemu je ova rasprava u jednoj kratkoj rečenici?" title_or_link_placeholder: "Upišite naslov ili ovdje zalijepite vezu" edit_reason_placeholder: "zašto izmjenjujete?" @@ -2123,6 +2304,7 @@ hr: abandon: "zatvorite skladatelja i odbacite skicu" enter_fullscreen: "unesite kompozitor preko cijelog zaslona" exit_fullscreen: "izađi iz cijelog zaslona u kompozitoru" + exit_fullscreen_prompt: "Pritisnite ESC za izlaz iz cijelog zaslona" show_toolbar: "prikaži alatnu traku skladatelja" hide_toolbar: "sakrij alatnu traku skladatelja" modal_ok: "U redu" @@ -2133,6 +2315,9 @@ hr: body: "Trenutno će ova poruka biti poslana samo vama!" slow_mode: error: "Ova je tema u usporenom načinu rada. Već ste nedavno objavili; možete ponovo objaviti u %{timeLeft}." + user_not_seen_in_a_while: + single: "Osoba kojoj šaljete poruku, %{usernames}, nije ovdje viđena jako dugo – %{time_ago}. Možda neće primiti vašu poruku. Možda ćete htjeti potražiti alternativne metode kontaktiranja %{usernames}." + multiple: "Sljedeće osobe koje šaljete porukama: %{usernames}, nisu ovdje viđeni već dugo — %{time_ago}. Možda neće primiti vašu poruku. Možda ćete htjeti potražiti alternativne metode kontaktiranja s njima." admin_options_title: "Neobavezna pravila osoblja za ovu temu" composer_actions: reply: Odgovori @@ -2166,6 +2351,7 @@ hr: ignore: "Zanemari" image_alt_text: aria_label: Zamjenski tekst za sliku + delete_image_button: Izbriši sliku notifications: tooltip: regular: @@ -2180,12 +2366,21 @@ hr: one: "%{count} nepročitana obavijest visokog prioriteta" few: "%{count} nepročitanih obavijesti visokog prioriteta" other: "%{count} nepročitanih obavijesti visokih prioriteta" + new_message_notification: + one: "%{count} obavijesti o novoj poruci" + few: "%{count} obavijesti o novoj poruci" + other: "%{count} obavijesti o novim porukama" + new_reviewable: + one: "%{count} novih za pregled" + few: "%{count} novih za pregled" + other: "%{count} novih recenziranih" title: "obavijesti o spominjanju @ime, odgovori na vaše objave i teme, poruke itd." none: "Nemoguće očitati obavijesti u ovom trenutku." empty: "Obavijesti nisu pronađene." post_approved: "Vaš post je odobren" reviewable_items: "stavke koje zahtijevaju pregled" watching_first_post_label: "Nova tema" + user_moved_post: "%{username} preselio" mentioned: "%{username} %{description}" group_mentioned: "%{username} %{description}" quoted: "%{username} %{description}" @@ -2214,6 +2409,7 @@ hr: invited_to_private_message: "

    %{username} %{description}" invited_to_topic: "%{username} %{description}" invitee_accepted: "%{username} prihvatio/la je vašu pozivnicu" + invitee_accepted_your_invitation: "prihvatio vaš poziv" moved_post: "%{username} premješteno %{description}" linked: "%{username} %{description}" granted_badge: "Dobio '%{description}'" @@ -2234,6 +2430,14 @@ hr: one: "Jesi li siguran? Imate %{count} važnih obavijesti." few: "Jesi li siguran? Imate %{count} važnih obavijesti." other: "Jesi li siguran? Imate %{count} važnih obavijesti." + bookmarks: + one: "Jesi li siguran? Imate %{count} nepročitanih podsjetnika za oznake." + few: "Jesi li siguran? Imate %{count} nepročitanih podsjetnika za oznake." + other: "Jesi li siguran? Imate %{count} nepročitanih podsjetnika za oznake." + messages: + one: "Jesi li siguran? Imate %{count} nepročitanih osobnih poruka." + few: "Jesi li siguran? Imate %{count} nepročitanih osobnih poruka." + other: "Jesi li siguran? Imate %{count} nepročitanih osobnih poruka." dismiss: "Skloni" cancel: "Odustani" group_message_summary: @@ -2348,6 +2552,7 @@ hr: status: "filteri prema statusu teme" full_search: "pokreće pretraživanje cijele stranice" full_search_key: "%{modifier} + Enter" + me: "prikazuje samo vaše postove" advanced: title: Napredni filtri posted_by: @@ -2418,12 +2623,63 @@ hr: current_user: "idi na korisničku stranicu" view_all: "pogledaj sve %{tab}" user_menu: + generic_no_items: "Na ovom popisu nema stavki." + sr_menu_tabs: "Kartice korisničkog izbornika" + view_all_notifications: "pogledajte sve obavijesti" + view_all_bookmarks: "pogledajte sve oznake" + view_all_messages: "pogledajte sve osobne poruke" tabs: + all_notifications: "Sve obavijesti" replies: "Odgovori" + replies_with_unread: + one: "Odgovori - %{count} nepročitanih odgovora" + few: "Odgovori - %{count} nepročitanih odgovora" + other: "Odgovori - %{count} nepročitanih odgovora" mentions: "Spomeni" + mentions_with_unread: + one: "Spominjanja - %{count} nepročitanih spominjanja" + few: "Spominjanja - %{count} nepročitanih spominjanja" + other: "Spominjanja - %{count} nepročitanih spominjanja" likes: "Like-ova" + likes_with_unread: + one: "Lajkovi - %{count} nepročitanih lajkova" + few: "Lajkovi - %{count} nepročitanih lajkova" + other: "Lajkovi - %{count} nepročitanih lajkova" + watching: "Gledane teme" + watching_with_unread: + one: "Gledane teme - %{count} nepročitanih gledanih tema" + few: "Gledane teme - %{count} nepročitanih gledanih tema" + other: "Gledane teme - %{count} nepročitanih gledanih tema" + messages: "Osobne poruke" + messages_with_unread: + one: "Osobne poruke - %{count} nepročitanih poruka" + few: "Osobne poruke - %{count} nepročitanih poruka" + other: "Osobne poruke - %{count} nepročitanih poruka" bookmarks: "Zabilješke" + bookmarks_with_unread: + one: "Oznake - %{count} nepročitanih oznaka" + few: "Oznake - %{count} nepročitanih oznaka" + other: "Oznake - %{count} nepročitanih oznaka" + review_queue: "Red čekanja za pregled" + review_queue_with_unread: + one: "Red čekanja za pregled - %{count} stavki treba pregledati" + few: "Red čekanja za pregled - %{count} stavki treba pregledati" + other: "Red čekanja za pregled - %{count} stavki treba pregledati" + other_notifications: "Ostale obavijesti" + other_notifications_with_unread: + one: "Ostale obavijesti - %{count} nepročitanih obavijesti" + few: "Ostale obavijesti - %{count} nepročitanih obavijesti" + other: "Ostale obavijesti - %{count} nepročitanih obavijesti" profile: "Profil" + reviewable: + view_all: "pogledaj sve stavke pregleda" + queue: "Red čekanja" + deleted_user: "(izbrisani korisnik)" + deleted_post: "(izbrisana objava)" + post_number_with_topic_title: "Članak #%{post_number} - %{title}" + new_post_in_topic: "novi post u %{title}" + user_requires_approval: "%{username} zahtijeva odobrenje" + default_item: "pregledna stavka #%{reviewable_id}" topics: new_messages_marker: "posljednji posjet" bulk: @@ -2556,7 +2812,52 @@ hr: show_links: "pokaži poveznice u temi" collapse_details: "Sažmi detalje teme" expand_details: "proširite pojedinosti o temi" + read_more_in_category: "Želite čitati više? Pregledajte ostale teme u %{categoryLink} ili pogledajte najnovije teme." + read_more: "Želite čitati više? Pregledajte sve kategorije ili pogledajte najnovije teme." unread_indicator: "Još nijedan član nije pročitao zadnji post ove teme." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Postoji # nepročitano} + few {Ima ih # nepročitanih} + other {Ima ih # nepročitanih} + } + { NEW, plural, + =0 {} + one { i # nova preostala tema,} + few { i # nove preostale teme,} + other { i # nove preostale teme,} + } + } + false { + { UNREAD, plural, + =0 {} + one {Preostalo je # nepročitanih tema,} + few {Preostalo je # nepročitanih tema,} + other {Preostalo je # nepročitanih tema,} + } + { NEW, plural, + =0 {} + one {Preostalo je # novih tema,} + few {Preostalo je # novih tema,} + other {Preostalo je # novih tema,} + } + } + other {} + } + { HAS_CATEGORY, select, + true { ili pregledajte druge teme u {categoryLink}} + false { ili pogledajte najnovije teme} + other {} + } + bumped_at_title: | + Prvi post: %{createdAtDate} + Objavljeno: %{bumpedAtDate} + browse_all_categories_latest: "Pregledajte sve kategorije ili pogledajte najnovije teme." + browse_all_categories_latest_or_top: "Pregledajte sve kategorije, pogledajte najnovije teme ili pogledajte vrh:" + browse_all_tags_or_latest: "Pregledajte sve oznake ili pogledajte najnovije teme." suggest_create_topic: Spremni za započeti novi razgovor? jump_reply_up: skoči na raniji odgovor jump_reply_down: skoči na kasniji odgovor @@ -2951,6 +3252,7 @@ hr: one: "pogledaj %{count} skriveni odgovor" few: "pogledaj %{count} skrivenih odgovora" other: "pogledaj %{count} skrivenih odgovora" + sr_reply_to: "Odgovorite na post #%{post_number} sa @%{username}" notice: new_user: "Ovo je prva objava %{user} - poželimo mu dobrodošlicu u zajednicu." returning_user: "Prošlo je dosta vremena otkad smo vidjeli %{user} — njihov posljednji post bio je %{time}." @@ -3001,6 +3303,7 @@ hr: few: "Nažalost, istodobno možete prenijeti samo %{count} datoteka." other: "Nažalost, istodobno možete prenijeti samo %{count} datoteka." upload_not_authorized: "Nažalost, datoteka koju pokušavate prenijeti nije autorizirana (autorizirana proširenja: %{authorized_extensions})." + no_uploads_authorized: "Nažalost, nijedna datoteka nije ovlaštena za učitavanje." image_upload_not_allowed_for_new_user: "Žao nam je, novi korisnici ne mogu učitavati slike." attachment_upload_not_allowed_for_new_user: "Žao nam je, novi korisnici ne mogu učitavati privitke." attachment_download_requires_login: "Žao nam je, morate biti prijavljeni da biste preuzimali privitke." @@ -3012,6 +3315,7 @@ hr: via_email: "ova objava stigla je preko e-maila" via_auto_generated_email: "ovaj je post stigao putem automatski generirane e-pošte" whisper: "ovu objavu potiho pošaljite moderatorima" + whisper_groups: "ovaj post je privatan i vidljiv je samo %{groupNames}" wiki: about: "ova objava je zajednička wiki objava" few_likes_left: "Hvala za dijeljenje ljubavi! Za danas vam je ostalo samo nekoliko lajkova." @@ -3102,6 +3406,11 @@ hr: one: "Jeste li sigurni da želite izbrisati taj post?" few: "Jeste li sigurni da želite izbrisati %{count}\" postova?" other: "Jeste li sigurni da želite izbrisati te %{count} postove?" + merge: + confirm: + one: "Jeste li sigurni da želite spojiti ove objave?" + few: "Jeste li sigurni da želite spojiti %{count} postova?" + other: "Jeste li sigurni da želite spojiti ovih %{count} objava?" revisions: controls: first: "Prva revizija" @@ -3175,6 +3484,7 @@ hr: all: "Sve kategorije" choose: "kategorija…" edit: "Izmijeni" + edit_title: "Uredite ovu kategoriju" edit_dialog_title: "Uredi: %{categoryName}" view: "Prikaži teme u kategoriji" back: "Natrag u kategoriju" @@ -3206,6 +3516,7 @@ hr: name: "Ime kategorije" description: "Opis" logo: "Logo kategorije" + logo_dark: "Slika logotipa kategorije tamnog načina rada" background_image: "Pozadinska slika kategorije" badge_colors: "Boje značke" background_color: "Pozadinska boja" @@ -3236,6 +3547,7 @@ hr: pending_permission_change_alert: "Niste dodali %{group} ovoj kategoriji; kliknite ovaj gumb da biste ih dodali." images: "Slike" email_in: "Prilagođena adresa dolaznog e-maila." + email_in_tooltip: "Možete odvojiti na više adresa e-pošte s | znakom." email_in_allow_strangers: "Prihvati emailove anonimnih korisnika bez računa" email_in_disabled: "Objavljivanje novih tema preko emaila je onemogućeno postavkama stranice. Da to omogućite objavljivanje tema preko emaila" email_in_disabled_click: 'omogućite "pošalji email u" opciju.' @@ -3260,6 +3572,7 @@ hr: this_year: "ove godine" position: "Položaj na stranici kategorija:" default_position: "Zadana pozicija" + position_disabled: "Kategorije će biti prikazane prema redoslijedu aktivnosti. Da biste kontrolirali redoslijed kategorija na popisima, omogućite postavku 'fiksnih pozicija kategorija'." minimum_required_tags: "Minimalan broj oznaka potrebnih u temi:" default_slow_mode: 'Omogućite "Spori način rada" za nove teme u ovoj kategoriji.' parent: "Nadkategorija" @@ -3529,6 +3842,7 @@ hr: this_month: "Mjesec" this_week: "Tjedan" today: "Danas" + browser_update: 'Nažalost, vaš preglednik nije podržan. Prebacite se na podržani preglednik za pregled bogatog sadržaja, prijavite se i odgovorite.' permission_types: full: "Otvoriti / Odgovoriti / Vidjeti" create_post: "Odgovoriti / Vidjeti" @@ -3551,6 +3865,7 @@ hr: shortcut_delimiter_slash: "%{shortcut1}/%{shortcut2}" shortcut_delimiter_space: "%{shortcut1} %{shortcut2}" title: "Prečaći tipkovnice" + short_title: "Prečaci" jump_to: title: "Idi na" home: "%{shortcut} Početak" @@ -3618,6 +3933,7 @@ hr: edit: "%{shortcut} Uredi objavu" delete: "%{shortcut} Izbriši objavu" mark_muted: "%{shortcut} Utišaj objavu" + mark_regular: "%{shortcut} Normalna (zadana) tema" mark_tracking: "%{shortcut} Prati temu" mark_watching: "%{shortcut} Promatraj temu" print: "%{shortcut} Ispis teme" @@ -3680,6 +3996,7 @@ hr: google: "Google kalendar" ics: "ICS" tagging: + all_tags: "Sve oznake" other_tags: "Ostale oznake" selector_all_tags: "sve oznake" selector_no_tags: "bez oznaka" @@ -3826,6 +4143,8 @@ hr: enabled: "Omogućen je siguran način rada, da biste izašli iz sigurnog načina rada, zatvorite ovaj prozor preglednika" image_removed: "(slika uklonjena)" pause_notifications: + title: "Pauziraj obavijesti za..." + label: "Pauziraj obavijesti" remaining: "%{remaining} preostalih" options: half_hour: "30 minuta" @@ -3848,11 +4167,15 @@ hr: no_activity_title: "Još nema aktivnosti" no_activity_body: "Dobrodošli u našu zajednicu! Ovdje ste potpuno novi i još niste sudjelovali u raspravama. Kao prvi korak, posjetite Top ili kategorije i samo počnite čitati! Odaberite %{heartIcon} na objavama koje vam se sviđaju ili o kojima želite saznati više. Dok budete sudjelovali, vaša će aktivnost biti navedena ovdje." no_replies_title: "Još niste odgovorili ni na jednu temu" + no_replies_title_others: "%{username} još nije odgovorio ni na jednu temu" + no_replies_body: "Kada otkrijete zanimljiv razgovor kojem želite pridonijeti, pritisnite gumb Odgovori izravno ispod bilo koje objave kako biste počeli odgovarati na tu objavu. Ili, ako biste radije odgovorili na opću temu, a ne na bilo koji pojedinačni post ili osobu, potražite gumb Odgovori na samom dnu teme ili ispod vremenske trake teme." no_drafts_title: "Niste pokrenuli nikakve skice" no_drafts_body: "Niste baš spremni za objavu? Automatski ćemo spremiti novu skicu i navesti je ovdje kad god počnete sastavljati temu, odgovor ili osobnu poruku. Odaberite gumb za odustajanje da biste odbacili ili spremili skicu za nastavak kasnije." no_likes_title: "Još vam se nije svidjela nijedna tema" + no_likes_title_others: "%{username} još nije označio da mu se sviđa nijedna tema" no_likes_body: "Sjajan način da uskočite i počnete pridonositi je da počnete čitati razgovore koji su se već vodili i odaberete %{heartIcon} na objavama koje vam se sviđaju!" no_topics_title: "Još niste pokrenuli nijednu temu" + no_topics_body: "Uvijek je najbolje pretražiti postojeće teme za razgovor prije nego započnete novu, ali ako ste sigurni da tema koju želite još ne postoji, samo naprijed i započnite vlastitu novu temu. Potražite gumb + Nova tema u gornjem desnom kutu popisa tema, kategorije ili oznake za početak stvaranja nove teme u tom području." no_topics_title_others: "%{username} još nije pokrenuo nijednu temu" no_read_topics_title: "Još niste pročitali nijednu temu" no_read_topics_body: "Kada počnete čitati rasprave, ovdje ćete vidjeti popis. Da biste počeli čitati, potražite teme koje vas zanimaju u Top ili kategorije ili pretražite po ključnoj riječi %{searchIcon}" @@ -3865,6 +4188,8 @@ hr: second_factor_auth: redirect_after_success: "Provjera autentičnosti drugog faktora je uspješna. Preusmjeravanje na prethodnu stranicu…" sidebar: + show_sidebar: "Prikaži bočnu traku" + hide_sidebar: "Sakrij bočnu traku" unread_count: one: "%{count} nepročitano" few: "%{count} nepročitano" @@ -3873,13 +4198,16 @@ hr: one: "%{count} novi" few: "%{count} novih" other: "%{count} novi" + toggle_section: "Uključi/isključi odjeljak" more: "Više" all_categories: "Sve kategorije" + all_tags: "Sve oznake" sections: about: header_link_text: "O nama" messages: header_link_text: "Poruke" + header_action_title: "Napravite osobnu poruku" links: inbox: "Primljeno" sent: "Poslano" @@ -3889,31 +4217,63 @@ hr: unread_with_count: "Nepročitano (%{count})" archive: "Arhiva" tags: + links: + add_tags: + content: "Dodaj oznake" + title: "Niste dodali nijednu oznaku. Kliknite za početak." + none: "Niste dodali nijednu oznaku." + click_to_get_started: "Kliknite ovdje da biste započeli." header_link_text: "Oznake" + header_action_title: "Uredite oznake bočne trake" + configure_defaults: "Konfigurirajte zadane postavke" categories: + links: + add_categories: + content: "Dodajte kategorije" + title: "Niste dodali nijednu kategoriju. Kliknite za početak." + none: "Niste dodali nijednu kategoriju." + click_to_get_started: "Kliknite ovdje da biste započeli." header_link_text: "Kategorije" + header_action_title: "Uredite svoje kategorije bočne trake" + configure_defaults: "Konfigurirajte zadane postavke" community: header_link_text: "Zajednica" + header_action_title: "Napravite temu" links: about: content: "O nama" + title: "Više detalja o ovoj stranici" admin: content: "Administrator" + title: "Postavke stranice i izvješća" badges: content: "Značke" + title: "Sve značke dostupne za osvajanje" everything: content: "Sve" title: "Sve teme" faq: content: "ČPP" + title: "Smjernice za korištenje ove stranice" groups: content: "Grupe" + title: "Popis dostupnih korisničkih grupa" users: content: "Korisnika" + title: "Popis svih korisnika" my_posts: content: "Moje objave" + title: "Moja nedavna aktivnost vezana uz temu" + title_drafts: "Moje neobjavljene skice" review: content: "Osvrt" + title: "Označene objave i druge stavke u redu čekanja" + pending_count: "%{count} na čekanju" + welcome_topic_banner: + title: "Kreirajte svoju temu dobrodošlice" + description: "Vaša tema dobrodošlice prva je stvar koju će novi članovi pročitati. Zamislite to kao svoj \"elevator pitch\" ili \"izjavu o misiji\". Recite svima za koga je ova zajednica, što mogu očekivati da će ovdje pronaći i što biste željeli da prvo učine." + button_title: "Započnite s uređivanjem" + until: "Do:" admin_js: type_to_filter: "upiši za filtriranje" admin: @@ -4062,6 +4422,9 @@ hr: other: "%{count} korisnici imaju nove domene e-pošte i bit će dodani u grupu." automatic_membership_associated_groups: "Korisnici koji su članovi grupe na ovdje navedenoj usluzi automatski će se dodati u ovu grupu kada se prijave s uslugom." primary_group: "Automatski postavi kao primarnu grupu" + alert: + primary_group: "Budući da je ovo primarna grupa, naziv '%{group_name}' koristit će se u CSS klasama koje svatko može vidjeti." + flair_group: "Budući da ova grupa ima smisla za svoje članove, ime '%{group_name}' bit će vidljivo svima." name_placeholder: "Ime grupe, bez razmaka, ista pravila kao za korisničko ime" primary: "Primarna grupa" no_primary: "(nema primarne grupe)" @@ -4071,6 +4434,7 @@ hr: about: "Ovdje izmijenite svoje članstvo u grupama i imena grupa" group_members: "Članovi grupe" delete: "Obriši" + delete_confirm: "Jeste li sigurni da želite izbrisati ovu grupu?" delete_failed: "Nemoguće obrisati grupu. Ako je ovo automatska grupa, ne možete ju obrisati." delete_automatic_group: Ovo je automatska grupa i ne može se izbrisati. delete_owner_confirm: "Ukloniti vlasničku privilegiju za \"%{username}\"?" @@ -4136,7 +4500,6 @@ hr: topics: read: Pročitajte temu ili određeni post u njoj. RSS je također podržan. write: Napravite novu temu ili objavite postojeću. - update: Ažurirajte temu. Promijenite naslov, kategoriju, oznake itd. read_lists: Čitajte popise tema kao što su vrhunski, novi, najnoviji itd. RSS je također podržan. posts: edit: Uredite bilo koji post ili određeni. @@ -4155,6 +4518,9 @@ hr: anonymize: Anonimizirajte korisničke račune. delete: Izbrišite korisničke račune. list: Dobijte popis korisnika. + user_status: + read: Pročitaj status korisnika. + update: Ažurirajte korisnički status. email: receive_emails: Kombinirajte ovaj opseg s prijemnikom pošte za obradu dolaznih e-poruka. badges: @@ -4179,6 +4545,7 @@ hr: create: "Kreiraj" edit: "Uredi" save: "Spremi" + description_label: "Okidači događaja" controls: "Kontrole" go_back: "Povratak na popis" payload_url: "URL nosivosti" @@ -4280,6 +4647,10 @@ hr: change_settings_short: "Postavke" howto: "Kako instalirati dodatke?" official: "Službeni dodatak" + broken_route: "Nije moguće konfigurirati vezu na '%{name}'. Provjerite jesu li programi za blokiranje oglasa onemogućeni i pokušajte ponovno učitati stranicu." + navigation_menu: + sidebar: "Bočna traka" + legacy: "Naslijeđe" backups: title: "Sigurnosne kopije" menu: @@ -4465,6 +4836,8 @@ hr: import_web_advanced: "Napredna..." import_file_tip: ".tar.gz, .zip ili .dcstyle.json datoteka koja sadrži temu" is_private: "Tema je u privatnom git repozitoriju" + finish_install: "Završite instalaciju teme" + last_attempt: "Proces instalacije nije dovršen, posljednji pokušaj:" remote_branch: "Naziv podružnice (izborno)" public_key: "Omogućite pristup sljedećem javnom ključu repo:" install: "Instaliraj" @@ -4474,6 +4847,8 @@ hr: install_git_repo: "Iz git repozitorija" install_create: "Stvori novo" duplicate_remote_theme: "Komponenta teme “%{name}” je već instalirana, jeste li sigurni da želite instalirati još jednu kopiju?" + force_install: "Tema se ne može instalirati jer Git repozitorij nije dostupan. Jeste li sigurni da želite nastaviti s instaliranjem?" + create_placeholder: "Stvorite rezervirano mjesto" about_theme: "O nama" license: "Licenca" version: "Verzija:" @@ -4495,6 +4870,7 @@ hr: has_overwritten_history: "Trenutna verzija teme više ne postoji jer je povijest Gita prepisana prisilnim pritiskom." add: "Dodaj" theme_settings: "Postavke teme" + overriden_settings_explanation: "Nadjačane postavke označene su točkom i imaju istaknutu boju. Za resetiranje ovih postavki na zadanu vrijednost, pritisnite gumb za resetiranje pored njih." no_settings: "Ova tema nema postavke." theme_translations: "Tematski prijevodi" empty: "Nema stvari" @@ -4654,6 +5030,7 @@ hr: last_seen_user: "Posljednji viđeni korisnik:" no_result: "Nije pronađen rezultat za sažetak." reply_key: "Ključ odgovora" + post_link_with_smtp: "Post & SMTP Detalji" skipped_reason: "Preskoči razlog" incoming_emails: from_address: "Od" @@ -4683,6 +5060,7 @@ hr: address_placeholder: "ime@primjer.com" type_placeholder: "sažetak, prijava..." reply_key_placeholder: "ključ odgovora" + smtp_transaction_response_placeholder: "SMTP ID" moderation_history: performed_by: "Izvođeno od" no_results: "Povijest moderiranja nije dostupna." @@ -4866,6 +5244,7 @@ hr: one: "pokazati %{count} riječ" few: "Prikaži %{count} riječi" other: "Prikaži %{count} riječi" + case_sensitive: "(osjetljivo na velika i mala slova)" download: Preuzmi clear_all: Obriši sve clear_all_confirm: "Jeste li sigurni da želite očistiti sve promatrane riječi za %{action} akciju?" @@ -4903,6 +5282,8 @@ hr: exists: "Već postoji" upload: "Dodaj iz datoteke" upload_successful: "Prijenos uspješno. Riječi su dodane." + case_sensitivity_label: "Je li osjetljiv na velika i mala slova" + case_sensitivity_description: "Samo riječi s odgovarajućim malim slovima" test: button_label: "Test" modal_title: "%{action}: Testirajte gledane riječi" @@ -4956,6 +5337,7 @@ hr: user: suspend_failed: "Nešto je pošlo po krivu pri suspenziji ovog korisnika %{error}" unsuspend_failed: "Nešto je pošlo po krivu pri ukidanju suspenzije ovog korisnika %{error}" + suspend_duration: "Suspendirati korisnika do:" suspend_reason_label: "Zažto ga suspendiraš? Ovaj tekst će biti vidljiv svima na korisničkom profilu korisnika, i biti će prikazan korisniku kad se pokuša prijaviti. Neka je kratak." suspend_reason_hidden_label: "Zašto suspendiraš? Ovaj tekst će biti prikazan korisniku kada se pokuša prijaviti. Držite ga kratko." suspend_reason: "Razlog" @@ -4979,7 +5361,9 @@ hr: silence_message: "Pošaljite poruku emailom." silence_message_placeholder: "(ostavite praznim za slanje zadane poruke)" suspended_until: "(do %{until})" + suspend_forever: "Zaustavite zauvijek" cant_suspend: "Ovaj korisnik ne može biti suspendiran." + cant_silence: "Ovaj korisnik ne može biti ušutkan." delete_posts_failed: "Došlo je do problema pri brisanju postova." post_edits: "Post izmjene" view_edits: "Prikaz uređivanja" @@ -4989,6 +5373,8 @@ hr: penalty_post_edit: "Uredi post" penalty_post_none: "Ne radi ništa" penalty_count: "Broj kazni" + penalty_history_MF: >- + U posljednjih 6 mjeseci ovaj je korisnik suspendiran { SUSPENDED, plural, one {# vrijeme} few {# puta} other {# puta} } i ušutkan { SILENCED, plural, one {# vrijeme} few {# puta} other {# puta} }. clear_penalty_history: title: "Obriši povijest kazne" description: "korisnici s kaznama ne mogu doseći TL3" @@ -5002,6 +5388,7 @@ hr: suspended: "Suspendiran?" staged: "Priređen?" show_admin_profile: "Administrator" + manage_user: "Upravljanje korisnikom" show_public_profile: "Pokaži javni profil" impersonate: "Predstavi se kao" action_logs: "Dnevnici akcija" @@ -5102,6 +5489,8 @@ hr: one: "Ne možete obrisati sve objave jer korisnik ima više od %{count} objava. (briši_sve_objave_max)" few: "Ne možete obrisati sve objave jer korisnik ima više od %{count} objava. (briši_sve_objave_max)" other: "Ne možete obrisati sve objave jer korisnik ima više od %{count} objava. (briši_sve_objave_max)" + delete_confirm_title: "Jeste li SIGURNI da želite izbrisati ovog korisnika? Ovo je trajno!" + delete_confirm: "Općenito je bolje anonimizirati korisnike umjesto brisanja, kako bi se izbjeglo uklanjanje sadržaja iz postojećih rasprava." delete_and_block: "Obriši i block ovaj email i IP adresu" delete_dont_block: "Samo obriši" deleting_user: "Brisanje korisnika..." @@ -5137,15 +5526,21 @@ hr: trust_level_2_users: "Korisnici na razini povjerenja 2" trust_level_3_requirements: "Predispozicije za razinu povjerenja 3" trust_level_locked_tip: "razina povjerenja zaključana, sistem neće promovirati ili demotirati korisnika" + trust_level_unlocked_tip: "razina povjerenja odključana, sistem će promovirati ili demotirati korisnika" lock_trust_level: "Zaključaj razinu povjerenja" unlock_trust_level: "Odključaj razinu povjerenja" silenced_count: "Utišano" suspended_count: "Suspendirani" last_six_months: "Posljednjih 6 mjeseci" + other_matches: + one: "Postoji %{count} drugi korisnik s istom IP adresom. Pregledajte i odaberite sumnjive koje ćete kazniti zajedno s %{username}." + few: "Postoji %{count} drugi korisnik s istom IP adresom. Pregledajte i odaberite sumnjive koje ćete kazniti zajedno s %{username}." + other: "Postoji %{count} drugih korisnika s istom IP adresom. Pregledajte i odaberite sumnjive koje ćete kazniti zajedno s %{username}." other_matches_list: username: "Korisničko ime" trust_level: "Razina povjerenja" read_time: "Vrijeme čitanja" + topics_entered: "Unesene teme" posts: "Postovi" tl3_requirements: title: "Predispozicije za razinu povjerenja 3" @@ -5408,6 +5803,7 @@ hr: embedding: get_started: "Ako želite ugraditi Discourse na drugu web-stranicu, počnite dodavanjem njegovog hosta." confirm_delete: "Jeste li sigurni da želite izbrisati tog hosta?" + sample: "Zalijepite sljedeći HTML kod na svoju web-lokaciju kako biste stvorili i ugradili teme diskursa. Zamijenite ZAMJENI_ME kanonskim URL-om stranice na koju ga ugrađujete." title: "Ugrađivanje" host: "Dopušteni hostovi" class_name: "Naziv klase" @@ -5442,6 +5838,7 @@ hr: destination: "Odredište" copy_to_clipboard: "Kopirajte stalnu vezu u međuspremnik" delete_confirm: Jeste li sigurni da želite izbrisati ovu stalnu vezu? + no_permalinks: "Još nemate nijednu trajnu vezu. Napravite novu stalnu vezu iznad kako biste ovdje počeli vidjeti popis svojih stalnih veza." form: label: "Novo:" add: "Dodaj" @@ -5458,10 +5855,14 @@ hr: replace: "Zamijeni" wizard_js: wizard: + jump_in: "Uskoči!" + finish: "Završi postavljanje." back: "Natrag" next: "Sljedeći" + configure_more: "Konfiguriraj više..." step-text: "Korak" step: "%{current} od %{total}" + upload: "Učitaj datoteku" uploading: "Učitavanje..." upload_error: "Žao nam je, dogodila se greška pri učitavanju te datoteke. Molimo pokušajte ponovo." staff_count: diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index 165a1a7266..c1eea0911b 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -1050,7 +1050,7 @@ hu: perm_denied_expl: "Letiltotta a figyelmeztetéseket. Engedélyezze őket a böngésző beállításaiban." disable: "Értesítések kikapcsolása" enable: "Értesítések bekapcsolása" - each_browser_note: 'Megjegyzés: Ezt a beállítást minden használt böngészőben módosítania kell. Ettől a beállítástól függetlenül minden értesítés le lesz tiltva, ha szünetelteti az értesítéseket a felhasználói menüből.' + each_browser_note: "Megjegyzés: Ezt a beállítást minden használt böngészőben módosítania kell. Ettől a beállítástól függetlenül minden értesítés le lesz tiltva, ha szünetelteti az értesítéseket a felhasználói menüből." consent_prompt: "Szeretne élő értesítéseket kapni, ha valaki válaszol a hozzászólásaira?" dismiss: "Elvetés" dismiss_notifications: "Összes elvetése" @@ -2765,6 +2765,7 @@ hu: one: "Jelenleg %{count} kítűzött témája van. A túl sok kiemelt téma megzavarhatja az új vagy névtelen felhasználókat. Biztos, hogy kitűz egy újabb témát ebben a kategóriában?" other: "Jelenleg %{count} kítűzött témája van. A túl sok kiemelt téma megzavarhatja az új vagy névtelen felhasználókat. Biztos, hogy kitűz egy újabb témát ebben a kategóriában?" unpin_globally: "Távolítsa el ezt a témát az összes téma listájának tetejéről." + global_pin_note: "A felhasználók saját maguk számára feloldhatják a témát." not_pinned_globally: "Nincsenek globálisan rögzített témák." already_pinned_globally: one: "Jelenleg globálisan kitűzött témák: %{count}" @@ -2970,6 +2971,7 @@ hu: convert_to_moderator: "Stábszín hozzáadása" revert_to_regular: "Stábszín eltávolítása" rebake: "HTML újjáépítése" + publish_page: "Oldal közzététele" unhide: "Elrejtés visszavonása" change_owner: "Tulajdonjog módosítása..." grant_badge: "Jelvény adományozása..." @@ -3296,6 +3298,9 @@ hu: lower_title_with_count: one: "%{count} olvasatlan" other: "%{count} olvasatlan" + unseen: + title: "Nem látott" + lower_title: "nem látott" new: lower_title_with_count: one: "%{count} új" @@ -3491,6 +3496,9 @@ hu: manage_groups: "Címke csoport kezelése" upload: "Címkék feltöltése" upload_successful: "A címkék sikeresen feltöltve" + delete_unused_confirmation_more_tags: + one: "%{tags} és még %{count}" + other: "%{tags} és még %{count}" delete_no_unused_tags: "Nincsenek fel nem használt címkék." tag_list_joiner: ", " delete_unused: "A nem használt címkék törlése" @@ -3560,6 +3568,7 @@ hu: enabled: "A biztonságos mód be van kapcsolva, hogy kilépj a biztonságos módból lépj ki ebből a keresési lapból" image_removed: "(kép eltávolítva)" pause_notifications: + label: "Értesítések szüneteltetése" remaining: "%{remaining} van hátra" options: half_hour: "30 percig" @@ -3880,7 +3889,7 @@ hu: topics: read: Olvasson el egy témát vagy egy adott hozzászólást. Az RSS is támogatva van. write: Hozzon létre új témát, vagy írjon egy meglévő témához. - update: Téma frissítése. A cím, kategória, címkék stb. módosítása. + update: Téma frissítése. Módosíthatja a címet, kategóriát, címkéket, státuszt, archetípust, featured_linket stb. read_lists: Témalisták olvasása, mint a top, új, legújabb, stb. Az RSS is támogatva van. posts: edit: Bármelyik hozzászólás vagy egy adott hozzászólás szerkesztése. @@ -4463,6 +4472,7 @@ hu: censor: "Cenzúra" require_approval: "Jóváhagyást igényel" flag: "Jelölés" + replace: "Csere" silence: "Elnémítás" link: "Hivatkozás" action_descriptions: @@ -4725,7 +4735,9 @@ hu: dashboard: "Vezérlőpult" navigation: "Navigáció" default_categories: + modal_description: "Szeretné visszamenőleg alkalmazni ezt a változtatást? Ez %{count} felhasználó beállítását fogja módosítani." modal_yes: "Igen" + modal_no: "Nem, csak mostantól alkalmazza a változtatást" badges: title: Jelvények new_badge: Új jelvény @@ -4826,6 +4838,7 @@ hu: action: label: "Szöveg cseréje…" modal: + title: "Szöveg cseréje" categories: "Kategóriák" topics: "Témák" replace: "Csere" diff --git a/config/locales/client.hy.yml b/config/locales/client.hy.yml index c283adcaa5..5420d02cdf 100644 --- a/config/locales/client.hy.yml +++ b/config/locales/client.hy.yml @@ -1026,6 +1026,7 @@ hy: primary: "Հիմնական Էլ. հասցե" secondary: "Երկրորդական Էլ. հասցեներ" primary_label: "հիմնական" + resent_label: "էլ. նամակն ուղարկված է" update_email: "Փոփոխել Էլ. Հասցեն" no_secondary: "Երկրորդական էլ. հասցեներ չկան" instructions: "Երբեք չի ցուցադրվում հանրությանը" @@ -1812,9 +1813,11 @@ hy: dismiss_new: "Չեղարկել Նորերը" toggle: "փոխանջատել թեմաների զանգվածային ընտրությունը" actions: "Զանգվածային Գործողությունները" + change_category: "Ավելացնել Կատեգորիա..." close_topics: "Փակել Թեմաները" archive_topics: "Արխիվացնել Թեմաները" move_messages_to_inbox: "Տեղափոխել Մուտքերի արկղ" + notification_level: "Ծանուցումներ..." choose_new_category: "Ընտրել նոր կատեգորիա թեմաների համար՝" selected: one: "Դուք ընտրել եք %{count} թեմա:" @@ -2001,6 +2004,7 @@ hy: unarchive: "Ապարխիվացնել Թեման" archive: "Արխիվացնել Թեման" reset_read: "Զրոյացնել Կարդացած Տվյալները" + make_public: "Ստեղծել Հրապարակային Թեմա..." make_private: "Ստեղծել Անձնական Նամակ" reset_bump_date: "Վերահաստատել Բարձրացման Ամսաթիվը" feature: @@ -2254,6 +2258,8 @@ hy: rebake: "Վերակառուցել HTML-ը" publish_page: "Էջի Հրատարակում" unhide: "Դարձնել Տեսանելի" + change_owner: "Փոխել Սեփականատիրոջը..." + grant_badge: "Շնորհել Կրծքանշան..." lock_post: "Արգելափակել Գրառումը" lock_post_description: "արգելել հրապարակողին խմբագրել այս գրառումը" unlock_post: "Արգելաբացել Գրառումը" @@ -2261,6 +2267,7 @@ hy: delete_topic_disallowed_modal: "Դուք թույլտվություն չունեք ջնջելու այս թեման: Եթե Դուք իսկապես ցանկանում եք, որ այն ջնջվի, դրոշակավորեք այն պատճառաբանության հետ միասին՝ մոդերատորի ուշադրությանը գրավելու համար:" delete_topic_disallowed: "Դուք թույլտվություն չունեք ջնջելու այս թեման" delete_topic: "ջնջել թեման" + add_post_notice: "Հաղորդագրություն Մոդերատորներից..." remove_timer: "հեռացնել ժամաչափիչը" actions: people: @@ -2454,6 +2461,7 @@ hy: flagging: title: "Շնորհակալ ենք, որ օգնում եք պահել մեր համայնքը քաղաքակիրթ:" action: "Դրոշակավորել Գրառումը" + take_action: "Ձեռնարկել Գործողություն..." take_action_options: default: title: "Ձեռնարկել Գործողություն" @@ -2826,6 +2834,7 @@ hy: delete: "Ջնջել" confirm_delete: "Դուք համոզվա՞ծ եք, որ ցանկանում եք ջնջել այս թեգի խումբը:" everyone_can_use: "Թեգերը կարող են օգտագործվել բոլորի կողմից:" + parent_tag_placeholder: "Ընտրովի" topics: none: unread: "Դուք չունեք չկարդացած թեմաներ:" @@ -2894,6 +2903,8 @@ hy: content: "Ադմին" badges: content: "Կրծքանշաններ" + everything: + content: "Բոլորը" faq: content: "ՀՏՀ" groups: @@ -2933,6 +2944,7 @@ hy: problems_found: "Որոշ խորհուրդներ՝ հիմնված Ձեր կայքի ընթացիկ կարգավորումների վրա" new_features: dismiss: "Չեղարկել" + learn_more: "Իմանալ ավելին" last_checked: "Վերջին ստուգումը՝ " refresh_problems: "Թարմացնել" no_problems: "Խնդիրներ չեն գտնվել:" diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index 55f0067e5c..13893df7de 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -1058,6 +1058,8 @@ id: title: "Surel" primary: "Email Utama" secondary: "Email Sekunder" + primary_label: "utama" + resent_label: "surel terkirim" update_email: "Ganti Alamat Surel" no_secondary: "Tidak ada email sekunder" instructions: "Jangan pernah tunjukkan ke publik." @@ -1173,9 +1175,13 @@ id: time_read: "Waktu Baca" days_visited: "Hari Berkunjung" account_age_days: "Umur akun dalam hari" + create: "Undang" valid_for: "Tautan undangan hanya berlaku untuk alamat surel: %{email}" invite_link: success: "Tautan undangan telah sukses dibuat!" + invite: + show_advanced: "Tampilkan Opsi Lanjutan" + hide_advanced: "Sembunyikan Opsi Lanjutan" bulk_invite: error: "Maaf, file harus dalam format CSV" password: @@ -1502,6 +1508,7 @@ id: title: "Cari" full_page_title: "Cari" results: "hasil" + post_format: "#%{post_number} oleh %{username}" search_google_button: "Google" search_button: "Cari" categories: "Kategori" @@ -1528,6 +1535,7 @@ id: select_all: "Pilih Semua" dismiss: "Bubar" move_messages_to_inbox: "Pindah ke Kotak Masuk" + notification_level: "Pemberitahuan..." choose_new_tags: "Silahkan pilih tag baru untuk topik-topik ini:" none: new: "Anda tidak memiliki topik baru" @@ -1561,8 +1569,10 @@ id: remove: "Nonaktifkan" topic_status_update: when: "Ketika:" + time_frame_required: "Silakan pilih kerangka waktu" duration: "Durasi" progress: + jump_prompt_to_date: "sampai saat ini" jump_prompt_or: "atau" notifications: reasons: @@ -1595,6 +1605,7 @@ id: title: "Balas" share: help: "share link ke topik ini" + invite_users: "Undang" invite_private: group_name: "nama grup" invite_reply: @@ -1633,9 +1644,17 @@ id: controls: first: "Revisi pertama" bookmarks: + create: "Buat penanda" + edit: "Sunting penanda" name: "Nama" options: "Pilihan" + actions: + delete_bookmark: + name: "Hapus penanda" + edit_bookmark: + name: "Sunting penanda" category: + all: "Semua kategori" edit: "Ubah" settings: "Pengaturan" tags: "Label" @@ -1661,6 +1680,7 @@ id: options: normal: "normal" ignore: "Abaikan" + low: "Rendah" high: "Tinggi" sort_options: default: "asal" @@ -1677,6 +1697,8 @@ id: notify_action: "Pesan" post_links: about: "perluas tautan untuk artikel ini" + title: + other: "%{count} lainnya" topic_statuses: warning: help: "Ini adalah peringatan resmi." @@ -1741,6 +1763,8 @@ id: actions: title: "Aksi" badges: + more_badges: + other: "+%{count} lainnya" none: "(kosong)" badge_grouping: trust_level: @@ -1774,6 +1798,7 @@ id: name_placeholder: "Nama" save: "Simpan" delete: "Hapus" + parent_tag_placeholder: "Opsional" topics: none: new: "Anda tidak memiliki topik baru" @@ -1790,6 +1815,7 @@ id: new_count: other: "%{count} baru" more: "Selengkapnya" + all_categories: "Semua kategori" sections: about: header_link_text: "Tentang" @@ -1812,6 +1838,8 @@ id: content: "Tentang" admin: content: "Admin" + everything: + content: "Semuanya" faq: content: "FAQ" groups: @@ -1820,6 +1848,7 @@ id: content: "Pengguna" review: content: "Ulasan" + until: "Sampai:" admin_js: type_to_filter: "ketik untuk memfilter..." admin: @@ -1838,6 +1867,7 @@ id: moderators: "Moderator:" private_messages_title: "Pesan" report_filter_any: "apa saja" + disabled: Dinonaktifkan filter_reports: Filter laporan reports: last_7_days: "7 terakhir" @@ -1872,6 +1902,7 @@ id: api: user: "Pengguna" created: Dibuat + never_used: (tidak pernah) revoke: "Cabut" show_details: Detil save: Simpan @@ -1884,6 +1915,7 @@ id: active: "Aktif" delivery_status: failed: "Gagal" + disabled: "Dinonaktifkan" events: request: "Pinta" body: "Konten" @@ -1964,6 +1996,9 @@ id: filters: title: "Filter" user_placeholder: "nama pengguna" + moderation_history: + actions: + delete_topic: "Topik Dihapus" logs: title: "Log" action: "Aksi" @@ -2043,6 +2078,8 @@ id: confirmation: cancel: "Batal" delete_and_block: "Hapus dan block email dan alamat IP ini" + reset_bounce_score: + label: "Reset" other_matches_list: username: "Nama Pengguna" trust_level: "Level Kepercayaan" @@ -2068,6 +2105,7 @@ id: site_text: edit: "ubah" settings: + reset: "reset" none: "Tak ada" site_settings: title: "Pengaturan" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 3b169081ee..14e3b5477e 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -176,6 +176,7 @@ it: banner: enabled: "Lo ha reso un banner il %{when}. Apparirà in cima ad ogni pagina finché non verrà chiuso dall'utente." disabled: "Ha rimosso questo banner il %{when}. Non apparirà più in cima ad ogni pagina." + forwarded: "Ha inoltrato l'email sopra" topic_admin_menu: "azioni argomento" skip_to_main_content: "Passa al contenuto principale" emails_are_disabled: "Tutte le e-mail in uscita sono state disabilitate a livello globale da un amministratore. Non sarà inviato nessun tipo di notifica via e-mail." @@ -1007,6 +1008,7 @@ it: notification_schedule: title: "Programmazione delle notifiche" label: "Attiva pianificazione delle notifiche personalizzata" + tip: "Al di fuori di queste ore le tue notifiche verranno messe in pausa." midnight: "Mezzanotte" none: "Nessun orario" monday: "lunedi" @@ -1050,12 +1052,16 @@ it: perm_denied_expl: "Hai negato il permesso per le notifiche. Autorizza le notifiche tramite le impostazioni del tuo browser." disable: "Disabilita Notifiche" enable: "Abilita Notifiche" + each_browser_note: "Nota: devi modificare questa impostazione su ogni browser che utilizzi. Tutte le notifiche verranno disabilitate quando le notifiche sono messe in pausa nel menu utente, indipendentemente da questa impostazione." consent_prompt: "Desideri ricevere notifiche in tempo reale quando qualcuno risponde a un tuo messaggio?" dismiss: "Ignora" dismiss_notifications: "Ignora tutti" dismiss_notifications_tooltip: "Imposta tutte le notifiche non lette come lette " dismiss_bookmarks_tooltip: "Contrassegna tutti i promemoria dei segnalibri non letti come già letti" dismiss_messages_tooltip: "Contrassegna tutte le notifiche di messaggi personali non letti come già letti" + no_likes_title: "Non hai ancora ricevuto nessun Mi piace" + no_likes_body: > + Riceverai una notifica qui ogni volta che qualcuno mette Mi piace a uno dei tuoi messaggi, in modo da poter vedere ciò che gli altri trovano utile. Gli altri vedranno lo stesso quando anche tu metti un Mi piace ai loro messaggi!

    Le notifiche per i Mi piace non vengono mai inviate tramite email, ma puoi regolare la modalità di ricezione delle notifiche sui Mi piace sul sito nelle tue preferenze di notifica. no_messages_title: "Non hai messaggi" no_messages_body: > Hai la necessità di avere una conversazione privata con qualcuno? Invia un messaggio selezionando l'avatar e utilizzando il pulsante del messaggio %{icon}

    Se hai bisogno di aiuto, puoi inviare un messaggio a un membro dello staff. @@ -1205,6 +1211,62 @@ it: warnings: "Avvertimenti ufficiali" read_more_in_group: "Vuoi continuare a leggere? Sfoglia altri messaggi in %{groupLink}." read_more: "Vuoi continuare a leggere? Sfoglia altri messaggi in messaggi personali." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {C'è # messaggio non letto} + other {Ci sono # messaggi non letti} + } + { NEW, plural, + =0 {} + one { e # nuovo messaggio rimanente, oppure puoi sfogliare altri messaggi in {groupLink}} + other { e # nuovi messaggi rimanenti, oppure puoi sfogliare altri messaggi in {groupLink}} + } + } + false { + { UNREAD, plural, + =0 {} + one {C'è # messaggio non letto rimanente, oppure puoi sfogliare altri messaggi in {groupLink}} + other {Ci sono # messaggi non letti rimanenti, oppure puoi sfogliare altri messaggi in {groupLink}} + } + { NEW, plural, + =0 {} + one {C'è # nuovo messaggio rimanente, oppure puoi sfogliare altri messaggi in {groupLink}} + other {Ci sono # nuovi messaggi rimanenti, oppure puoi sfogliare altri messaggi in {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {C'è # messaggio non letto} + other {Ci sono # messaggi non letti} + } + { NEW, plural, + =0 {} + one { e # nuovo messaggio rimanente, oppure puoi sfogliare altri messaggi personali} + other { e # nuovi messaggi rimanenti, oppure puoi sfogliare altri messaggi personali} + } + } + false { + { UNREAD, plural, + =0 {} + one {C'è # messaggio non letto rimanente, oppure puoi sfogliare altri messaggi personali} + other {Ci sono # messaggi non letti rimanenti, oppure puoi sfogliare altri messaggi personali} + } + { NEW, plural, + =0 {} + one {C'è # nuovo messaggio rimanente, oppure puoi sfogliare altri messaggi personali} + other {Ci sono # nuovi messaggi rimanenti, oppure puoi sfogliare altri messaggi personali} + } + } + other {} + } preferences_nav: account: "Account" security: "Sicurezza" @@ -1269,7 +1331,14 @@ it: use: "Usa l'app Authenticator" enforced_notice: "È necessario abilitare l'autenticazione a due fattori prima di accedere a questo sito." disable: "Disabilita" + disable_confirm: "Vuoi davvero disabilitare tutti i metodi di autenticazione a due fattori?" delete: "Elimina" + delete_confirm_header: "Questi autenticatori basati su token e chiavi di sicurezza fisica verranno eliminati:" + delete_confirm_instruction: "Per confermare, scrivi %{confirm} nella casella sottostante." + delete_single_confirm_title: "Cancellazione di un autenticatore" + delete_single_confirm_message: "Stai per cancellare %{name}. Quest'azione non può essere annullata. Se cambi idea, dovrai registrare nuovamente questo autenticatore." + delete_backup_codes_confirm_title: "Cancellazione codici di backup" + delete_backup_codes_confirm_message: "Stai per eliminare i codici di backup. Quest'azione non può essere annullata. Se cambi idea, dovrai generare nuovamente questi codici di backup." save: "Salva" edit: "Modifica" edit_title: "Modifica Authenticator" @@ -1436,6 +1505,8 @@ it: title: "Il titolo della pagina di background mostra il conteggio di:" notifications: "Nuove notifiche" contextual: "Nuovo contenuto nella pagina" + bookmark_after_notification: + title: "Dopo l'invio di una notifica di promemoria per segnalibri:" like_notification_frequency: title: "Notifica alla ricezione di \"Mi piace\"." always: "Sempre" @@ -1661,6 +1732,7 @@ it: save: "Salva" set_custom_status: "Imposta stato personalizzato" what_are_you_doing: "Cosa fai?" + pause_notifications: "Metti in pausa le notifiche" remove_status: "Rimuovi lo stato" user_tips: primary: "Fatto!" @@ -1714,6 +1786,24 @@ it: logout_disabled: "La disconnessione è disabilitata quando il sito è in modalità di sola lettura." staff_writes_only_mode: enabled: "Questo sito è in modalità di sola lettura per lo staff. Puoi continuare a navigare nel sito, ma le risposte, i \"Mi piace\" e altre azioni sono limitate ai soli membri dello staff." + too_few_topics_and_posts_notice_MF: | + Cominciamo la discussione! { currentTopics, plural, + one {C'è # argomento} + other {Ci sono # argomenti} + } e { currentPosts, plural, + one {# messaggio} + other {# messaggi} + }. I visitatori hanno bisogno di altri contenuti per leggere e rispondere: consigliamo almeno { requiredTopics, plural, + one {# argomento} + other {# argomenti} + } e { requiredPosts, plural, + one {# messaggio} + other {# messaggi} + }. Solo lo staff può vedere questo messaggio. + too_few_topics_notice_MF: | + Cominciamo la discussione! { currentTopics, plural, one {C'è # argomento} other {Ci sono# argomenti}}. I visitatori hanno bisogno di altri contenuti per leggere e rispondere: consigliamo almeno { requiredTopics, plural, one {# argomento} other {# argomenti}}. Solo lo staff può vedere questo messaggio. + too_few_posts_notice_MF: | + Cominciamo la discussione! { currentPosts, plural, one {C'è # messaggio} other {Ci sono # messaggi}}. I visitatori hanno bisogno di altri contenuti per leggere e rispondere: consigliamo almeno { requiredPosts, plural, one {# messaggio} other {# messaggi}}. Solo lo staff può vedere questo messaggio. logs_error_rate_notice: reached_hour_MF: | {relativeAge}{rate, plural, one {# errore/ora} other {# errori/ore}} ha raggiunto il limite di {limit, plural, one {# errore/ora} other {# errori/ora}} impostato dal sito. @@ -1821,6 +1911,9 @@ it: username: "Utente" password: "Password" show_password: "Mostra" + hide_password: "Nascondi" + show_password_title: "Mostra password" + hide_password_title: "Nascondi password" second_factor_title: "Autenticazione a Due Fattori" second_factor_description: "Inserisci il codice di autenticazione dalla tua app:" second_factor_backup: "Accedi utilizzando un codice di backup" @@ -1842,6 +1935,7 @@ it: blank_username_or_password: "Inserisci la tua email o il tuo nome utente, e la password." reset_password: "Reimposta Password" logging_in: "Accesso in corso..." + previous_sign_up: "Hai già un account?" or: "Oppure" authenticating: "Autenticazione..." awaiting_activation: "Il tuo account è in attesa di attivazione, utilizza il collegamento per la password dimenticata per ricevere un'altra email di attivazione." @@ -2016,10 +2110,17 @@ it: private: "Hai menzionato %{username} ma non riceverà alcuna notifica perché non ha accesso a questo messaggio privato. Dovrai invitarlo a questo messaggio privato." muted_topic: "Hai menzionato @%{username} ma non riceverà alcuna notifica perché ha silenziato questo argomento." not_allowed: "Hai menzionato @%{username} ma non riceverà alcuna notifica perché non è stato invitato a questo argomento." + cannot_see_group_mention: + not_mentionable: "Non puoi menzionare il gruppo @%{group}." + some_not_allowed: + one: "Hai menzionato @%{group} ma solo %{count} membro riceverà la notifica perché gli altri membri non sono in grado di vedere questo messaggio personale. Dovrai invitarli a questo messaggio personale." + other: "Hai menzionato @%{group} ma solo %{count} membri riceveranno la notifica perché gli altri membri non sono in grado di vedere questo messaggio personale. Dovrai invitarli a questo messaggio personale." + not_allowed: "Hai menzionato @%{group} ma nessuno dei suoi membri riceverà la notifica perché non sono in grado di vedere questo messaggio personale. Dovrai invitarli a questo messaggio personale." here_mention: one: "Menzionando @%{here}, verrà inviata una notifica a %{count} utente: confermi?" other: "Menzionando @%{here}, verrà inviata una notifica a %{count} utenti: confermi?" duplicate_link: "Sembra che il tuo collegamento a %{domain} sia già stato pubblicato in questo argomento da @%{username} in una risposta di %{ago} - sei sicuro di volerlo pubblicare ancora?" + duplicate_link_same_user: "Sembra che tu abbia già pubblicato un link a %{domain} in questo argomento in una risposta il %{ago}: sei sicuro di volerlo pubblicare di nuovo?" reference_topic_title: "RE: %{title}" error: title_missing: "Il titolo è richiesto" @@ -2155,6 +2256,12 @@ it: high_priority: one: "%{count} notifica ad alta priorità non letta" other: "%{count} notifiche ad alta priorità non lette" + new_message_notification: + one: "%{count} notifica di nuovi messaggi" + other: "%{count} notifiche di nuovi messaggi" + new_reviewable: + one: "%{count} nuovo contenuto da rivedere" + other: "%{count} nuovi contenuti da rivedere" title: "notifiche di menzioni @nome, risposte ai tuoi messaggi e argomenti ecc." none: "Impossibile caricare le notifiche al momento." empty: "Nessuna notifica trovata." @@ -2200,6 +2307,7 @@ it: reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} - completato" + new_features: "Nuove funzionalità disponibili!" dismiss_confirmation: body: default: @@ -2254,6 +2362,7 @@ it: membership_request_consolidated: "nuove richieste di iscrizione" reaction: "nuova reazione" votes_released: "Il voto è stato sbloccato" + new_features: "sono state rilasciate nuove funzionalità di Discourse!" upload_selector: uploading: "In caricamento" processing: "Caricamento in corso" @@ -2322,6 +2431,7 @@ it: status: "filtra per stato dell'argomento" full_search: "avvia la ricerca a pagina intera" full_search_key: "%{modifier} + Invio" + me: "mostra solo i tuoi messaggi" advanced: title: Filtri avanzati posted_by: @@ -2562,7 +2672,48 @@ it: show_links: "mostra i collegamenti in questo argomento" collapse_details: "comprimi i dettagli dell'argomento" expand_details: "espandi i dettagli dell'argomento" + read_more_in_category: "Vuoi saperne di più? Sfoglia altri argomenti in %{categoryLink} o visualizza gli argomenti più recenti." + read_more: "Vuoi saperne di più? Sfoglia tutte le categorie o visualizza gli argomenti più recenti." unread_indicator: "Nessun utente ha ancora letto l'ultimo messaggio di questo Argomento." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {C'è # argomento non letto} + other {Ci sono # argomenti non letti} + } + { NEW, plural, + =0 {} + one { e # nuovo argomento rimanente,} + other { e # nuovi argomenti rimanenti,} + } + } + false { + { UNREAD, plural, + =0 {} + one {C'è # argomento non letto rimanente,} + other {Ci sono # argomenti non letti rimanenti,} + } + { NEW, plural, + =0 {} + one {C'è # nuovo argomento rimanente,} + other {Ci sono # nuovi argomenti rimanenti,} + } + } + other {} + } + { HAS_CATEGORY, select, + true { oppure puoi sfogliare altri argomenti in {categoryLink}} + false { oppure puoi visualizzare gli argomenti più recenti} + other {} + } + bumped_at_title: | + Primo messaggio: %{createdAtDate} + Pubblicato: %{bumpedAtDate} + browse_all_categories_latest: "Sfoglia tutte le categorie o visualizza gli argomenti più recenti." + browse_all_categories_latest_or_top: "Sfoglia tutte le categorie, visualizza gli argomenti più recenti oppure guarda dall'inizio:" + browse_all_tags_or_latest: "Sfoglia tutte le etichette o visualizza gli argomenti più recenti." suggest_create_topic: Pronto per iniziare una nuova conversazione? jump_reply_up: passa a una risposta precedente jump_reply_down: passa a una risposta successiva @@ -2983,6 +3134,7 @@ it: one: "Spiacenti, puoi caricare solo %{count} file alla volta." other: "Spiacenti, puoi caricare solo %{count} file alla volta." upload_not_authorized: "Spiacenti, il file che stai cercando di caricare non è autorizzato (estensioni autorizzate: %{authorized_extensions})." + no_uploads_authorized: "Spiacenti, non ci sono file che possono essere caricati." image_upload_not_allowed_for_new_user: "Spiacenti, i nuovi utenti non possono caricare immagini." attachment_upload_not_allowed_for_new_user: "Spiacenti, i nuovi utenti non possono caricare allegati." attachment_download_requires_login: "Spiacenti, devi effettuare l'accesso per poter scaricare gli allegati." @@ -2994,6 +3146,7 @@ it: via_email: "questo messaggio è arrivato via email" via_auto_generated_email: "questo messaggio è arrivato tramite una email auto generata" whisper: "questo messaggio è un sussurro privato per i moderatori" + whisper_groups: "questo messaggio è un sussurro privato visibile solo a %{groupNames}" wiki: about: "questo messaggio è una wiki" few_likes_left: "Grazie per aver condiviso affetto! Hai ancora pochi \"Mi piace\" rimasti per oggi." @@ -3216,6 +3369,7 @@ it: pending_permission_change_alert: "Non hai aggiunto %{group} a questa categoria; fai clic su questo pulsante per aggiungerlo." images: "Immagini" email_in: "Indirizzo email personalizzato:" + email_in_tooltip: "Puoi separare più indirizzi e-mail con il carattere |." email_in_allow_strangers: "Accetta email da utenti anonimi senza alcun account" email_in_disabled: "Le Impostazioni Sito non permettono di creare nuovi argomenti via email. Per abilitare la creazione di argomenti via email," email_in_disabled_click: 'abilita l''impostazione "email in".' @@ -3496,7 +3650,6 @@ it: this_week: "Settimana" today: "Oggi" browser_update: 'Purtroppo, il tuo browser non è supportato. Per visualizzare i contenuti multimediali, passa a un browser supportato, accedi e rispondi.' - safari_13_warning: Questo sito a breve non supporterà più iOS e Safari nelle versioni 13 e precedenti. Rimarrà disponibile una versione semplificata a sola lettura. (Altre informazioni) permission_types: full: "Crea / Rispondi / Visualizza" create_post: "Rispondi / Visualizza" @@ -3784,6 +3937,8 @@ it: enabled: "La modalità sicura è attiva, per disattivarla chiudi il browser" image_removed: "(immagine rimossa)" pause_notifications: + title: "Metti in pausa le notifiche per..." + label: "Metti in pausa le notifiche" remaining: "%{remaining} rimanenti" options: half_hour: "30 minuti" @@ -3827,12 +3982,15 @@ it: second_factor_auth: redirect_after_success: "Autenticazione a due fattori riuscita. Reindirizzamento alla pagina precedente…" sidebar: + show_sidebar: "Mostra barra laterale" + hide_sidebar: "Nascondi barra laterale" unread_count: one: "%{count} non letto" other: "%{count} non letti" new_count: one: "%{count} nuovo" other: "%{count} nuovi" + toggle_section: "Interruttore sezione" more: "Altro" all_categories: "Tutte le categorie" all_tags: "Tutte le etichette" @@ -3841,6 +3999,7 @@ it: header_link_text: "Informazioni" messages: header_link_text: "Messaggi" + header_action_title: "Crea un messaggio personale" links: inbox: "Posta in arrivo" sent: "Inviati" @@ -3857,6 +4016,7 @@ it: none: "Non hai aggiunto nessuna etichetta." click_to_get_started: "Clicca qui per iniziare." header_link_text: "Etichette" + header_action_title: "Modifica le etichette nella barra laterale" configure_defaults: "Configura impostazioni predefinite" categories: links: @@ -3866,32 +4026,43 @@ it: none: "Non hai aggiunto nessuna categoria." click_to_get_started: "Clicca qui per iniziare." header_link_text: "Categorie" + header_action_title: "Modifica le categorie nella barra laterale" configure_defaults: "Configura impostazioni predefinite" community: header_link_text: "Community" + header_action_title: "Crea un argomento" links: about: content: "Informazioni" + title: "Maggiori dettagli su questo sito" admin: content: "Amministratore" + title: "Impostazioni e rapporti del sito" badges: content: "Distintivi" + title: "Tutti i distintivi disponibili" everything: content: "Tutti" title: "Tutti gli argomenti" faq: content: "FAQ" + title: "Linee guida per l'utilizzo di questo sito" groups: content: "Gruppi" + title: "Elenco dei gruppi di utenti disponibili" users: content: "Utenti" + title: "Elenco di tutti gli utenti" my_posts: content: "I miei Messaggi" + title: "La mia recente attività sull'argomento" + title_drafts: "Le mie bozze non pubblicate" draft_count: one: "%{count} bozza" other: "%{count} bozze" review: content: "Revisiona" + title: "Messaggi segnalati e altri elementi in coda" pending_count: "%{count} in attesa" welcome_topic_banner: title: "Crea il tuo argomento di benvenuto" @@ -4044,6 +4215,9 @@ it: other: "%{count} utenti hanno i nuovi domini di email e verranno aggiunti al gruppo." automatic_membership_associated_groups: "Gli utenti iscritti a un gruppo su uno dei servizi qui elencati saranno aggiunti automaticamente a questo gruppo quando accedono con tale servizio." primary_group: "Imposta automaticamente come gruppo principale" + alert: + primary_group: "Poiché si tratta di un gruppo primario, il nome '%{group_name}' verrà utilizzato nelle classi CSS che possono essere visualizzate da chiunque." + flair_group: "Poiché questo gruppo ha elementi distintivi per i suoi membri, il nome \"%{group_name}\" sarà visibile a chiunque." name_placeholder: "Nome del gruppo, senza spazi, stesse regole del nome utente" primary: "Gruppo Primario" no_primary: "(nessun gruppo primario)" @@ -4053,6 +4227,10 @@ it: about: "Modifica qui la tua appartenenza ai gruppi e i loro nomi" group_members: "Membri del gruppo" delete: "Cancella" + delete_confirm: "Vuoi davvero cancellare questo gruppo?" + delete_with_messages_confirm: + one: "La cancellazione di questo gruppo farà diventare orfano %{count} messaggio e i membri del gruppo non potranno più accedervi." + other: "La cancellazione di questo gruppo farà diventare orfani %{count} messaggi e i membri del gruppo non potranno più accedervi." delete_failed: "Impossibile cancellare il gruppo. Se questo è un gruppo automatico, non può essere eliminato." delete_automatic_group: Questo è un gruppo automatico e non può essere eliminato. delete_owner_confirm: "Rimuovere i privilegi per '%{username}'?" @@ -4118,7 +4296,6 @@ it: topics: read: Leggi un argomento o un messaggio specifico in esso. È supportato anche RSS. write: Crea un nuovo argomento o pubblica un messaggio su uno esistente. - update: Aggiorna un argomento. Modifica il titolo, la categoria, le etichette, ecc. read_lists: Leggi liste di argomenti come Popolari, Nuovi, Recenti, ecc. E' supportato anche l'RSS. posts: edit: Modifica qualsiasi messaggio o uno specifico. @@ -4137,6 +4314,9 @@ it: anonymize: Rendi anonimi gli account utente. delete: Elimina account utente. list: Ottieni un elenco di utenti. + user_status: + read: Leggi lo stato dell'utente. + update: Aggiorna lo stato dell'utente. email: receive_emails: Combina questo ambito con il destinatario della posta per elaborare le email in arrivo. badges: @@ -4161,6 +4341,7 @@ it: create: "Crea" edit: "Modifica" save: "Salva" + description_label: "Attivazione di eventi" controls: "Controlli" go_back: "Torna all'elenco" payload_url: "URL di Payload" @@ -4263,6 +4444,8 @@ it: broken_route: "Impossibile configurare il collegamento a '%{name}'. Assicurati che i blocchi degli annunci siano disabilitati e prova a ricaricare la pagina." navigation_menu: sidebar: "Barra laterale" + header_dropdown: "Intestazione a discesa" + legacy: "Compatibile" backups: title: "Backup" menu: @@ -4947,6 +5130,7 @@ it: user: suspend_failed: "Si è verificato un errore durante la sospensione di questo utente %{error}" unsuspend_failed: "Si è verificato un errore durante la riattivazione di questo utente %{error}" + suspend_duration: "Sospendi utente fino a:" suspend_reason_label: "Perché stai sospendendo l'utente? Questo testo sarà visibile a tutti nella pagina del profilo dell'utente, e gli verrà mostrato tutte le volte che tenterà di accedere. Scrivi un testo breve." suspend_reason_hidden_label: "Perché stai sospendendo l'utente? Questo testo gli verrà mostrato tutte le volte che tenterà di accedere. Scrivi un testo breve." suspend_reason: "Motivo" @@ -4970,7 +5154,9 @@ it: silence_message: "Messaggio Email" silence_message_placeholder: "(lasciare vuoto per inviare il messaggio predefinito)" suspended_until: "(fino a %{until})" + suspend_forever: "Sospendi definitivamente" cant_suspend: "Questo utente non può essere sospeso." + cant_silence: "Questo utente non può essere silenziato." delete_posts_failed: "C'è stato un problema con la cancellazione dei messaggi." post_edits: "Modifiche Messaggio" view_edits: "Visualizza modifiche" @@ -4980,6 +5166,8 @@ it: penalty_post_edit: "Modifica il messaggio" penalty_post_none: "Non fare nulla" penalty_count: "Conteggio Penalità" + penalty_history_MF: >- + Negli ultimi 6 mesi questo utente è stato sospeso { SUSPENDED, plural, one {# volta} other {# volte} } e silenziato { SILENCED, plural, one {# volta} other {# volte} }. clear_penalty_history: title: "Cancella cronologia delle penalizzazioni" description: "utenti con penalizzazioni non possono accedere al Livello di affidabilità 3" @@ -5134,10 +5322,14 @@ it: silenced_count: "Silenziati" suspended_count: "Sospesi" last_six_months: "Ultimi 6 mesi" + other_matches: + one: "C'è %{count} altro utente con lo stesso indirizzo IP. Esamina e seleziona quelli sospetti da penalizzare insieme a %{username}." + other: "Ci sono altri %{count} utenti con lo stesso indirizzo IP. Esamina e seleziona quelli sospetti da penalizzare insieme a %{username}." other_matches_list: username: "Nome utente" trust_level: "Livello di attendibilità" read_time: "Tempo di lettura" + topics_entered: "Argomenti inseriti" posts: "Messaggi" tl3_requirements: title: "Requisiti per livello di attendibilità 3" @@ -5454,8 +5646,10 @@ it: finish: "Esci dalla configurazione" back: "Indietro" next: "Avanti" + configure_more: "Configura altro..." step-text: "Intervallo" step: "%{current} di %{total}" + upload: "Carica file" uploading: "Caricamento..." upload_error: "Spiacenti, c'è stato un errore caricando il file. Per favore, prova di nuovo." staff_count: diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 456811fa38..375fd1e039 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -148,6 +148,7 @@ ja: banner: enabled: "%{when}にこれをバナーにしました。ユーザーが閉じるまで各ページの上部に表示されます。" disabled: "%{when}にこのバナーを削除しました。今後ページの上部に表示されることはありません。" + forwarded: "上記のメールを転送しました" topic_admin_menu: "トピックの操作" skip_to_main_content: "メインコンテンツにスキップ" emails_are_disabled: "メールの送信は管理者によって無効化されています。メール通知は一切送信されません。" @@ -950,6 +951,7 @@ ja: notification_schedule: title: "通知スケジュール" label: "カスタム通知スケジュールを有効にする" + tip: "この時間外は、通知が停止されます。" midnight: "深夜" none: "なし" monday: "月曜日" @@ -993,12 +995,16 @@ ja: perm_denied_expl: "通知へのアクセスが拒否されました。ブラウザの設定から通知を許可してください。" disable: "通知を無効にする" enable: "通知を有効にする" + each_browser_note: "注意: 使用するすべてのブラウザでこの設定を変更する必要があります。ユーザーメニューから通知を一時停止にした場合、この設定に関係なくすべての通知が無効になります。" consent_prompt: "あなたの投稿に返信があったときライブ通知しますか?" dismiss: "閉じる" dismiss_notifications: "すべて閉じる" dismiss_notifications_tooltip: "すべての未読の通知を既読にします" dismiss_bookmarks_tooltip: "未読のブックマークリマインダーをすべて既読にします" dismiss_messages_tooltip: "個人メッセージの未読の通知をすべて既読にします" + no_likes_title: "まだ「いいね!」を受け取っていません" + no_likes_body: > + 誰かがあなたの投稿の 1 つに「いいね!」するとここで通知されるため、他の人が評価するものを確認することができます。あなたが他の人の投稿に「いいね!」する場合も、その人に通知されます!

    「いいね!」の通知はメールで送信されませんが、サイトの通知設定で「いいね!」に関する通知の受信方法を調整することができます。 no_messages_title: "メッセージはありません" no_messages_body: > 通常の会話の外で、誰かと直接個人的に話す必要がありますか?相手のアバターを選択して、%{icon} メッセージボタンを使うと、その人にメッセージを送信できます。

    ヘルプが必要な場合は、スタッフメンバーにメッセージを送信してください。 @@ -1146,6 +1152,54 @@ ja: warnings: "運営スタッフからの警告" read_more_in_group: "もっと読みますか?%{groupLink} で他のメッセージを閲覧できます。" read_more: "もっと読みますか?個人メッセージで他のメッセージを閲覧できます。" + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + other {残り # 件の未読} + } + { NEW, plural, + =0 {} + other {と # 件の新着メッセージがあります。または {groupLink} で他のメッセージを閲覧してください} + } + } + false { + { UNREAD, plural, + =0 {} + other {残り # 件の未読メッセージがあります。または {groupLink} で他のメッセージを閲覧してください} + } + { NEW, plural, + =0 {} + other {残り # 件の新着メッセージがあります。または {groupLink} で他のメッセージを閲覧してください} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + other {残り # 件の未読} + } + { NEW, plural, + =0 {} + other {と # 件の新着メッセージがあります。または他の個人メッセージを閲覧してください} + } + } + false { + { UNREAD, plural, + =0 {} + other {残り # 件の未読メッセージがあります。または他の個人メッセージを閲覧してください} + } + { NEW, plural, + =0 {} + other {残り # 件の新着メッセージがあります。または他の個人メッセージを閲覧してください} + } + } + other {} + } preferences_nav: account: "アカウント" security: "セキュリティ" @@ -1208,7 +1262,14 @@ ja: use: "認証アプリを使用する" enforced_notice: "このサイトにアクセスするには二要素認証を有効にする必要があります。" disable: "無効化" + disable_confirm: "二要素認証を無効にしてもよろしいですか?" delete: "削除" + delete_confirm_header: "これらのトークンベース認証と物理セキュリティキーは削除されます:" + delete_confirm_instruction: "確認するには、下のボックスに %{confirm} と入力します。" + delete_single_confirm_title: "認証アプリの削除" + delete_single_confirm_message: "%{name} を削除しようとしています。この操作を元に戻すことはできません。変更する場合は、この認証アプリをもう一度登録する必要があります。" + delete_backup_codes_confirm_title: "バックアップコードの削除" + delete_backup_codes_confirm_message: "バックアップコードを削除しようとしています。この操作を元に戻すことはできません。変更する場合は、このバックアップコードをもう一度生成する必要があります。" save: "保存" edit: "編集" edit_title: "認証アプリの編集" @@ -1374,6 +1435,8 @@ ja: title: "バックグラウンドのページのタイトルに表示する件数:" notifications: "新しい通知" contextual: "新しいページのコンテンツ" + bookmark_after_notification: + title: "ブックマークのリマインダー通知が送信された後:" like_notification_frequency: title: "「いいね!」された時に通知する" always: "常時" @@ -1590,6 +1653,7 @@ ja: save: "保存" set_custom_status: "カスタムステータスを設定する" what_are_you_doing: "何をしていますか?" + pause_notifications: "通知を停止する" remove_status: "ステータスを削除する" user_tips: primary: "了解!" @@ -3331,7 +3395,6 @@ ja: this_week: "今週" today: "今日" browser_update: '残念ながら、あなたのブラウザはサポートされていません。リッチコンテンツを表示するにはサポートされているブラウザに切り替えてから、ログインして返信してください。' - safari_13_warning: このサイトは、まもなく iOS と Safari バージョン 13 以下のサポートを終了します。簡易版の読み取り専用バージョンは、引き続き利用可能です。(詳細) permission_types: full: "作成 / 返信 / 閲覧" create_post: "返信 / 閲覧" @@ -3935,7 +3998,6 @@ ja: topics: read: トピックまたはその中の特定の投稿を読み取ります。RSS もサポートされています。 write: 新しいトピックまたは既存のトピックに投稿を作成します。 - update: トピックを更新します。タイトル、カテゴリ、タグなどを変更します。 read_lists: 人気、新規、最新などのトピックリストを読み取ります。RSS もサポートされています。 posts: edit: 任意の投稿または特定の投稿を編集します。 diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 696db1a595..9532b58f0b 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -1798,6 +1798,7 @@ ko: categories_and_top_topics: "카테고리 및 주요 글" categories_boxes: "서브카테고리가 있는 박스" categories_boxes_with_topics: "추천 주제의 박스" + subcategories_with_featured_topics: "추천 글이 포함된 하위 카테고리" shortcut_modifier_key: shift: "Shift" ctrl: "Ctrl" @@ -3178,6 +3179,9 @@ ko: help: "읽지 않은 게시물을 포함한 현재 구독 또는 추적 중인 주제" lower_title_with_count: other: "%{count} unread" + unseen: + title: "읽지 않음" + lower_title: "읽지 않음" new: lower_title_with_count: other: "%{count} new" @@ -3219,7 +3223,6 @@ ko: this_week: "주" today: "오늘" browser_update: '브라우저 버전이 너무 낮아 이 사이트에서 작동하지 않습니다. 브라우저를 업그레이드하여 서식 있는 콘텐츠를 표시하고 로그인하여 댓글도 달아보세요.' - safari_13_warning: 이 사이트는 곧 iOS 및 Safari 버전 13 이하에 대한 지원을 중단할 예정입니다. 단순화된 읽기 전용 버전은 계속 사용할 수 있습니다. (더 보기) permission_types: full: "생성 / 댓글 / 보기" create_post: "댓글 / 보기" @@ -3559,6 +3562,7 @@ ko: content: "카테고리 추가" click_to_get_started: "시작하려면 여기를 클릭하십시오." header_link_text: "카테고리" + configure_defaults: "기본값 구성" community: header_link_text: "커뮤니티" links: @@ -3586,6 +3590,7 @@ ko: welcome_topic_banner: title: "환영 글 만들기" button_title: "편집 시작" + until: "까지:" admin_js: type_to_filter: "필터링하려면 입력..." admin: diff --git a/config/locales/client.lt.yml b/config/locales/client.lt.yml index f80713d935..cbd2870051 100644 --- a/config/locales/client.lt.yml +++ b/config/locales/client.lt.yml @@ -114,6 +114,11 @@ lt: few: "%{count} dienos" many: "%{count} dienų" other: "%{count} dienų" + x_months: + one: "%{count} mėnuo" + few: "%{count} mėnesių" + many: "%{count} mėnesių" + other: "%{count} mėnesių" date_year: "YYYY-MM-D" medium_with_ago: x_minutes: @@ -369,6 +374,21 @@ lt: confirm: "Jūs turite nebaigtą juodraštį šiai tema. Ką norėtumėte daryti su juo?" yes_value: "Išmesti" no_value: "Tęsti redagavimą" + topic_count_categories: + one: "Peržiūrėti %{count} naują arba atnaujintą temą" + few: "Peržiūrėkite %{count} naujų ar atnaujintų temų" + many: "Peržiūrėkite %{count} naujų ar atnaujintų temų" + other: "Peržiūrėkite %{count} naujų ar atnaujintų temų" + topic_count_latest: + one: "Peržiūrėti %{count} naują arba atnaujintą temą" + few: "Peržiūrėkite %{count} naujų ar atnaujintų temų" + many: "Peržiūrėkite %{count} naujų ar atnaujintų temų" + other: "Peržiūrėkite %{count} naujų ar atnaujintų temų" + topic_count_unseen: + one: "Peržiūrėti %{count} naują arba atnaujintą temą" + few: "Peržiūrėkite %{count} naujų ar atnaujintų temų" + many: "Peržiūrėkite %{count} naujų ar atnaujintų temų" + other: "Peržiūrėkite %{count} naujų ar atnaujintų temų" topic_count_new: one: "Pamatyk %{count} naują temą" few: "Pamatyk %{count} naujas temas" @@ -449,6 +469,11 @@ lt: grouped_by_topic: "Grupuojama pagal temą" none: "Nėra elementų, kuriuos reikia peržiūrėti." view_pending: "laukiama peržiūros" + topic_has_pending: + one: "Šioje temoje yra %{count} pranešimas, laukiantis patvirtinimo" + few: "Šioje temoje yra %{count} pranešimų, laukiančių patvirtinimo" + many: "Šioje temoje yra %{count} pranešimų, laukiančių patvirtinimo" + other: "Šioje temoje yra %{count} pranešimų, laukiančių patvirtinimo" title: "Peržiūra" topic: "Tema:" filtered_topic: "Išfiltravote turinį, kurį galima peržiūrėti vienoje temoje." @@ -465,6 +490,22 @@ lt: name: "Vardas" fields: "Laukai" reject_reason: "Priežastis" + user_percentage: + agreed: + one: "%{count}% sutinka" + few: "%{count}% sutinka" + many: "%{count}% sutinka" + other: "%{count}% sutinka" + disagreed: + one: "%{count}% nesutinka" + few: "%{count}% nesutinka" + many: "%{count}% nesutinka" + other: "%{count}% nesutinka" + ignored: + one: "%{count}% ignoruoja" + few: "%{count}% ignoruoja" + many: "%{count}% ignoruoja" + other: "%{count}% ignoruoja" topics: topic: "Tema" reviewable_count: "Skaičiuoti" @@ -552,6 +593,16 @@ lt: title: "Kodėl atmetate šį vartotoją?" send_email: "Siųsti atmetimo laišką" relative_time_picker: + minutes: + one: "minutė" + few: "minutės" + many: "minutės" + other: "minutės" + hours: + one: "valanda" + few: "valandos" + many: "valandos" + other: "valandos" days: one: "diena" few: "dienos" @@ -903,9 +954,19 @@ lt: few: "%{count} temos" many: "%{count}temų" other: "%{count}temos" + topic_stat: + one: "%{number} / %{unit}" + few: "%{number} / %{unit}" + many: "%{number} / %{unit}" + other: "%{number} / %{unit}" topic_stat_unit: week: "savaitė" month: "mėnesis" + topic_stat_all_time: + one: "Iš viso %{number}" + few: "Iš viso %{number}" + many: "Iš viso %{number}" + other: "Iš viso %{number}" n_more: "Kategorijos (dar %{count} ) ..." ip_lookup: title: IP adreso peržiųra @@ -1492,6 +1553,16 @@ lt: time_read_title: "%{duration} (visas laikas)" recent_time_read: "paskutinis skaitymo laikas" recent_time_read_title: "%{duration} (per paskutines 60 dienų)" + topic_count: + one: "sukurta tema" + few: "sukurtos temos" + many: "sukurtos temos" + other: "sukurtos temos" + post_count: + one: "įrašas sukurtas" + few: "įrašai sukurti" + many: "įrašai sukurti" + other: "įrašai sukurti" likes_given: one: "duota" few: "duota" @@ -1502,6 +1573,16 @@ lt: few: "gauta" many: "gauta" other: "gauta" + days_visited: + one: "aplankyta diena" + few: "apsilankymo diena" + many: "apsilankymo dienų" + other: "apsilankymo dienų" + topics_entered: + one: "tema peržiūrėta" + few: "peržiūrėtos temos" + many: "peržiūrėtos temos" + other: "peržiūrėtos temos" bookmark_count: one: "žymės" few: "žymės" @@ -1722,6 +1803,7 @@ lt: twitter: name: "Twitter" title: "Prisijunkite naudodami “Twitter”" + sr_title: "Prisijunkite naudodami “Twitter”" instagram: name: "Instagram" title: "Prisijunkite naudodami “Instagram”" @@ -1778,12 +1860,27 @@ lt: few: "%{count} temos šioje kategorijoje" many: "%{count} temų šioje kategorijoje" other: "%{count} temos šioje kategorijoje" + plus_subcategories_title: + one: "%{name} ir viena subkategorija" + few: "%{name} ir %{count} subkategorijos" + many: "%{name} ir %{count} subkategorijos" + other: "%{name} ir %{count} subkategorijos" + plus_subcategories: + one: "+ %{count} subkategorija" + few: "+ %{count} subkategorijos" + many: "+ %{count} subkategorijos" + other: "+ %{count} subkategorijos" select_kit: delete_item: "Ištrinti %{name}" filter_by: "Filtruoti pagal: %{name}" select_to_filter: "Pasirinkite vertę, kurią norite filtruoti" default_header_text: Pasirinkti... no_content: Atitikmenų nerasta + results_count: + one: "%{count} rezultatas" + few: "%{count} rezultatai" + many: "%{count} rezultatai" + other: "%{count} rezultatai" filter_placeholder: Paieška... filter_placeholder_with_any: Ieškoti arba sukurti... create: "Sukurti: '%{content}'" @@ -1845,6 +1942,11 @@ lt: similar_topics: "Jūsų tema panaši į..." drafts_offline: "juodraščiai ne ryšio zonoje" edit_conflict: "redaguoti konfliktą" + group_mentioned: + one: "Paminėdami %{group}, jūs ketinate pranešti %{count} asmeniui - ar esate tikri?" + few: "Minėdami %{group}, jūs ketinate pranešti %{count} žmonėms - ar esate tikri?" + many: "Minėdami %{group}, jūs ketinate pranešti %{count} žmonėms - ar esate tikri?" + other: "Minėdami %{group}, jūs ketinate pranešti %{count} žmonėms - ar esate tikri?" cannot_see_mention: category: "Paminėjote @%{username} , tačiau jiems nebus pranešta, nes jie neturi prieigos prie šios kategorijos. Turėsite juos įtraukti į grupę, kuri turi prieigą prie šios kategorijos." private: "Paminėjote @%{username} , tačiau jiems nebus pranešta, nes jie negali matyti šio asmeninio pranešimo. Turėsite juos pakviesti į šią asmeninę žinutę." @@ -1957,6 +2059,11 @@ lt: few: "%{count}neskaitytos žinutės" many: "%{count}neskaitytų žinučių" other: "%{count}neskaitytos žinutės" + high_priority: + one: "%{count} neskaitytas aukšto prioriteto pranešimas" + few: "%{count} neskaitytų aukšto prioriteto pranešimų" + many: "%{count} neskaitytų aukšto prioriteto pranešimų" + other: "%{count} neskaitytų aukšto prioriteto pranešimų" title: "pranešimai kai paminimas @name , atsakomi tavo įrašai, temos, žinutės ir pan." none: "Šiuo metu neįmanoma pakrauti pranešimų." empty: "Pranešimų nėra" @@ -1984,6 +2091,11 @@ lt: few: "%{username}, %{username2}ir %{count} kiti" many: "%{username}, %{username2} ir %{count}kitų" other: "%{username}, %{username2} ir %{count}kitų" + liked_consolidated_description: + one: "patiko %{count} jūsų įrašų" + few: "patiko %{count} jūsų įrašų" + many: "patiko %{count} jūsų įrašų" + other: "patiko %{count} jūsų įrašų" liked_consolidated: "%{username} %{description}" private_message: "%{username} %{description}" invited_to_private_message: "

    %{username} %{description}" @@ -2116,6 +2228,7 @@ lt: tips: category_tag: "filtrai pagal kategoriją ar žymą" author: "filtruoti pagal įrašo autorių" + in: "filtruoti pagal metaduomenis (pvz. pavadinime)" status: "filtruoti pagal temos būseną" full_search: "paleidžiama viso puslapio paieška" full_search_key: "%{modifier} + Įveskite" @@ -2205,10 +2318,25 @@ lt: delete: "Ištrinti temas" dismiss: "Praleisti" dismiss_read: "Praleisti visas neperskaitytas" + dismiss_read_with_selected: + one: "Atmesti %{count} neskaitytą" + few: "Atmesti %{count} neskaitytų" + many: "Atmesti %{count} neskaitytų" + other: "Atmesti %{count} neskaitytų" dismiss_button: "Praleisti..." + dismiss_button_with_selected: + one: "Atsisakyti (%{count})…" + few: "Atsisakyti (%{count})…" + many: "Atsisakyti (%{count})…" + other: "Atsisakyti (%{count})…" dismiss_tooltip: "Praleisti tik naujus įrašus ar nebesekti temos" also_dismiss_topics: "Nebesekti šių temų, kad jos niekada nebūtų rodomos, kaip neperskaitytos" dismiss_new: "Praleisti Naujas" + dismiss_new_with_selected: + one: "Atsisakyti naujo (%{count})" + few: "Atsisakyti naujo (%{count})" + many: "Atsisakyti naujo (%{count})" + other: "Atsisakyti naujo (%{count})" toggle: "perjungti temų pasirinkimus" actions: "Veiksmai" change_category: "Nustatyti kategoriją..." @@ -2229,6 +2357,16 @@ lt: choose_append_tags: "Pasirinkite naujas žymas, kurias norite pridėti šioms temoms:" changed_tags: "Šių temų žymos buvo pakeistos." remove_tags: "Pašalinti visas žymes" + confirm_remove_tags: + one: "Visos žymos bus pašalintos iš šios temos. Ar esate tikras?" + few: "Visos žymės bus pašalintos iš %{count} temų. Ar esate tikras?" + many: "Visos žymės bus pašalintos iš %{count} temų. Ar esate tikras?" + other: "Visos žymės bus pašalintos iš %{count} temų. Ar esate tikras?" + progress: + one: "Pažanga: %{count} tema" + few: "Pažanga: %{count} temos" + many: "Pažanga: %{count} temos" + other: "Pažanga: %{count} temos" none: unread: "Jūs neturite neperskaitytų temų." unseen: "Jūs neturite nematytų temų." @@ -2383,6 +2521,16 @@ lt: auto_reminder: "Jums bus priminta apie šią temą %{timeLeft}." auto_delete_replies: "Atsakymai šia tema automatiškai ištrinami po %{duration}." auto_close_title: "Automatinio uždarymo nustatymai" + auto_close_immediate: + one: "Paskutinis įrašas temoje jau yra %{count} valandų, todėl tema bus nedelsiant uždaryta." + few: "Paskutinis įrašas temoje jau yra %{count} valandų, todėl tema bus nedelsiant uždaryta." + many: "Paskutinis įrašas temoje jau yra %{count} valandų, todėl tema bus nedelsiant uždaryta." + other: "Paskutinis įrašas temoje jau yra %{count} valandų, todėl tema bus nedelsiant uždaryta." + auto_close_momentarily: + one: "Paskutinis įrašas temoje jau yra %{count} valandų, todėl tema bus akimirksniu uždaryta." + few: "Paskutiniai įrašai temoje jau yra %{count} valandų, todėl temos bus akimirksniu uždaryta." + many: "Paskutiniai įrašai temoje jau yra %{count} valandų, todėl temos bus akimirksniu uždaryta." + other: "Paskutiniai įrašai temoje jau yra %{count} valandų, todėl temos bus akimirksniu uždaryta." timeline: back: "Atgal" back_description: "Grįžkite prie paskutinio neskaityto įrašo" @@ -2470,6 +2618,11 @@ lt: help: "Pasidalink šios temos nuoroda" instructions: "Dalintis nuoroda šioje temoje:" copied: "Temos nuoroda nukopijuota." + restricted_groups: + one: "Matoma tik grupės nariams: %{groupNames}" + few: "Matoma tik grupių nariams: %{groupNames}" + many: "Matoma tik grupių nariams: %{groupNames}" + other: "Matoma tik grupių nariams: %{groupNames}" invite_users: "Kviesti" print: title: "Spausdinti" @@ -2580,6 +2733,11 @@ lt: action: "pereiti prie esamo pranešimo" radio_label: "Esama žinutė" participants: "Dalyviai" + instructions: + one: "Pasirinkite pranešimą, į kurį norite perkelti tą įrašą." + few: "Pasirinkite pranešimą, į kurį norite perkelti tuos %{count} įrašus." + many: "Pasirinkite pranešimą, į kurį norite perkelti tuos %{count} įrašus." + other: "Pasirinkite pranešimą, į kurį norite perkelti tuos %{count} įrašus." merge_posts: title: "Sulieti pasirinktus įrašus" action: "sulieti pasirinktus įrašus" @@ -2602,6 +2760,11 @@ lt: action: "pakeisti valdymo teises" error: "Įvyko klaida keičiant įrašų valdymo teisę." placeholder: "naujojo valdytojo vartotojo vardas" + instructions_without_old_user: + one: "Pasirinkite naują įrašo savininką" + few: "Prašome pasirinkti naujų %{count} įrašų savininką" + many: "Prašome pasirinkti naujų %{count} įrašų savininką" + other: "Prašome pasirinkti naujų %{count} įrašų savininką" change_timestamp: action: "pakeisti laiko formatą" invalid_timestamp: "Laiko formatas negali būti ateityje." @@ -2685,6 +2848,16 @@ lt: few: "%{count} asmenims patiko šis pranešimas. Spustelėkite, jei norite peržiūrėti" many: "%{count} žmonių patiko šis įrašas. Spustelėkite norėdami peržiūrėti" other: "%{count} žmonėms patiko šis pranešimas. Spustelėkite, jei norite peržiūrėti" + filtered_replies_hint: + one: "Peržiūrėkite šį įrašą ir jo atsakymą" + few: "Peržiūrėkite šį įrašą ir jo %{count} atsakymus" + many: "Peržiūrėkite šį įrašą ir jo %{count} atsakymus" + other: "Peržiūrėkite šį įrašą ir jo %{count} atsakymus" + filtered_replies_viewing: + one: "Peržiūrėti %{count} atsakymą į" + few: "Peržiūrėti %{count} atsakymus į" + many: "Peržiūrėti %{count} atsakymus į" + other: "Peržiūrėti %{count} atsakymus į" in_reply_to: "Įkelti pirminį įrašą" view_all_posts: "Peržiūrėti visus įrašus" errors: @@ -2694,6 +2867,11 @@ lt: file_too_large: "Deja, failas per didelis (maksimalus dydis yra %{max_size_kb}kb). Kodėl neįkėlus didelio failo į bendrinimo debesyje paslaugą ir tuomet įklijuokite nuorodą?" file_too_large_humanized: "Deja, failas per didelis (maksimalus dydis yra %{max_size}kb). Kodėl neįkėlus didelio failo į bendrinimo debesyje paslaugą ir tuomet įklijuokite nuorodą?" too_many_uploads: "Atsiprašome, bet jūs galite įkelti tik vieną failą vienu metu." + too_many_dragged_and_dropped_files: + one: "Atsiprašome, galite įkelti tik %{count} failą vienu metu." + few: "Atsiprašome, galite įkelti tik %{count} failus vienu metu." + many: "Atsiprašome, galite įkelti tik %{count} failus vienu metu." + other: "Atsiprašome, galite įkelti tik %{count} failus vienu metu." upload_not_authorized: "Atsiprašome, failas, kurį bandote įkelti, nėra autorizuotas (įgalioti plėtiniai: %{authorized_extensions})." image_upload_not_allowed_for_new_user: "Atsiprašome, bet nauji vartotojai negali įkelti nuotraukų." attachment_upload_not_allowed_for_new_user: "Atsiprašome, bet nauji vartotojai negali įkelti priedų." @@ -2754,6 +2932,11 @@ lt: unlock_post_description: "leisti paskelbusiam asmeniui redaguoti šį įrašą" delete_topic_disallowed_modal: "Neturite leidimo ištrinti šios temos. Jei tikrai norite, kad jis būtų ištrintas, kartu su argumentais pateikite vėliavą moderatoriui." delete_topic_disallowed: "neturite leidimo ištrinti šios temos" + delete_topic_confirm_modal: + one: "Ši tema šiuo metu turi daugiau nei %{count} peržiūrą ir gali būti populiari paieškos vieta. Ar tikrai norite visiškai ištrinti šią temą, o ne ją redaguoti, kad patobulintumėte?" + few: "Ši tema šiuo metu turi daugiau nei %{count} peržiūrų ir gali būti populiari paieškos vieta. Ar tikrai norite visiškai ištrinti šią temą, o ne ją redaguoti, kad patobulintumėte?" + many: "Ši tema šiuo metu turi daugiau nei %{count} peržiūrų ir gali būti populiari paieškos vieta. Ar tikrai norite visiškai ištrinti šią temą, o ne ją redaguoti, kad patobulintumėte?" + other: "Ši tema šiuo metu turi daugiau nei %{count} peržiūrų ir gali būti populiari paieškos vieta. Ar tikrai norite visiškai ištrinti šią temą, o ne ją redaguoti, kad patobulintumėte?" delete_topic_confirm_modal_yes: "Taip, ištrinti šią temą" delete_topic_confirm_modal_no: "Ne, palikti šią temą" delete_topic_error: "Ištrinant šią temą įvyko klaida" @@ -2770,11 +2953,21 @@ lt: few: "mėgstate" many: "mėgstate" other: "mėgstate" + read: + one: "skaityti" + few: "skaityti" + many: "skaityti" + other: "skaityti" like_capped: one: "ir %{count}kitas mėgsta tai" few: "ir %{count}kitų mėgsta tai" many: "ir %{count}kitų mėgsta tai " other: "ir %{count} kitų mėgsta tai" + read_capped: + one: "ir %{count} kiti tai skaito" + few: "ir %{count} kiti tai skaito" + many: "ir %{count} kiti tai skaito" + other: "ir %{count} kiti tai skaito" sr_post_likers_list_description: "vartotojai, kuriems patiko šis pranešimas" sr_post_readers_list_description: "vartotojai, kurie perskaitė šį pranešimą" by_you: @@ -3022,6 +3215,11 @@ lt: few: "liko %{count}..." many: "liko %{count}..." other: "liko %{count}..." + left: + one: "Liko %{count}" + few: "Liko %{count}" + many: "Liko %{count}" + other: "Liko %{count}" flagging_topic: title: "Ačiū, kad padedi padedi išlaikyti forumą civilizuotu!" action: "Temos su vėliavomis" @@ -3340,19 +3538,39 @@ lt: changed: "žymės pakeistos:" tags: "Žymos" choose_for_topic: "pasirenkamos žymos" + choose_for_topic_required: + one: "pasirinkite bent %{count} žymę..." + few: "pasirinkite bent %{count} žymų..." + many: "pasirinkite bent %{count} žymų..." + other: "pasirinkite bent %{count} žymų..." info: "Informacija" default_info: "Ši žyma neapsiriboja jokiomis kategorijomis ir neturi sinonimų." staff_info: "Norėdami pridėti apribojimų, įtraukite šią žymą į žymų grupę." category_restricted: "Ši žyma skirta tik kategorijoms, prie kurių neturite prieigos teisės." synonyms: "Sinonimai" save: "Išsaugokite žymos pavadinimą ir aprašymą" + category_restrictions: + one: "Tai gali būti naudojama tik šioje kategorijoje:" + few: "Tai gali būti naudojama tik šiose kategorijose:" + many: "Tai gali būti naudojama tik šiose kategorijose:" + other: "Tai gali būti naudojama tik šiose kategorijose:" edit_synonyms: "Redaguoti sinonimus" add_synonyms_label: "Pridėti sinonimus:" add_synonyms: "Pridėti" remove_synonym: "Pašalinti sinonimą" delete_synonym_confirm: 'Ar tikrai norite ištrinti sinonimą "%{tag_name}“?' delete_tag: "Ištrinti žymą" + delete_confirm: + one: "Ar tikrai norite ištrinti šią žymą ir pašalinti ją iš %{count} temos, kuriai ji priskirta?" + few: "Ar tikrai norite ištrinti šią žymą ir pašalinti ją iš %{count} temų, kurioms ji priskirta?" + many: "Ar tikrai norite ištrinti šią žymą ir pašalinti ją iš %{count} temų, kurioms ji priskirta?" + other: "Ar tikrai norite ištrinti šią žymą ir pašalinti ją iš %{count} temų, kurioms ji priskirta?" delete_confirm_no_topics: "Ar tikrai norite ištrinti šią žymą?" + delete_confirm_synonyms: + one: "Jo sinonimas taip pat bus ištrintas." + few: "%{count} sinonimai taip pat bus ištrinti." + many: "%{count} sinonimai taip pat bus ištrinti." + other: "%{count} sinonimai taip pat bus ištrinti." edit_tag: "Redaguoti žymos pavadinimą ir aprašymą" description: "Aprašymas" sort_by: "Rūšiuoti pagal:" @@ -3363,6 +3581,11 @@ lt: upload: "Įkelti žymas" upload_description: "Įkelkite csv failą, kad sukurtumėte masines žymas" upload_successful: "Žymos sėkmingai įkeltos" + delete_unused_confirmation: + one: "%{count} žyma bus ištrinta: %{tags}" + few: "%{count} žymų bus ištrintos: %{tags}" + many: "%{count} žymų bus ištrintos: %{tags}" + other: "%{count} žymų bus ištrintos: %{tags}" delete_no_unused_tags: "Nėra nepanaudotų žymių." tag_list_joiner: ", " delete_unused: "Ištrinkite nepanaudotas žymas" @@ -3526,6 +3749,7 @@ lt: content: "Mano Įrašai" review: content: "Peržiūra" + until: "Iki:" admin_js: type_to_filter: "įrašyk kažką dėl filtro..." admin: @@ -3571,6 +3795,11 @@ lt: space_used_and_free: "%{usedSize} (%{freeSize} nemokama)" uploads: "Įkėlimai" backups: "Atsarginės kopijos" + backup_count: + one: "%{count} atsarginė kopija %{location}" + few: "%{count} atsarginių kopijų %{location}" + many: "%{count} atsarginių kopijų %{location}" + other: "%{count} atsarginių kopijų %{location}" lastest_backup: "Naujausi: %{date}" traffic_short: "Srautas" traffic: "Application web requests" @@ -3661,6 +3890,11 @@ lt: effects: Efektai trust_levels_none: "Nieko" automatic_membership_email_domains: "Vartotojai, kurie užsiregistravo su el. paštu, kuris sutampa su šiame sąraše esančiu el. paštu bus automatiškai pridėti į šią grupę:" + automatic_membership_user_count: + one: "%{count} vartotojas turi naujus el. pašto domenus ir bus pridėtas prie grupės." + few: "%{count} vartotojų turi naujus el. pašto domenus ir bus įtraukti į grupę." + many: "%{count} vartotojų turi naujus el. pašto domenus ir bus įtraukti į grupę." + other: "%{count} vartotojų turi naujus el. pašto domenus ir bus įtraukti į grupę." automatic_membership_associated_groups: "Vartotojai, kurie yra čia išvardytos paslaugos grupės nariai, bus automatiškai įtraukti į šią grupę, kai jie prisijungs prie paslaugos." primary_group: "Automatiškai nustatyk pagrindinę grupę" name_placeholder: "Grupės pavadinimas, be tarpų, taisyklės kaip ir slapyvardžiui" @@ -3814,8 +4048,19 @@ lt: events: none: "Nėra susijusių įvykių." redeliver: "Iš naujo pristatyti" + incoming: + one: "Yra naujas įvykis." + few: "Yra %{count} naujų įvykių." + many: "Yra %{count} naujų įvykių." + other: "Yra %{count} naujų įvykių." + completed_in: + one: "Baigta per %{count} sekundžių." + few: "Baigta per %{count} sekundžių." + many: "Baigta per %{count} sekundžių." + other: "Baigta per %{count} sekundžių." request: "Užklausa" response: "Atsakymas" + headers: "Antraštės" body: "Turinys" status: "Būsenos kodas" event_id: "ID" @@ -4362,6 +4607,11 @@ lt: title: "Peržiūrėti Žodžiai" search: "paieška" clear_filter: "Išvalyti" + show_words: + one: "rodyti %{count} žodį" + few: "rodyti %{count} žodžių" + many: "rodyti %{count} žodžių" + other: "rodyti %{count} žodžių" download: Atsisiųsti clear_all: Išvalyti viską clear_all_confirm: "Ar tikrai norite išvalyti visus stebėtus žodžius %{action} veiksmui?" @@ -4611,6 +4861,11 @@ lt: posts: "Pranešimai" tl3_requirements: title: "Reikalavimai 3 pasitikėjimo lygiui" + table_title: + one: "Paskutinę dieną:" + few: "Per pastarąsias %{count} dienų:" + many: "Per pastarąsias %{count} dienų:" + other: "Per pastarąsias %{count} dienų:" value_heading: "Reikšmė" requirement_heading: "Reikalavimas" visits: "Apsilankymai" @@ -4908,6 +5163,11 @@ lt: step: "%{current} iš %{total}" uploading: "Įkeliama..." upload_error: "Atsiprašome, įvyko klaida įkeliant šį dokumentą. Prašome pamėginti dar kartą." + staff_count: + one: "Jūsų bendruomenėje yra %{count} darbuotojų (jūs)." + few: "Jūsų bendruomenėje yra %{count} darbuotojų, įskaitant jus." + many: "Jūsų bendruomenėje yra %{count} darbuotojų, įskaitant jus." + other: "Jūsų bendruomenėje yra %{count} darbuotojų, įskaitant jus." invites: add_user: "pridėti" none_added: "Jūs nekvietėte jokių darbuotojų. Ar tikrai norite tęsti?" diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml index eff9c32bcf..a175567468 100644 --- a/config/locales/client.lv.yml +++ b/config/locales/client.lv.yml @@ -69,6 +69,10 @@ lv: zero: "0 d" one: "%{count} d" other: "%{count} d" + x_months: + zero: "%{count}mēn" + one: "%{count}mēn" + other: "%{count}mēn" about_x_years: zero: "0 g" one: "%{count} g" @@ -409,6 +413,19 @@ lv: name: "Vārds" fields: "Lauki" reject_reason: "Iemesls" + user_percentage: + agreed: + zero: "%{count}% piekrīt" + one: "%{count}% piekrīt" + other: "%{count}% piekrīt" + disagreed: + zero: "%{count}% nepiekrīt" + one: "%{count}% nepiekrīt" + other: "%{count}% nepiekrīt" + ignored: + zero: "%{count}% ignorē" + one: "%{count}% ignorē" + other: "%{count}% ignorē" topics: topic: "Tēmas" reviewable_count: "Skaits" @@ -829,9 +846,17 @@ lv: zero: "Nav tēmu" one: "%{count} tēma" other: "%{count} tēmas" + topic_stat: + zero: "%{number} / %{unit}" + one: "%{number} / %{unit}" + other: "%{number} / %{unit}" topic_stat_unit: week: "nedēļa" month: "mēnesis" + topic_stat_all_time: + zero: "Kopā %{number}" + one: "Kopā %{number}" + other: "Kopā %{number}" n_more: "Kategorijas (vēl%{count} ) ..." ip_lookup: title: IP adreses meklēšana @@ -1668,6 +1693,10 @@ lv: filter_placeholder: Meklēt... filter_placeholder_with_any: Meklēt vai izveidot... create: "Izveidot: '%{content}'" + max_content_reached: + zero: "Jūs varat izvēlēties tikai %{count} vienumu." + one: "Jūs varat izvēlēties tikai %{count} vienumu." + other: "Jūs varat izvēlēties tikai %{count} vienumu." date_time_picker: from: "No" to: Kam @@ -1916,15 +1945,25 @@ lv: delete: "Dzēst tēmas" dismiss: "Nerādīt" dismiss_read: "Nerādīt visu neizlasīto" + dismiss_read_with_selected: + zero: "Noraidīt %{count} nelasītus" + one: "Noraidīt %{count} nelasītus" + other: "Noraidīt %{count} nelasītus" dismiss_button: "Nerādīt..." + dismiss_button_with_selected: + zero: "Noraidīt (%{count})…" + one: "Noraidīt (%{count})…" + other: "Noraidīt (%{count})…" dismiss_tooltip: "Nerādīt tikai jaunos ierakstus vai pārtraukt sekot tēmām" also_dismiss_topics: "Pārtraukt sekot šīm tēmām, lai tās man vairs nekad nerādītos kā neizlasītas" dismiss_new: "Nerādīt jaunus" toggle: "darbības ar vairākām tēmām" actions: "Darbības ar vairumu" + change_category: "Norādīt sadaļu..." close_topics: "Slēgt tēmas" archive_topics: "Arhivēt tēmas" move_messages_to_inbox: "Pārvietot uz iesūtni" + notification_level: "Paziņojumi..." choose_new_category: "Izvēlēties jaunu sadaļu šīm tēmām:" selected: zero: "Jūs izvēlējāties 0. tēmas." @@ -2014,6 +2053,7 @@ lv: remove: "Noņemt taimeri" publish_to: "Publicēt: " when: "Kad:" + time_frame_required: "Lūdzu, izvēlieties laika periodu" duration: "Ilgums" publish_to_category: title: "Ieplānot publicēšanu " @@ -2106,6 +2146,7 @@ lv: unarchive: "Izņemt tēmu no arhīva" archive: "Arhivēt tēmu" reset_read: "Atstatīt visu kā nelasītu" + make_public: "Izveidot publisku tēmu..." feature: pin: "Piespraust tēmu" unpin: "Atspraust tēmu" @@ -2276,6 +2317,8 @@ lv: image_upload_not_allowed_for_new_user: "Atvainojiet, jaunie lietotāji nevar augšuplādēt attēlus." attachment_upload_not_allowed_for_new_user: "Atvainojiet, jaunie lietotāji nevar augšuplādēt pielikumus." attachment_download_requires_login: "Atvainojiet, jums jābūt ienākušam forumā, lai varētu lejuplādēt pielikumus." + cancel_composer: + discard: "Izmest" via_email: "šis ieraksts atnāca e-pastā" via_auto_generated_email: "šis ieraksts atnāca automātiski ģenerētā e-pastā" whisper: "šis ieraksts ir privāts čuksts moderatoriem" @@ -2304,6 +2347,8 @@ lv: revert_to_regular: "Noņemt darbinieka krāsu" rebake: "Pārbūvēt HTML" unhide: "Noņemt slēpšanu" + change_owner: "Mainīt īpašnieku..." + grant_badge: "Piešķirt Žetonu..." delete_topic: "Dzēst tēmu" actions: people: @@ -2418,6 +2463,8 @@ lv: options: normal: "Normāls" ignore: "Ignorēt" + low: "Zems" + high: "Augsta" sort_options: default: "noklusējuma" likes: "Atzinības" @@ -2574,6 +2621,9 @@ lv: zero: "%{count} nelasītas" one: "%{count} nelasīta" other: "%{count} nelasītas" + unseen: + title: "Neredzēts" + lower_title: "neredzēts" new: lower_title_with_count: zero: "%{count} jaunas" @@ -2754,6 +2804,7 @@ lv: save: "Saglabāt" delete: "Dzēst" confirm_delete: "Vai jūs esat drošs, ka vēlaties dzēst šo tagu grupu?" + parent_tag_placeholder: "Pēc izvēles" topics: none: unread: "Jums nav nelasītu tēmu." @@ -2817,6 +2868,8 @@ lv: content: "Administrators" badges: content: "Žetoni" + everything: + content: "Viss" faq: content: "BUJ" groups: @@ -2850,6 +2903,7 @@ lv: latest_version: "Pēdējais" new_features: dismiss: "Nerādīt" + learn_more: "Uzzināt vairāk" last_checked: "Pēdējā pārbaude" refresh_problems: "Pārlādēt" no_problems: "Problēmas nav atrastas." @@ -2868,6 +2922,7 @@ lv: general_tab: "Vispārīgi" security_tab: "Drošība" report_filter_any: "jebkura" + disabled: Atslēgt reports: today: "Šodien" yesterday: "Vakar" @@ -2928,6 +2983,7 @@ lv: user: "Lietotājs" title: "API" created: Radīts + never_used: (nekad) generate: "Radīt" revoke: "Atsaukt" all_users: "Visi lietotāji" @@ -2967,6 +3023,7 @@ lv: inactive: "Neaktīvs" failed: "Neizdevies" successful: "Veiksmīgs" + disabled: "Atslēgt" events: none: "Nav saistītu notikumu." redeliver: "Atkārtot piegādi" @@ -3083,6 +3140,7 @@ lv: customize_desc: "Pielāgot:" title: "Dizaini" create: "Izveidot" + create_type: "Tips" create_name: "Vārds" long_title: "Labot jūsu vietnes krāsas, CSS un HTML saturu" edit: "Labot" @@ -3102,6 +3160,7 @@ lv: add_upload: "Pievienot augšupielādējamu resursu" upload_file_tip: "Izvēlēties resursu augšupielādei (png, woff2, u.c...)" upload: "Augšupielādēt" + discard: "Izmest" css_html: "Specifisks CSS/HTML" edit_css_html: "Labot CSS/HTML" edit_css_html_help: "Jūs neesat izmainījis nekādus CSS vai HTML failus" @@ -3132,6 +3191,7 @@ lv: description: "Atzinības pogas krāsa." email_style: css: "CSS" + reset: "Atiestatīt uz noklusējumu" email: title: "E-pasti" settings: "Iestatījumi" @@ -3193,6 +3253,9 @@ lv: address_placeholder: "Vārds@epasts.lv" type_placeholder: "apkopojums, reģistrācija ..." reply_key_placeholder: "atbildes atslēga" + moderation_history: + actions: + delete_topic: "Temats dzēsts" logs: title: "Žurnāls" action: "Darbība" @@ -3264,6 +3327,7 @@ lv: backup_destroy: "iznīcināt dublējumu (backup)" reviewed_post: "pārbaudītie ieraksti" custom_staff: "Iestatīt darbību" + post_approved: "ziņa apstiprināta" screened_emails: title: "Pārbaudītie e-pasti" description: "Kad kāds mēģina izveidot jaunu kontu sekojošās e-pasta adreses tiks pārbaudītas, un reģistrācija tiks apturēta, vai kāda cita darbība tiks veikta." @@ -3578,7 +3642,9 @@ lv: dashboard: "Administrācijas panelis" navigation: "Pārvietošanās" default_categories: + modal_description: "Vai vēlaties šīs izmaiņas piemērot vēsturiski? Tas mainīs %{count} esošo lietotāju preferences." modal_yes: "Jā" + modal_no: "Nē, piemērojiet izmaiņas tikai turpmāk" badges: title: Žetoni new_badge: Jauns Žetons diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index 1b6c7481d7..1f47fd798d 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -878,9 +878,15 @@ nb_NO: topic_sentence: one: "%{count} emne" other: "%{count} emner" + topic_stat: + one: "%{number} / %{unit}" + other: "%{number} / %{unit}" topic_stat_unit: week: "uke" month: "måned" + topic_stat_all_time: + one: "%{number} totalt" + other: "%{number} totalt" n_more: "Kategorier (ytterligere%{count})..." ip_lookup: title: Slå opp IP-adresse @@ -2157,14 +2163,22 @@ nb_NO: one: "Avvis %{count} uleste" other: "Avvis %{count} uleste" dismiss_button: "Forkast…" + dismiss_button_with_selected: + one: "Avvis (%{count})…" + other: "Avvis (%{count})…" dismiss_tooltip: "Forkast kun nye innlegg eller slutt å overvåke emner" also_dismiss_topics: "Slutt å overvåke disse emnene slik at de aldri igjen vises til meg som ulest" dismiss_new: "Forkast nye" + dismiss_new_with_selected: + one: "Avvis Ny (%{count})" + other: "Avvis Ny (%{count})" toggle: "slå på/av massevelging av emner" actions: "Massehandlinger" + change_category: "Velg kategori..." close_topics: "Lukk emner" archive_topics: "Arkiver emner" move_messages_to_inbox: "Flytt til innboks" + notification_level: "Varsler..." change_notification_level: "Endre varslingsnivå" choose_new_category: "Velg den nye kategorien for emnene:" selected: @@ -2393,12 +2407,14 @@ nb_NO: open: "Åpne emne" close: "Lukk emne" multi_select: "Velg innlegg…" + slow_mode: "Sett sakte modus..." timed_update: "Sett opp tidsbestemt handling for emne…" pin: "Fest emne…" unpin: "Løsne emne…" unarchive: "Opphev arkivering av emne" archive: "Arkiver emne" reset_read: "Tilbakestill lesedata" + make_public: "Gjør til offentlig emne..." make_private: "Gjør om til personlig melding" reset_bump_date: "Tilbakestille dato emnet ble flyttet øverst" feature: @@ -2682,6 +2698,8 @@ nb_NO: rebake: "Generer HTML på nytt" publish_page: "Side Publisering" unhide: "Vis" + change_owner: "Endre eierskap..." + grant_badge: "Tildel merke..." lock_post: "Lås innlegg" lock_post_description: "forhindre innleggsskriveren fra å redigere dette innlegget" unlock_post: "Lås opp innlegg" @@ -2695,6 +2713,8 @@ nb_NO: delete_topic_confirm_modal_no: "Nei, behold dette emnet" delete_topic_error: "Det oppstod en feil under sletting av emnet" delete_topic: "slett emne" + add_post_notice: "Legg til medarbeider varsel..." + change_post_notice: "Merknad om endring av personalet..." delete_post_notice: "Slett personalmerknad" remove_timer: "Fjern timer" edit_timer: "rediger timeren" @@ -3425,6 +3445,8 @@ nb_NO: content: "Administrator" badges: content: "Merker" + everything: + content: "Alt" faq: content: "O-S-S" groups: @@ -3435,6 +3457,7 @@ nb_NO: content: "Mine innlegg" review: content: "Gjennomgang" + until: "Inntil:" admin_js: type_to_filter: "skriv for å filtrere…" admin: @@ -3597,6 +3620,8 @@ nb_NO: user: "Bruker" title: "API" created: Opprettet + updated: Oppdatert + never_used: (aldri) generate: "Generer API-nøkkel" revoke: "Trekk tilbake" all_users: "Alle brukere" @@ -3877,6 +3902,7 @@ nb_NO: no_overwrite: "Ugyldig variabelnavn. Kan ikke overskrive en eksisterende variabel." must_be_unique: "Ugyldig variabelnavn. Må være unikt." upload: "Last opp" + discard: "Forkast" css_html: "Egendefinert CSS/HTML" edit_css_html: "Rediger CSS/HTML" edit_css_html_help: "Du har ikke redigert noe CSS eller HTML" @@ -3884,6 +3910,7 @@ nb_NO: import_web_tip: "Pakkebrønn inneholdende drakt" is_private: "Drakten er i et privat git repository" public_key: "Gi den følgende public keyen tilgang til repoet:" + install: "Installer" installed: "Installert" install_popular: "Populært" about_theme: "Om" @@ -3891,6 +3918,7 @@ nb_NO: version: "Versjon:" enable: "Aktiver" disable: "Deaktiver" + disabled: "Denne komponenten har blitt deaktivert." update_to_latest: "Oppdater til seneste" check_for_updates: "Se etter oppdateringer" updating: "Oppdaterer…" @@ -4538,7 +4566,9 @@ nb_NO: dashboard: "Dashbord" navigation: "Navigasjon" default_categories: + modal_description: "Ønsker du å bruke denne endringens historikk? Dette vil endre innstillinger for %{count} eksisterende brukere." modal_yes: "Ja" + modal_no: "Nei, bare gjelder endring fremover" badges: title: Merker new_badge: Nytt merke diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 7792e8e4ba..53ec76a0d6 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -3510,7 +3510,6 @@ nl: this_week: "Week" today: "Vandaag" browser_update: 'Je browser wordt niet ondersteund helaas. Schakel over naar een ondersteunde browser om rijke inhoud te bekijken, je aan te melden en te antwoorden.' - safari_13_warning: Deze website verwijdert binnenkort de ondersteuning voor iOS- en Safari-versie 13 en lager. Er blijft een vereenvoudigde alleen-lezen versie beschikbaar. (Meer informatie) permission_types: full: "Maken / Antwoorden / Weergeven" create_post: "Antwoorden / Weergeven" @@ -4140,7 +4139,6 @@ nl: topics: read: Een topic of een specifiek bericht erin lezen. RSS wordt ook ondersteund. write: Een nieuw topic maken of bericht in een bestaand topic plaatsen. - update: Werk een topic bij. Wijzig de titel, categorie, tags, enz. read_lists: Topiclijsten zoals Top, Nieuw, Nieuwste, etc. lezen. RSS wordt ook ondersteund. posts: edit: Bewerk elk bericht of een specifieke. diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 01c80d213f..f2fcc07a7f 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -1166,6 +1166,7 @@ pl_PL: perm_denied_expl: "Odmówiłeś/łaś dostępu dla powiadomień. Pozwól na powiadomienia w ustawieniach przeglądarki." disable: "Wyłącz powiadomienia" enable: "Włącz powiadomienia" + each_browser_note: "Uwaga: musisz zmienić to ustawienie w każdej używanej przeglądarce. Wszystkie powiadomienia zostaną wyłączone, jeśli wstrzymasz powiadomienia z menu użytkownika, niezależnie od tego ustawienia." consent_prompt: "Czy chcesz otrzymywać natychmiastowe powiadomienia, gdy ktoś odpowiada na twoje posty?" dismiss: "Odrzuć" dismiss_notifications: "Odrzuć wszystkie" @@ -2355,6 +2356,11 @@ pl_PL: few: "%{count} powiadomienia o nowej wiadomości" many: "%{count} powiadomień o nowych wiadomościach" other: "%{count} powiadomień o nowych wiadomościach" + new_reviewable: + one: "%{count} nowy do sprawdzenia" + few: "%{count} nowe do sprawdzenia" + many: "%{count} nowych do sprawdzenia" + other: "%{count} nowych do sprawdzenia" title: "powiadomienia o wywołanej @nazwie, odpowiedzi do twoich wpisów i tematów, prywatne wiadomości, itp" none: "Nie udało się załadować listy powiadomień." empty: "Nie znaleziono powiadomień." @@ -3853,7 +3859,6 @@ pl_PL: this_week: "Tydzień" today: "Dzisiaj" browser_update: 'Niestety, twoja przeglądarka nie jest obsługiwana. Proszę przełączyć się na obsługiwaną przeglądarkę, aby móc oglądać bogatą zawartość, zalogować się i odpowiedzieć.' - safari_13_warning: Ta strona wkrótce przestanie obsługiwać systemy iOS i Safari w wersji 13 i niższej. Nadal będzie dostępna uproszczona wersja tylko do odczytu. (więcej informacji) permission_types: full: "tworzyć / odpowiadać / przeglądać" create_post: "odpowiadać / przeglądać" @@ -4195,6 +4200,7 @@ pl_PL: no_drafts_title: "Nie rozpocząłeś żadnych szkiców" no_drafts_body: "Nie jesteś gotowy do opublikowania? Automatycznie zapiszemy nową wersję roboczą i wyświetlimy ją tutaj za każdym razem, gdy zaczniesz pisać temat, odpowiedź lub wiadomość osobistą. Wybierz przycisk anulowania, aby odrzucić lub zapisać wersję roboczą, aby kontynuować później." no_likes_title: "Nie polubiłeś jeszcze żadnych tematów" + no_likes_title_others: "%{username} nie polubił jeszcze żadnego tematu" no_likes_body: "Świetnym sposobem na włączenie się do dyskusji i rozpoczęcie współtworzenia jest rozpoczęcie czytania rozmów, które już miały miejsce, i kliknięcie %{heartIcon} przy postach, które Ci się podobają!" no_topics_title: "Nie rozpocząłeś jeszcze żadnych tematów" no_topics_body: "Zawsze najlepiej jest przeszukać stronę w poszukiwaniu istniejących tematów konwersacji przed rozpoczęciem nowej, ale jeśli masz pewność, że temat, którego szukasz nie istnieje, śmiało rozpocznij nowy temat. Poszukaj przycisku + Nowy temat w prawym górnym rogu listy tematów, kategorii lub tagu, aby rozpocząć tworzenie nowego tematu w tym obszarze." @@ -4230,6 +4236,7 @@ pl_PL: header_link_text: "O stronie" messages: header_link_text: "Wiadomości" + header_action_title: "Utwórz osobistą wiadomość" links: inbox: "Skrzynka odbiorcza" sent: "Wysłane" @@ -4246,6 +4253,7 @@ pl_PL: none: "Nie dodałeś żadnych tagów." click_to_get_started: "Kliknij tutaj, aby rozpocząć." header_link_text: "Etykiety" + header_action_title: "Edytuj tagi paska bocznego" configure_defaults: "Skonfiguruj ustawienia domyślne" categories: links: @@ -4255,25 +4263,33 @@ pl_PL: none: "Nie dodałeś żadnych kategorii." click_to_get_started: "Kliknij tutaj, aby rozpocząć." header_link_text: "Kategorie" + header_action_title: "Edytuj kategorie paska bocznego" configure_defaults: "Skonfiguruj ustawienia domyślne" community: header_link_text: "Społeczność" + header_action_title: "Utwórz temat" links: about: content: "O stronie" + title: "Więcej szczegółów na temat tej witryny" admin: content: "Administracja" + title: "Ustawienia witryny i raporty" badges: content: "Odznaki" + title: "Wszystkie odznaki dostępne do zdobycia" everything: content: "Wszystko" title: "Wszystkie tematy" faq: content: "FAQ" + title: "Wskazówki dotyczące korzystania z tej witryny" groups: content: "Grupy" + title: "Lista dostępnych grup użytkowników" users: content: "Użytkownicy" + title: "Lista wszystkich użytkowników" my_posts: content: "Wysłane" title: "Moja ostatnia aktywność w temacie" @@ -4520,7 +4536,6 @@ pl_PL: topics: read: Przeczytaj temat lub konkretny post w nim. Obsługiwany jest również format RSS. write: Utwórz nowy temat lub post w istniejącym. - update: Zaktualizuj temat. Zmień tytuł, kategorię, tagi itp. read_lists: Czytaj listy tematów, takie jak najpopularniejsze, nowe, najnowsze itp. Obsługiwany jest również format RSS. posts: edit: Edytuj dowolny post lub konkretny. @@ -4672,6 +4687,7 @@ pl_PL: broken_route: "Nie można skonfigurować łącza do '%{name}'. Upewnij się, że blokery reklam są wyłączone i spróbuj ponownie załadować stronę." navigation_menu: sidebar: "Pasek boczny" + legacy: "Przestarzały" backups: title: "Kopie zapasowe" menu: @@ -5082,6 +5098,7 @@ pl_PL: address_placeholder: "nazwa@example.com" type_placeholder: "streszczenie, rejestracja…" reply_key_placeholder: "klucz odpowiedzi" + smtp_transaction_response_placeholder: "SMTP ID" moderation_history: performed_by: "Wykonane przez" no_results: "Brak dostępnej historii moderacji." diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index c87d064baa..0aed18210f 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -2531,6 +2531,7 @@ pt: unarchive: "Desarquivar Tópico" archive: "Arquivar Tópico" reset_read: "Repor Data de Leitura" + make_public: "Criar tópico publico..." make_private: "Tornar Mensagem Pessoal" reset_bump_date: "Reset à Data do Bump" feature: @@ -2813,6 +2814,7 @@ pt: publish_page: "Publicação de Página" unhide: "Mostrar" change_owner: "Alterar Proprietário..." + grant_badge: "Atribuir Crachá..." lock_post: "Bloquear Post" lock_post_description: "impedir o autor de editar esta publicação" unlock_post: "Desbloquear Post" @@ -2823,6 +2825,8 @@ pt: delete_topic_confirm_modal_no: "Não, mantenha este tópico" delete_topic_error: "Ocorreu um erro ao excluir este tópico" delete_topic: "eliminar tópico" + add_post_notice: "Adicionar Nota da Equipe..." + change_post_notice: "Alterar Aviso da Equipa..." delete_post_notice: "Apagar Aviso da Equipa" remove_timer: "remover timer" edit_timer: "editar temporizador" @@ -3435,6 +3439,7 @@ pt: topics: none: unread: "Não tem tópicos por ler." + unseen: "Não tem tópicos por ler." new: "Não tem novos tópicos." read: "Ainda não leu nenhum tópico." posted: "Ainda não publicou em qualquer tópico." @@ -3458,6 +3463,7 @@ pt: enabled: "O modo de segurança está activado, para sair do modo de segurança feche esta janela do navegador" image_removed: "(imagem removida)" pause_notifications: + label: "Pausar notificações" remaining: "%{remaining} restante" options: half_hour: "30 minutos" @@ -3509,6 +3515,8 @@ pt: content: "Administrador" badges: content: "Crachás" + everything: + content: "Tudo" faq: content: "FAQ" groups: @@ -3519,6 +3527,7 @@ pt: content: "As Minhas publicações" review: content: "Revisão" + until: "Até:" admin_js: type_to_filter: "digite para filtrar..." admin: @@ -3805,6 +3814,8 @@ pt: change_settings: "Alterar Configurações" change_settings_short: "Configurações" howto: "Como instalo plugins?" + navigation_menu: + sidebar: "Barra Lateral" backups: title: "Fazer Cópias de Segurança" menu: @@ -4133,6 +4144,9 @@ pt: address_placeholder: "nome@exemplo.com" type_placeholder: "resumo, subscrever..." reply_key_placeholder: "chave de resposta" + moderation_history: + actions: + delete_topic: "Tópico eliminado" logs: title: "Logs" action: "Ação" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index cab81e385a..2f5dfb0000 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -176,6 +176,7 @@ pt_BR: banner: enabled: "Tornou isto um banner %{when} atrás. Isso será mostrado no topo de todas as páginas até que seja descartado pelo(a) usuário(a)." disabled: "Removeu este banner %{when} atrás. Ele não irá mais aparecer no topo de todas as páginas." + forwarded: "Encaminhou o e-mail acima" topic_admin_menu: "ações de tópico" skip_to_main_content: "Ir para o conteúdo principal" emails_are_disabled: "Todos os envios de e-mail foram desabilitados globalmente por um administrador. Nenhuma notificação por e-mail de qualquer tipo será enviada." @@ -1007,6 +1008,7 @@ pt_BR: notification_schedule: title: "Agendamento de notificações" label: "Ativar agendamento de notificações personalizada" + tip: "Fora desse horário, suas notificações serão pausadas." midnight: "Meia-noite" none: "Nenhum" monday: "Segunda-feira" @@ -1050,12 +1052,16 @@ pt_BR: perm_denied_expl: "Você negou permissão para notificações. Permita as notificações nas configurações do seu navegador." disable: "Desativar notificações" enable: "Ativar notificações" + each_browser_note: "Observação: é preciso alterar esta configuração em todos os navegadores que você usar. Todas as notificações serão desativadas se você pausar as notificações no menu do usuário, independentemente desta configuração." consent_prompt: "Você quer notificações em tempo real quando as pessoas responderem às suas postagens?" dismiss: "Descartar" dismiss_notifications: "Descartar tudo" dismiss_notifications_tooltip: "Marcar todas as notificações não lidas como lidas" dismiss_bookmarks_tooltip: "Marcar todos os lembretes de favoritos não lidos como lidos" dismiss_messages_tooltip: "Marcar todas as notificações de mensagens pessoais não lidas como lidas" + no_likes_title: "Você ainda não recebeu nenhuma curtida" + no_likes_body: > + Você receberá uma notificação aqui sempre que alguém curtir uma de suas postagens, para que possa saber o que os outros estão achando valioso. Outros também verão o mesmo quando você curtir as postagens deles!

    Notificações de curtidas nunca são enviadas para seu e-mail, mas você pode configurar a forma de receber notificações sobre curtidas no site ajustando suas preferências de notificação. no_messages_title: "Você não tem mensagens" no_messages_body: > Precisa ter uma conversa pessoal direta, fora do fluxo de conversa normal? Envie uma mensagem ao selecionar o seu avatar e usar o botão de mensagem %{icon}.

    Se precisar de ajuda, envie uma mensagem a um membro da equipe. @@ -1205,6 +1211,62 @@ pt_BR: warnings: "Avisos oficiais" read_more_in_group: "Quer ler mais? Veja outras mensagens em %{groupLink}." read_more: "Quer ler mais? Veja outras mensagens em mensagens pessoais." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Há # não lidas} + other {Há # não lidas} + } + { NEW, plural, + =0 {} + one { e # uma nova message restante, ou navegue por outras messages em {groupLink}} + other { e # novas messages restantes, ou navegue por outras mensagens em {groupLink}} + } + } + false { + { UNREAD, plural, + =0 {} + one {Há # unread message restante, ou navegue por outras messages em {groupLink}} + other {Há # unread messages restantes, ou navegue por outras mensagens em {groupLink}} + } + { NEW, plural, + =0 {} + one {Há # uma nova message restante, ou navegue por outras messages em {groupLink}} + other {Há # novas messages restantes, ou navegue por outras mensagens em {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Há # mensagem não lida} + other {Há # mensagens não lidas} + } + { NEW, plural, + =0 {} + one { e # nova mensagem restante, ou navegue por outras mensagens pessoais} + other { e # novas mensagens restantes, ou navegue por outras mensagens pessoais} + } + } + false { + { UNREAD, plural, + =0 {} + one {Resta # mensagem não lida, ou navegue por outras mensagens pessoais} + other {Restam # mensagens não lidas, ou navegue por outras mensagens pessoais} + } + { NEW, plural, + =0 {} + one {Resta # nova mensagem, ou navegue por outras mensagens pessoais} + other {Restam # novas mensagens, ou navegue por outras mensagens pessoais} + } + } + other {} + } preferences_nav: account: "Conta" security: "Segurança" @@ -1269,7 +1331,14 @@ pt_BR: use: "Usar app autenticador" enforced_notice: "Você precisa ativar a autenticação de dois fatores antes de acessar este site." disable: "Desativar" + disable_confirm: "Tem certeza de que deseja desativar a autenticação de dois fatores?" delete: "Excluir" + delete_confirm_header: "Estes autenticadores baseados em token e chaves de segurança física serão excluídos:" + delete_confirm_instruction: "Para confirmar, digite %{confirm} na caixa abaixo." + delete_single_confirm_title: "Excluindo um autenticador" + delete_single_confirm_message: "Você está excluindo %{name}. Você não pode desfazer esta ação. Se mudar de ideia, terá de registar novamente este autenticador." + delete_backup_codes_confirm_title: "Excluindo códigos de backup" + delete_backup_codes_confirm_message: "Você está excluindo códigos de backup. Você não pode desfazer esta ação. Se você mudar de ideia, terá que regenerar os códigos de backup." save: "Salvar" edit: "Editar" edit_title: "Editar autenticador" @@ -1436,6 +1505,8 @@ pt_BR: title: "O plano de fundo do título da página exibe a contagem de:" notifications: "Novas notificações" contextual: "Novo conteúdo da página" + bookmark_after_notification: + title: "Depois que uma notificação de lembrete de marcador é enviada:" like_notification_frequency: title: "Notificar ao receber curtida" always: "Sempre" @@ -1661,6 +1732,7 @@ pt_BR: save: "Salvar" set_custom_status: "Definir status personalizados" what_are_you_doing: "O que você está fazendo?" + pause_notifications: "Pausar notificações" remove_status: "Remover status" user_tips: primary: "Entendi!" @@ -1714,6 +1786,36 @@ pt_BR: logout_disabled: "Não é possível sair enquanto o site estiver em modo somente leitura. Esse recurso está desativado." staff_writes_only_mode: enabled: "Este site está no modo apenas para funcionários. Continue navegando, mas as respostas, curtidas e outras ações são limitadas apenas aos membros da equipe." + too_few_topics_and_posts_notice_MF: | + Vamos iniciar a discussão! Há { currentTopics, plural, + one {# tópico} + other {# tópicos} + } e { currentPosts, plural, + one {# postagem} + other {# postagens} + }. Os visitantes precisam de mais para ler e responder. Recomendamos pelo menos { requiredTopics, plural, + one {# tópico} + other {# tópicos} + } e { requiredPosts, plural, + one {# postagem} + other {# postagens} + }. Somente a equipe pode ver esta mensagem. + too_few_topics_notice_MF: | + Vamos iniciar a discussão! Há { currentTopics, plural, + one {# tópico} + other {# tópicos} + }. Os visitantes precisam de mais para ler e responder. Recomendamos pelo menos { requiredTopics, plural, + one {# tópico} + other {# tópicos} + }. Somente a equipe pode ver esta mensagem. + too_few_posts_notice_MF: | + Vamos iniciar a discussão! Há { currentPosts, plural, + one {# postagem} + other {# postagens} + }. Os visitantes precisam de mais para ler e responder. Recomendamos pelo menos { requiredPosts, plural, + one {# postagem} + other {# postagens} + }. Somente a equipe pode ver esta mensagem. logs_error_rate_notice: reached_hour_MF: | {relativeAge}a quantidade de {rate, plural, one {# erro/hora} other {# erros/hora}} alcançou o limite de configuração do site de {limit, plural, one {# erro/hora} other {# erros/hora}}. @@ -1821,6 +1923,9 @@ pt_BR: username: "Usuário(a)" password: "Senha" show_password: "Exibir" + hide_password: "Ocultar" + show_password_title: "Mostrar senha" + hide_password_title: "Ocultar senha" second_factor_title: "Autenticação de dois fatores" second_factor_description: "Digite o código de autenticação do seu aplicativo:" second_factor_backup: "Entrar usando um código de backup" @@ -1842,6 +1947,7 @@ pt_BR: blank_username_or_password: "Digite seu e-mail ou nome do(a) usuário(a) e a senha." reset_password: "Redefinir senha" logging_in: "Entrando..." + previous_sign_up: "Já tem uma conta?" or: "Ou" authenticating: "Autenticando..." awaiting_activation: "Sua conta está aguardando ativação, utilize o link \"Esqueci a senha\" para enviar um novo e-mail de ativação." @@ -2016,10 +2122,17 @@ pt_BR: private: "Você mencionou @%{username}, mas ele(a) não será notificado(a), pois não pode ver esta mensagem pessoal. Você precisará convidá-lo(a) para esta mensagem pessoal." muted_topic: "Você mencionou @%{username}, ele(a) não receberá notificação porque este tópico foi silenciado." not_allowed: "Você mencionou @%{username}, ele(a) não receberá notificação porque não recebeu convite para este tópico." + cannot_see_group_mention: + not_mentionable: "Você não pode mencionar o grupo @%{group}." + some_not_allowed: + one: "Você mencionou @%{group} , mas apenas %{count} membro será notificado porque os outros membros não podem ver esta mensagem pessoal. Você precisará convidá-los para esta mensagem pessoal." + other: "Você mencionou @%{group} , mas apenas %{count} membros serão notificados porque os outros membros não podem ver esta mensagem pessoal. Você precisará convidá-los para esta mensagem pessoal." + not_allowed: "Você mencionou @%{group}, mas nenhum dos seus membros será notificado(a), pois não poderá ver esta mensagem pessoal. Você precisará convidá-lo(a) para esta mensagem pessoal." here_mention: one: "Ao mencionar @%{here}, você está prestes a notificar o(a) usuário(a) %{count}. Tem certeza?" other: "Ao mencionar @%{here}, você está prestes a notificar os(as) usuário(as) %{count}. Tem certeza?" duplicate_link: "Parece que o seu link de %{domain} já foi postado neste tópico por @%{username} em uma resposta em %{ago}. Tem certeza de que deseja postar novamente?" + duplicate_link_same_user: "Parece que você já postou um link para %{domain} neste tópico em uma resposta em %{ago}. Tem certeza de que deseja postá-lo novamente?" reference_topic_title: "RE: %{title}" error: title_missing: "O título é obrigatório" @@ -2155,6 +2268,12 @@ pt_BR: high_priority: one: "%{count} notificação não lida de alta prioridade" other: "%{count} notificações não lidas de alta prioridade" + new_message_notification: + one: "%{count} notificação de nova mensagem" + other: "%{count} notificações de novas mensagens" + new_reviewable: + one: "%{count} novo para revisar" + other: "%{count} novos para revisar" title: "notificações de menção de @nome, respostas às suas postagens, tópicos, mensagens, etc" none: "Não foi possível carregar notificações no momento." empty: "Nenhuma notificação foi encontrada." @@ -2200,6 +2319,7 @@ pt_BR: reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} - concluído(a)" + new_features: "Novos recursos disponíveis!" dismiss_confirmation: body: default: @@ -2254,6 +2374,7 @@ pt_BR: membership_request_consolidated: "novas solicitações de associação" reaction: "nova reação" votes_released: "Voto emitido" + new_features: "novos recursos do Discourse foram lançados!" upload_selector: uploading: "Enviando" processing: "Processando envio" @@ -2563,7 +2684,48 @@ pt_BR: show_links: "exibir links neste tópico" collapse_details: "recolher detalhes do tópico" expand_details: "expandir detalhes do tópico" + read_more_in_category: "Quer ler mais? Navegue por outros tópicos em %{categoryLink} ou veja os tópicos mais recentes." + read_more: "Quer ler mais? Navegue por todas as categorias ou veja os tópicos mais recentes." unread_indicator: "Nenhum membro leu a última postagem deste tópico ainda." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Há # tópico não lido} + other {Há # tópicos não lidos} + } + { NEW, plural, + =0 {} + one { e # novo tópico restante,} + other { e # novos tópicos restantes,} + } + } + false { + { UNREAD, plural, + =0 {} + one {Resta # tópico não lido,} + other {Restam # tópicos não lidos,} + } + { NEW, plural, + =0 {} + one {Resta # novo tópico,} + other {Restam # novos tópicos,} + } + } + other {} + } + { HAS_CATEGORY, select, + true { ou navegue por outros tópicos em {categoryLink}} + false { ou veja os tópicos mais recentes} + other {} + } + bumped_at_title: | + Primeira postagem: %{createdAtDate} + Postada: %{bumpedAtDate} + browse_all_categories_latest: "Navegue por todas as categorias ou veja os tópicos mais recentes." + browse_all_categories_latest_or_top: "Navegue por todas as categorias, veja os tópicos mais recentes ou veja os principais:" + browse_all_tags_or_latest: "Navegue por todas as etiquetas ou visualize os tópicos mais recentes." suggest_create_topic: Tudo pronto para começar uma nova conversa? jump_reply_up: pular para a primeira resposta jump_reply_down: pular para a última resposta @@ -2984,6 +3146,7 @@ pt_BR: one: "Desculpe, é possível carregar apenas %{count} arquivo por vez." other: "Desculpe, é possível carregar apenas %{count} arquivos por vez." upload_not_authorized: "Desculpe, o arquivo que você está tentando enviar não é permitido (extensões permitidas: %{authorized_extensions})." + no_uploads_authorized: "Desculpe, nenhum arquivo está autorizado a ser carregado." image_upload_not_allowed_for_new_user: "Desculpe, usuários(as) novos(as) não podem enviar imagens." attachment_upload_not_allowed_for_new_user: "Desculpe, usuários(as) novos(as) não podem enviar anexos." attachment_download_requires_login: "Desculpe, você precisa entrar para baixar arquivos anexos." @@ -2995,6 +3158,7 @@ pt_BR: via_email: "postagem recebida via e-mail" via_auto_generated_email: "esta mensagem chegou por um e-mail gerado automaticamente" whisper: "esta mensagem é um sussuro privado para a moderação" + whisper_groups: "esta postagem é um sussurro privado visível apenas para %{groupNames}" wiki: about: "esta postagem é uma wiki" few_likes_left: "Obrigado por compartilhar o amor! Restam apenas algumas curtidas sobrando para você usar hoje." @@ -3217,6 +3381,7 @@ pt_BR: pending_permission_change_alert: "Você não adicionou %{group} a esta categoria. Clique neste botão para adicionar." images: "Imagens" email_in: "Endereço de e-mail de entrada personalizado:" + email_in_tooltip: "Você pode separar múltiplos endereços de e-mail com o caractere |." email_in_allow_strangers: "Aceitar e-mails de usuários anônimos sem contas" email_in_disabled: "Postar novos tópicos via e-mail está desativado nas configurações do site. Para ativar respostas em novos tópicos via e-mail, " email_in_disabled_click: 'ative a configuração de "e-mail em".' @@ -3497,7 +3662,6 @@ pt_BR: this_week: "Semana" today: "Hoje" browser_update: 'Infelizmente, seu navegador não é compatível. Use um navegador compatível para visualizar um conteúdo interessante, entrar com a conta e responder.' - safari_13_warning: Este site em breve removerá o suporte para iOS e Safari versões 13 e anteriores. Uma versão simplificada somente para leitura permanecerá disponível. (mais informações) permission_types: full: "Criar/Responder/Ver" create_post: "Responder/Ver" @@ -3785,6 +3949,8 @@ pt_BR: enabled: "O modo seguro está ativado. Para sair do modo seguro, feche a janela do navegador" image_removed: "(imagem removida)" pause_notifications: + title: "Pausar notificações para..." + label: "Pausar notificações" remaining: "%{remaining} restante(s)" options: half_hour: "30 minutos" @@ -3828,12 +3994,15 @@ pt_BR: second_factor_auth: redirect_after_success: "A autenticação de segundo fator foi bem-sucedida. Redirecionando para a página anterior…" sidebar: + show_sidebar: "Mostrar barra lateral" + hide_sidebar: "Ocultar barra lateral" unread_count: one: "%{count} não lido" other: "%{count} não lidos" new_count: one: "%{count} nova" other: "%{count} novas" + toggle_section: "Alternar seção" more: "Mais" all_categories: "Todas as categorias" all_tags: "Todas as etiquetas" @@ -3842,6 +4011,7 @@ pt_BR: header_link_text: "Sobre" messages: header_link_text: "Mensagens" + header_action_title: "Criar uma mensagem pessoal" links: inbox: "Caixa de entrada" sent: "Enviadas" @@ -3858,6 +4028,7 @@ pt_BR: none: "Você não adicionou nenhuma etiqueta." click_to_get_started: "Clique aqui para começar." header_link_text: "Etiquetas" + header_action_title: "Editar suas etiquetas da barra lateral" configure_defaults: "Configurar padrões" categories: links: @@ -3867,32 +4038,43 @@ pt_BR: none: "Você não adicionou nenhuma categoria." click_to_get_started: "Clique aqui para começar." header_link_text: "Categorias" + header_action_title: "Editar suas categorias de barra lateral" configure_defaults: "Configurar padrões" community: header_link_text: "Comunidade" + header_action_title: "Criar um tópico" links: about: content: "Sobre" + title: "Mais detalhes sobre este site" admin: content: "Administrador(a)" + title: "Configurações e relatórios do site" badges: content: "Emblemas" + title: "Todos os emblemas disponíveis para ganhar" everything: content: "Tudo" title: "Todos os Tópicos" faq: content: "FAQ" + title: "Diretrizes para usar este site" groups: content: "Grupos" + title: "Lista de grupos de usuários disponíveis" users: content: "Usuários(as)" + title: "Lista de todos os usuários" my_posts: content: "Minhas postagens" + title: "Minha atividade de tópico recente" + title_drafts: "Meus rascunhos não publicados" draft_count: one: "%{count} rascunho" other: "%{count} rascunhos" review: content: "Revisar" + title: "Publicações sinalizadas e outros itens na fila" pending_count: "%{count} pendente" welcome_topic_banner: title: "Crie seu Tópico de Boas-Vindas" @@ -4045,6 +4227,9 @@ pt_BR: other: "%{count} usuários(as) tem os novos domínios de e-mail e serão adicionados(as) ao grupo." automatic_membership_associated_groups: "Usuários(as) que são membros de um grupo em um serviço listado aqui serão automaticamente adicionados a este grupo ao entrar no serviço." primary_group: "Definido automaticamente como grupo principal" + alert: + primary_group: "Como este é um grupo primário, o nome '%{group_name}' será usado nas classes CSS que podem ser visualizadas por qualquer pessoa." + flair_group: "Como esse grupo tem distinção para seus membros, o nome '%{group_name}' ficará visível para qualquer pessoa." name_placeholder: "Nome do grupo, sem espaços, igual à regra de nome do(a) usuário(a)" primary: "Grupo primário" no_primary: "(nenhum grupo primário)" @@ -4054,6 +4239,10 @@ pt_BR: about: "Edite a associação no seu grupo e os nomes aqui" group_members: "Membros do grupo" delete: "Apagar" + delete_confirm: "Tem certeza de que deseja excluir este grupo?" + delete_with_messages_confirm: + one: "Excluir este grupo fará com que %{count} mensagem fique órfã. Os membros do grupo não poderão mais acessá-la." + other: "Excluir este grupo fará com que %{count} mensagens fiquem órfãs. Os membros do grupo não poderão mais acessá-las." delete_failed: "Não é possível excluir o grupo. Se for um grupo automático, não poderá ser desfeito." delete_automatic_group: Este é um grupo automático e não pode ser excluído. delete_owner_confirm: "Remover privilégio de proprietário(a) de \"%{username}\"?" @@ -4119,7 +4308,7 @@ pt_BR: topics: read: Leia um tópico ou uma postagem específica nele. RSS também é compatível. write: Crie um novo tópico ou poste em algum que já existe. - update: Atualize um tópico. Altere o título, categoria, etiquetas, etc. + update: Atualize um tópico. Altere o título, categoria, etiquetas, status, arquétipo, feature_link etc. read_lists: Leia listas de tópico como melhores, novidades, mais recentes. RSS também é compatível. posts: edit: Edite qualquer postagem ou especifique uma. @@ -4138,6 +4327,9 @@ pt_BR: anonymize: Torne anônimas contas do(a) usuário(a). delete: Excluir contas de usuário(as). list: Obtenha uma lista de usuários(as). + user_status: + read: Ler status do usuário. + update: Atualizar status do usuário. email: receive_emails: Combine este escopo com o destinatário(a) para processar e-mails recebidos. badges: @@ -4162,6 +4354,7 @@ pt_BR: create: "Criar" edit: "Editar" save: "Salvar" + description_label: "Gatilhos de evento" controls: "Controles" go_back: "Voltar para a lista" payload_url: "URL do conteúdo" @@ -4264,6 +4457,8 @@ pt_BR: broken_route: "Não foi possível configurar o link para \"%{name}\". Verifique se os bloqueadores de anúncios estão desativados e tente recarregar a página." navigation_menu: sidebar: "Barra Lateral" + header_dropdown: "Menu dropdown de cabeçalho" + legacy: "Legado" backups: title: "Backups" menu: @@ -4948,6 +5143,7 @@ pt_BR: user: suspend_failed: "Algo deu errado ao suspender este(a) usuário(a) %{error}" unsuspend_failed: "Algo deu errado ao reativar este(a) usuário(a) %{error}" + suspend_duration: "Suspender usuário até:" suspend_reason_label: "Por que você está suspendendo? Este texto será visível para todos na página de perfil deste(a) usuário(a) e será mostrado ao(à) usuário(a) ao tentar entrar. Seja breve." suspend_reason_hidden_label: "Por que você está suspendendo? Este texto será exibido para o(a) usuário(a) quando tentar entrar. Seja breve." suspend_reason: "Motivo" @@ -4971,7 +5167,9 @@ pt_BR: silence_message: "Mensagem do e-mail" silence_message_placeholder: "(deixe em branco para enviar a mensagem padrão)" suspended_until: "(até %{until})" + suspend_forever: "Suspender para sempre" cant_suspend: "Este(a) usuário(a) não pode ser suspenso(a)." + cant_silence: "Este(a) usuário(a) não pode ser silenciado(a)." delete_posts_failed: "Houve um problema ao excluir as postagens." post_edits: "Postar edições" view_edits: "Visualizar edições" @@ -4981,6 +5179,8 @@ pt_BR: penalty_post_edit: "Editar a postagem" penalty_post_none: "Não fazer nada" penalty_count: "Contagem de penalidades" + penalty_history_MF: >- + Nos últimos 6 meses este usuário foi suspenso { SUSPENDED, plural, one {# vez} other {# vezes} } e silenciado { SILENCED, plural, one {# vez} other {# vezes} }. clear_penalty_history: title: "Limpar histórico de penalidades" description: "usuários(as) com penalidades não podem alcançar TL3" @@ -5135,10 +5335,14 @@ pt_BR: silenced_count: "Silenciado(a)" suspended_count: "Suspenso(a)" last_six_months: "Últimos seis meses" + other_matches: + one: "Há %{count} outro usuário com o mesmo endereço IP. Revise e selecione os suspeitos para penalizar junto com %{username}." + other: "Há %{count} outros usuários com o mesmo endereço IP. Revise e selecione os suspeitos para penalizar junto com %{username}." other_matches_list: username: "Nome de usuário(a)" trust_level: "Nível de confiança" read_time: "Tempo de leitura" + topics_entered: "Tópicos inseridos" posts: "Postagens" tl3_requirements: title: "Requisitos para o nível de confiança 3" @@ -5455,8 +5659,10 @@ pt_BR: finish: "Sair da configuração" back: "Voltar" next: "Próximo" + configure_more: "Configurar mais..." step-text: "Intervalo" step: "%{current} de %{total}" + upload: "Subir arquivo" uploading: "Enviando..." upload_error: "Desculpe, houve um erro ao enviar o arquivo. Tente novamente." staff_count: diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 9895fa1c45..fd2bf003d0 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -87,6 +87,10 @@ ro: date_month: "DD MMMM" date_year: "MMM 'YY" medium: + less_than_x_minutes: + one: "mai puțin de %{count} de minute în urmă" + few: "cu mai puțin de %{count} minute în urmă" + other: "cu mai puțin de %{count} minute în urmă" x_minutes: one: "%{count} min" few: "%{count} min" @@ -103,6 +107,10 @@ ro: one: "%{count} zi" few: "%{count} zile" other: "%{count} de zile" + x_months: + one: "%{count} lună" + few: "%{count} luni" + other: "%{count} luni" about_x_years: one: "aproximativ %{count} an" few: "aproximativ %{count} ani" @@ -875,6 +883,14 @@ ro: topic_stat_unit: week: "săptămană" month: "lună" + topic_stat_sentence_week: + one: "%{count} subiect nou în ultima săptămână." + few: "%{count} subiecte noi în ultima săptămână." + other: "%{count} subiecte noi în ultima săptămână." + topic_stat_sentence_month: + one: "%{count} subiect nou în ultima lună." + few: "%{count} subiecte noi în ultima lună." + other: "%{count} subiecte noi în ultima lună." n_more: "Categorii (%{count} mai multe)..." ip_lookup: title: Căutare adresă IP @@ -1351,6 +1367,9 @@ ro: valid_for: "Link-ul de invitare este valid doar pentru următoarele adrese de email: %{email}" invite_link: success: "Link de invitare generat cu succes!" + invite: + show_advanced: "Afișați opțiunile avansate" + hide_advanced: "Ascundeți opțiunile avansate" bulk_invite: none: "Nu există invitații de afișat pe această pagină." text: "Invitație în masă" @@ -1940,9 +1959,11 @@ ro: dismiss_new: "Anulează cele noi" toggle: "activează selecția multiplă a subiectelor" actions: "Acțiuni multiple" + change_category: "Alege categoria..." close_topics: "Închide subiectele" archive_topics: "Arhivează subiectele" move_messages_to_inbox: "Mută în „Primite”" + notification_level: "Notificări..." choose_new_category: "Alege o nouă categorie pentru acest subiect" selected: one: "Ai selectat un subiect." @@ -2034,7 +2055,19 @@ ro: description: "Pentru a promova discuții aprofundate în mișcare rapidă sau controversate, utilizatorii trebuie să aștepte înainte de a posta din nou pe acest subiect." enable: "Activează" remove: "Dezactivează" + hours: "Ore:" + minutes: "De minute:" durations: + 10_minutes: "10 De minute" + 15_minutes: "15 De minute" + 30_minutes: "30 De minute" + 45_minutes: "45 De minute" + 1_hour: "1 Oră" + 2_hours: "2 Ore" + 4_hours: "4 Ore" + 8_hours: "8 Ore" + 12_hours: "12 Ore" + 24_hours: "24 Ore" custom: "Durată personalizată" topic_status_update: title: "Temporizator subiect" @@ -2080,6 +2113,7 @@ ro: title: Evoluția subiectului jump_prompt: "sari la..." jump_prompt_long: "Sari la..." + jump_prompt_to_date: "la data" jump_prompt_or: "sau" notifications: title: schimbă frecvența cu care vei fi notificat despre acest subiect @@ -2139,6 +2173,7 @@ ro: archive: "Arhivează subiect" invisible: "Ascunde subiectul" reset_read: "Resetează datele despre subiecte citite" + make_public: "Transformă în subiect public..." make_private: "Transformă în mesaj privat" feature: pin: "Fixează subiectul" @@ -2358,6 +2393,8 @@ ro: revert_to_regular: "Șterge culoarea pentru membrii echipei" rebake: "Reconstruieşte HTML" unhide: "Arată" + change_owner: "Schimbă proprietarul..." + grant_badge: "Acordă ecuson..." lock_post: "Blochează postarea" unlock_post: "Deblochează postarea" delete_topic_disallowed_modal: "Nu ai permisiuni suficiente pentru a șterge această discuție. Dacă vrei ca ea să fie ștearsă, marcheaz-o trimițând un mesaj explicativ moderatorului." @@ -2510,6 +2547,7 @@ ro: options: normal: "Normal" ignore: "Ignoră" + low: "Scăzut" high: "Ridicată" sort_options: default: "implicit" @@ -2532,6 +2570,7 @@ ro: flagging: title: "Îți mulțumim că ne ajuți să păstrăm o comunitate civilizată!" action: "Marcare" + take_action: "Acționează..." take_action_options: default: title: "Acționează" @@ -2872,6 +2911,7 @@ ro: save: "Salvează" delete: "Șterge" confirm_delete: "Ești sigur că vrei să ștergi acest grup de etichete?" + parent_tag_placeholder: "Opțional" topics: none: unread: "Nu ai niciun subiect necitit." @@ -2953,6 +2993,8 @@ ro: content: "Administrator" badges: content: "Ecusoane" + everything: + content: "Totul" faq: content: "Întrebări frecvente" groups: @@ -2963,6 +3005,7 @@ ro: content: "Postările mele" review: content: "Revizuire" + until: "Pana cand:" admin_js: type_to_filter: "tastează pentru a filtra..." admin: @@ -2986,6 +3029,7 @@ ro: latest_version: "Ultima" new_features: dismiss: "Înlătură" + learn_more: "Află mai multe" last_checked: "Ultima verificare" refresh_problems: "Reîmprospătează" no_problems: "Nu a apărut nicio problemă." @@ -3069,6 +3113,7 @@ ro: title: "API" created: Creat updated: Actualizat + never_used: (niciodată) generate: "Generare" revoke: "Revocă" all_users: "Toți utilizatorii" @@ -3341,6 +3386,7 @@ ro: description: "Culoarea butonului de apreciere." email_style: css: "CSS" + reset: "Resetare la valorile implicite" email: title: "Email" settings: "Opțiuni" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 9aa24f4798..8dda0e0267 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -232,6 +232,7 @@ ru: banner: enabled: "Создал(а) баннер %{when}, который будет отображаться сверху на всех страницах, пока пользователь не скроет его." disabled: "Удалил(а) баннер %{when}. Он не будет отображаться вверху каждой страницы." + forwarded: "Переадресовал(а) вышеуказанное письмо" topic_admin_menu: "действия администратора над темой" skip_to_main_content: "Перейти к основному контенту" emails_are_disabled: "Все исходящие письма были глобально отключены администратором. Никакие уведомления по электронной почте отправляться не будут." @@ -289,6 +290,7 @@ ru: delete: "Удалить" generic_error: "Произошла ошибка." generic_error_with_reason: "Произошла ошибка: %{error}" + multiple_errors: "Произошло несколько ошибок: %{errors}" sign_up: "Регистрация" log_in: "Вход" age: "Возраст" @@ -355,6 +357,7 @@ ru: like_count: "Лайки" topic_count: "Темы" post_count: "Записи" + user_count: "Регистрации" active_user_count: "Активные пользователи" contact: "Контакты" contact_info: "В случае возникновения критической ошибки или срочного вопроса, касающегося этого сайта, свяжитесь с нами по адресу %{contact_info}." @@ -1119,6 +1122,7 @@ ru: notification_schedule: title: "Расписание уведомлений" label: "Включить расписание уведомлений" + tip: "Вне указанных здесь часов уведомления приостанавливаются." midnight: "Полночь" none: "Уведомления отключены" monday: "Понедельник" @@ -1162,12 +1166,16 @@ ru: perm_denied_expl: "Вы запретили уведомления в браузере. Разрешите уведомления в настройках браузера." disable: "Отключить уведомления" enable: "Включить уведомления" + each_browser_note: "Примечание: этот параметр необходимо изменить в каждом используемом браузере. Если приостановить уведомления в меню пользователя, они будут отключены независимо от этого параметра." consent_prompt: "Вы хотите получать уведомления в реальном времени, когда пользователи отвечают на ваши записи?" dismiss: "Пометить прочитанными" dismiss_notifications: "Отклонить всё" dismiss_notifications_tooltip: "Пометить все непрочитанные уведомления прочитанными" dismiss_bookmarks_tooltip: "Пометить все непрочитанные напоминания о закладках как прочитанные" dismiss_messages_tooltip: "Отметить все уведомления о непрочитанных личных сообщениях как прочитанные" + no_likes_title: "Вы еще не получили ни одного лайка" + no_likes_body: > + Когда кому-то из пользователей понравится одна из ваших записей, здесь появится уведомление. Когда вы поставите лайк кому-то, этот пользователь получит аналогичное уведомление.

    Уведомления о лайках на почту не приходят. Изменить способ получения уведомлений о лайках можно в настройках уведомлений. no_messages_title: "У вас нет сообщений" no_messages_body: > Необходимо непубличное общение с кем-то из участников форума? Напишите сообщение, кликнув на аватар участника и нажав на кнопку %{icon}.

    Если нужна помощь, можете написать нашему сотруднику. @@ -1187,6 +1195,7 @@ ru: dynamic_favicon: "Показывать количество на значке браузера" skip_new_user_tips: description: "Не выдавать награды и не показывать советы новым пользователям" + reset_seen_user_tips: "Показать советы для пользователей еще раз" theme_default_on_all_devices: "Сделать эту тему темой по умолчанию на всех моих устройствах" color_scheme_default_on_all_devices: "Установить цветовую схему по умолчанию на всех моих устройствах" color_scheme: "Цветовая схема" @@ -1213,6 +1222,9 @@ ru: tags_section: "Раздел тегов" tags_section_instruction: "Выбранные теги будут отображаться в соответствующем разделе боковой панели." navigation_section: "Навигация" + list_destination_instruction: "Когда на боковой панели появляется новый контент…" + list_destination_default: "использовать ссылку по умолчанию и показать значок для нового" + list_destination_unread_new: "использовать ссылку на непрочитанные (новые) и показать количество нового" change: "изменить" featured_topic: "Избранная тема" moderator: "%{user} — модератор" @@ -1317,6 +1329,78 @@ ru: warnings: "Официальные предупреждения" read_more_in_group: "Хотите почитать ещё? Просмотрите другие сообщения в группе %{groupLink}." read_more: "Хотите почитать ещё? Просмотрите другие сообщения в разделе «Личные сообщения»." + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Осталось # непрочитанное} + few {Осталось # непрочитанных} + many {Осталось # непрочитанных} + other {Осталось # непрочитанного} + } + { NEW, plural, + =0 {} + one { и # новое сообщение, вы также можете посмотреть другие сообщения в группе {groupLink}} + few { и # новых сообщения, вы также можете посмотреть другие сообщения в группе {groupLink}} + many { и # новых сообщений, вы также можете посмотреть другие сообщения в группе {groupLink}} + other { и # нового сообщения, вы также можете посмотреть другие сообщения в группе {groupLink}} + } + } + false { + { UNREAD, plural, + =0 {} + one {Осталось # непрочитанное сообщение, вы также можете посмотреть другие сообщения в группе {groupLink}} + few {Осталось # непрочитанных сообщения, вы также можете посмотреть другие сообщения в группе {groupLink}} + many {Осталось # непрочитанных сообщений, вы также можете посмотреть другие сообщения в группе {groupLink}} + other {Осталось # непрочитанного сообщения, вы также можете посмотреть другие сообщения в группе {groupLink}} + } + { NEW, plural, + =0 {} + one {Осталось # новое сообщение, вы также можете посмотреть другие сообщения в группе {groupLink}} + few {Осталось # новых сообщения, вы также можете посмотреть другие сообщения в группе {groupLink}} + many {Осталось # новых сообщений, вы также можете посмотреть другие сообщения в группе {groupLink}} + other {Осталось # нового сообщения, вы также можете посмотреть другие сообщения в группе {groupLink}} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Осталось # непрочитанное} + few {Осталось # непрочитанных} + many {Осталось # непрочитанных} + other {Осталось # непрочитанного} + } + { NEW, plural, + =0 {} + one { и # новое сообщение, вы также можете посмотреть другие личные сообщения} + few { и # новых сообщения, вы также можете посмотреть другие личные сообщения} + many { и # новых сообщений, вы также можете посмотреть другие личные сообщения} + other { и # нового сообщения, вы также можете посмотреть другие личные сообщения} + } + } + false { + { UNREAD, plural, + =0 {} + one {Осталось # непрочитанное сообщение, вы также можете посмотреть другие личные сообщения} + few {Осталось # непрочитанных сообщения, вы также можете посмотреть другие личные сообщения} + many {Осталось # непрочитанных сообщений, вы также можете посмотреть другие личные сообщения} + other {Осталось # непрочитанного сообщения, вы также можете посмотреть другие личные сообщения} + } + { NEW, plural, + =0 {} + one {Осталось # новое сообщение, вы также можете посмотреть другие личные сообщения} + few {Осталось # новых сообщения, вы также можете посмотреть другие личные сообщения} + many {Осталось # новых сообщений, вы также можете посмотреть другие личные сообщения} + other {Осталось # нового сообщения, вы также можете посмотреть другие личные сообщения} + } + } + other {} + } preferences_nav: account: "Аккаунт" security: "Безопасность" @@ -1385,7 +1469,14 @@ ru: use: "Используйте приложение аутентификации" enforced_notice: "Перед входом на сайт вы должны включить двухфакторную аутентификацию." disable: "Отключить" + disable_confirm: "Действительно отключить двухфакторную аутентификацию?" delete: "Удалить" + delete_confirm_header: "Следующие аутентификаторы на токенах и физические электронные ключи будут удалены:" + delete_confirm_instruction: "Для подтверждения введите %{confirm} в поле ниже." + delete_single_confirm_title: "Удаление аутентификатора" + delete_single_confirm_message: "Вы удаляете аутентификатор «%{name}». Это действие отменить нельзя. Если вы передумаете, его придется зарегистрировать заново." + delete_backup_codes_confirm_title: "Удаление резервных кодов" + delete_backup_codes_confirm_message: "Вы удаляете резервные коды. Это действие отменить нельзя. Если вы передумаете, их придется сгенерировать заново." save: "Сохранить" edit: "Изменить" edit_title: "Изменить приложение аутентификации" @@ -1554,6 +1645,8 @@ ru: title: "В заголовке фоновой страницы отображать:" notifications: "Количество новых уведомлений" contextual: "Количество контента новой страницы" + bookmark_after_notification: + title: "После отправки напоминания о закладке:" like_notification_frequency: title: "Уведомлять при получении лайка" always: "Всегда" @@ -1797,6 +1890,7 @@ ru: save: "Сохранить" set_custom_status: "Поменять статус" what_are_you_doing: "Что вы сейчас делаете?" + pause_notifications: "Приостановить уведомления" remove_status: "Удалить статус" user_tips: primary: "Понятно!" @@ -1807,6 +1901,15 @@ ru: topic_timeline: title: "Шкала времени" content: "Вы можете быстро прокрутить запись, используя шкалу времени." + post_menu: + title: "Меню записи" + content: "Нажав на три точки, вы увидите дополнительные варианты действий с записью." + topic_notification_levels: + title: "Теперь эта тема у вас в отслеживаемых" + content: "Этот колокольчик позволяет настроить уведомления для конкретных тем и целых категорий." + suggested_topics: + title: "Читайте еще!" + content: "Вот темы, которые вам может быть интересно прочитать в следующий раз." loading: "Загрузка…" errors: prev_page: "при попытке загрузки" @@ -1841,6 +1944,52 @@ ru: logout_disabled: "Выход отключён, пока сайт находится в режиме «только для чтения»" staff_writes_only_mode: enabled: "Сайт находится в режиме «только для персонала». Продолжайте просмотр, но отвечать, ставить лайки и выполнять другие действия, влияющие на контент, могут только сотрудники." + too_few_topics_and_posts_notice_MF: | + Давайте приступим к обсуждению! Есть { currentTopics, plural, + one {# тема} + few {# темы} + many {# тем} + other {# темы} + } и { currentPosts, plural, + one {# запись} + few {# записи} + many {# записей} + other {# записи} + }. Пользователи должны больше читать и отвечать — мы рекомендуем по крайней мере {requiredTopics, plural, + one {# тему} + few {# темы} + many {# тем} + other {# темы} + } и { requiredPosts, plural, + one {# запись} + few {# записи} + many {# записей} + other {# записи} + }. Только персонал может видеть это сообщение. + too_few_topics_notice_MF: | + Давайте приступим к обсуждению! Есть { currentTopics, plural, + one {# тема} + few {# темы} + many {# тем} + other {# темы} + }. Пользователи должны больше читать и отвечать — мы рекомендуем по крайней мере {requiredTopics, plural, + one {# тему} + few {# темы} + many {# тем} + other {# темы} + }. Только персонал может видеть это сообщение. + too_few_posts_notice_MF: | + Давайте приступим к обсуждению! Есть { currentPosts, plural, + one {# запись} + few {# записи} + many {# записей} + other {# записи} + }. Пользователи должны больше читать и отвечать — мы рекомендуем по крайней мере {requiredPosts, plural, + one {# запись} + few {# записи} + many {# записей} + other {# записи} + }. Только персонал может видеть это сообщение. logs_error_rate_notice: reached_hour_MF: | {relativeAge}{rate, plural, one {# ошибка в час} few {# ошибки в час} many {# ошибок в час} other {# ошибки в час}}: достигнут предел для настроек сайта — {limit, plural, one {# ошибка в час} few {# ошибки в час} many {# ошибок в час} other {# ошибки в час}}. @@ -1918,7 +2067,7 @@ ru: associate: "Уже есть аккаунт? Войдите в систему для привязки аккаунта %{provider}." forgot_password: title: "Сброс пароля" - action: "Я забыл(-а) свой пароль" + action: "Пароль утерян" invite: "Введите имя пользователя или адрес электронной почты, и мы отправим вам ссылку для сброса пароля." invite_no_username: "Введите адрес электронной почты, и мы отправим вам ссылку для сброса пароля." reset: "Сбросить пароль" @@ -1952,6 +2101,9 @@ ru: username: "Пользователь" password: "Пароль" show_password: "Показать" + hide_password: "Скрыть" + show_password_title: "Показать пароль" + hide_password_title: "Скрыть пароль" second_factor_title: "Двухфакторная аутентификация" second_factor_description: "Введите код аутентификации из приложения:" second_factor_backup: "Войти с помощью резервного кода" @@ -1973,6 +2125,7 @@ ru: blank_username_or_password: "Введите эл. почту или имя пользователя и пароль." reset_password: "Сбросить пароль" logging_in: "Вход…" + previous_sign_up: "Уже есть аккаунт?" or: "или" authenticating: "Проверка…" awaiting_activation: "Аккаунт ожидает активации через ссылку, указанную в отправленном письме. Чтобы повторно выслать активационное письмо, используйте кнопку сброса пароля." @@ -2031,6 +2184,7 @@ ru: success: "Аккаунт создан и вы вошли в него." name_label: "Имя" password_label: "Пароль" + existing_user_can_redeem: "Примите приглашение в тему или группу." password_reset: continue: "Перейти на %{site_name}" emoji_set: @@ -2162,12 +2316,21 @@ ru: private: "Вы упомянули @%{username}, но уведомление отправлено не будет, потому что пользователь не может видеть это личное сообщение. Вам нужно пригласить его в это личное сообщение." muted_topic: "Вы упомянули @%{username}, но уведомление отправлено не будет, потому что пользователь отключил уведомления в этой теме." not_allowed: "Вы упомянули @%{username}, но уведомление отправлено не будет, потому что пользователь не был приглашен в эту тему." + cannot_see_group_mention: + not_mentionable: "Вы не можете упоминать группу «@%{group}»." + some_not_allowed: + one: "Вы упомянули группу «@%{group}», но уведомлен будет только %{count} пользователь: остальные не могут видеть это личное сообщение. Вам нужно пригласить их в это личное сообщение." + few: "Вы упомянули группу «@%{group}», но уведомлены будут только %{count} пользователя: остальные не могут видеть это личное сообщение. Вам нужно пригласить их в это личное сообщение." + many: "Вы упомянули группу «@%{group}», но уведомлены будут только %{count} пользователей: остальные не могут видеть это личное сообщение. Вам нужно пригласить их в это личное сообщение." + other: "Вы упомянули группу «@%{group}», но уведомлены будут только %{count} пользователя: остальные не могут видеть это личное сообщение. Вам нужно пригласить их в это личное сообщение." + not_allowed: "Вы упомянули группу «@%{group}», но никто из ее участников уведомлен не будет: они не могут видеть это личное сообщение. Вам нужно пригласить их в это личное сообщение." here_mention: one: "Упоминая @%{here}, вы уведомите %{count} пользователя — вы уверены?" few: "Упоминая @%{here}, вы уведомите %{count} пользователей — вы уверены?" many: "Упоминая @%{here}, вы уведомите %{count} пользователей — вы уверены?" other: "Упоминая @%{here}, вы уведомите %{count} пользователя — вы уверены?" duplicate_link: "Ссылка на %{domain} уже была размещена пользователем @%{username} в этом ответе %{ago}. Разместить её ещё раз?" + duplicate_link_same_user: "Ссылка на %{domain} уже была размещена вами здесь в этом ответе %{ago}. Разместить её ещё раз?" reference_topic_title: "RE: %{title}" error: title_missing: "Требуется название темы" @@ -2317,6 +2480,16 @@ ru: few: "%{count} непрочитанных уведомления с высоким приоритетом" many: "%{count} непрочитанных уведомлений с высоким приоритетом" other: "%{count} непрочитанного уведомления с высоким приоритетом" + new_message_notification: + one: "%{count} уведомление о новых сообщениях" + few: "%{count} уведомления о новых сообщениях" + many: "%{count} уведомлений о новых сообщениях" + other: "%{count} уведомления о новых сообщениях" + new_reviewable: + one: "%{count} новый элемент на проверку" + few: "%{count} новых элемента на проверку" + many: "%{count} новых элементов на проверку" + other: "%{count} нового элемента на проверку" title: "уведомления об упоминании @имени, об ответах на ваши записи, темы, сообщения и т. д." none: "Не удается загрузить уведомления." empty: "Уведомления не найдены." @@ -2370,6 +2543,7 @@ ru: reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" votes_released: "%{description} — завершено" + new_features: "Появились новые функции!" dismiss_confirmation: body: default: @@ -2432,6 +2606,7 @@ ru: membership_request_consolidated: "новые запросы на членство" reaction: "Новая реакция" votes_released: "Голосование опубликовано" + new_features: "появились новые функции Discourse!" upload_selector: uploading: "Загрузка" processing: "Обработка загружаемого контента" @@ -2502,6 +2677,7 @@ ru: status: "Фильтр по статусу темы" full_search: "Запуск поиска по всей странице" full_search_key: "%{modifier} + Enter" + me: "показывает только ваши записи" advanced: title: Расширенный поиск posted_by: @@ -2780,7 +2956,56 @@ ru: show_links: "Показать ссылки в теме" collapse_details: "Скрыть подробную информацию о теме" expand_details: "Показать подробную информацию о теме" + read_more_in_category: "Хотите почитать что-нибудь ещё? Можно просмотреть темы в категории %{categoryLink} или ознакомиться с последними темами." + read_more: "Хотите почитать что-нибудь ещё? Можно просмотреть все категории или ознакомиться с последними темами." unread_indicator: "Никто ещё не дочитал до конца этой темы." + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + one {Осталась # непрочитанная} + few {Остались # непрочитанные} + many {Осталось # непрочитанных} + other {Остались # непрочитанной} + } + { NEW, plural, + =0 {} + one { и # новая тема,} + few { и # новые темы,} + many { и # новых тем,} + other { и # новой темы,} + } + } + false { + { UNREAD, plural, + =0 {} + one {Осталась # непрочитанная тема,} + few {Остались # непрочитанные темы,} + many {Осталось # непрочитанных тем,} + other {Остались # непрочитанной темы,} + } + { NEW, plural, + =0 {} + one {Осталась # новая тема,} + few {Остались # новые темы,} + many {Осталось # новых тем,} + other {Остались # новой темы,} + } + } + other {} + } + { HAS_CATEGORY, select, + true { вы также можете посмотреть другие темы в категории {categoryLink}} + false { вы также можете просмотреть последние темы} + other {} + } + bumped_at_title: | + Первая запись: %{createdAtDate} + Опубликована: %{bumpedAtDate} + browse_all_categories_latest: "Просмотрите все категории или ознакомьтесь с последними темами." + browse_all_categories_latest_or_top: "Просмотрите все категории, ознакомьтесь с последними или самыми популярными темами:" + browse_all_tags_or_latest: "Просмотрите все теги или ознакомьтесь с последними темами." suggest_create_topic: Готовы начать новое обсуждение? jump_reply_up: Перейти к более ранним ответам jump_reply_down: Перейти к более поздним ответам @@ -3251,6 +3476,7 @@ ru: many: "За раз можно загрузить не более %{count} файлов." other: "За раз можно загрузить не более %{count} файла." upload_not_authorized: "Вы не можете загрузить файл данного типа (список разрешённых типов файлов: %{authorized_extensions})." + no_uploads_authorized: "Загрузка файлов не разрешена." image_upload_not_allowed_for_new_user: "Загрузка изображений недоступна новым пользователям." attachment_upload_not_allowed_for_new_user: "Загрузка файлов недоступна новым пользователям." attachment_download_requires_login: "Войдите, чтобы скачать прикреплённые файлы." @@ -3262,6 +3488,7 @@ ru: via_email: "Это сообщение получено по электронной почте" via_auto_generated_email: "Это автоматическое сообщение получено по электронной почте" whisper: "Это скрытое сообщение и оно доступно только модераторам" + whisper_groups: "это скрытая запись, доступная только группам %{groupNames}" wiki: about: "Это вики-запись" few_likes_left: "Спасибо, что делитесь любовью. На сегодня у вас осталось несколько лайков." @@ -3502,6 +3729,7 @@ ru: pending_permission_change_alert: "Вы не добавили %{group} в эту категорию; нажмите эту кнопку для добавления." images: "Изображения" email_in: "Специальный адрес для входящих писем:" + email_in_tooltip: "Адреса электронной почты разделяются символом вертикальной черты «|»." email_in_allow_strangers: "Принимать письма от анонимных пользователей, не имеющих аккаунтов" email_in_disabled: "Создание новых тем через электронную почту отключено в настройках сайта. Чтобы разрешить создание новых тем через электронную почту, " email_in_disabled_click: 'включите настройку «email in».' @@ -3526,6 +3754,7 @@ ru: this_year: "за год" position: "Позиция на странице категорий:" default_position: "Позиция по умолчанию" + position_disabled: "Категории будут отображаться в порядке активности. Чтобы настроить их порядок в списках, включите настройку fixed category positions." minimum_required_tags: "Минимальное количество тегов, требуемых в теме:" default_slow_mode: 'Включать замедленный режим для тем, создаваемых в этой категории.' parent: "Родительская категория" @@ -3811,7 +4040,6 @@ ru: this_week: "За неделю" today: "Сегодня" browser_update: 'Ваш браузер не поддерживается. Обновите его для полноценной работы с сайтом.' - safari_13_warning: Этот сайт скоро прекратит поддержку iOS и Safari версии 13 и ниже. Упрощённая версия только для чтения останется доступной. (больше информации) permission_types: full: "Создавать / Отвечать / Просматривать" create_post: "Отвечать / Просматривать" @@ -4125,6 +4353,8 @@ ru: enabled: "Включён безопасный режим. Чтобы выйти из него, закройте это окно браузера." image_removed: "(изображение удалено)" pause_notifications: + title: "Приостановить уведомления на…" + label: "Приостановить уведомления" remaining: "Осталось %{remaining}" options: half_hour: "В течение 30 минут" @@ -4168,6 +4398,8 @@ ru: second_factor_auth: redirect_after_success: "Двухфакторная аутентификация выполнена. Перенаправление на предыдущую страницу…" sidebar: + show_sidebar: "Показать боковую панель" + hide_sidebar: "Скрыть боковую панель" unread_count: one: "%{count} непрочитанная" few: "%{count} непрочитанные" @@ -4178,6 +4410,7 @@ ru: few: "%{count} новые" many: "%{count} новых" other: "%{count} новой" + toggle_section: "Свернуть / развернуть раздел" more: "Ещё" all_categories: "Все категории" all_tags: "Все теги" @@ -4186,6 +4419,7 @@ ru: header_link_text: "Информация" messages: header_link_text: "Личные сообщения" + header_action_title: "Создать личное сообщение" links: inbox: "Входящие" sent: "Отправленные" @@ -4195,35 +4429,54 @@ ru: unread_with_count: "Непрочитанные (%{count})" archive: "Архив" tags: + links: + add_tags: + content: "Добавить теги" + title: "Вы не добавили ни одного тега. Чтобы добавить их, нажмите сюда." none: "Вы не добавили ни одного тега." click_to_get_started: "Нажмите здесь, чтобы начать." header_link_text: "Теги" + header_action_title: "Редактировать теги боковой панели" configure_defaults: "Настроить значения по умолчанию" categories: + links: + add_categories: + content: "Добавить категории" + title: "Вы не добавили ни одной категории. Чтобы добавить их, нажмите сюда." none: "Вы не добавили ни одной категории." click_to_get_started: "Нажмите здесь, чтобы начать." header_link_text: "Категории" + header_action_title: "Редактировать категории боковой панели" configure_defaults: "Настроить значения по умолчанию" community: header_link_text: "Сообщество" + header_action_title: "Создать тему" links: about: content: "О форуме" + title: "Подробнее об этом сайте" admin: content: "Администратор" + title: "Отчеты и настройки сайта" badges: content: "Награды" + title: "Все награды, которые можно получить" everything: content: "Все" title: "Все темы" faq: content: "Ответы на вопросы" + title: "Рекомендации по использованию сайта" groups: content: "Группы" + title: "Список доступных групп пользователей" users: content: "Пользователи" + title: "Список пользователей" my_posts: content: "Мои записи" + title: "Мои недавняя активность в темах" + title_drafts: "Мои неопубликованные черновики" draft_count: one: "%{count} черновик" few: "%{count} черновика" @@ -4231,6 +4484,7 @@ ru: other: "%{count} черновика" review: content: "Очередь проверки" + title: "Записи, на которые поступили жалобы, и другие элементы в очереди" pending_count: "В ожидании: %{count}" welcome_topic_banner: title: "Создать приветственную тему" @@ -4387,6 +4641,9 @@ ru: other: "Следующее количество пользователей с новыми почтовыми доменами будут добавлены в группу: %{count}." automatic_membership_associated_groups: "Пользователи, являющиеся членами группы в службе, указанной здесь, будут автоматически добавлены в эту группу при входе в систему с помощью указанной службы." primary_group: "Автоматически устанавливается в качестве основной группы" + alert: + primary_group: "Это основная группа, поэтому название «%{group_name}» будет использоваться в классах CSS, которые могут просматривать все." + flair_group: "У этой группы есть отметка для участников, поэтому название «%{group_name}» будет видно всем." name_placeholder: "Без пробелов, по тем же правилам, что и для имен пользователей" primary: "Основная группа" no_primary: "(нет основной группы)" @@ -4396,6 +4653,12 @@ ru: about: "Здесь можно редактировать членство в группе и её название" group_members: "Участники группы" delete: "Удалить" + delete_confirm: "Действительно удалить эту группу?" + delete_with_messages_confirm: + one: "Удаление приведет к тому, что участники группы утратят доступ к %{count} сообщению." + few: "Удаление приведет к тому, что участники группы утратят доступ к %{count} сообщениям." + many: "Удаление приведет к тому, что участники группы утратят доступ к %{count} сообщениям." + other: "Удаление приведет к тому, что участники группы утратят доступ к %{count} сообщения." delete_failed: "Невозможно удалить группу. Если группа была создана автоматически, то она не может быть удалена." delete_automatic_group: Это автоматическая группа и она не может быть удалена. delete_owner_confirm: "Отозвать права владельца у пользователя «%{username}»?" @@ -4461,7 +4724,6 @@ ru: topics: read: Чтение темы или конкретного сообщения в ней. RSS также поддерживается. write: Создание новой темы или записи в уже существующей теме. - update: Обновление темы. Изменение названия, категории, тегов и т. д. read_lists: Чтение тем в разделах «Последние», «Новые», «Обсуждаемые» и т. д. RSS также поддерживается. posts: edit: Редактирование записи. @@ -4480,6 +4742,9 @@ ru: anonymize: Анонимизация аккаунтов пользователей. delete: Удаление аккаунтов пользователей. list: Получение списка пользователей. + user_status: + read: Чтение статуса пользователя. + update: Обновление статуса пользователя. email: receive_emails: Объединение этой области действия с получателем почты для обработки входящих писем. badges: @@ -4504,6 +4769,7 @@ ru: create: "Создать" edit: "Изменить" save: "Сохранить" + description_label: "Активаторы событий" controls: "Управление" go_back: "Вернуться к списку" payload_url: "Ссылка для отправки" @@ -4607,8 +4873,11 @@ ru: change_settings_short: "Настройки" howto: "Как установить плагин?" official: "Официальный плагин" + broken_route: "Не удается настроить ссылку на «%{name}». Отключите блокировщики рекламы и попробуйте перезагрузить страницу." navigation_menu: sidebar: "Боковая панель" + header_dropdown: "Выпадающий список заголовка" + legacy: "Старая версия" backups: title: "Резервные копии" menu: @@ -5297,6 +5566,7 @@ ru: user: suspend_failed: "Ошибка заморозки пользователя: %{error}" unsuspend_failed: "Ошибка разморозки пользователя: %{error}" + suspend_duration: "Заморозка пользователя до:" suspend_reason_label: "Укажите причину заморозки. Данный текст будет виден всем на странице профиля пользователя и будет показан пользователю, когда он попытается войти в систему. Введите краткое описание." suspend_reason_hidden_label: "Укажите причину заморозку. Этот текст будет показан пользователю, когда он попытается войти в систему. Введите краткое описание." suspend_reason: "Причина" @@ -5320,7 +5590,9 @@ ru: silence_message: "Сообщение на электронную почту" silence_message_placeholder: "(оставьте незаполненным, если необходимо отправить стандартное сообщение)" suspended_until: "(заморожен до %{until})" + suspend_forever: "Бессрочная заморозка" cant_suspend: "Этого пользователя нельзя заморозить." + cant_silence: "Этого пользователя нельзя заблокировать." delete_posts_failed: "При удалении записей возникла ошибка." post_edits: "Правки записей" view_edits: "Просмотр правок" @@ -5330,6 +5602,8 @@ ru: penalty_post_edit: "Редактировать запись" penalty_post_none: "Ничего не делать" penalty_count: "Количество нарушений" + penalty_history_MF: >- + За последние шесть месяцев этот пользователь был заморожен { SUSPENDED, plural, one {# раз} few {# раза} many {# раз} other {# раза} } и заблокирован { SILENCED, plural, one {# раз} few {# раза} many {# раз} other {# раза} }. clear_penalty_history: title: "Очистить историю нарушений" description: "Пользователи с нарушениями не могут достичь третьего уровня доверия" @@ -5494,15 +5768,22 @@ ru: trust_level_2_users: "Пользователи с уровнем доверия 2" trust_level_3_requirements: "Требования для уровня доверия 3" trust_level_locked_tip: "Уровень доверия заблокирован, система не сможет изменять уровень доверия пользователя" + trust_level_unlocked_tip: "уровень доверия разблокирован, система может изменять уровень доверия пользователя" lock_trust_level: "Заблокировать изменение уровня доверия" unlock_trust_level: "Разблокировать изменение уровня доверия" silenced_count: "Заблокированные" suspended_count: "Замороженные" last_six_months: "За последние 6 месяцев" + other_matches: + one: "Есть еще %{count} пользователь с таким же IP-адресом. Проверьте и выберите, кого еще наказать вместе с пользователем %{username}." + few: "Есть еще %{count} пользователя с таким же IP-адресом. Проверьте и выберите, кого еще наказать вместе с пользователем %{username}." + many: "Есть еще %{count} пользователей с таким же IP-адресом. Проверьте и выберите, кого еще наказать вместе с пользователем %{username}." + other: "Есть еще %{count} пользователя с таким же IP-адресом. Проверьте и выберите, кого еще наказать вместе с пользователем %{username}." other_matches_list: username: "Имя пользователя" trust_level: "Уровень доверия" read_time: "Время чтения" + topics_entered: "Посещено тем" posts: "Записи" tl3_requirements: title: "Требования для уровня доверия 3" @@ -5802,6 +6083,7 @@ ru: destination: "Назначения" copy_to_clipboard: "Скопировать ссылку в буфер обмена" delete_confirm: Удалить эту постоянную ссылку? + no_permalinks: "У вас ещё нет постоянных ссылок. Как только вы создадите постоянную ссылку (см. выше), здесь появится список ваших постоянных ссылок." form: label: "Новая постоянная ссылка:" add: "Добавить" @@ -5822,8 +6104,10 @@ ru: finish: "Выйти из настройки" back: "Назад" next: "Далее" + configure_more: "Другие настройки…" step-text: "Шаг" step: "%{current} из %{total}" + upload: "Загрузить файл" uploading: "Загрузка…" upload_error: "Не удалось загрузить файл. Попробуйте ещё раз." staff_count: diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 3f5b13f5cf..8dfaa53ab7 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -888,6 +888,7 @@ sk: primary: "Primárny e-mail" secondary: "Sekundárne e-maily" primary_label: "primárny" + resent_label: "email odoslaný" update_email: "Zmeniť email" no_secondary: "Žiadne sekundárne e-maily" ok: "Pošleme vám email pre potvrdenie" @@ -1441,6 +1442,7 @@ sk: close_topics: "Uzavrieť tému" archive_topics: "Archivuj témy" move_messages_to_inbox: "Presuň do prijatej pošty" + notification_level: "Upozornenia..." choose_new_category: "Vyberte pre tému novú kategóriu:" selected: one: "Označíli ste %{count} tému." @@ -1604,6 +1606,7 @@ sk: unarchive: "Zruš archiváciu témy" archive: "Archívuj tému" reset_read: "Zrušiť načítané údaje" + make_public: "Spraviť verejnou témou..." feature: pin: "Pripni tému" unpin: "Odopni tému" @@ -1806,6 +1809,8 @@ sk: revert_to_regular: "Odobrať farbu personálu" rebake: "Pregenerovať HTML" unhide: "Odokryť" + change_owner: "Zmeniť vlastníctvo..." + grant_badge: "Udeliť odznak..." lock_post: "Zamknúť príspevok" unlock_post: "Odblokovať príspevok" delete_topic: "odstrániť tému" @@ -1927,6 +1932,7 @@ sk: flagging: title: "Ďakujeme, že pomáhate udržiavať slušnosť v našej komunite!" action: "Označ príspevok" + take_action: "Vykonať akciu..." take_action_options: default: title: "Vykonať akciu" @@ -2240,6 +2246,7 @@ sk: save: "Uložiť" delete: "Vymazať" confirm_delete: "Ste si istý, že chcete zmazať túto skupinu štítkov?" + parent_tag_placeholder: "Nepovinné" topics: none: unread: "Nemáte žiadnu neprečítanú tému" @@ -2336,6 +2343,7 @@ sk: latest_version: "Najnovšie" new_features: dismiss: "Zahodiť" + learn_more: "Zistiť viac" last_checked: "Naposledy overené" refresh_problems: "Obnoviť" no_problems: "Nenašli sa žiadne problémy." @@ -2424,6 +2432,7 @@ sk: user: "Používateľ" title: "API" created: Vytvorené + never_used: (nikdy) generate: "Generovať" revoke: "Zrušiť" all_users: "Všetci používatelia" diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index 2b016d0bf9..4a8d0ca02d 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -1416,6 +1416,7 @@ sl: invite: new_title: "Ustvari povabilo" instructions: "Delite to povezavo in takoj omogočite dostop do te strani:" + expires_in_time: "Poteče čez %{time}" show_advanced: "Pokaži dodatne možnosti" add_to_groups: "Dodaj v skupine" expires_at: "Poteče po" @@ -2131,9 +2132,11 @@ sl: dismiss_new: "Opusti nove" toggle: "preklopi množično izbiro tem" actions: "Množična dejanja" + change_category: "Določi kategorijo..." close_topics: "Zapri teme" archive_topics: "Arhiviraj teme" move_messages_to_inbox: "Prestavi v Prejeto" + notification_level: "Obvestila..." change_notification_level: "Spremeni raven obveščanja" choose_new_category: "Izberi novo kategorijo za temo:" selected: @@ -2364,12 +2367,14 @@ sl: open: "Odpri temo" close: "Zapri temo" multi_select: "Izberite prispevke..." + slow_mode: "Nastavi počasni način..." timed_update: "Nastavi opomnik teme..." pin: "Pripni temo" unpin: "Odpni temo" unarchive: "Odarhiviraj temo" archive: "Arhiviraj temo" reset_read: "Ponastavi podatke o branosti" + make_public: "Spremeni v javno temo..." make_private: "Spremeni v ZS" reset_bump_date: "Ponastavi izpostavljanje" feature: @@ -2664,6 +2669,8 @@ sl: rebake: "Obnovi HTML" publish_page: "Objavljanje strani" unhide: "Ponovni prikaži" + change_owner: "Spremeni lastnika..." + grant_badge: "Podeli značko..." lock_post: "Zakleni prispevek" lock_post_description: "onemogoči avtorju da ureja prispevek" unlock_post: "Odkleni prispevek" @@ -2676,6 +2683,8 @@ sl: few: "Ta tema ima trenutno več kot %{count} oglede in je morda priljubljena tarča iskanja. Ali ste prepričani, da želite to temo v celoti izbrisati, namesto da bi jo z urejanjem poskusili izboljšati?" other: "Ta tema ima trenutno več kot %{count} ogledov in je morda priljubljena tarča iskanja. Ali ste prepričani, da želite to temo v celoti izbrisati, namesto da bi jo z urejanjem poskusili izboljšati?" delete_topic: "izbriši temo" + add_post_notice: "Dodaj obvestilo osebja..." + change_post_notice: "Spremeni obvestilo osebja..." delete_post_notice: "Odstrani obvestilo osebja" remove_timer: "odstrani opomnik" edit_timer: "uredi časovnik" @@ -2876,6 +2885,7 @@ sl: flagging: title: "Hvala, da pomagate ohraniti prijazno skupnost!" action: "Prijavi prispevek" + take_action: "Ukrepaj..." take_action_options: default: title: "Ukrepaj" @@ -3316,6 +3326,7 @@ sl: everyone_can_use: "Oznake lahko uporablja kdorkoli" usable_only_by_groups: "Oznake so vidne vsem, vendar jih lahko uporabljajo le naslednje skupine" visible_only_to_groups: "Oznake so vidne le naslednjim skupinam" + parent_tag_placeholder: "Neobvezno" topics: none: unread: "Nimate neprebranih tem." @@ -3395,6 +3406,8 @@ sl: content: "Admin" badges: content: "Značke" + everything: + content: "Vse" faq: content: "Pravila skupnosti" groups: @@ -3405,6 +3418,7 @@ sl: content: "Moji prispevki" review: content: "Pregled" + until: "Do:" admin_js: type_to_filter: "vnesite za filter..." admin: @@ -3563,6 +3577,7 @@ sl: user: "Uporabnik" title: "API" created: Ustvarjeno + never_used: (nikoli) generate: "Ustvari" revoke: "Prekliči" all_users: "Vsi uporabniki" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 6d6ed9841a..259ec3d4ba 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -274,6 +274,7 @@ sq: save: "Ruaj" cancel: "Anulo" filters: + all_categories: "(të gjitha kategoritë)" type: title: "Lloji" refresh: "Rifresko" @@ -675,6 +676,7 @@ sq: email: title: "Email" primary_label: "parësor" + resent_label: "emaili u dërgua" update_email: "Ndrysho email" instructions: "Mos e shfaq në publik." ok: "Do ju nisim emailin e konfirmimit" @@ -1169,6 +1171,7 @@ sq: close_topics: "Mbyll temat" archive_topics: "Arkivo temat" move_messages_to_inbox: "Transfero në inbox" + notification_level: "Njoftimet..." choose_new_category: "Zgjidhni kategorinë e re për temat: " selected: one: "Keni zgjedhur %{count} temë." @@ -1313,6 +1316,7 @@ sq: unarchive: "Çarkivoje temën" archive: "Arkivoje temën" reset_read: "Reset Read Data" + make_public: "Bëje temën publike..." feature: pin: "Ngjite temën" unpin: "Çngjite temën" @@ -1488,6 +1492,8 @@ sq: revert_to_regular: "Hiq ngjyrën e stafit" rebake: "Rindërtoni HTML" unhide: "Çfshi" + change_owner: "Ndrysho zotëruesin..." + grant_badge: "Dhuroni Stemë..." delete_topic: "fshi temën" actions: people: @@ -1608,6 +1614,7 @@ sq: flagging: title: "Faleminderit për ndihmën që i jepni këtij komuniteti!" action: "Sinjalizo postimin" + take_action: "Vepro..." take_action_options: default: title: "Vepro" @@ -1993,6 +2000,7 @@ sq: latest_version: "Të fundit" new_features: dismiss: "Hiqe" + learn_more: "Mëso më shumë" last_checked: "Verifikimi i fundit" refresh_problems: "Rifresko" no_problems: "Nuk u gjet asnjë gabim." @@ -2066,6 +2074,7 @@ sq: user: "Përdorues" title: "API" created: Krijuar + never_used: (asnjëherë) generate: "Gjenero" revoke: "Revoko" all_users: "Gjithë Përdoruesit" diff --git a/config/locales/client.sr.yml b/config/locales/client.sr.yml index 1f53b9cdcd..56b3116227 100644 --- a/config/locales/client.sr.yml +++ b/config/locales/client.sr.yml @@ -130,6 +130,10 @@ sr: one: "%{count} дан раније" few: "%{count} дана раније" other: "%{count} дана раније" + x_months: + one: "pre %{count} mesec dana" + few: "pre %{count} mesec dana" + other: "pre %{count} mesec dana" later: x_days: one: "%{count} дан касније" @@ -451,6 +455,8 @@ sr: types: reviewable_user: title: "Korisnik" + reviewable_post: + title: "Post" approval: title: "Potrebno odobrenje" description: "Primili smo tvoj novi post ali on najpre mora biti odobren od strane moderatora. Budi strpljiv." @@ -611,7 +617,7 @@ sr: "12": "Poslato" "13": "Primljeno" "14": "U toku" - "15": "Drafts" + "15": "Нацрти" categories: all: "sve kategorije" all_subcategories: "sve" @@ -682,6 +688,7 @@ sr: trust_level: "Nivo poverenja" notifications: "Obaveštenja" statistics: "Statistike" + dismiss: "Одбаци" dismiss_notifications_tooltip: "Označi sve nepročitana obavestenja kao pročitana" color_schemes: regular: "Stalni član" @@ -786,6 +793,7 @@ sr: email: title: "E-mail" primary_label: "primarna" + resent_label: "email je poslat" update_email: "Promeni e-mail" ok: "Poslaćemo vam e-mail za potvrdu" invalid: "Molimo unesite validnu e-mail adresu" @@ -884,6 +892,9 @@ sr: create: "Pozovi" invite_link: success: "Link pozivnice je uspešno kreiran!" + invite: + show_advanced: "Прикажи напредне опције" + hide_advanced: "Сакриј напредне опције" password: title: "Šifra" too_short: "Vaša šifra je prekratka." @@ -954,8 +965,12 @@ sr: title: "Profilna Slika" title: title: "Naslov" + none: "(ništa)" + flair: + none: "(ništa)" primary_group: title: "Primarna Grupa" + none: "(ništa)" filters: all: "Sve" stream: @@ -1195,11 +1210,13 @@ sr: select_all: "Izaberi sve" defer: "Odloži" delete: "Obriši Teme" + dismiss: "Одбаци" dismiss_new: "Odbaci Novo" toggle: "uključi/isključi grupni odabir tema" actions: "Grupne aktivnosti" close_topics: "Zatvori Teme" archive_topics: "Arhiviraj teme" + notification_level: "Obaveštenja..." choose_new_category: "Izaberite novu kategoriju za vašu temu." selected: one: "Odabrali ste %{count} temu." @@ -1283,6 +1300,7 @@ sr: replies_short: "%{current} / %{total}" progress: title: napredak teme + jump_prompt_to_date: "до датума" jump_prompt_or: "ili" notifications: reasons: @@ -1324,6 +1342,7 @@ sr: unarchive: "Vrati temu iz arhiva" archive: "Arhiviraj temu" reset_read: "Poništi podatke o pročitanom" + make_public: "Napravi javnu temu..." feature: pin: "Zakači temu" unpin: "Otkači temu" @@ -1447,6 +1466,7 @@ sr: revert_to_regular: "Ukloni Boje Osoblja" rebake: "Popravi HTML" unhide: "Poništi sakrivanje" + grant_badge: "Dodeli Značku..." delete_topic: "obriši temu" actions: by_you: @@ -1546,6 +1566,7 @@ sr: flagging: title: "Hvala što pomažete u održavanju naše zajednice pristojnom." action: "Označi Poruku Zastavom" + take_action: "Preduzmi Akciju..." take_action_options: default: title: "Preduzmi Akciju" @@ -1743,6 +1764,7 @@ sr: few: "+još %{count} član" other: "+još %{count} član" select_badge_for_title: Odaberi značku koju češ koristit kao titulu + none: "(ništa)" badge_grouping: getting_started: name: Početak @@ -1788,6 +1810,7 @@ sr: footer_nav: back: "Nazad" share: "Podeli" + dismiss: "Одбаци" pause_notifications: options: custom: "Posebna" @@ -1859,6 +1882,9 @@ sr: version_check_pending: "Čini se da ste nedavno nadogradili. Fantastično!" installed_version: "Instalirano" latest_version: "Poslednje" + new_features: + dismiss: "Одбаци" + learn_more: "Saznaj više" last_checked: "Zadnje provereno" refresh_problems: "Osveži" no_problems: "Nisu pronađeni problemi." @@ -1917,6 +1943,7 @@ sr: user: "Korisnik" title: "API" created: Napravljeno + never_used: (nikad) generate: "Generiši" revoke: "Povuci" all_users: "Svi korisnici" @@ -2493,6 +2520,7 @@ sr: topic_id: "ID Teme" topic_title: "Tema" post_id: "ID Poruke" + post_title: "Post" category_title: "Kategorija" form: label: "Novo:" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index 65b67bbc17..4db0b44825 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -1052,7 +1052,7 @@ sv: perm_denied_expl: "Du nekade tillåtelse för aviseringar. Tillåt aviseringar via din webbläsares inställningar." disable: "Inaktivera aviseringar" enable: "Aktivera aviseringar" - each_browser_note: 'Obs: Du måste ändra den här inställningen i varje webbläsare du använder. Alla aviseringar kommer att inaktiveras om du pausar aviseringar från användarmenyn, oavsett denna inställning.' + each_browser_note: "Obs: Du måste ändra den här inställningen i varje webbläsare du använder. Alla aviseringar kommer att inaktiveras om du pausar aviseringar från användarmenyn, oavsett denna inställning." consent_prompt: "Vill du ha realtidsaviseringar när personer svarar på dina inlägg?" dismiss: "Avfärda" dismiss_notifications: "Avfärda alla" @@ -3638,7 +3638,6 @@ sv: this_week: "Vecka" today: "Idag" browser_update: 'Tyvärr stöds inte din webbläsare. Vänligen byt till en webbläsare som stöds för att se innehåll, logga in och svara.' - safari_13_warning: Webbplatsen har fullt stöd för redigering av innehåll med webbläsare på iOS 12.5 och 13 till januari 2023. Därefter krävs iOS 14 eller senare. Äldre versioner av iOS kan då endast läsa förenklat innehåll. (mer information) permission_types: full: "Skapa / svara / se" create_post: "Svara / se" @@ -4285,7 +4284,7 @@ sv: topics: read: Läs ett ämne eller ett specifikt inlägg i det. RSS stöds också. write: Skapa ett nytt ämne eller inlägg till en befintligt sådan. - update: Uppdatera ett ämne. Ändra titel, kategori, taggar, etc. + update: Uppdatera ett ämne. Ändra titel, kategori, taggar, status, arketyp, featured_link etc. read_lists: Läs ämneslistor som topp, ny, senaste osv. RSS stöds också. posts: edit: Redigera ett inlägg eller ett specifikt inlägg. diff --git a/config/locales/client.sw.yml b/config/locales/client.sw.yml index a984f674b4..20bab69224 100644 --- a/config/locales/client.sw.yml +++ b/config/locales/client.sw.yml @@ -287,6 +287,7 @@ sw: reject_reason: "Sababu" topics: topic: "Mada" + deleted: "[Mada Imefutwa]" details: "maelezo" unique_users: one: "Mtumiaji mmoja" @@ -790,6 +791,7 @@ sw: primary: "Barua pepe ya awali" secondary: "Barua pepe" primary_label: "msingi" + resent_label: "barua pepe imetumwa" update_email: "Badilisha Barua Pepe" ok: "Tutakutumia barua pepe kuthibitisha" invalid: "Andika barua pepe iliyo sahihi" @@ -1450,9 +1452,11 @@ sw: dismiss_new: "Ondosha Mpya" toggle: "Badili kwa wingi chaguo la topiki" actions: "Vitendo za Jumla" + change_category: "Seti Kategoria..." close_topics: "Funga Mada" archive_topics: "Hifadhi Mada kwenye nyaraka" move_messages_to_inbox: "Hamishia kwenye kisanduku-pokezi" + notification_level: "Taarifa..." choose_new_category: "Chagua kategoria mpya kwa ajili ya mada:" selected: one: "Umechagua mada %{count}." @@ -1622,6 +1626,7 @@ sw: unarchive: "Ondoa Mada kwenye Nyaraka" archive: "Weka Mada kwenye Nyaraka" reset_read: "Anzisha Upya Usomaji wa Taarifa" + make_public: "Fanya Mada iwe ya Umma..." make_private: "Tengeneza Ujumbe Binafsi" feature: pin: "Bandika Mada" @@ -1805,6 +1810,8 @@ sw: revert_to_regular: "Ondoa Rangi ya Wasaidizi" rebake: "Tengeneza upya HTML" unhide: "Onesha" + change_owner: "Badilisha Umiliki..." + grant_badge: "Toa Beji..." lock_post: "Funga Chapisho" lock_post_description: "mzuie mchapishaji kuhariri chapisho hili" unlock_post: "Fungua Chapisho" @@ -1952,6 +1959,7 @@ sw: flagging: title: "Asante kwa kuendeleza ustaarabu kwenye jumuiya yetu!" action: "Ripoti Chapisho" + take_action: "Fanya Kitendo..." take_action_options: default: title: "Fanya Kitendo" @@ -2221,6 +2229,7 @@ sw: delete: "Futa" confirm_delete: "Una uhakika unataka kufuta kikundi cha lebo hii?" everyone_can_use: "Lebo zinaweza kutumiwa na kila mtu" + parent_tag_placeholder: "Sio muhimu" topics: none: unread: "Hauna mada ambazo hazijasomwa." @@ -2315,6 +2324,7 @@ sw: latest_version: "Hivi Karibuni" new_features: dismiss: "Ondosha" + learn_more: "Jifunze zaidi" last_checked: "Mara ya Mwisho imeangaliwa" refresh_problems: "Rudisha Tena" no_problems: "Hakuna matatizo yaliyopatikana." @@ -2414,6 +2424,7 @@ sw: user: "Mtumiaji" title: "API" created: Imetengenezwa + never_used: (kamwe) generate: "Tengeneza" revoke: "Futa" all_users: "Watumiaji Wote" diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index 441dc5a36b..fd1511ed9e 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -498,6 +498,7 @@ te: email: title: "ఈమెయిల్" primary_label: "ప్రాథమిక" + resent_label: "ఈమెయిల్ పంపిన" update_email: "ఈమెయిల్ మార్చు" ok: "ద్రువపరుచుటకు మీకు ఈమెయిల్ పంపాము" invalid: "దయచేసి చెల్లుబాటులోని ఈమెయిల్ చిరునామా రాయండి" diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml index 336033fa66..01747bceeb 100644 --- a/config/locales/client.th.yml +++ b/config/locales/client.th.yml @@ -1627,6 +1627,7 @@ th: close_topics: "ปิดกระทู้" archive_topics: "คลังกระทู้" move_messages_to_inbox: "ย้ายไปกล่องขาเข้า" + notification_level: "การแจ้งเตือน..." choose_new_category: "เลือกหมวดหมู่ใหม่ให้กระทู้" selected: other: "คุณได้เลือก %{count} กระทู้" @@ -1793,6 +1794,7 @@ th: unarchive: "เลิกเก็บกระทู้เข้าคลัง" archive: "เก็บกระทู้เข้าคลัง" reset_read: "ล้างข้อมูลการอ่าน" + make_public: "ทำให้กระทู้เป็นสาธารณะ..." make_private: "สร้างข้อความส่วนตัว" feature: pin: "ปักหมุดกระทู้" @@ -2413,6 +2415,7 @@ th: save: "บันทึก" delete: "ลบ" everyone_can_use: "ทุกคนสามารถใช้แท็กได้" + parent_tag_placeholder: "ทางเลือก" topics: none: unread: "คุณไม่มีกระทู้ที่ยังไม่ได้อ่าน" @@ -2468,6 +2471,8 @@ th: content: "แอดมิน" badges: content: "เหรียญ" + everything: + content: "ทุกสิ่ง" faq: content: "คำถามที่พบบ่อย" groups: @@ -2480,6 +2485,7 @@ th: other: "%{count}แบบร่าง" review: content: "รีวิว" + until: "จนถึง:" admin_js: type_to_filter: "พิมพ์เพื่อกรอง..." admin: @@ -2500,6 +2506,7 @@ th: latest_version: "ล่าสุด" new_features: dismiss: "ซ่อน" + learn_more: "เรียนรู้เพิ่มเติม" last_checked: "ตรวจล่าสุด" refresh_problems: "รีเฟรช" no_problems: "ไม่พบปัญหา" @@ -2771,6 +2778,9 @@ th: filters: user_placeholder: "ผู้ใช้" address_placeholder: "name@example.com" + moderation_history: + actions: + delete_topic: "กระทู้ถูกลบ" logs: created_at: "สร้าง" ip_address: "ไอพี" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 7ea2053253..4933d1932d 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -1052,6 +1052,7 @@ tr_TR: perm_denied_expl: "Bildirim izinlerini reddettiniz. Bildirimlere tarayıcı ayarlarınızdan izin verebilirsiniz." disable: "Bildirimleri Devre Dışı Bırak" enable: "Bildirimleri Etkinleştir" + each_browser_note: "Not: Kullandığınız her tarayıcıda bu ayarı değiştirmeniz gerekir. Bu ayardan bağımsız olarak, kullanıcı menüsünden bildirimleri duraklattığınızda tüm bildirimler devre dışı bırakılır." consent_prompt: "Gönderilerinize yanıt verildiğinde canlı bildirim almak ister misiniz?" dismiss: "Kapat" dismiss_notifications: "Tümünü Kapat" @@ -3637,7 +3638,6 @@ tr_TR: this_week: "Hafta" today: "Bugün" browser_update: 'Ne yazık ki tarayıcınız desteklenmiyor. Lütfen zengin içeriği görüntülemek, giriş yapmak ve yanıtlamak için desteklenen bir tarayıcıya geçin.' - safari_13_warning: Bu site yakında iOS ve Safari sürüm 13 ve altını desteklemeyecek. Basitleştirilmiş ve salt okunur bir sürüm kullanılabilir olmaya devam edecek. (daha fazla bilgi) permission_types: full: "Oluştur / Yanıtla / Bak" create_post: "Yanıtla / Bak" @@ -3978,6 +3978,7 @@ tr_TR: new_count: one: "%{count} yeni" other: "%{count} yeni" + toggle_section: "Bölümü değiştir" more: "Daha fazla" all_categories: "Tüm kategoriler" all_tags: "Tüm etiketler" @@ -3986,6 +3987,7 @@ tr_TR: header_link_text: "Hakkında" messages: header_link_text: "Mesajlar" + header_action_title: "Kişisel mesaj oluşturun" links: inbox: "Gelen Kutusu" sent: "Gönderilen" @@ -4002,6 +4004,7 @@ tr_TR: none: "Etiket eklemediniz." click_to_get_started: "Başlamak için buraya tıklayın." header_link_text: "Etiketler" + header_action_title: "Kenar çubuğu etiketlerinizi düzenleyin" configure_defaults: "Varsayılanları yapılandır" categories: links: @@ -4011,25 +4014,33 @@ tr_TR: none: "Kategori eklemediniz." click_to_get_started: "Başlamak için buraya tıklayın." header_link_text: "Kategoriler" + header_action_title: "Kenar çubuğu kategorilerinizi düzenleyin" configure_defaults: "Varsayılanları yapılandır" community: header_link_text: "Topluluk" + header_action_title: "Yeni konu oluşturun" links: about: content: "Hakkında" + title: "Bu site hakkında daha fazla bilgi" admin: content: "Yönetici" + title: "Site ayarları ve raporları" badges: content: "Rozetler" + title: "Kazanılabilecek tüm rozetler" everything: content: "Her şey" title: "Tüm konular" faq: content: "SSS" + title: "Bu siteyi kullanmak için yönergeler" groups: content: "Gruplar" + title: "Mevcut kullanıcı gruplarının listesi" users: content: "Kullanıcılar" + title: "Tüm kullanıcıların listesi" my_posts: content: "Gönderilerim" title: "Son konu etkinliğim" @@ -4039,6 +4050,7 @@ tr_TR: other: "%{count} taslak" review: content: "İncele" + title: "Bayrak eklenen gönderiler ve diğer sıraya alınmış ögeler" pending_count: "%{count} beklemede" welcome_topic_banner: title: "Karşılama Konunuzu oluşturun" @@ -4191,6 +4203,9 @@ tr_TR: other: "%{count} kullanıcı yeni e-posta alan adlarına sahip ve gruba eklenecek." automatic_membership_associated_groups: "Burada listelenen bir hizmette bir grubun üyesi olan kullanıcılar, hizmete giriş yaptıklarında otomatik olarak bu gruba eklenir." primary_group: "Otomatik olarak birincil grup olarak ayarla" + alert: + primary_group: "Bu birincil bir grup olduğundan, herkes tarafından görüntülenebilen CSS sınıflarında \"%{group_name}\" adı kullanılacak." + flair_group: "Bu grubun üyelerine karşı becerisi olduğu için \"%{group_name}\" adı herkes tarafından görünür olacak." name_placeholder: "Grup adı, boşluk yok, kullanıcı adı kuralıyla aynı şekilde" primary: "Birincil grup" no_primary: "(birincil grup yok)" @@ -4269,7 +4284,6 @@ tr_TR: topics: read: Bir konuyu veya içindeki belirli bir gönderiyi okuyun. RSS de desteklenir. write: Yeni bir konu oluşturun veya mevcut bir konuya gönderin. - update: Bir konuyu güncelleyin. Başlığı, kategoriyi, etiketleri vb. değiştirin. read_lists: En iyi, yeni, en son vb. gibi konu listelerini okuyun. RSS de desteklenir. posts: edit: Herhangi bir gönderiyi veya belirli bir gönderiyi düzenleyin. diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 3b259f110d..80ec5c8a98 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -3830,7 +3830,6 @@ uk: this_week: "Тиждень" today: "Today" browser_update: 'На жаль, ваш браузер не підтримується. Будь ласка, перейдіть на підтримуваний браузер , щоб повноцінно переглянути вміст, увійти та відповісти.' - safari_13_warning: Цей сайт незабаром перестане підтримувати версії iOS і Safari 13 і нижче. Спрощена версія "тільки для читання" залишиться доступною. (більше інформації) permission_types: full: "Створювати / Відповідати / Бачити" create_post: "Відповісти / Див" @@ -4488,7 +4487,6 @@ uk: topics: read: Читати тему або певні дописи в ній. Також підтримується RSS. write: Створіть нову тему чи допис в наявній. - update: Оновіть тему. Змініть назву, розділ, теґи тощо. read_lists: Читайте списки тем, як топ, нові, останні тощо. RSS також підтримується. posts: edit: Редагуйте будь-який допис або якийсь конкретний. diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index e57e066fe6..32a4e0d072 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -2190,6 +2190,7 @@ ur: tips: category_tag: "زمرہ یا ٹیگ کے لحاظ سے فلٹرز" author: "پوسٹ مصنف کے ذریعہ فلٹرز" + in: "میٹا ڈیٹا کےذریعہ سے فلٹرز (مثلاً ان: عنوان، ان:ذاتی، ان:پن کیاہوا)" status: "موضوع کی حیثیت کے لحاظ سے فلٹرز" full_search: "مکمل صفحہ کی تلاش کا آغاز" full_search_key: "%{modifier} + درج کریں" @@ -3645,6 +3646,8 @@ ur: content: "ایڈمن" badges: content: "بَیج" + everything: + content: "تمام" faq: content: "عمومی سوالات" groups: @@ -3655,6 +3658,7 @@ ur: content: "میری پوسٹ" review: content: "جائزہ لیں" + until: "جب تک:" admin_js: type_to_filter: "فِلٹر کرنے کے لئے ٹائپ کریں..." admin: @@ -3875,7 +3879,6 @@ ur: topics: read: اس میں کوئی موضوع یا کوئی مخصوص پوسٹ پڑھیں۔ آر ایس ایس کی بھی حمایت حاصل ہے۔ write: ایک نیا موضوع بنائیں یا موجودہ موضوع پر پوسٹ کریں۔ - update: ایک موضوع کو اپ ڈیٹ کریں۔ عنوان، زمرہ، ٹیگز وغیرہ تبدیل کریں۔ read_lists: ٹاپک، نیا، تازہ ترین، وغیرہ جیسے عنوانات کی فہرستیں پڑھیں۔ RSS بھی تعاون یافتہ ہے۔ posts: edit: کسی بھی پوسٹ یا مخصوص میں ترمیم کریں۔ diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 33d4c35905..e8ff17917a 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -3573,6 +3573,7 @@ vi: other: "%{count} mới" more: "Thêm" all_categories: "Tất cả danh mục" + all_tags: "Tất cả các thẻ" sections: about: header_link_text: "Giới thiệu" @@ -3837,7 +3838,6 @@ vi: topics: read: Đọc một chủ đề hoặc một bài viết cụ thể trong đó. RSS cũng được hỗ trợ. write: Tạo một chủ đề mới hoặc đăng một chủ đề hiện có. - update: Cập nhật chủ đề. Thay đổi tiêu đề, danh mục, thẻ, v.v. read_lists: Đọc danh sách chủ đề như hàng đầu, mới, mới nhất, v.v. RSS cũng được hỗ trợ. posts: edit: Chỉnh sửa bất kỳ bài đăng nào hoặc một bài đăng cụ thể. diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index cfe5b6e540..7545ccba46 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -148,6 +148,7 @@ zh_CN: banner: enabled: "%{when}将此设置为横幅。在用户忽略前,它将显示在每个页面的顶部。" disabled: "%{when}移除了此横幅。它将不再显示在每个页面的顶部。" + forwarded: "转发了上述电子邮件" topic_admin_menu: "话题操作" skip_to_main_content: "跳转到主要内容" emails_are_disabled: "所有外发电子邮件已被管理员全局禁用。任何类型的电子邮件通知都不会发出。" @@ -950,6 +951,7 @@ zh_CN: notification_schedule: title: "通知时间表" label: "启用自定义通知时间表" + tip: "在这些时间以外,将暂停您的通知。" midnight: "午夜" none: "无" monday: "星期一" @@ -993,12 +995,16 @@ zh_CN: perm_denied_expl: "您拒绝了通知的权限。通过您的浏览器设置允许通知。" disable: "禁用通知" enable: "启用通知" + each_browser_note: "注意:您必须在使用的每个浏览器上更改此设置。如果您从用户菜单暂停通知,则无论此设置如何,所有通知都将被禁用。" consent_prompt: "当其他人回复您的帖子时是否接收实时通知?" dismiss: "忽略" dismiss_notifications: "全部忽略" dismiss_notifications_tooltip: "将所有未读通知标记为已读" dismiss_bookmarks_tooltip: "将所有未读书签提醒标记为已读" dismiss_messages_tooltip: "将所有未读个人信息通知标记为已读" + no_likes_title: "您尚未收到任何赞" + no_likes_body: > + 每当有人为您的帖子点赞时,您都会在此处收到通知,这样您就可以看到其他人认为有价值的内容。当您也点赞他们的帖子时,其他人也会在此处看到通知!

    点赞通知绝不会通过电子邮件发送给您,但您可以在通知偏好设置中调整您在站点上接收点赞通知的方式。 no_messages_title: "您没有任何消息" no_messages_body: > 需要直接与某人对话而不是公开讨论?选择他的头像并点击 %{icon} 消息按钮,向他们发送消息。

    如果您需要帮助,可以向管理人员发送消息。 @@ -1146,6 +1152,54 @@ zh_CN: warnings: "官方警告" read_more_in_group: "想阅读更多?浏览 %{groupLink} 中的其他消息。" read_more: "想阅读更多?在个人消息中浏览其他消息。" + read_more_group_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + other {有 # 条未读消息} + } + { NEW, plural, + =0 {} + other {和 # 条新消息,或浏览{groupLink}中的其他消息} + } + } + false { + { UNREAD, plural, + =0 {} + other {还有 # 条未读消息,或浏览{groupLink}中的其他消息} + } + { NEW, plural, + =0 {} + other {还有 # 条新消息,或浏览{groupLink}中的其他消息} + } + } + other {} + } + read_more_personal_pm_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + other {有 # 条未读消息} + } + { NEW, plural, + =0 {} + other {和 # 条新消息,或浏览其他个人消息} + } + } + false { + { UNREAD, plural, + =0 {} + other {还有 # 条未读消息,或浏览其他个人消息} + } + { NEW, plural, + =0 {} + other {还有 # 条新消息,或浏览其他个人消息} + } + } + other {} + } preferences_nav: account: "帐户" security: "安全性" @@ -1208,7 +1262,14 @@ zh_CN: use: "使用身份验证器应用" enforced_notice: "在访问此站点之前,您需要启用双重身份验证。" disable: "禁用" + disable_confirm: "确定要禁用双重身份验证吗?" delete: "删除" + delete_confirm_header: "这些基于令牌的身份验证器和实体安全密钥将被删除:" + delete_confirm_instruction: "要确认,请在下面的框中输入 %{confirm}。" + delete_single_confirm_title: "删除身份验证器" + delete_single_confirm_message: "您正在删除 %{name}。此操作无法撤消。如果您改变主意,则必须重新注册此身份验证器。" + delete_backup_codes_confirm_title: "删除备份代码" + delete_backup_codes_confirm_message: "您正在删除备份代码。此操作无法撤消。如果您改变主意,则必须重新生成备份代码。" save: "保存" edit: "编辑" edit_title: "编辑身份验证器" @@ -1374,6 +1435,8 @@ zh_CN: title: "背景页面标题显示数量:" notifications: "新通知" contextual: "新页面内容" + bookmark_after_notification: + title: "发送书签提醒通知后:" like_notification_frequency: title: "被赞时通知" always: "始终" @@ -1590,6 +1653,7 @@ zh_CN: save: "保存" set_custom_status: "设置自定义状态" what_are_you_doing: "您在做什么?" + pause_notifications: "暂停通知" remove_status: "移除状态" user_tips: primary: "知道了!" @@ -1643,6 +1707,28 @@ zh_CN: logout_disabled: "当站点处于只读模式时,退出被禁用。" staff_writes_only_mode: enabled: "此站点处于管理人员专用模式。请继续浏览,但回复、赞和其他操作仅限于管理人员。" + too_few_topics_and_posts_notice_MF: | + 让我们开始讨论吧!现在有 { currentTopics, plural, + other {# 个话题} + }和 { currentPosts, plural, + other {# 篇帖子} + }。访客需要阅读和回复更多 – 我们建议至少有 { requiredTopics, plural, + other {# 个话题} + }和 { requiredPosts, plural, + other {# 篇帖子} + }。只有管理人员可以看到此消息。 + too_few_topics_notice_MF: | + 让我们开始讨论吧!/a>现在有 { currentTopics, plural, + other {# 个话题} + }。访客需要阅读和回复更多 - 我们建议至少有 { requiredTopics, plural, + other {# 个话题} + }。只有管理人员可以看到此消息。 + too_few_posts_notice_MF: | + 让我们开始讨论吧!现在有{ currentPosts, plural, + other {# 篇帖子} + }。访客需要阅读和回复更多 - 我们建议至少有 { requiredPosts, plural, + other {# 篇帖子} + }。只有管理人员可以看到此消息。 logs_error_rate_notice: reached_hour_MF: | {relativeAge}{rate, plural, one {# 个错误/小时} other {# 个错误/小时}}达到了站点设置中 {limit, plural, one {# 个错误/小时} other {# 个错误/小时}}的限制。 @@ -1748,6 +1834,9 @@ zh_CN: username: "用户" password: "密码" show_password: "显示" + hide_password: "隐藏" + show_password_title: "显示密码" + hide_password_title: "隐藏密码" second_factor_title: "双重身份验证" second_factor_description: "请输入应用中的身份验证代码:" second_factor_backup: "使用备份代码登录" @@ -1769,6 +1858,7 @@ zh_CN: blank_username_or_password: "请输入您的电子邮件或用户名和密码。" reset_password: "重置密码" logging_in: "正在登录…" + previous_sign_up: "已经有帐户?" or: "或者" authenticating: "正在进行身份验证…" awaiting_activation: "您的帐户正在等待激活,请使用忘记密码链接发出另一封激活电子邮件。" @@ -1935,9 +2025,15 @@ zh_CN: private: "您提及了 @%{username},但该用户无法访问此个人消息,因此不会收到通知。您需要邀请该用户加入此个人消息。" muted_topic: "您提及了 @%{username},但该用户已将此话题设为免打扰,因此不会收到通知。" not_allowed: "您提及了 @%{username},但该用户未被邀请加入此话题,因此不会收到通知。" + cannot_see_group_mention: + not_mentionable: "您不能提及群组 @%{group}。" + some_not_allowed: + other: "您提及了 @%{group},但只有 %{count} 位成员会收到通知,因为其他成员无法看到这条个人消息。您需要邀请他们加入这条个人消息。" + not_allowed: "您提及了 @%{group},但任何成员都不会收到通知,因为他们无法看到这条个人消息。您需要邀请他们加入这条个人消息。" here_mention: other: "提及 @%{here},即表示您将通知 %{count} 个用户 – 确定吗?" duplicate_link: "好像 @%{username} 在话题 %{ago}的回复中已经发布 %{domain} 的链接 - 确定要再次发布吗?" + duplicate_link_same_user: "您似乎已经在 %{ago}对此话题的回复中发布了指向%{domain}的链接 - 确定要再次发布吗?" reference_topic_title: "回复:%{title}" error: title_missing: "标题为必填项" @@ -2066,6 +2162,10 @@ zh_CN: other: "%{count} 条未读消息" high_priority: other: "%{count} 个未读高优先级通知" + new_message_notification: + other: "%{count} 条新消息通知" + new_reviewable: + other: "%{count} 个新的可审核条目" title: "提及 (@) 您的用户名、回复您的帖子和话题、消息等的通知" none: "目前无法加载通知。" empty: "找不到通知。" @@ -2107,6 +2207,7 @@ zh_CN: reaction: "%{username} %{description}" reaction_2: "%{username}、%{username2} %{description}" votes_released: "%{description} - 已完成" + new_features: "有新功能可用!" dismiss_confirmation: body: default: @@ -2157,6 +2258,7 @@ zh_CN: membership_request_consolidated: "新成员资格请求" reaction: "新回应" votes_released: "投票额度已释放" + new_features: "已发布新的 Discourse 功能!" upload_selector: uploading: "正在上传" processing: "正在处理上传" @@ -2446,7 +2548,44 @@ zh_CN: show_links: "显示此话题中的链接" collapse_details: "收起话题详细信息" expand_details: "展开话题详细信息" + read_more_in_category: "想阅读更多?请浏览%{categoryLink}中的其他话题或查看最新话题。" + read_more: "想阅读更多?请浏览所有类别查看最新话题。" unread_indicator: "还没有成员读过此话题的最新帖子。" + read_more_MF: | + { HAS_UNREAD_AND_NEW, select, + true { + { UNREAD, plural, + =0 {} + other {有 # 个未读话题} + } + { NEW, plural, + =0 {} + other {和 # 个新话题,} + } + } + false { + { UNREAD, plural, + =0 {} + other {还有 # 个未读话题,} + } + { NEW, plural, + =0 {} + other {还有 # 个新话题,} + } + } + other {} + } + { HAS_CATEGORY, select, + true {或浏览{categoryLink}中的其他话题} + false {或查看最新话题} + other {} + } + bumped_at_title: | + 第一篇帖子:%{createdAtDate} + 发布日期:%{bumpedAtDate} + browse_all_categories_latest: "浏览所有类别查看最新话题。" + browse_all_categories_latest_or_top: "浏览所有类别查看最新话题或查看热门话题:" + browse_all_tags_or_latest: "浏览所有标签查看最新话题。" suggest_create_topic: 准备好开始新对话了吗? jump_reply_up: 跳转到较早的回复 jump_reply_down: 跳转到较晚的回复 @@ -2842,6 +2981,7 @@ zh_CN: too_many_dragged_and_dropped_files: other: "抱歉,一次只能上传 %{count} 个文件。" upload_not_authorized: "抱歉,您尝试上传的文件不被允许(允许的扩展名:%{authorized_extensions})。" + no_uploads_authorized: "抱歉,未授权上传任何文件。" image_upload_not_allowed_for_new_user: "抱歉,新用户无法上传图片。" attachment_upload_not_allowed_for_new_user: "抱歉,新用户无法上传附件。" attachment_download_requires_login: "抱歉,您需要登录才能下载附件。" @@ -2853,6 +2993,7 @@ zh_CN: via_email: "此帖子通过电子邮件送达" via_auto_generated_email: "此帖子通过自动生成的电子邮件送达" whisper: "此帖子是对版主的密语" + whisper_groups: "此帖子为私聊,仅对 %{groupNames} 可见" wiki: about: "此帖子是一个 Wiki" few_likes_left: "谢谢您分享爱!您今天只剩下几个赞了。" @@ -3066,6 +3207,7 @@ zh_CN: pending_permission_change_alert: "您还没有将 %{group} 添加到此类别;点击此按钮添加它们。" images: "图片" email_in: "自定义传入电子邮件地址:" + email_in_tooltip: "您可以使用 | 字符分隔多个电子邮件地址。" email_in_allow_strangers: "接受来自没有帐户的匿名用户的电子邮件" email_in_disabled: "通过电子邮件发布新话题的功能在站点设置中禁用。要启用通过电子邮件发布新话题," email_in_disabled_click: '请启用“email in”设置。' @@ -3331,7 +3473,6 @@ zh_CN: this_week: "周" today: "今天" browser_update: '很抱歉,您的浏览器不受支持。请切换到支持的浏览器查看富内容、登录和回复。' - safari_13_warning: 此站点将很快移除对 iOS 和 Safari 13 及以下版本的支持。简化的只读版本将保持可用。(更多信息) permission_types: full: "创建/回复/查看" create_post: "回复/查看" @@ -3606,6 +3747,8 @@ zh_CN: enabled: "安全模式已启用,要退出安全模式,请关闭此浏览器窗口" image_removed: "(图片被移除)" pause_notifications: + title: "暂停通知…" + label: "暂停通知" remaining: "剩余 %{remaining}" options: half_hour: "30 分钟" @@ -3649,10 +3792,13 @@ zh_CN: second_factor_auth: redirect_after_success: "第二重身份验证成功。正在重定向到之前的页面…" sidebar: + show_sidebar: "显示边栏" + hide_sidebar: "隐藏边栏" unread_count: other: "%{count} 未读" new_count: other: "%{count} 新" + toggle_section: "切换版块" more: "更多" all_categories: "所有类别" all_tags: "所有标签" @@ -3661,6 +3807,7 @@ zh_CN: header_link_text: "关于" messages: header_link_text: "消息" + header_action_title: "创建个人消息" links: inbox: "收件箱" sent: "已发送" @@ -3677,6 +3824,7 @@ zh_CN: none: "您还没有添加任何标签。" click_to_get_started: "点击此处开始。" header_link_text: "标签" + header_action_title: "编辑边栏标签" configure_defaults: "配置默认值" categories: links: @@ -3686,31 +3834,42 @@ zh_CN: none: "您还没有添加任何类别。" click_to_get_started: "点击此处开始。" header_link_text: "类别" + header_action_title: "编辑边栏类别" configure_defaults: "配置默认值" community: header_link_text: "社区" + header_action_title: "创建话题" links: about: content: "关于" + title: "关于此站点的更多详细信息" admin: content: "管理员" + title: "站点设置和报告" badges: content: "徽章" + title: "所有可获得的徽章" everything: content: "一切" title: "所有话题" faq: content: "常见问题解答" + title: "使用此站点的准则" groups: content: "群组" + title: "可用用户群组列表" users: content: "用户" + title: "所有用户列表" my_posts: content: "我的帖子" + title: "我最近的话题活动" + title_drafts: "我的未发布草稿" draft_count: other: "%{count} 个草稿" review: content: "审核" + title: "被举报的帖子和其他排队的条目" pending_count: "%{count} 待处理" welcome_topic_banner: title: "创建您的欢迎话题" @@ -3861,6 +4020,9 @@ zh_CN: other: "%{count} 个用户具有新的电子邮件网域,将被添加到群组中。" automatic_membership_associated_groups: "作为此处列出的服务的群组成员的用户将在使用该服务登录时自动添加到该群组。" primary_group: "自动设置为主要群组" + alert: + primary_group: "由于这是一个主要群组,名称“%{group_name}”将用于任何人都可以查看的 CSS 类中。" + flair_group: "由于此群组具有成员的标识,名称“%{group_name}”对所有人可见。" name_placeholder: "群组名称,没有空格,与用户名规则相同" primary: "主要群组" no_primary: "(无主要群组)" @@ -3870,6 +4032,9 @@ zh_CN: about: "在此处编辑您的群组成员资格和名称" group_members: "群组成员" delete: "删除" + delete_confirm: "确定要删除此群组吗?" + delete_with_messages_confirm: + other: "删除此群组将导致 %{count} 条消息成为孤立消息,群组成员将无法再访问它们。" delete_failed: "无法删除群组。如果此群组是自动生成的,则无法删除。" delete_automatic_group: 这是一个自动生成的群组,无法删除。 delete_owner_confirm: "移除 '%{username}' 的所有者权限?" @@ -3935,7 +4100,6 @@ zh_CN: topics: read: 阅读一个话题或其中的一个帖子。也支持 RSS。 write: 创建一个新话题或发布到现有话题。 - update: 更新话题。更改标题、类别、标签等。 read_lists: 阅读诸如热门、新、最新等话题列表。也支持 RSS。 posts: edit: 编辑任意帖子或特定帖子。 @@ -3954,6 +4118,9 @@ zh_CN: anonymize: 对用户帐户进行匿名化处理。 delete: 删除用户帐户。 list: 获取用户列表。 + user_status: + read: 读取用户状态。 + update: 更新用户状态。 email: receive_emails: 将此范围与邮件收件人结合来处理传入电子邮件。 badges: @@ -3978,6 +4145,7 @@ zh_CN: create: "创建" edit: "编辑" save: "保存" + description_label: "事件触发器" controls: "控件" go_back: "返回列表" payload_url: "有效负载 URL" @@ -4078,6 +4246,8 @@ zh_CN: broken_route: "无法配置“%{name}”的链接。确保禁用广告拦截器并尝试重新加载页面。" navigation_menu: sidebar: "边栏" + header_dropdown: "标题下拉菜单" + legacy: "旧版" backups: title: "备份" menu: @@ -4760,6 +4930,7 @@ zh_CN: user: suspend_failed: "封禁此用户时出错 %{error}" unsuspend_failed: "取消封禁此用户时出错 %{error}" + suspend_duration: "将用户封禁至:" suspend_reason_label: "您为什么封禁该用户?此文本将在用户的个人资料页面上对所有人可见,并且当用户尝试登录时,他们将看到此文本。尽量简洁。" suspend_reason_hidden_label: "您为什么封禁该用户?当用户尝试登录时将看到此文本。尽量简洁。" suspend_reason: "原因" @@ -4783,7 +4954,9 @@ zh_CN: silence_message: "电子邮件消息" silence_message_placeholder: "(留空以发送默认消息)" suspended_until: "(直到 %{until})" + suspend_forever: "永久封禁" cant_suspend: "无法封禁此用户。" + cant_silence: "无法将此用户禁言。" delete_posts_failed: "删除帖子时出现问题。" post_edits: "帖子编辑" view_edits: "查看编辑" @@ -4793,6 +4966,8 @@ zh_CN: penalty_post_edit: "编辑帖子" penalty_post_none: "无操作" penalty_count: "处罚计数" + penalty_history_MF: >- + 在过去 6 个月中,此用户被封禁 { SUSPENDED, plural, other {# 次} },被禁言 { SILENCED, plural, other {# 次} }。 clear_penalty_history: title: "清除处罚历史记录" description: "有处罚的用户无法达到信任级别 3" @@ -4944,10 +5119,13 @@ zh_CN: silenced_count: "被禁言" suspended_count: "被封禁" last_six_months: "过去 6 个月" + other_matches: + other: "还有 %{count} 位用户具有相同的 IP 地址。查看并选择要与 %{username} 一起处罚的可疑用户。" other_matches_list: username: "用户名" trust_level: "信任级别" read_time: "阅读时间" + topics_entered: "输入的话题" posts: "帖子" tl3_requirements: title: "信任级别 3 的要求" @@ -5262,8 +5440,10 @@ zh_CN: finish: "退出设置" back: "返回" next: "下一步" + configure_more: "配置更多…" step-text: "步骤" step: "%{current}/%{total}" + upload: "上传文件" uploading: "正在上传…" upload_error: "抱歉,上传该文件时出错。请重试。" staff_count: diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 690e925bab..dffe4a148f 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -1133,6 +1133,7 @@ zh_TW: title: "使用者卡背景" instructions: "背景會被置中,且默認寬度為850px。" change_featured_topic: + title: "特色主題" instructions: "此主題的連結將顯示您的使用者卡和個人檔案上。" email: title: "電子郵件" @@ -1303,6 +1304,10 @@ zh_TW: valid_for: "邀請連結只對這個郵件地址有效:%{email}" invite_link: success: "邀請連結生成成功!" + invite: + copy_link: "複製連結" + show_advanced: "顯示進階選項" + hide_advanced: "隱藏進階選項" bulk_invite: none: "此頁面上沒有邀請函顯示。" error: "上傳的檔案必須是 csv 格式。" @@ -1481,6 +1486,7 @@ zh_TW: username: "使用者" password: "密碼" show_password: "顯示" + second_factor_title: "兩步驟驗證" second_factor_description: "請輸入應用程式中的驗證碼:" second_factor_backup_description: "請輸入一組您的備用碼" caps_lock_warning: "大寫鎖定中" @@ -1767,6 +1773,7 @@ zh_TW: confirm_body: "成功! 通知已啟用" custom: "新的通知由%{username}在%{site_title}" titles: + edited: "已編輯" liked: "新的讚" watching_first_post: "新話題" liked_consolidated: "新的讚" @@ -1882,9 +1889,11 @@ zh_TW: dismiss_new: "設定新貼文為已讀" toggle: "批次切換選擇話題" actions: "批次操作" + change_category: "設定分類..." close_topics: "關閉話題" archive_topics: "已封存的話題" move_messages_to_inbox: "移動到收件匣" + notification_level: "通知..." choose_new_category: "為話題選擇新類別:" selected: other: "你已選擇了 %{count} 個話題。" @@ -1961,6 +1970,19 @@ zh_TW: enable: "啟用" enabled_until: "啟用直到:" remove: "停用" + hours: "小時:" + minutes: "分鐘:" + durations: + 10_minutes: "10 分鐘" + 15_minutes: "15 分鐘" + 30_minutes: "30 分鐘" + 45_minutes: "45 分鐘" + 1_hour: "1 小時" + 2_hours: "2 小時" + 4_hours: "4 小時" + 8_hours: "8 小時" + 12_hours: "12 小時" + 24_hours: "24 小時" topic_status_update: title: "話題計時器" save: "設定計時器" @@ -2068,6 +2090,7 @@ zh_TW: invisible: "隱藏主題" visible: "顯示主題" reset_read: "重置讀取資料" + make_public: "設置為公共話題..." make_private: "設置為私訊" reset_bump_date: "重設上浮日期" feature: @@ -2295,6 +2318,8 @@ zh_TW: revert_to_regular: "移除工作人員顏色" rebake: "重建 HTML" unhide: "取消隱藏" + change_owner: "更改作者..." + grant_badge: "升級徽章..." lock_post: "封鎖貼文" lock_post_description: "禁止發文者編輯此貼文" unlock_post: "解除封鎖貼文" @@ -2302,6 +2327,7 @@ zh_TW: delete_topic_disallowed_modal: "您沒有權限刪除此話題。若您認為它應被刪除,請向板主檢舉並附上原因。" delete_topic_disallowed: "您沒有刪除此話題的權限。" delete_topic: "刪除話題" + add_post_notice: "加入工作人員通知..." actions: people: like: @@ -2350,10 +2376,13 @@ zh_TW: title: "顯示電子郵件HTML格式" button: "HTML" bookmarks: + create: "建立書籤" edit: "編輯書籤" name: "名稱" options: "選項" actions: + delete_bookmark: + name: "刪除書籤" edit_bookmark: name: "編輯書籤" description: "編輯書籤名稱或更改提醒日期和時間" @@ -2480,9 +2509,12 @@ zh_TW: moderation: "管理" appearance: "外觀" email: "電子信箱" + list_filters: + all: "所有話題" flagging: title: "感謝幫助社群遠離邪惡!" action: "檢舉貼文" + take_action: "執行動作..." take_action_options: default: title: "執行動作" @@ -2597,6 +2629,9 @@ zh_TW: help: "你所關注或追蹤的話題有未讀貼文" lower_title_with_count: other: "%{count} 個未讀" + unseen: + title: "未讀" + lower_title: "未讀" new: lower_title_with_count: other: "%{count} 近期" @@ -2737,6 +2772,7 @@ zh_TW: download_calendar: download: "下載" tagging: + all_tags: "所有標籤" other_tags: "其他標籤" selector_all_tags: "所有標籤" selector_no_tags: "無標籤" @@ -2802,6 +2838,7 @@ zh_TW: delete: "刪除" confirm_delete: "確定要刪除此標籤組嗎?" everyone_can_use: "所有使用者都能使用標籤。" + parent_tag_placeholder: "選擇性" topics: none: unread: "你沒有未讀話題。" @@ -3046,6 +3083,7 @@ zh_TW: user: "使用者" title: "API" created: 已建立 + never_used: (永不) generate: "產生" revoke: "撤銷" all_users: "所有使用者" @@ -3413,6 +3451,7 @@ zh_TW: email_style: heading: "自訂電子郵件風格" css: "CSS" + reset: "重設為預設值" email: title: "電子郵件" settings: "設定" @@ -3908,6 +3947,7 @@ zh_TW: disabled: "在使用者卡片上隱藏" searchable: title: "可搜索" + enabled: "可搜索" field_types: text: "文字區域" confirm: "確認" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 0f7ef9095c..125ba5243e 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -116,7 +116,7 @@ ar: errors: empty_email_error: "يحدث ذلك عندما تكون النسخة الأولية من الرسالة الإلكترونية التي استلمناها خالية." no_message_id_error: "يحدث ذلك عندما لا تحتوي الرسالة الإلكترونية على الرأس 'Message-Id'." - auto_generated_email_error: "يحدث ذلك عند ضبط رأس \"الأولوية\" على: قائمة، أو رسالة غير مرغوب فيها أو مجمَّعة، أو رد تلقائي، أو عندما يحتوي أي رأس آخر على: \"مُرسَلة تلقائيًا\"، أو \"رد تلقائي\"، أو \"منشأة تلقائيًا\"." + auto_generated_email_error: "يحدث ذلك عند تعيين رأس \"الأولوية\" على: قائمة، أو رسالة غير مرغوب فيها أو مجمَّعة، أو رد تلقائي، أو عندما يحتوي أي رأس آخر على: \"مُرسَلة تلقائيًا\"، أو \"رد تلقائي\"، أو \"منشأة تلقائيًا\"." no_body_detected_error: "يحدث ذلك عندما يتعذَّر علينا استخراج نص ولا توجد مرفقات." no_sender_detected_error: "يحدث ذلك عندما لا نتمكن من العثور على عنوان بريد إلكتروني صالح في الرأس \"من\"." from_reply_by_address_error: "يحدث عندما يتطابق الرأس \"من\" مع عنوان البريد الإلكتروني في \"رد بواسطة\"." @@ -228,7 +228,7 @@ ar: s3_backup_requires_s3_settings: "لا يمكنك استخدام S3 كمكان للنسخ الاحتياطي ما لم تُدخِل \"%{setting_name}\"." s3_bucket_reused: "لا يمكنك استخدام الحاوية نفسها لـ \"s3_upload_bucket\" و\"s3_backup_bucket\" معًا. اختر حاويةً أخرى أو استخدم مسارًا مختلفًا لكل حاوية." secure_uploads_requirements: "يجب تفعيل التحميل إلى S3 قبل تفعيل التحميلات الآمنة." - share_quote_facebook_requirements: "يجب عليك ضبط معرِّف تطبيق Facebook لتفعيل مشاركة الاقتباسات على Facebook." + share_quote_facebook_requirements: "يجب عليك تعيين معرِّف تطبيق Facebook لتفعيل مشاركة الاقتباسات على Facebook." second_factor_cannot_enforce_with_socials: "لا يمكنك فرض المصادقة الثنائية عند تفعيل عمليات تسجيل الدخول بحسابات التواصل الاجتماعي. يجب عليك أولًا إيقاف تسجيل الدخول عبر: %{auth_provider_names}" second_factor_cannot_be_enforced_with_disabled_local_login: "لا يمكنك فرض المصادقة الثنائية في حال إيقاف عمليات تسجيل الدخول المحلية." second_factor_cannot_be_enforced_with_discourse_connect_enabled: "لا يمكنك فرض المصادقة الثنائية في حال إيقاف عمليات تسجيل الدخول من خلال DiscourseConnect." @@ -262,10 +262,11 @@ ar:

    إذا كنت تتذكر كلمة مرورك، يمكنك تسجيل الدخول.

    -

    بخلاف ذلك، يُرجى إعادة ضبط كلمة المرور.

    +

    بخلاف ذلك، يُرجى إعادة تعيين كلمة المرور.

    not_found_template_link: |

    لم يعُد من الممكن استرداد هذه الدعوة إلى %{site_name}. يُرجى أن تطلب من الشخص الذي دعاك أن يُرسل إليك دعوة جديدة.

    existing_user_cannot_redeem: "لا يمكن استخدام هذه الدعوة. اطلب من الشخص الذي دعاك أن يُرسل إليك دعوة جديدة." + existing_user_already_redemeed: "لقد استخدمت رمز الدعوة هذا بالفعل." user_exists: "ليست هناك حاجة لدعوة %{email}، فلديه حساب بالفعل!" invite_exists: "لقد أرسلت بالفعل دعوة إلى %{email}." invalid_email: "%{email} ليس عنوان بريد إلكتروني صالحًا." @@ -292,7 +293,7 @@ ar: max_rows: "تم إرسال أول %{max_bulk_invites} دعوة. حاول تقسيم الملف إلى أجزاء أصغر." error: "حدث خطأ في أثناء تحميل هذا الملف. يُرجى إعادة المحاولة لاحقًا." invite_link: - email_taken: "عنوان البريد الإلكتروني هذا مُستخدَم بالفعل. إذا كان لديك حساب بالفعل، يُرجى تسجيل الدخول أو إعادة ضبط كلمة المرور." + email_taken: "عنوان البريد الإلكتروني هذا مُستخدَم بالفعل. إذا كان لديك حساب بالفعل، يُرجى تسجيل الدخول أو إعادة تعيين كلمة المرور." max_redemptions_limit: "يجب أن يكون بين 2 و%{max_limit}." topic_invite: failed_to_invite: "لا يمكن دعوة المستخدم إلى هذا الموضوع دون عضوية في إحدى المجموعات التالية: %{group_names}." @@ -489,8 +490,8 @@ ar: already_bookmarked_post: "لا يمكنك وضع إشارة مرجعية نفس المنشور نفسه مرتَين." already_bookmarked: "لا يمكنك وضع إشارة مرجعية على %{type} نفسه مرتين." too_many: "عذرًا، لا يمكنك إضافة أكثر من %{limit} إشارة مرجعية، انتقل إلى %{user_bookmarks_url} لإزالة بعضها." - cannot_set_past_reminder: "لا يمكنك ضبط تذكير بالإشارة المرجعية في الماضي." - cannot_set_reminder_in_distant_future: "لا يمكنك ضبط تذكير بالإشارة المرجعية بعد أكثر من 10 أيام في المستقبل." + cannot_set_past_reminder: "لا يمكنك تعيين تذكير بالإشارة المرجعية في الماضي." + cannot_set_reminder_in_distant_future: "لا يمكنك تعيين تذكير بالإشارة المرجعية بعد أكثر من 10 أيام في المستقبل." time_must_be_provided: "يجب إدخال الوقت لجميع التذكيرات" for_topic_must_use_first_post: "يمكنك استخدام المنشور الأول فقط لوضع إشارة مرجعية على الموضوع." bookmarkable_id_type_required: "مطلوب اسم السجل ونوعه للعلامة المرجعية." @@ -598,6 +599,7 @@ ar: يمكنك التعديل على ردك السابق لإضافة اقتباس عن طريق تمييز النص والضغط على زر اقتباس الرد الذي يظهر. يسهُل على الجميع قراءة الموضوعات التي يوجد بها عدد قليل من الردود التفصيلية بدلًا من الكثير من الردود الفردية الصغيرة. + dominating_topic: لقد نشرت كثيرًا في هذا الموضوع! فكِّر في منح الآخرين فرصة للرد هنا ومناقشة الأمور مع بعضهم البعض أيضًا. get_a_room: لقد رددت على @%{reply_username} %{count} من المرات، هل تعلم أنه يمكنك إرسال رسالة شخصية إليه بدلًا من ذلك؟ too_many_replies: | ### لقد بلغت حد الردود في هذا الموضوع @@ -637,6 +639,7 @@ ar: target_user_not_found: "تعذَّر العثور على أحد المستخدمين الذين تُرسل إليهم هذه الرسالة." unable_to_update: "حدث خطأ في أثناء تحديث هذا الموضوع." unable_to_tag: "حدث خطأ في أثناء وضع وسم على الموضوع." + unable_to_unlist: "عذرًا، لا يمكن إنشاء موضوع غير مُدرَج." featured_link: invalid: "غير صالح. يجب أن يتضمَّن عنوان URL http:// أو https://." user: @@ -775,6 +778,7 @@ ar: many: "%{count} إعجابًا" other: "%{count} إعجاب" cannot_permanently_delete: + many_posts: "يتضمَّن هذا الموضوع منشورات محذوفة. يُرجى حذفها بشكلٍ دائم قبل حذف الموضوع بشكلٍ دائم." wait_or_different_admin: "يجب عليك الانتظار لمدة %{time_left} قبل حذف هذا المنشور نهائيًا أو يجب على مسؤول آخر فعل ذلك." rate_limiter: slow_down: "لقد نفَّذت هذا الإجراء عدة مرات، يُرجى إعادة المحاولة في وقت لاحق." @@ -971,7 +975,7 @@ ar: other: "منذ حوالي %{count} عام تقريبًا" password_reset: no_token: 'عذرًا! لم يعُد الرابط الذي استخدمته يعمل. يمكنك تسجيل الدخول الآن. إذا نسيت كلمة مرورك، يمكنك طلب رابط لإعادة تعيينها.' - title: "إعادة ضبط كلمة المرور" + title: "إعادة تعيين كلمة المرور" success: "لقد غيَّرت كلمة مرورك وسجَّلت الدخول بنجاح." success_unapproved: "لقد غيَّرت كلمة مرورك بنجاح." email_login: @@ -1062,6 +1066,7 @@ ar: email_body: "%{link}\n\n%{message}" inappropriate: title: "غير لائق" + description: 'يتضمَّن هذا المنشور محتوًى قد يعتبره شخص عاقل هجوميًا أو مسيئًا أو يتضمَّن سلوكًا ينمُّ عن كراهية أو يمثِّل انتهاكًا لإرشادات مجتمعنا.' short_description: 'ينتهك إرشادات مجتمعنا.' notify_user: title: "مراسلة @%{username}" @@ -1127,6 +1132,7 @@ ar: short_description: "هذا إعلان" inappropriate: title: "غير لائق" + description: 'يتضمَّن هذا الموضوع محتوًى قد يعتبره شخص عاقل هجوميًا أو مسيئًا أو يتضمَّن سلوكًا ينمُّ عن كراهية أو يمثِّل انتهاكًا لإرشادات مجتمعنا.' long_form: "أبلغ عن هذا الموضوع كغير لائق" short_description: 'ينتهك إرشادات مجتمعنا.' notify_moderators: @@ -1164,12 +1170,14 @@ ar: mailing_list_mode: "إيقاف تشغيل وضع القائمة البريدية" all: "عدم مراسلتي من %{sitename}" different_user_description: "لقد سجَّلت الدخول حاليًا كمستخدم مختلف عن الذي راسلناه. يُرجى تسجيل الخروج أو دخول وضع التخفي، ثم إعادة المحاولة." + not_found_description: "عذرًا، لم نتمكن من العثور على هذا الاشتراك. من المحتمل أن الرابط الموجود في بريدك الإلكتروني قديم جدًا وانتهت صلاحيته؟" + user_not_found_description: "عذرًا، لم نتمكن من العثور على مستخدم لهذا الاشتراك. من المحتمل أنك تحاول إلغاء الاشتراك في حساب لم يعُد موجودًا." log_out: "تسجيل الخروج" submit: "حفظ التفضيلات" digest_frequency: title: "أنت تتلقى رسائل إلكترونية تلخيصية %{frequency}" never_title: "أنت لا تتلقى رسائل إلكترونية تلخيصية" - select_title: "ضبط مدى تكرار الرسائل الإلكترونية التلخيصية على:" + select_title: "تعيين مدى تكرار الرسائل الإلكترونية التلخيصية على:" never: "أبدًا" every_30_minutes: "كل 30 دقيقة" every_hour: "كل ساعة" @@ -1200,6 +1208,7 @@ ar: write: "كتابة الكل" one_time_password: "إنشاء رمز تسجيل الدخول لمرة واحدة" bookmarks_calendar: "قراءة تذكيرات الإشارات المرجعية" + user_status: "قراءة حالة المستخدم وتحديثها" invalid_public_key: "عذرًا، المفتاح العام غير صالح." invalid_auth_redirect: "عذرًا، هذا المضيف auth_redirect غير مسموح به." invalid_token: "الرمز مفقود أو غير صالح أو منتهي الصلاحية." @@ -1293,10 +1302,12 @@ ar: author: المؤلف edit_reason: السبب consolidated_api_requests: + title: "طلبات API الموحَّدة" xaxis: - api: "API" - user_api: "API للمستخدم" + api: "واجهة برمجة التطبيقات" + user_api: "واجهة برمجة التطبيقات للمستخدم" yaxis: "اليوم" + description: "طلبات API لمفاتيح API العادية ومفاتيح API للمستخدم." dau_by_mau: title: "المستخدمون النشطون يوميًا/المستخدمون النشطون شهريًا" xaxis: "اليوم" @@ -1560,12 +1571,12 @@ ar: sidekiq_warning: 'Sidekiq ليس قيد التشغيل! يتم تنفيذ العديد من المهام، مثل إرسال الرسائل الإلكترونية، بشكلٍ غير متزامن من خلال sidekiq. يُرجى التأكد من وجود عملية sidekiq واحدة على الأقل قيد التشغيل. معرفة المزيد عن Sidekiq من هنا.' queue_size_warning: "عدد المهام في قائمة الانتظار هو %{queue_size}، وهذا رقم كبير. قد يكون ذلك مؤشرًا على وجود مشكلة في عمليات Sidekiq، أو قد تحتاج إضافة المزيد من عمال Sidekiq." memory_warning: "يعمل الخادم بإجمالي ذاكرة أقل من 1 غ.ب. يوصى بتوفُّر 1 غ.ب من الذاكرة على الأقل." - google_oauth2_config_warning: 'تم إعداد الخادم للسماح بالاشتراك وتسجيل الدخول باستخدام Google OAuth2 (enable_google_oauth2_logins)، ولكن لم يتم ضبط معرِّف العميل والقيم السرية للعميل. انتقل إلى إعدادات الموقع وحدِّث الإعدادات. راجع هذا الدليل لمعرفة المزيد.' - facebook_config_warning: 'تم إعداد الخادم للسماح بالاشتراك وتسجيل الدخول باستخدام Facebook (enable_facebook_logins)، ولكن لم يتم ضبط معرِّف العميل والقيم السرية للعميل. انتقل إلى the إعدادات الموقع وحدِّث الإعدادات. راجع هذا الدليل لمعرفة المزيد.' - twitter_config_warning: 'تم إعداد الخادم للسماح بالاشتراك وتسجيل الدخول باستخدام Twitter (enable_twitter_logins)، ولكن لم يتم ضبط معرِّف العميل والقيم السرية للعميل. انتقل إلى the إعدادات الموقع وحدِّث الإعدادات. راجع هذا الدليل لمعرفة المزيد.' - github_config_warning: 'تم إعداد الخادم للسماح بالاشتراك وتسجيل الدخول باستخدام GitHub (enable_github_logins)، ولكن لم يتم ضبط معرِّف العميل والقيم السرية للعميل. انتقل إلى إعدادات الموقع وحدِّث الإعدادات. راجع هذا الدليل لمعرفة المزيد.' - s3_config_warning: 'تم إعداد الخادم لتحميل الملفات إلى S3، ولكن لم يتم ضبط أحد الإعدادات التالية على الأقل: s3_access_key_id أو s3_secret_access_key أو s3_use_iam_profile أو s3_upload_bucket. انتقل إلى إعدادات الموقع وحدِّث الإعدادات. راجع "طريقة إعداد تحميلات الصور إلى S3" لمعرفة المزيد.' - s3_backup_config_warning: 'تم إعداد الخادم لتحميل النسخ الاحتياطية إلى S3، ولكن لم يتم ضبط أحد الإعدادات التالية على الأقل: s3_access_key_id أو s3_secret_access_key أو s3_use_iam_profile أو s3_backup_bucket. انتقل إلى إعدادات الموقع وحدِّث الإعدادات. راجع "طريقة إعداد تحميلات الصور إلى S3" لمعرفة المزيد.' + google_oauth2_config_warning: 'تم إعداد الخادم للسماح بالاشتراك وتسجيل الدخول باستخدام Google OAuth2 (enable_google_oauth2_logins)، ولكن لم يتم تعيين معرِّف العميل والقيم السرية للعميل. انتقل إلى إعدادات الموقع وحدِّث الإعدادات. راجع هذا الدليل لمعرفة المزيد.' + facebook_config_warning: 'تم إعداد الخادم للسماح بالاشتراك وتسجيل الدخول باستخدام Facebook (enable_facebook_logins)، ولكن لم يتم تعيين معرِّف العميل والقيم السرية للعميل. انتقل إلى the إعدادات الموقع وحدِّث الإعدادات. راجع هذا الدليل لمعرفة المزيد.' + twitter_config_warning: 'تم إعداد الخادم للسماح بالاشتراك وتسجيل الدخول باستخدام Twitter (enable_twitter_logins)، ولكن لم يتم تعيين معرِّف العميل والقيم السرية للعميل. انتقل إلى the إعدادات الموقع وحدِّث الإعدادات. راجع هذا الدليل لمعرفة المزيد.' + github_config_warning: 'تم إعداد الخادم للسماح بالاشتراك وتسجيل الدخول باستخدام GitHub (enable_github_logins)، ولكن لم يتم تعيين معرِّف العميل والقيم السرية للعميل. انتقل إلى إعدادات الموقع وحدِّث الإعدادات. راجع هذا الدليل لمعرفة المزيد.' + s3_config_warning: 'تم إعداد الخادم لتحميل الملفات إلى S3، ولكن لم يتم تعيين أحد الإعدادات التالية على الأقل: s3_access_key_id أو s3_secret_access_key أو s3_use_iam_profile أو s3_upload_bucket. انتقل إلى إعدادات الموقع وحدِّث الإعدادات. راجع "طريقة إعداد تحميلات الصور إلى S3" لمعرفة المزيد.' + s3_backup_config_warning: 'تم إعداد الخادم لتحميل النسخ الاحتياطية إلى S3، ولكن لم يتم تعيين أحد الإعدادات التالية على الأقل: s3_access_key_id أو s3_secret_access_key أو s3_use_iam_profile أو s3_backup_bucket. انتقل إلى إعدادات الموقع وحدِّث الإعدادات. راجع "طريقة إعداد تحميلات الصور إلى S3" لمعرفة المزيد.' s3_cdn_warning: 'تم إعداد الخادم لتحميل الملفات إلى S3، ولكن لم يتم إعداد شبكة توصيل المحتوى على S3. يمكن أن يؤدي ذلك إلى تكاليف باهظة لخدمة S3 وأداء أبطأ للموقع. راجع "استخدام مساحة تخزين الكائنات للتحميلات" لمعرفة المزيد.' image_magick_warning: 'تم إعداد الخادم لإنشاء صور مصغَّرة للصور الكبيرة، ولكن لم يتم تثبيت ImageMagick. ثبِّت ImageMagick باستخدام مدير الحزم المفضَّل لديك أو نزِّل أحدث إصدار.' failing_emails_warning: 'هناك %{num_failed_jobs} مهمة بريد إلكتروني فشلت. تحقَّق من app.yml وتأكَّد من صحة إعدادات خادم البريد. راجع المهام التي فشلت في Sidekiq.' @@ -1623,6 +1634,11 @@ ar: allow_duplicate_topic_titles_category: "اسمح بالموضوعات ذات العناوين المتطابقة والمتكررة إذا كانت الفئة مختلفة. يجب أن تكون القيمة allow_duplicate_topic_titles مضبوطة على False." unique_posts_mins: "عدد الدقائق قبل أن يتمكن العضو من إنشاء منشور يتضمَّن المحتوى نفسه مجددًا" educate_until_posts: "إظهار اللوحة التعليمية المنبثقة للمستخدمين الجدُد عندما يبدأ العضو في كتابة منشوراته الأولى (n)" + title: "اسم هذا الموقع. مرئي لجميع الزوار، بما في ذلك المستخدمون المجهولون." + site_description: "صِف هذا الموقع بجملة واحدة. مرئي لجميع الزوار، بما في ذلك المستخدمون المجهولون." + short_site_description: "وصف قصير من بضع كلمات. مرئي لجميع الزوار، بما في ذلك المستخدمون المجهولون." + contact_email: "عنوان البريد الإلكتروني لجهة الاتصال الرئيسية المسؤولة عن هذا الموقع. يتم استخدامه للإشعارات المهمة ويتم عرضه أيضًا على صفحة /about. مرئي للمستخدمين المجهولين على المواقع العامة." + contact_url: "عنوان URL للتواصل لهذا الموقع. عندما يكون موجودًا، فإنه يستبدل عنوان البريد الإلكتروني على /about، ويكون مرئيًا للمستخدمين المجهولين على المواقع العامة." crawl_images: "استعادة الصور من عناوين URL البعيدة لإدراج الأبعاد الصحيحة للطول والعرض" download_remote_images_to_local: "يمكنك تحويل الصور البعيدة (المرتبطة برابط ساخن) عن طريق تنزيلها؛ يحفظ ذلك المحتوى حتى إذا تمت إزالة الصور من الموقع البعيد في المستقبل." download_remote_images_threshold: "الحد الأدنى من مساحة القرص اللازمة لتنزيل الصور البعيدة محليًا (بالنسبة المئوية)" @@ -1633,11 +1649,13 @@ ar: editing_grace_period_max_diff: "الحد الأقصى لعدد تغييرات الأحرف المسموح بها في فترة السماح بالتعديل، وحفظ مراجعة أخرى للمنشور إذا تم تغيير المزيد (مستوى الثقة 0 و1)" editing_grace_period_max_diff_high_trust: "الحد الأقصى لعدد تغييرات الأحرف المسموح بها في فترة السماح بالتعديل، مع حفظ مراجعة أخرى للمنشور إذا تم تغيير المزيد (مستوى الثقة 2 وأعلى)" staff_edit_locks_post: "سيتم قفل التعديل على المنشورات إذا تم تعديلها بواسطة أعضاء فريق العمل" - post_edit_time_limit: "يمكن للمؤلف من مستوى الثقة 0 أو 1 تعديل منشوراته لمدة (n) دقيقة بعد النشر. اضبط القيمة على 0 لإتاحة التعديل للأبد." - tl2_post_edit_time_limit: "يمكن للمؤلف من مستوى الثقة 2 وأعلى تعديل منشوراته لمدة (n) دقيقة بعد النشر. اضبط القيمة على 0 لإتاحة التعديل للأبد." + post_edit_time_limit: "يمكن للمؤلف من مستوى الثقة 0 أو 1 تعديل منشوراته لمدة (n) دقيقة بعد النشر. اتعيين القيمة على 0 لإتاحة التعديل للأبد." + tl2_post_edit_time_limit: "يمكن للمؤلف من مستوى الثقة 2 وأعلى تعديل منشوراته لمدة (n) دقيقة بعد النشر. اتعيين القيمة على 0 لإتاحة التعديل للأبد." edit_history_visible_to_public: "السماح للجميع برؤية النسخ السابقة من المنشورات التي تم تعديلها. عند إيقاف هذا الإعداد، سيتمكن أعضاء الفريق فقط من رؤيتها." - delete_removed_posts_after: "سيتم حذف المنشورات التي أزالها المؤلف بعد (n) ساعة. في حال الضبط على 0، سيتم حذف المنشورات على الفور." + delete_removed_posts_after: "سيتم حذف المنشورات التي أزالها المؤلف بعد (n) ساعة. في حال التعيين إلى 0، سيتم حذف المنشورات على الفور." notify_users_after_responses_deleted_on_flagged_post: "عند الإبلاغ عن منشور وإزالته، سيتلقى جميع المستخدمين الذين ردوا على المنشور وتمت إزالة ردودهم إشعارًا." + max_image_width: "الحد الأقصى لعرض الصور المصغَّرة في منشور. سيتم تغيير حجم الصور ذات العرض الأكبر من ذلك وتبسيطها." + max_image_height: "الحد الأقصى لارتفاع الصور المصغَّرة في منشور. سيتم تغيير حجم الصور ذات الارتفاع الأكبر من ذلك وتبسيطها." responsive_post_image_sizes: "تغيير حجم صور المعاينة المبسَّطة للسماح بالشاشات ذات كثافة النقاط العالية بنسب البكسل التالية. قم بإزالة جميع القيم لإيقاف الصور المتجاوبة." fixed_category_positions: "في حال تحديده، ستتمكن من ترتيب الفئات ترتيبًا ثابتًا. وإذا ألغيت تحديده، فسيتم إدراج الفئات بترتيب النشاط." fixed_category_positions_on_create: "في حال تحديده، سيتم الاحتفاظ بترتيب الفئات في مربع حوار إنشاء الموضوع (requires fixed_category_positions)." @@ -1669,12 +1687,12 @@ ar: apple_touch_icon: "الصورة المُستخدَمة كشعار/صورة البداية على Android. سيتم تغيير حجمها تلقائيًا إلى 512 × 512. وإذا تركتها فارغة، فسيتم استخدام large_icon." opengraph_image: "صورة opengraph افتراضية، يتم استخدامها عندما لا تحتوي الصفحة على صورة أخرى مناسبة. وإذا تركتها فارغة، فسيتم استخدام large_icon." twitter_summary_large_image: "بطاقة Twitter 'summary large image' (ينبغي ألا يقل عرضها عن 280، وارتفاعها عن 150، وألا تكون بتنسيق .svg). في حال تركها فارغة، يتم إنشاء البيانات الوصفية التقليدية باستخدام opengraph_image ما دامت هي أيضًا ليست بتنسيق .svg" - notification_email: "عنوان البريد الإلكتروني المُستخدَم في حقل \"من:\" عند إرسال جميع رسائل النظام الأساسية. يجب ضبط سجلات SPF وDKIM وreverse PTR للنطاق المحدَّد هنا بشكلٍ صحيح لتصل الرسالة الإلكترونية." + notification_email: "عنوان البريد الإلكتروني المُستخدَم في حقل \"من:\" عند إرسال جميع رسائل النظام الأساسية. يجب تعيين سجلات SPF وDKIM وreverse PTR للنطاق المحدَّد هنا بشكلٍ صحيح لتصل الرسالة الإلكترونية." email_custom_headers: "قائمة مفصولة بشرائط عمودية لرؤوس البريد الإلكتروني المخصَّصة" email_subject: "تنسيق موضوع قابل للتخصيص للرسائل الإلكترونية القياسية. راجع https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" detailed_404: "يوفِّر مزيدًا من التفاصيل للمستخدمين بشأن سبب عدم تمكُّنهم من الوصول إلى موضوع معيَّن. ملاحظة: هذا الإعداد أقل أمانًا لأن المستخدمين سيعرفون إذا كان عنوان URL مرتبطًا بموضوع صالح." enforce_second_factor: "يفرض على المستخدمين تفعيل المصادقة الثنائية. حدِّد \"الكل\" لفرضها على جميع المستخدمين. حدِّد \"فريق العمل\" لفرضها على المستخدمين من فريق العمل فقط." - force_https: "فرض استخدام HTTPS فقط على الموقع. تحذير: لا تفعِّل هذا الإعداد إلا بعد التأكد من أن HTTPS مُعد بالكامل ويعمل فى جميع أنحاء الموقع! هل تحقَّقت من شبكة توصيل المحتوى (CDN)، وعمليات تسجيل الدخول على جميع شبكات التواصل الاجتماعى، وأي شعارات أو تبعيات خارجية للتأكد من أنها جميعًا متوافقه مع HTTPS؟" + force_https: "فرض استخدام HTTPS فقط على الموقع. تحذير: لا تقم بتمكين هذا الإعداد إلا بعد التأكد من أن HTTPS مُعد بالكامل ويعمل فى جميع أنحاء الموقع! هل تحقَّقت من شبكة توصيل المحتوى (CDN)، وعمليات تسجيل الدخول على جميع شبكات التواصل الاجتماعى، وأي شعارات أو تبعيات خارجية للتأكد من أنها جميعًا متوافقه مع HTTPS؟" same_site_cookies: "استخدام ملفات تعريف ارتباط الموقع نفسه، فهي تقضي على جميع مؤشرات CSRF على المتصفحات المدعومة (Lax أو Strict). تحذير: سيعمل Strict فقط على المواقع التي تفرض تسجيل الدخول وتستخدم طريقة مصادقة خارجية." summary_score_threshold: "الحد الأدنى من النقاط المطلوبة لتضمين منشور في \"تلخيص هذا الموضوع\"" summary_posts_required: "الحد الأدنى من عدد المنشورات في الموضوع قبل تفعيل \"تلخيص هذا الموضوع\". سيتم تطبيق التغييرات على هذا الإعداد بأثر رجعي في غضون أسبوع." @@ -1686,7 +1704,7 @@ ar: personal_message_enabled_groups: "اسمح للمستخدمين داخل هذه المجموعات بإنشاء رسائل والرد على الرسائل. تتضمن مجموعات مستوى الثقة جميع مستويات الثقة التي تزيد عن هذا الرقم؛ على سبيل المثال، يتيح اختيار trust_level_1 أيضًا لمستخدمي trust_level_2 و3 و4 إرسال رسائل خاصة. لاحظ أن الموظفين يمكنهم دائمًا إرسال الرسائل بغض النظر عن السبب." enable_system_message_replies: "يسمح للمستخدمين بالرد على رسائل النظام، حتى إذا كانت الرسائل الشخصية متوقفة." enable_chunked_encoding: "تفعيل استجابات الترميز المقسَّمة بواسطة الخادم. تعمل هذه الميزة على معظم الإعدادات، ولكن قد يتم تخزين بعض الخوادم الوكيلة مؤقتًا، مما يتسبب في تأخير الاستجابات" - long_polling_base_url: "عنوان URL الأساسي المُستخدَم في الاستقصاء الطويل (عندما تخدم شبكة توصيل المحتوى (CDN) محتوًى ديناميكيًا، احرص على ضبط هذا على سحب الموارد من المصدر) eg: http://origin.site.com" + long_polling_base_url: "عنوان URL الأساسي المُستخدَم في الاستقصاء الطويل (عندما تخدم شبكة توصيل المحتوى (CDN) محتوًى ديناميكيًا، احرص على تعيين هذا على سحب الموارد من المصدر) eg: http://origin.site.com" polling_interval: "المدة الزمنية المفترض أن يظل العملاء الذين سجَّلوا الدخول متصلين خلالها بالمللي ثانية عندما لا يكون الاستقصاء الطويل مستخدمًا" anon_polling_interval: "المدة الزمنية المفترض استقصاء العملاء المجهولين خلالها بالمللي ثانية" background_polling_interval: "المدة الزمنية المفترض استقصاء العملاء المجهولين خلالها بالمللي ثانية (عندما تكون النافذة في الخلفية)" @@ -1705,9 +1723,9 @@ ar: tl2_additional_flags_per_day_multiplier: "زيادة حد البلاغات اليومية لمستوى الثقة 2 (عضو) بالضرب في هذا الرقم" tl3_additional_flags_per_day_multiplier: "زيادة حد البلاغات اليومية لمستوى الثقة 3 (منتظم) بالضرب في هذا الرقم" tl4_additional_flags_per_day_multiplier: "زيادة حد البلاغات اليومية لمستوى الثقة 4 (قائد) بالضرب في هذا الرقم" - num_users_to_silence_new_user: "إخفاء جميع منشورات المستخدم الجديد ومنع النشر في المستقبل إذا تلقَّت منشوراته spam_flags_to_silence_new_user بلاغ عن سلوك غير مرغوب فيه من هذا العدد من المستخدمين المختلفين. اضبط القيمة على 0 للإيقاف." - num_tl3_flags_to_silence_new_user: "إخفاء جميع منشورات المستخدم الجديد ومنع النشر في المستقبل إذا تلقَّت منشوراته العديد من البلاغات من num_tl3_users_to_silence_new_user مستخدم مختلف من مستوى الثقة 3. اضبط القيمة على 0 للإيقاف." - num_tl3_users_to_silence_new_user: "إخفاء جميع منشورات المستخدم الجديد ومنع النشر في المستقبل إذا تلقَّت منشوراته num_tl3_flags_to_silence_new_user من هذا العدد من المستخدمين المختلفين من مستوى الثقة 3. اضبط القيمة على 0 للإيقاف." + num_users_to_silence_new_user: "إخفاء جميع منشورات المستخدم الجديد ومنع النشر في المستقبل إذا تلقَّت منشوراته spam_flags_to_silence_new_user بلاغ عن سلوك غير مرغوب فيه من هذا العدد من المستخدمين المختلفين. اتعيين القيمة على 0 للإيقاف." + num_tl3_flags_to_silence_new_user: "إخفاء جميع منشورات المستخدم الجديد ومنع النشر في المستقبل إذا تلقَّت منشوراته العديد من البلاغات من num_tl3_users_to_silence_new_user مستخدم مختلف من مستوى الثقة 3. اتعيين القيمة على 0 للإيقاف." + num_tl3_users_to_silence_new_user: "إخفاء جميع منشورات المستخدم الجديد ومنع النشر في المستقبل إذا تلقَّت منشوراته num_tl3_flags_to_silence_new_user من هذا العدد من المستخدمين المختلفين من مستوى الثقة 3. اتعيين القيمة على 0 للإيقاف." notify_mods_when_user_silenced: "إرسال رسالة إلى جميع المشرفين إذا تم كتم الخدمة تلقائيًا" flag_sockpuppets: "الإبلاغ عن منشورَين على أنهما يُحتمَل أنا يكونا غير مرغوب فيهما إذا ردَّ مستخدم جديد على موضوع من عنوان IP نفسه الذي ردَّ منه مثل المستخدم الذي بدأ الموضوع" traditional_markdown_linebreaks: "استخدام فواصل الأسطر التقليدية في Markdown، والتي تتطلب مسافتين في النهاية لإدراج فاصل سطر." @@ -1720,7 +1738,7 @@ ar: invite_code: "يجب على المستخدم كتابة هذا الرمز للسماح بتسجيل الحساب، ويتم تجاهله عندما يكون فارغًا (غير حساس لحالة الأحرف)" approve_suspect_users: "إضافة المستخدمين المشبوهين إلى قائمة انتظار المراجعة. أدخل المستخدمون المشبوهون سيرة ذاتية/موقع إلكتروني ولكن ليس لديهم نشاط قراءة." review_every_post: "يجب مراجعة جميع المنشورات. تحذير! لا يوصى بهذا الإعداد للمواقع المزدحمة." - pending_users_reminder_delay_minutes: "إرسال إشعار إلى المشرفين إذا كان المستخدمون الجُدد ينتظرون الموافقة لمدة أطول من هذا العدد من الدقائق. اضبطها على -1 لإيقاف الإشعارات." + pending_users_reminder_delay_minutes: "إرسال إشعار إلى المشرفين إذا كان المستخدمون الجُدد ينتظرون الموافقة لمدة أطول من هذا العدد من الدقائق. قم بالتعيين إلى -1 لإيقاف الإشعارات." persistent_sessions: "سيظل المستخدمون مسجَّلين الدخول عند إغلاق متصفح الويب" maximum_session_age: "سيظل المستخدم مسجَّلًا الدخول لمدة n ساعة بعد زيارته اﻷخيرة." ga_version: "إصدار Google Universal Analytics الذي سيتم استخدامه: v3 (analytics.js)، v4 (gtag)" @@ -1731,13 +1749,13 @@ ar: enable_escaped_fragments: "ارجع إلى واجهة برمجة تطبيقات Ajax-Crawling من Google إذا لم يتم اكتشاف زاحف ويب. راجع https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" moderators_manage_categories_and_groups: "السماح للمشرفين بإنشاء وإدارة الفئات والمجموعات" moderators_change_post_ownership: "السماح للمشرفين بتغيير ملكية المنشور" - cors_origins: "الأصول المسموح بها لطلبات الموارد متعددة المصادر (CORS). بجب أن يتضمَّن كل مصدر http:// أو https://. ويجب ضبط متغير البيئة للقيمة DISCOURSE_ENABLE_CORS على True لتفعيل CORS." + cors_origins: "الأصول المسموح بها لطلبات الموارد متعددة المصادر (CORS). بجب أن يتضمَّن كل مصدر http:// أو https://. ويجب تعيين متغير البيئة للقيمة DISCOURSE_ENABLE_CORS على True لتفعيل CORS." use_admin_ip_allowlist: "لا يمكن للمسؤولين تسجيل الدخول إلا إذا كانوا على عنوان IP محدَّد في قائمة عناوين IP الخاضعة للمراقبة (المسؤول > السجلات > عناوين IP الخاضعة للمراقبة)." blocked_ip_blocks: "قائمة وحدات عناوين IP الخاصة التي لا ينبغي لمنصة Discourse الزحف إليها مطلقًا" allowed_internal_hosts: "قائمة المضيفات الداخلية التي يمكن لمنصة Discourse الزحف إليها بأمان من أجل لوحة المعاينة والأغراض الأخرى" allowed_onebox_iframes: "قائمة نطاقات iframe src المسموح بها عبر تضمينات Onebox. سيسمح `*` بجميع محركات Onebox الافتراضية." allowed_iframes: "قائمة بادئات نطاقات iframe src التي يمكن أن يسمح بها Discourse بأمان في المنشورات" - allowed_crawler_user_agents: "وكلاء المستخدمين لزاحفات الويب التي يجب السماح لها بالوصول إلى الموقع. تحذير! سيؤدي ضبط هذا الإعداد إلى عدم السماح بجميع الزاحفات غير المُدرَجة هنا!" + allowed_crawler_user_agents: "وكلاء المستخدمين لزاحفات الويب التي يجب السماح لها بالوصول إلى الموقع. تحذير! سيؤدي تعيين هذا الإعداد إلى عدم السماح بجميع الزاحفات غير المُدرَجة هنا!" blocked_crawler_user_agents: "الكلمة الفريدة غير حساسة لحالة الأحرف في سلسلة وكيل المستخدم والتي تحدِّد زاحفات الويب التي لا ينبغي السماح لها بالوصول إلى الموقع. لا تنطبق إذا تم تعريف قائمة السماح." slow_down_crawler_user_agents: 'وكلاء المستخدم لبرامج زحف الويب التي يجب أن يكون معدَّلها محدودًا كما تم إعداده في "slow down crawler rate". يجب أن تتكون كل قيمة من 3 أحرف على الأقل.' slow_down_crawler_rate: "إذا تم تحديد slow_down_crawler_user_agents، فسيتم تطبيق هذا المعدَّل على جميع الزاحفات (عدد ثواني التأخير بين الطلبات)" @@ -1746,7 +1764,7 @@ ar: content_security_policy_collect_reports: "تفعيل جمع تقارير انتهاك سياسة أمان المحتوى في /csp_reports" content_security_policy_frame_ancestors: "تقييد من يمكنه تضمين هذا الموقع في iframes عبر سياسة أمان المحتوى. يمكن التحكُّم في المضيفات المسموح بها في التضمين" content_security_policy_script_src: "مصادر النصوص الإضافية المُدرَجة في قائمة السماح. يتم تضمين المضيف الحالي وشبكة توصيل المحتوى بشكلٍ افتراضي. راجع الحد من هجمات XSS باستخدام سياسة أمان المحتوى." - invalidate_inactive_admin_email_after_days: "ستحتاج حسابات المسؤولين الذين لم يزوروا الموقع في هذا العدد من الأيام إلى إعادة التحقُّق من صحة عنوان بريدهم الإلكتروني قبل تسجيل الدخول. اضبط القيمة على 0 للإيقاف." + invalidate_inactive_admin_email_after_days: "ستحتاج حسابات المسؤولين الذين لم يزوروا الموقع في هذا العدد من الأيام إلى إعادة التحقُّق من صحة عنوان بريدهم الإلكتروني قبل تسجيل الدخول. اتعيين القيمة على 0 للإيقاف." top_menu: "تحديد العناصر التي تظهر في لوحة تنقُّل الصفحة الرئيسية، وبأي ترتيب. مثال: الجديدة|غير المقروءة|الفئات|الأكثر نشاطًا|المقروءة|المنشورة|الإشارات المرجعية" post_menu: "تحديد العناصر التي تظهر في قائمة المنشور وترتيبها. مثال: تعديل|إبلاغ|حذف|مشاركة|إشارة مرجعية|رد" post_menu_hidden_items: "عناصر القائمة التي سيتم إخفاؤها افتراضيًا في قائمة المنشور ما لم يتم النقر على رمز الثلاث نقاط لتوسيع القائمة." @@ -1775,10 +1793,7 @@ ar: max_favorite_badges: "الحد الأقصى لعدد الشارات التي يمكن للمستخدم تحديدها" whispers_allowed_groups: "السماح بالاتصالات الخاصة داخل الموضوعات لأعضاء المجموعات المحدَّدة." allow_index_in_robots_txt: "حدِّد في ملف robots.txt أن هذا الموقع يسمح لمحركات بحث الويب بفهرسته. وفي حالات استثنائية، يمكنك تجاوز ملف robots.txt بشكلٍ دائم." - blocked_email_domains: "قائمة مفصولة بشرائط عمودية لنطاقات البريد الإلكتروني التي لا يتم السماح للمستخدمين بتسجيل حسابات عليها. مثال: mailinator.com|trashmail.net" - allowed_email_domains: "قائمة مفصولة بشرائط عمودية لنطاقات البريد الإلكتروني التي يجب على المستخدمين تسجيل حسابات عليها. تحذير: لن يتم السماح بالمستخدمين المسجَّلين على نطاقات بريد إلكتروني أخرى بخلاف المذكورة هنا!" normalize_emails: "تحقَّق مما إذا كان البريد الإلكتروني الذي تم تطبيعه فريدًا. يزيل البريد الإلكتروني الذي تم تطبيعه جميع النقاط من اسم المستخدم وكل شيء بين الرمزين + و@." - auto_approve_email_domains: "ستتم الموافقة تلقائيًا على المستخدمين الذين لديهم عناوين بريد إلكتروني من قائمة النطاقات هذه." hide_email_address_taken: "عدم إعلام المستخدمين بوجود حساب بعنوان بريد إلكتروني معيَّن في أثناء التسجيل أو في أثناء عملية \"نسيت كلمة المرور\". طلب البريد الإلكتروني الكامل لطلبات \"نسيت كلمة المرور\"." log_out_strict: "تسجيل خروج المستخدم من جميع الجلسات على جميع الأجهزة عند تسجيل الخروج" version_checks: "فحص Discourse Hub للحصول على تحديثات الإصدار وإظهار رسائل الإصدار الجديد على /admin لوحة المعلومات" @@ -1799,7 +1814,7 @@ ar: auth_immediately: "إعادة التوجيه تلقائيًا إلى نظام تسجيل الدخول الخارجي دون تفاعل المستخدم. يسري ذلك فقط عندما تكون القيمة login_required مضبوطة على True، ولا يوجد سوى طريقة مصادقة خارجية واحدة" enable_discourse_connect: "تفعيل تسجيل الدخول عبر DiscourseConnect (المعروف سابقًا باسم \"Discourse SSO\") (تحذير: *يجب* التحقُّق من صحة عناوين البريد الإلكتروني للمستخدمين من خلال الموقع الخارجي!)" verbose_discourse_connect_logging: "تسجيل تشخصيات DiscourseConnect المطوَّلة ذات الصلة في /logs" - enable_discourse_connect_provider: "تنفيذ بروتوكول موفِّر DiscourseConnect (المعروف سابقًا باسم \"Discourse SSO\") في نقطة نهاية /session/sso_provider، يتطلب ضبط discourse_connect_provider_secrets" + enable_discourse_connect_provider: "تنفيذ بروتوكول موفِّر DiscourseConnect (المعروف سابقًا باسم \"Discourse SSO\") في نقطة نهاية /session/sso_provider، يتطلب تعيين discourse_connect_provider_secrets" discourse_connect_url: "عنوان URL لنقطة نهاية DiscourseConnect (يجب أن يتضمَّن http:// أو https://)" discourse_connect_secret: "السلسلة السرية المُستخدَمة لمصادقة معلومات DiscourseConnect بشكلٍ مشفَّر، تأكد من أنها تتكوَّن من 10 أحرف أو أكثر" discourse_connect_provider_secrets: "قائمة بالأزواج السرية للنطاق والتي تستخدم DiscourseConnect. تأكَّد من أن طول رمز DiscourseConnect السري هو 10 أحرف أو أكثر. يمكن استخدام رمز حرف البدل * لمطابقة أي نطاق أو جزء منه فقط (على سبيل المثال، *.example.com)." @@ -1842,6 +1857,7 @@ ar: discord_trusted_guilds: 'السماح لأعضاء خوادم Discord هذه فقط بتسجيل الدخول عبر Discord. استخدم المعرِّف الرقمي للخادم. لمزيد من المعلومات، راجع الإرشادات هنا. اتركه فارغًا للسماح بأي خادم.' enable_backups: "السماح للمسؤولين بإنشاء نسخ احتياطية من المنتدى" allow_restore: "السماح بالاستعادة، والتي يمكنها استبدال جميع بيانات الموقع! اتركها على \"متوقفة\" ما لم تكن تخطط لاستعادة نسخة احتياطية" + maximum_backups: "الحد الأقصى من النسخ الاحتياطية التي يجب الاحتفاظ بها. يتم حذف النسخ الاحتياطية القديمة تلقائيًا" automatic_backups_enabled: "تشغيل النسخ الاحتياطية التلقائية كما هو محدَّد في تردد النسخ الاحتياطي" backup_frequency: "عدد الأيام بين النسخ الاحتياطية" s3_backup_bucket: "الحاوية البعيدة للاحتفاظ بالنسخ الاحتياطية. تحذير: تأكد من أنها حاوية خاصة." @@ -1876,13 +1892,13 @@ ar: max_topic_invitations_per_minute: "الحد الأقصى لعدد الدعوات إلى الموضوعات التي يمكن للمستخدم إرسالها في الدقيقة" max_logins_per_ip_per_hour: "الحد الأقصى لعدد عمليات تسجيل الدخول المسموح بها لكل عنوان IP في الساعة" max_logins_per_ip_per_minute: "الحد الأقصى لعدد عمليات تسجيل الدخول المسموح بها لكل عنوان IP في الدقيقة" - max_post_deletions_per_minute: "الحد الأقصى لعدد المنشورات التي يمكن للمستخدم حذفها في الدقيقة. اضبط القيمة على 0 لإيقاف عمليات حذف المنشورات." - max_post_deletions_per_day: "الحد الأقصى لعدد المنشورات التي يمكن للمستخدم حذفها يوميًا. اضبط القيمة على 0 لإيقاف عمليات حذف المنشورات." + max_post_deletions_per_minute: "الحد الأقصى لعدد المنشورات التي يمكن للمستخدم حذفها في الدقيقة. اتعيين القيمة على 0 لإيقاف عمليات حذف المنشورات." + max_post_deletions_per_day: "الحد الأقصى لعدد المنشورات التي يمكن للمستخدم حذفها يوميًا. اتعيين القيمة على 0 لإيقاف عمليات حذف المنشورات." invite_link_max_redemptions_limit: "لا يمكن أن يتجاوز الحد الأقصى لعمليات الاسترداد المسموح بها لروابط الدعوة هذه القيمة." invite_link_max_redemptions_limit_users: "لا يمكن أن يتجاوز الحد الأقصى لعمليات الاسترداد المسموح بها لروابط الدعوة التي تم إنشاؤها بواسطة مستخدمين منتظمين هذه القيمة." alert_admins_if_errors_per_minute: "عدد الأخطاء في الدقيقة لظهور تنبيه للمسؤول. تعطِّل القيمة \"0\" هذه الميزة. ملاحظة: تتطلَّب إعادة التشغيل." - alert_admins_if_errors_per_hour: "عدد الأخطاء في الساعة لتشغيل تنبيه المسؤول. يؤدي ضبط القيمة على 0 إلى إيقاف هذه الميزة. ملاحظة: يتطلب إعادة التشغيل." - categories_topics: "عدد الموضوعات التي سيتم عرضها في صفحة /categories. إذا تم ضبطها على 0، فستتم محاولة العثور على قيمة تلقائيًا للحفاظ على تناسق العمودين (الفئات والموضوعات)." + alert_admins_if_errors_per_hour: "عدد الأخطاء في الساعة لتشغيل تنبيه المسؤول. يؤدي تعيين القيمة على 0 إلى إيقاف هذه الميزة. ملاحظة: يتطلب إعادة التشغيل." + categories_topics: "عدد الموضوعات التي سيتم عرضها في صفحة /categories. إذا تم تعيينها إلى 0، فستتم محاولة العثور على قيمة تلقائيًا للحفاظ على تناسق العمودين (الفئات والموضوعات)." suggested_topics: "عدد الموضوعات المقترحة التي تظهر أسفل الموضوع" limit_suggested_to_category: "عرض الموضوعات من الفئة الحالية فقط في الموضوعات المقترحة" suggested_topics_max_days_old: "يجب ألا يزيد عمر الموضوعات المقترحة عن n من الأيام." @@ -1890,7 +1906,7 @@ ar: clean_up_uploads: "إزالة التحميلات المعزولة غير المرجعية لمنع الاستضافة غير القانونية. تحذير: قد ترغب في عمل نسخة احتياطية من دليل /uploads قبل تفعيل هذا الإعداد." clean_orphan_uploads_grace_period_hours: "فترة السماح (بالساعات) قبل إزالة تحميل معزول" purge_deleted_uploads_grace_period_days: "فترة السماح (بالأيام) قبل مسح تحميل محذوف" - purge_unactivated_users_grace_period_days: "فترة السماح (بالأيام) قبل حذف المستخدم الذي لم ينشِّط حسابه. اضبط القيمة على 0 لعدم مسح المستخدمين الذين لم يتم تنشيط حساباتهم أبدًا." + purge_unactivated_users_grace_period_days: "فترة السماح (بالأيام) قبل حذف المستخدم الذي لم ينشِّط حسابه. اتعيين القيمة على 0 لعدم مسح المستخدمين الذين لم يتم تنشيط حساباتهم أبدًا." enable_s3_uploads: "وضع التحميلات على مساحة تخزين Amazon S3. مهم: يتطلب ذلك بيانات اعتماد S3 صالحة (معرِّف مفتاح الوصول ومفتاح الوصول السري)." s3_use_iam_profile: 'استخدام ملف تعريف مثيل AWS EC2 لمنح الوصول إلى حاوية S3. ملاحظة: يتطلب تفعيل ذلك تشغيل Discourse في مثيل EC2 مهيَّأ بشكلٍ مناسب، ويتجاوز إعدادات "s3 access key id" و"s3 secret access key".' s3_upload_bucket: "اسم حاوية Amazon S3 التي سيتم تحميل الملفات إليها. تحذير: يجب كتابة الاسم بأحرف صغيرة بلا نقاط أو شرطات سفلية." @@ -1933,7 +1949,7 @@ ar: tl2_requires_likes_given: "عدد تسجيلات الإعجاب التي يجب على المستخدم تسجيلها قبل الترقية إلى مستوى الثقة 2" tl2_requires_topic_reply_count: "عدد الموضوعات التي يجب على المستخدم الرد عليها قبل الترقية إلى مستوى الثقة 2" tl3_time_period: "الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3 (بالأيام)" - tl3_requires_days_visited: "الحد الأدنى لعدد الأيام التي يحتاجها المستخدم لزيارة الموقع في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم للتأهُّل للترقية إلى مستوى الثقة 3. اضبطها على فترة زمنية أعلى من الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3 لإيقاف الترقيات إلى مستوى الثقة 3. (0 أو أعلى)" + tl3_requires_days_visited: "الحد الأدنى لعدد الأيام التي يحتاجها المستخدم لزيارة الموقع في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم للتأهُّل للترقية إلى مستوى الثقة 3. قم بالتعيين إلى فترة زمنية أعلى من الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3 لإيقاف الترقيات إلى مستوى الثقة 3. (0 أو أعلى)" tl3_requires_topics_replied_to: "الحد الأدنى لعدد الموضوعات التي يحتاج المستخدم إلى الرد عليها في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم للتأهُّل للترقية إلى مستوى الثقة 3 (0 أو أعلى)" tl3_requires_topics_viewed: "النسبة المئوية للموضوعات التي تم إنشاؤها في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم للتأهُّل للترقية إلى مستوى الثقة 3 (0 إلى 100)" tl3_requires_topics_viewed_cap: "أقصى عدد مطلوب من الموضوعات التي تم عرضها في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم" @@ -1965,9 +1981,9 @@ ar: newuser_max_links: "عدد الروابط التي يمكن للمستخدم الجديد إضافتها إلى المنشور" newuser_max_embedded_media: "عدد عناصر الوسائط المضمَّنة التي يمكن لمستخدم جديد إضافتها إلى المنشور" newuser_max_attachments: "عدد المرفقات التي يمكن للمستخدم الجديد إضافتها إلى المنشور" - newuser_max_mentions_per_post: "الحد الأقصى لعدد إشعارات @name التي يمكن لمستخدم جديد استخدامها في المنشور" + newuser_max_mentions_per_post: "الحد الأقصى لعدد إشعارات @الاسم التي يمكن لمستخدم جديد استخدامها في المنشور" newuser_max_replies_per_topic: "الحد الأقصى لعدد الردود التي يمكن لمستخدم جديد إجراؤها في موضوع واحد حتى يرد عليها شخص ما" - max_mentions_per_post: "الحد الأقصى لعدد إشعارات @name التي يمكن لأي شخص استخدامها في المنشور" + max_mentions_per_post: "الحد الأقصى لعدد إشعارات @الاسم التي يمكن لأي شخص استخدامها في المنشور" max_users_notified_per_group_mention: "الحد الأقصى لعدد المستخدمين الذين قد يتلقون إشعارًا إذا تمت الإشارة إلى المجموعة (لن يتم إرسال الإشعارات إذا تم استيفاء الحد الأقصى)" enable_mentions: "السماح للمستخدمين بالإشارة إلى مستخدمين آخرين" here_mention: "الاسم المستخدم للإشارة باستخدام الرمز @ للسماح للمستخدمين المتميزين بإعلام ما يصل إلى 'max_here_mentioned' من الأشخاص المشاركين في الموضوع. يجب ألا يكون اسم مستخدم موجودًا." @@ -2016,20 +2032,21 @@ ar: faq_url: "إذا كان لديك صفحة أسئلة شائعة مستضافة في مكانٍ آخر وتريد استخدامها، فأدخِل عنوان URL لها كاملًا هنا." tos_url: "إذا كان لديك مستند شروط خدمة مستضاف في مكانٍ آخر وتريد استخدامه، فأدخِل عنوان URL له كاملًا هنا." privacy_policy_url: "إذا كان لديك مستند مستضاف في مكان آخر لسياسة الخصوصية وتريد استخدامه، فأدخِل عنوان URL الكامل هنا." + log_anonymizer_details: "ما إذا كان سيتم الاحتفاظ بتفاصيل المستخدم في السجل بعد إخفاء هويته." newuser_spam_host_threshold: "عدد المرات التي يمكن فيها لمستخدم جديد نشر رابط للمضيف نفسه ضمن منشورات `newuser_spam_host_threshold` قبل اعتبارها غير مرغوب فيها" allowed_spam_host_domains: "قائمة بالنطاقات المستبعدة من اختبار المضيف غير المرغوب فيه. لن يتم تقييد المستخدمين الجُدد أبدًا من إنشاء منشورات تحتوي على روابط إلى هذه النطاقات." staff_like_weight: "الأهمية التي يجب منحها لإعجابات فريق العمل (تساوي أهمية تسجيلات الإعجاب من غير فريق العمل 1)" topic_view_duration_hours: "عد مرة عرض الموضوع الجديد مرة واحدة لكل IP/مستخدم كل N ساعة" user_profile_view_duration_hours: "عد عرض الملف الشخصي للمستخدم الجديد مرة واحدة لكل IP/مستخدم كل N ساعة" levenshtein_distance_spammer_emails: "عدد الأحرف المختلفة الذي سيسمح بمطابقة جزئية عند مطابقة الرسائل الإلكترونية غير المرغوب فيها" - max_new_accounts_per_registration_ip: "توقَّف عن قبول عمليات الاشتراك الجديدة من عنوان IP هذا إذا كان هناك بالفعل (n) حساب من مستوى الثقة 0 منه (ولم يكن لفريق العمل أو من المستوى الثقة 2 أو أعلى). اضبط القيمة على 0 لإيقاف الحد." + max_new_accounts_per_registration_ip: "توقَّف عن قبول عمليات الاشتراك الجديدة من عنوان IP هذا إذا كان هناك بالفعل (n) حساب من مستوى الثقة 0 منه (ولم يكن لفريق العمل أو من المستوى الثقة 2 أو أعلى). اتعيين القيمة على 0 لإيقاف الحد." min_ban_entries_for_roll_up: "عند النقر على الزر \"تجميع\"، سيتم إنشاء إدخال حظر جديد في الشبكة الفرعية إذا كان هناك (N) من الإدخالات على الأقل." max_age_unmatched_emails: "حذف إدخالات البريد الإلكتروني الخاضعة للمراقبة غير المتطابقة بعد (N) يوم" max_age_unmatched_ips: "حذف إدخالات عناوين IP الخاضعة للمراقبة غير المتطابقة بعد (N) يوم" num_flaggers_to_close_topic: "الحد الأدنى لعدد البلاغات المطلوب لإيقاف موضوع مؤقتًا للتدخل" num_hours_to_close_topic: "عدد الساعات المطلوبة لإيقاف موضوع مؤقتًا للتدخل" auto_respond_to_flag_actions: "تفعيل الرد التلقائي عند تجاهل بلاغ" - min_first_post_typing_time: "الحد الأدنى للمدة الزمنية التي يجب على المستخدم الكتابة خلالها بالمللي ثانية، إذا لم يتم استيفاء الحد، فسيدخل المنشور تلقائيًا في قائمة انتظار الموافقة. اضبطها على 0 للإيقاف (لا يوصى بذلك)" + min_first_post_typing_time: "الحد الأدنى للمدة الزمنية التي يجب على المستخدم الكتابة خلالها بالمللي ثانية، إذا لم يتم استيفاء الحد، فسيدخل المنشور تلقائيًا في قائمة انتظار الموافقة. قم بالتعيين إلى 0 للإيقاف (لا يوصى بذلك)" auto_silence_fast_typers_on_first_post: "كتم المستخدمين الذين لا يلتزمون بمدة min_first_post_typing_time تلقائيًا" auto_silence_fast_typers_max_trust_level: "الحد الأقصى لمستوى الثقة المطلوب لكتم أصحاب الطباعة السريعة تلقائيًا" auto_silence_first_post_regex: "التعبير العادي غير الحساس لحالة الأحرف الذي إذا تم تمريره سيؤدي إلى كتم أول منشور بواسطة المستخدم وإرساله إلى قائمة انتظار الموافقة. مثال: ستؤدي كتابة raging|a[bc]a إلى كتم جميع المنشورات التي تتضمَّن raging أو aba أو aca في البداية. ينطبق ذلك على أول منشور فقط. تم إيقافه: استخدم \"كتم الكلمات المُراقَبة\" بدلًا منه." @@ -2061,17 +2078,22 @@ ar: block_auto_generated_emails: "حظر الرسائل الإلكترونية الواردة التي تم تحديدها على أنها منشأة تلقائيًا." ignore_by_title: "تجاهل الرسائل الإلكترونية الواردة بناءً على عنوانها" mailgun_api_key: "مفتاح API السري لمحرك Mailgun والمُستخدَم في التحقُّق من رسائل خطاف الويب" + sendgrid_verification_key: "مفتاح التحقق Sendgrid المُستخدَم للتحقق من رسائل خطاف الويب." + mailjet_webhook_token: "الرمز المُستخدَم للتحقق من حمولة خطاف الويب. يجب تمريره في صورة معلمة الاستعلام 't' لخطاف الويب؛ على سبيل المثال: https://example.com/webhook/mailjet?t=supersecret" + mandrill_authentication_key: "مفتاح المصادقة Mandrill المُستخدَم للتحقق من رسائل خطاف الويب." + postmark_webhook_token: "الرمز المُستخدَم للتحقق من حمولة خطاف الويب. يجب تمريره في صورة معلمة الاستعلام 't' لخطاف الويب؛ على سبيل المثال: https://example.com/webhook/postmark?t=supersecret" + sparkpost_webhook_token: "الرمز المُستخدَم للتحقق من حمولة خطاف الويب. يجب تمريره في صورة معلمة الاستعلام 't' لخطاف الويب؛ على سبيل المثال: https://example.com/webhook/sparkpost?t=supersecret" soft_bounce_score: "نقاط الارتداد التي تتم إضافتها إلى المستخدم عند حدوث ارتداد مؤقت" hard_bounce_score: "نقاط الارتداد التي تتم إضافتها إلى المستخدم عند حدوث ارتداد دائم" bounce_score_threshold: "أقصى عدد من نقاط الارتداد قبل أن نتوقف عن مراسلة مستخدم عبر البريد الإلكتروني" - reset_bounce_score_after_days: "إعادة ضبط نقاط الارتداد تلقائيًا بعد X يوم" + reset_bounce_score_after_days: "إعادة تعيين نقاط الارتداد تلقائيًا بعد X يوم" blocked_attachment_content_types: "قائمة الكلمات الرئيسية المُستخدَمة في حظر المرفقات بناءً على نوع المحتوى" blocked_attachment_filenames: "قائمة الكلمات الرئيسية المُستخدَمة في حظر المرفقات بناءً على اسم الملف" forwarded_emails_behaviour: "كيفية التعامل مع البريد الإلكتروني المُعاد توجيهه إلى Discourse" always_show_trimmed_content: "عرض جزء مقتطع من الرسائل الإلكترونية الواردة بشكلٍ دائم. تحذير: قد يكشف هذا الجزء عناوين البريد الإلكتروني." trim_incoming_emails: "اقتطاع جزء من الرسائل الإلكترونية الواردة غير ذات الصلة" private_email: "عدم تضمين محتوى من منشورات أو موضوعات في عنوان الرسالة الإلكترونية أو نص الرسالة الإلكترونية. ملاحظة: يؤدي أيضًا إلى إيقاف ملخصات البريد الإلكتروني." - email_total_attachment_size_limit_kb: "أقصى حجم إجمالي بالكيلوبايت للملفات المُرفَقة بالرسائل الإلكترونية الصادرة. اضبط القيمة على 0 لإيقاف إرسال المرفقات." + email_total_attachment_size_limit_kb: "أقصى حجم إجمالي بالكيلوبايت للملفات المُرفَقة بالرسائل الإلكترونية الصادرة. اتعيين القيمة على 0 لإيقاف إرسال المرفقات." post_excerpts_in_emails: "إرسال المقتطفات بدلًا من المنشورات الكاملة بشكلٍ دائم في رسائل الإشعارات عبر البريد الإلكتروني" raw_email_max_length: "عدد الأحرف التي يجب تخزينها للرسائل الإلكترونية الواردة" raw_rejected_email_max_length: "عدد الأحرف التي يجب تخزينها للرسائل الإلكترونية الواردة التي تم رفضها" @@ -2100,14 +2122,14 @@ ar: imap_polling_old_emails: "الحد الأقصى لعدد الرسائل الإلكترونية القديمة (المُعالَجة) التي سيتم تحديثها في كل مرة يتم فيها الاستقصاء عن مربع IMAP (0 للجميع)" imap_polling_new_emails: "الحد الأقصى لعدد الرسائل الإلكترونية الجديدة (غير المُعالَجة) المطلوب تحديثها في كل مرة يتم فيها الاستقصاء عن مربع IMAP" imap_batch_import_email: "الحد الأدنى لعدد الرسائل الإلكترونية الجديدة التي تؤدي إلى تشغيل وضع الاستيراد (يوقف تنبيهات المنشور)" - email_prefix: "[label] المُستخدَم في موضوع الرسائل الإلكترونية. سيتم استخدام \"العنوان\" افتراضيًا إذا لم يتم ضبطه." + email_prefix: "[label] المُستخدَم في موضوع الرسائل الإلكترونية. سيتم استخدام \"العنوان\" افتراضيًا إذا لم يتم تعيينه." email_site_title: "عنوان الموقع المُستخدَم كمُرسِل للرسائل الإلكترونية من الموقع. سيتم استخدام \"العنوان\" افتراضيًا إذا لم يتم تحديده. إذا كان \"العنوان\" يحتوي على أحرف غير مسموح بها في سلاسل مُرسِل الرسالة الإلكترونية، فاستخدم هذا الإعداد." find_related_post_with_key: "استخدام `reply key` فقط للعثور على المنشور الذي تم الرد عليه. تحذير: يسمح إيقاف هذا الإعداد بانتحال هوية المستخدم بناءً على عنوان البريد الإلكتروني." minimum_topics_similar: "عدد الموضوعات التي يجب أن تكون موجودة قبل تقديم موضوعات مشابهة عند كتابة موضوعات جديدة" relative_date_duration: "عدد الأيام بعد النشر التي سيتم بعدها عرض تواريخ النشر بشكلٍ نسبي (7 أيام) بدلًا من عرضها بشكلٍ مطلق (20 فبراير)" delete_user_max_post_age: "عدم السماح بحذف المستخدمين الذين مضى على منشورهم الأول أكثر من (x) يوم" delete_all_posts_max: "الحد الأقصى لعدد المنشورات التي يمكن حذفها دفعة واحدة باستخدام الزر \"حذف جميع المنشورات\". إذا كان لدى المستخدم أكثر من هذا العدد من المنشورات، فلا يمكن حذف جميع المنشورات دفعة واحدة ولا يمكن حذف المستخدم." - delete_user_self_max_post_count: "الحد الأقصى لعدد المنشورات التي يمكن للمستخدم إنشاؤها مع السماح بالحذف الذاتي للحساب. اضبطها على -1 لإيقاف الحذف الذاتي للحساب." + delete_user_self_max_post_count: "الحد الأقصى لعدد المنشورات التي يمكن للمستخدم إنشاؤها مع السماح بالحذف الذاتي للحساب. قم بالتعيين إلى -1 لإيقاف الحذف الذاتي للحساب." username_change_period: "الحد الأقصى لعدد الأيام بعد التسجيل التي يمكن للحسابات فيها تغيير اسم المستخدم (0 لعدم السماح بتغيير اسم المستخدم)" email_editable: "السماح للمستخدمين بتغيير عنوان بريدهم الإلكتروني بعد التسجيل" logout_redirect: "الموقع الذي سيتم إعادة توجيه المتصفح إليه بعد تسجيل الخروج (على سبيل المثال: https://example.com/logout)" @@ -2132,10 +2154,10 @@ ar: enable_user_directory: "توفير دليل للمستخدمين للتصفُّح" enable_group_directory: "توفير دليل بالمجموعات للتصفُّح" enable_category_group_moderation: "السماح للمجموعات بالإشراف على المحتوى في فئات معيَّنة" - group_in_subject: "ضبط %%{optional_pm} في موضوع الرسالة الإلكترونية على اسم المجموعة الأولى في رسالة خاصة، راجع: تخصيص تنسيق الموضوع للرسائل الإلكترونية القياسية" + group_in_subject: "لتعيين %%{optional_pm} في موضوع الرسالة الإلكترونية على اسم المجموعة الأولى في رسالة خاصة، راجع: تخصيص تنسيق الموضوع للرسائل الإلكترونية القياسية" allow_anonymous_posting: "السماح للمستخدمين بالتبديل إلى الوضع المجهول" anonymous_posting_min_trust_level: "الحد الأدنى لمستوى الثقة المطلوب لتفعيل النشر المجهول" - anonymous_account_duration_minutes: "لحماية إخفاء الهوية، أنشئ حسابًا مجهولًا جديدًا كل N دقيقة لكل مستخدم. مثال: إذا تم الضبط على 600، سيتم إنشاء حساب مجهول جديد بمجرد مرور 600 دقيقة على آخر منشور وتحوُّل المستخدم إلى مجهول." + anonymous_account_duration_minutes: "لحماية إخفاء الهوية، أنشئ حسابًا مجهولًا جديدًا كل N دقيقة لكل مستخدم. مثال: إذا تم التعيين إلى 600، سيتم إنشاء حساب مجهول جديد بمجرد مرور 600 دقيقة على آخر منشور وتحوُّل المستخدم إلى مجهول." hide_user_profiles_from_public: "إيقاف بطاقات المستخدمين والملفات الشخصية لهم ودليل المستخدم للمستخدمين المجهولين" allow_users_to_hide_profile: "السماح للمستخدمين بإخفاء ملفاتهم الشخصية ووجودهم" allow_featured_topic_on_user_profiles: "السماح للمستخدمين بعرض رابط لموضوع على بطاقة المستخدم والملف الشخصي" @@ -2144,10 +2166,10 @@ ar: log_personal_messages_views: "تسجيل مرات عرض الرسائل الشخصية بواسطة المسؤول للمستخدمين/المجموعات الأخرى" ignored_users_count_message_threshold: "إرسال إشعار إلى المشرفين إذا تم تجاهل مستخدم معيَّن بواسطة هذا العدد من المستخدمين الآخرين" ignored_users_message_gap_days: "وقت الانتظار قبل إرسال إشعار إلى المشرفين مرة أخرى بشأن مستخدم تم تجاهله بواسطة الكثيرين" - clean_up_inactive_users_after_days: "عدد الأيام قبل إزالة مستخدم غير نشط (مستوى الثقة 0 بلا أي منشورات). اضبط القيمة على 0 لإيقاف التنظيف." - clean_up_unused_staged_users_after_days: "عدد الأيام قبل إزالة مستخدم مؤقت غير مُستخدَم (مستوى الثقة 0 بلا أي منشورات). اضبط القيمة على 0 لإيقاف التنظيف." - user_selected_primary_groups: "السماح للمستخدمين بضبط مجموعتهم الأساسية" - max_notifications_per_user: "الحد الأقصى من الإشعارات لكل مستخدم، سيتم حذف الإشعارات القديمة إذا تم تجاوز هذا الرقم. يتم فرض هذا الإعداد أسبوعيًا. اضبط القيمة على 0 للإيقاف." + clean_up_inactive_users_after_days: "عدد الأيام قبل إزالة مستخدم غير نشط (مستوى الثقة 0 بلا أي منشورات). اتعيين القيمة على 0 لإيقاف التنظيف." + clean_up_unused_staged_users_after_days: "عدد الأيام قبل إزالة مستخدم مؤقت غير مُستخدَم (مستوى الثقة 0 بلا أي منشورات). اتعيين القيمة على 0 لإيقاف التنظيف." + user_selected_primary_groups: "السماح للمستخدمين بتعيين مجموعتهم الأساسية" + max_notifications_per_user: "الحد الأقصى من الإشعارات لكل مستخدم، سيتم حذف الإشعارات القديمة إذا تم تجاوز هذا الرقم. يتم فرض هذا الإعداد أسبوعيًا. اتعيين القيمة على 0 للإيقاف." allowed_user_website_domains: "سيتم التحقُّق من الموقع الإلكتروني للمستخدم مقابل هذه النطاقات. قائمة مفصولة بشرائط عمودية." allow_profile_backgrounds: "السماح للمستخدمين بتحميل خلفيات للملفات الشخصية" sequential_replies_threshold: "عدد المنشورات التي يجب على المستخدم إنشاؤها في على التوالي في الموضوع قبل أن يتم تذكيره بوجود عدد كبير جدًا من الردود المتسلسلة" @@ -2163,7 +2185,7 @@ ar: disable_system_edit_notifications: "إيقاف إشعارات التعديل بواسطة مستخدم النظام عندما يكون \"download_remote_images_to_local\" نشطًا." disable_category_edit_notifications: "إيقاف إشعارات تعديل الفئة في الموضوعات." disable_tags_edit_notifications: "إيقاف إشعارات تعديل الوسوم في الموضوعات." - notification_consolidation_threshold: "عدد إشعارات الإعجاب أو طلبات العضوية المتلقاة قبل دمج الإشعارات في رسالة واحدة. اضبط القيمة على 0 للإيقاف." + notification_consolidation_threshold: "عدد إشعارات الإعجاب أو طلبات العضوية المتلقاة قبل دمج الإشعارات في رسالة واحدة. اتعيين القيمة على 0 للإيقاف." likes_notification_consolidation_window_mins: "المدة بالدقائق التي يتم فيها دمج إشعارات الإعجاب في إشعار واحد بمجرد الوصول إلى الحد الأقصى. يمكن إعداد الحد الأقصى عبر `SiteSetting.notification_consolidation_threshold`." automatically_unpin_topics: "إلغاء تثبيت الموضوعات تلقائيًا عندما يصل المستخدم إلى النهاية" read_time_word_count: "عدد الكلمات في الدقيقة لحساب الوقت المقدَّر للقراءة" @@ -2173,22 +2195,23 @@ ar: app_association_android: "محتويات نقطة نهاية .well-known/assetlinks.json المُستخدَمة في API لروابط الأصول الرقمية من Google" app_association_ios: "محتويات نقطة نهاية apple-app-site-association المُستخدَمة لإنشاء روابط عامة بين هذا الموقع وتطبيقات iOS" share_anonymized_statistics: "مشاركة إحصاءات الاستخدام المجهولة" - auto_handle_queued_age: "التعامل تلقائيًا مع السجلات التي تنتظر المراجعة بعد هذا العدد من الأيام. سيتم تجاهل البلاغات. وسيتم رفض المنشورات والمستخدمين في قائمة الانتظار. اضبط القيمة على 0 لإيقاف هذه الميزة." + auto_handle_queued_age: "التعامل تلقائيًا مع السجلات التي تنتظر المراجعة بعد هذا العدد من الأيام. سيتم تجاهل البلاغات. وسيتم رفض المنشورات والمستخدمين في قائمة الانتظار. اتعيين القيمة على 0 لإيقاف هذه الميزة." penalty_step_hours: "العقوبات الافتراضية لكتم المستخدمين أو تعليقهم بالساعات. يتم إعداد الإساءة الأولى بشكلٍ افتراضي على القيمة الأولى، والإساءة الثانية بشكلٍ افتراضي على القيمة الثانية وهكذا." svg_icon_subset: "إضافة 5 أيقونات إضافية من FontAwesome ترغب في تضمينها في أصولك. استخدم البادئة \"-fa\" للرموز الثابتة، و\"-far\" للرموز العادية و\"-fab\" لرموز الوسوم التجارية." - max_prints_per_hour_per_user: "الحد الأقصى لعدد مرات ظهور الصفحة /print (اضبط القيمة على 0 للإيقاف)" + max_prints_per_hour_per_user: "الحد الأقصى لعدد مرات ظهور الصفحة /print (اتعيين القيمة على 0 للإيقاف)" full_name_required: "حقل الاسم الكامل مطلوب في الملف الشخصي للمستخدم" enable_names: "إظهار الاسم الكامل للمستخدم في ملفه الشخصي وبطاقة المستخدم والرسائل الإلكترونية. يمكنك إيقاف هذا الإعداد لإخفاء الاسم الكامل في كل مكان." - display_name_on_posts: "إظهار الاسم الكامل للمستخدم في منشوراته بالإضافة إلى اسم المستخدم الخاص به @username" + display_name_on_posts: "إظهار الاسم الكامل للمستخدم في منشوراته بالإضافة إلى اسم المستخدم الخاص به @اسم المستخدم" show_time_gap_days: "عرض الفجوة الزمنية في الموضوع إذا تم إنشاء منشورين بفارق هذا العدد من الأيام" short_progress_text_threshold: "بعد أن يتجاوز عدد المنشورات في الموضوع هذا الرقم، سيعرض شريط التقدُّم رقم المنشور الحالي فقط. إذا غيَّرت عرض شريط التقدُّم، فقد تحتاج إلى تغيير هذه القيمة." - warn_reviving_old_topic_age: "سيتم عرض تحذير عندما يبدأ شخص ما في الرد على موضوع يكون فيه الرد الأخير أقدم من هذا العدد من الأيام. يمكنك إيقاف هذا الإعداد عن طريق الضبط على 0." + default_code_lang: "تمييز جملة لغة البرمجة الافتراضية المطبَّق على كتل الرموز البرمجية (auto، text، ruby، python، إلى آخره). يجب أن تكون القيمة حاضرة أيضًا في إعداد الموقع `highlighted languages`." + warn_reviving_old_topic_age: "سيتم عرض تحذير عندما يبدأ شخص ما في الرد على موضوع يكون فيه الرد الأخير أقدم من هذا العدد من الأيام. يمكنك إيقاف هذا الإعداد عن طريق التعيين إلى 0." autohighlight_all_code: "فرض تمييز التعليمات البرمجية على جميع كتل التعليمات البرمجية مسبقة التنسيق حتى في حال عدم تحديد اللغة بشكلٍ صريح." highlighted_languages: "القواعد المضمَّنة لتمييز البنية. (تحذير: قد يؤثر تضمين عدد كبير جدًا من اللغات على الأداء) راجع: https://highlightjs.org/static/demo للحصول على عرض توضيحي" show_copy_button_on_codeblocks: "إضافة زر إلى كتل الرموز البرمجية لنسخ محتويات الكتل إلى حافظة المستخدم." embed_any_origin: "السماح بالمحتوى القابل للتضمين بغض النظر عن المصدر. هذا الإعداد مطلوب لتطبيقات الأجهزة الجوَّالة ذات نموذج HTML ثابت." embed_topics_list: "دعم تضمين HTML لقوائم الموضوعات" - embed_set_canonical_url: "ضبط عنوان URL الأساسي للموضوعات المضمَّنة على عنوان URL للمحتوى المضمَّن" + embed_set_canonical_url: "تعيين عنوان URL الأساسي للموضوعات المضمَّنة على عنوان URL للمحتوى المضمَّن" embed_truncate: "اقتطاع المنشورات المضمَّنة" embed_unlisted: "سيتم إلغاء إدراج الموضوعات المستوردة حتى يرد المستخدم." embed_support_markdown: "دعم تنسيق Markdown للمنشورات المضمَّنة" @@ -2196,11 +2219,11 @@ ar: allowed_href_schemes: "الأنظمة المسموح بها في الروابط بالإضافة إلى http وhttps" embed_post_limit: "الحد الأقصى لعدد المنشورات التي سيتم تضمينها" embed_username_required: "اسم المستخدم مطلوب لإنشاء الموضوع" - notify_about_flags_after: "إرسال رسالة خاصة إلى المشرفين إذا كانت هناك بلاغات لم يتم التعامل معها بعد هذا العدد من الساعات. اضبط القيمة على 0 للإيقاف." + notify_about_flags_after: "إرسال رسالة خاصة إلى المشرفين إذا كانت هناك بلاغات لم يتم التعامل معها بعد هذا العدد من الساعات. اتعيين القيمة على 0 للإيقاف." show_create_topics_notice: "إظهار إشعار يطلب من المسؤولين إنشاء بعض الموضوعات إذا كان الموقع يحتوي على أقل من 5 موضوعات عامة" delete_drafts_older_than_n_days: "حذف المسودات الأقدم من (n) يوم" - delete_merged_stub_topics_after_days: "عدد الأيام التي يجب انتظارها قبل حذف الموضوعات البديلة المدمجة بالكامل تلقائيًا. اضبط القيمة على 0 لعدم حذف الموضوعات البديلة أبدًا." - bootstrap_mode_min_users: "الحد الأدنى لعدد المستخدمين المطلوب لإيقاف وضع التمهيد (اضبط القيمة على 0 للإيقاف)" + delete_merged_stub_topics_after_days: "عدد الأيام التي يجب انتظارها قبل حذف الموضوعات البديلة المدمجة بالكامل تلقائيًا. اتعيين القيمة على 0 لعدم حذف الموضوعات البديلة أبدًا." + bootstrap_mode_min_users: "الحد الأدنى لعدد المستخدمين المطلوب لإيقاف وضع التمهيد (اتعيين القيمة على 0 للإيقاف)" prevent_anons_from_downloading_files: "منع المستخدمين المجهولين من تنزيل المرفقات" secure_media: "تم إيقافه: استخدم الإعداد secure_uploads بدلًا من ذلك، ستتم إزالته في Discourse 3.0." secure_uploads: 'يقيِّد الوصول إلى كل التحميلات (الصور ومقاطع الفيديو والمقاطع الصوتية والنصوص وملفات PDF وملفات ZIP وغير ذلك). بخلاف ذلك، سيكون الوصول مقيَّدًا فقط لتحميلات الوسائط في الرسائل والفئات الخاصة. تحذير: هذا الإعداد معقَّد ويتطلب فهمًا إداريًا عميقًا. انظر موضوع التحميلات الآمنة على Meta لمعرفة التفاصيل.' @@ -2218,15 +2241,15 @@ ar: approve_unless_trust_level: "ضرورة الموافقة على المنشورات للمستخدمين دون مستوى الثقة هذا" approve_new_topics_unless_trust_level: "ضرورة الموافقة على المنشورات للمستخدمين دون مستوى الثقة هذا" approve_unless_staged: "ضرورة الموافقة على الموضوعات والمنشورات الجديدة للمستخدمين المؤقتين" - notify_about_queued_posts_after: "إرسال إشعار إلى جميع المشرفين إذا كانت هناك منشورات تنتظر المراجعة لأكثر من هذا العدد من الساعات. اضبط القيمة على 0 لإيقاف هذه الإشعارات." + notify_about_queued_posts_after: "إرسال إشعار إلى جميع المشرفين إذا كانت هناك منشورات تنتظر المراجعة لأكثر من هذا العدد من الساعات. اتعيين القيمة على 0 لإيقاف هذه الإشعارات." auto_close_messages_post_count: "الحد الأقصى لعدد المنشورات المسموح بها في رسالة قبل إغلاقها تلقائيًا (0 للإيقاف)" auto_close_topics_post_count: "الحد الأقصى لعدد المنشورات المسموح بها في موضوع قبل إغلاقه تلقائيًا (0 للإيقاف)" auto_close_topics_create_linked_topic: "إنشاء موضوع مرتبط جديد عندما يكون الموضوع مغلقًا تلقائيًا بناءً على الإعداد `auto close topics post count`" - code_formatting_style: "ضبط زر الرموز البرمجية في أداة الإنشاء بشكلٍ افتراضي على نمط تنسيق الرموز البرمجية هذا" + code_formatting_style: "تعيين زر الرموز البرمجية في أداة الإنشاء بشكلٍ افتراضي على نمط تنسيق الرموز البرمجية هذا" max_allowed_message_recipients: "الحد الأقصى المسموح به من المستلمين في الرسالة" watched_words_regular_expressions: "الكلمات المُراقَبة هي تعبيرات عادية" enable_diffhtml_preview: "الميزة التجريبية التي تستخدم diffHTML لمزامنة المعاينة بدلًا من إعادة العرض بالكامل" - enable_fast_edit: "يفعِّل تحديد جزء صغير من نص المنشور ليتم تعديله مباشرةً." + enable_fast_edit: "يقم بتمكين تحديد جزء صغير من نص المنشور ليتم تعديله مباشرةً." old_post_notice_days: "عدد الأيام قبل أن يصبح الإشعار بشأن المنشور قديمًا" new_user_notice_tl: "الحد الأدنى لمستوى الثقة المطلوب لرؤية الإشعارات بشأن منشور مستخدم جديد" returning_user_notice_tl: "الحد الأدنى من مستوى الثقة المطلوب لرؤية الإشعارات بشأن منشور مستخدم عائد" @@ -2238,8 +2261,8 @@ ar: skip_auto_delete_reply_likes: "تخطي حذف المنشورات التي تحتوي على هذا العدد من تسجيلات الإعجاب أو أكثر عند حذف الردود القديمة تلقائيًا" default_email_digest_frequency: "عدد المرات التي يتلقى فيها المستخدمون رسائل إلكترونية تلخيصية بشكلٍ افتراضي" default_include_tl0_in_digests: "تضمين المنشورات من المستخدمين الجُدد في الرسائل الإلكترونية التلخيصية بشكلٍ افتراضي. يمكن للمستخدمين تغيير ذلك في تفضيلاتهم." - default_email_level: "ضبط المستوى الافتراضي لإرسال الإشعارات عبر البريد الإلكتروني للموضوعات العادية" - default_email_messages_level: "ضبط المستوى الافتراضي لإرسال الإشعارات عبر البريد الإلكتروني عندما يراسل شخص ما المستخدم" + default_email_level: "تعيين المستوى الافتراضي لإرسال الإشعارات عبر البريد الإلكتروني للموضوعات العادية" + default_email_messages_level: "تعيين المستوى الافتراضي لإرسال الإشعارات عبر البريد الإلكتروني عندما يراسل شخص ما المستخدم" default_email_mailing_list_mode: "إرسال رسالة إلكترونية لكل منشور جديد بشكلٍ افتراضي" default_email_mailing_list_mode_frequency: "سيتلقى المستخدمون الذين فعَّلوا وضع القائمة البريدية الرسائل الإلكترونية لهذا العدد من المرات بشكلٍ افتراضي" disable_mailing_list_mode: "عدم السماح للمستخدمين بتفعيل وضع القائمة البريدية (يمنع إرسال أي رسائل إلكترونية من القائمة البريدية.)" @@ -2261,7 +2284,7 @@ ar: default_categories_muted: "قائمة الفئات التي يتم كتم صوتها بشكلٍ افتراضي" default_categories_watching_first_post: "قائمة الفئات التي سيتم فيها مراقبة أول منشور في كل موضوع جديد بشكلٍ افتراضي." default_categories_normal: "قائمة الفئات التي لا يتم كتمها بشكلٍ افتراضي. يكون هذا الإعداد مفيدًا عند تفعيل إعداد الموقع `mute_all_categories_by_default`." - mute_all_categories_by_default: "ضبط مستوى الإشعارات الافتراضي لجميع الفئات على الكتم. يتطلب من المستخدمين الاشتراك في الفئات لتظهر في صفحات \"الأحدث\" و\"الفئات\". إذا كنت ترغب في تعديل الإعدادات الافتراضية للمستخدمين المجهولين، فاضبط إعدادات `default_categories_`." + mute_all_categories_by_default: "تعيين مستوى الإشعارات الافتراضي لجميع الفئات على الكتم. يتطلب من المستخدمين الاشتراك في الفئات لتظهر في صفحات \"الأحدث\" و\"الفئات\". إذا كنت ترغب في تعديل الإعدادات الافتراضية للمستخدمين المجهولين، فاتعيين إعدادات `default_categories_`." default_tags_watching: "قائمة الوسوم التي تتم مراقبتها بشكلٍ افتراضي" default_tags_tracking: "قائمة الوسوم التي يتم تتبُّعها بشكلٍ افتراضي" default_tags_muted: "قائمة الوسوم التي يتم كتمها بشكلٍ افتراضي" @@ -2270,13 +2293,13 @@ ar: default_title_count_mode: "الوضع الافتراضي لعداد عنوان الصفحة" retain_web_hook_events_period_days: "عدد أيام الاحتفاظ بسجلات أحداث خطافات الويب" retry_web_hook_events: "إعادة المحاولة تلقائيًا 4 مرات لأحداث خطافات الويب التي فشلت. الفجوات الزمنية بين مرات إعادة المحاولة هي 1 و5 و25 و125 دقيقة." - revoke_api_keys_days: "عدد الأيام قبل إلغاء مفتاح API غير المُستخدَم تلقائيًا (0 للضبط على \"أبدًا\")" + revoke_api_keys_days: "عدد الأيام قبل إلغاء مفتاح API غير المُستخدَم تلقائيًا (0 للتعيين على \"أبدًا\")" allow_user_api_keys: "السماح بإنشاء مفاتيح API للمستخدم" allow_user_api_key_scopes: "قائمة النطاقات المسموح بها لمفاتيح API للمستخدم" min_trust_level_for_user_api_key: "مستوى الثقة المطلوب لإنشاء مفاتيح API للمستخدم" allowed_user_api_auth_redirects: "عنوان URL المسموح به لإعادة توجيه المصادقة لمفاتيح API للمستخدم. يمكن استخدام رمز البدل * لمطابقة أي جزء منه (على سبيل المثال،www.example.com/*)." allowed_user_api_push_urls: "عناوين URL المسموح لها بإرسال المعلومات إلى API للمستخدم قبل طلبها من الخادم" - expire_user_api_keys_days: "عدد الأيام قبل انتهاء صلاحية مفتاح API للمستخدم تلقائيًا (0 للضبط على \"أبدًا\")" + expire_user_api_keys_days: "عدد الأيام قبل انتهاء صلاحية مفتاح API للمستخدم تلقائيًا (0 للتعيين على \"أبدًا\")" tagging_enabled: "السماح بالوسوم في الموضوعات؟" min_trust_to_create_tag: "الحد الأدنى لمستوى الثقة المطلوب لإنشاء وسم" max_tags_per_topic: "الحد الأقصى لعدد الوسوم التي يمكن تطبيقها على موضوع" @@ -2298,6 +2321,8 @@ ar: shared_drafts_min_trust_level: "السماح للمستخدمين برؤية المسودات المشتركة وتعديلها" push_notifications_prompt: "عرض رسالة المطالبة بموافقة المستخدم" push_notifications_icon: "رمز الشارة الذي يظهر في ركن الإشعارات. يوصى باستخدام صورة PNG أحادية اللون بمقاس 96 × 96 وخلفية شفافة." + enable_desktop_push_notifications: "تمكين الإشعارات الفورية على سطح المكتب" + push_notification_time_window_mins: "الانتظار (n) من الدقائق قبل إرسال إشعار فوري. يساعد ذلك على منع إرسال الإشعارات الفورية إلى مستخدم متصل ونشط." base_font: "الخط الأساسي الذي سيتم استخدامه لمعظم النصوص على الموقع. يمكن تجاوز السمات عبر خاصية CSS المخصَّصة `--font-family`." heading_font: "الخط الذي سيتم استخدامه للعناوين على الموقع. يمكن تجاوز السمات عبر خاصية CSS المخصَّصة `--heading-font-family`." enable_sitemap: "إنشاء خريطة موقع لموقعك وتضمينها في ملف robots.txt." @@ -2319,9 +2344,13 @@ ar: use_name_for_username_suggestions: "استخدام الاسم الكامل للمستخدم عند اقتراح أسماء المستخدمين." suggest_weekends_in_date_pickers: "تضمين عطلات نهاية الأسبوع (السبت والأحد) في اقتراحات منتقي التاريخ (يمكنك إيقاف هذا الإعداد إذا كنت تستخدم Discouse في أيام الأسبوع فقط؛ أي من الاثنين إلى الجمعة)." splash_screen: "يعرض شاشة تحميل مؤقتة أثناء تحميل أصول الموقع" + navigation_menu: "تحديد قائمة التنقل التي سيتم استخدامها. يمكن للمستخدمين تخصيص الشريط الجانبي وقائمة التنقل في رأس الصفحة. يتوفَّر خيار \"القائمة القديمة\" للتوافق مع الإصدارات السابقة." default_sidebar_categories: "سيتم عرض الفئات المحدَّدة ضمن قسم فئات الشريط الجانبي بشكلٍ افتراضي." default_sidebar_tags: "سيتم عرض الوسوم المحدَّدة ضمن قسم فئات الشريط الجانبي بشكلٍ افتراضي." + enable_new_notifications_menu: "يمكِّن قائمة الإشعارات الجديدة لقائمة التنقل القديمة." enable_new_user_profile_nav_groups: "تجريبي: سيتم عرض قائمة التنقل الخاصة بملف تعريف المستخدم الجديد لمستخدمي المجموعات المحدَّدة" + enable_experimental_topic_timeline_groups: "تجريبي: سيظهر الجدول الزمني المُعاد تصميمه لمستخدمي المجموعات المحدَّدة" + enable_experimental_hashtag_autocomplete: "تجريبي: استخدم نظام الإكمال التلقائي #hashtag الجديد للفئات والوسوم التي تعرض العنصر المحدَّد بشكلٍ مختلف وحسَّنت نتائج البحث" errors: invalid_css_color: "لون غير صالح. أدخِل اسم لون أو قيمة سداسية عشرية." invalid_email: "عنوان البريد الإلكتروني غير صالح." @@ -2342,12 +2371,12 @@ ar: invalid_reply_by_email_address: "يجب أن تحتوي القيمة على \"%{reply_key}\" وأن تكون مختلفة عن الرسالة الإلكترونية للإشعار." invalid_alternative_reply_by_email_addresses: "يجب أن تحتوي جميع القيم على \"%{reply_key}\" وأن تكون مختلفة عن الرسالة الإلكترونية للإشعار." invalid_domain_hostname: "يجب ألا يتضمَّن الرمز * أو ؟." - pop3_polling_host_is_empty: "يجب عليك ضبط `pop3 polling host` قبل تفعيل استقصاء POP3." - pop3_polling_username_is_empty: "يجب عليك ضبط `pop3 polling username` قبل تفعيل استقصاء POP3." - pop3_polling_password_is_empty: "يجب عليك ضبط `pop3 polling password` قبل تفعيل استقصاء POP3." + pop3_polling_host_is_empty: "يجب عليك تعيين `pop3 polling host` قبل تفعيل استقصاء POP3." + pop3_polling_username_is_empty: "يجب عليك تعيين `pop3 polling username` قبل تفعيل استقصاء POP3." + pop3_polling_password_is_empty: "يجب عليك تعيين `pop3 polling password` قبل تفعيل استقصاء POP3." pop3_polling_authentication_failed: "فشلت مصادقة POP3. يُرجى التحقُّق من بيانات اعتماد pop3 لديك." - reply_by_email_address_is_empty: "يجب عليك ضبط `reply by email address` قبل تفعيل الرد عن طريق البريد الإلكتروني." - email_polling_disabled: "يجب عليك ضبط الاستقصاء اليدوي أو من خلال POP3 قبل تفعيل الرد عن طريق البريد الإلكتروني." + reply_by_email_address_is_empty: "يجب عليك تعيين `reply by email address` قبل تفعيل الرد عن طريق البريد الإلكتروني." + email_polling_disabled: "يجب عليك تعيين الاستقصاء اليدوي أو من خلال POP3 قبل تفعيل الرد عن طريق البريد الإلكتروني." user_locale_not_enabled: "يجب عليك تفعيل `allow user locale` أولًا قبل تفعيل هذا الإعداد." personal_message_enabled_groups_invalid: "يجب عليك تحديد مجموعة واحدة على الأقل لهذا الإعداد. إذا كنت لا تريد أن يقوم أي شخص باستثناء فريق العمل بإرسال الرسائل الشخصية، فاختر مجموعة فريق العمل." invalid_regex: "التعبير العادي غير صالح أو غير مسموح به." @@ -2355,30 +2384,31 @@ ar: email_editable_enabled: "يجب إيقاف `email editable` قبل تفعيل هذا الإعداد." staged_users_disabled: "يجب عليك تفعيل `staged users` أولًا قبل تفعيل هذا الإعداد." reply_by_email_disabled: "يجب عليك تفعيل `reply by email` أولًا قبل تفعيل هذا الإعداد." - discourse_connect_url_is_empty: "يجب عليك ضبط `discourse connect url` قبل تفعيل هذا الإعداد." + discourse_connect_url_is_empty: "يجب عليك تعيين `discourse connect url` قبل تفعيل هذا الإعداد." discourse_connect_invite_only: "لا يمكنك تفعيل DiscourseConnect والدعوة فقط في الوقت نفسه." enable_local_logins_disabled: "يجب عليك تفعيل `enable local logins` أولًا قبل تفعيل هذا الإعداد." - min_username_length_exists: "لا يمكنك ضبط الحد الأدنى لطول اسم المستخدم على قيمة أطول من أقصر اسم مستخدم (%{username})." - min_username_length_range: "لا يمكنك ضبط الحد الأدنى ليكون أكبر من الحد الأقصى." - max_username_length_exists: "لا يمكنك ضبط الحد الأقصى لطول اسم المستخدم على قيمة أقصر من أطول اسم مستخدم (%{username})." - max_username_length_range: "لا يمكنك ضبط الحد الأدنى ليكون أقل من الحد الأقصى." + min_username_length_exists: "لا يمكنك تعيين الحد الأدنى لطول اسم المستخدم على قيمة أطول من أقصر اسم مستخدم (%{username})." + min_username_length_range: "لا يمكنك تعيين الحد الأدنى ليكون أكبر من الحد الأقصى." + max_username_length_exists: "لا يمكنك تعيين الحد الأقصى لطول اسم المستخدم على قيمة أقصر من أطول اسم مستخدم (%{username})." + max_username_length_range: "لا يمكنك تعيين الحد الأدنى ليكون أقل من الحد الأقصى." invalid_hex_value: "يجب أن تكون قيم اللون عبارة عن رموز سداسية عشرية مكوَّنة من 6 أرقام." empty_selectable_avatars: "يجب عليك تحميل صورتَين رمزيَّتَين قابلتَين للتحديد على الأقل قبل تفعيل هذا الإعداد." category_search_priority: - low_weight_invalid: "لا يمكنك ضبط الأهمية على قيمة أكبر من أو تساوي 1." - high_weight_invalid: "لا يمكنك ضبط الأهمية على قيمة أقل من أو تساوي 1." + low_weight_invalid: "لا يمكنك تعيين الأهمية على قيمة أكبر من أو تساوي 1." + high_weight_invalid: "لا يمكنك تعيين الأهمية على قيمة أقل من أو تساوي 1." allowed_unicode_usernames: regex_invalid: "التعبير العادي غير صالح: %{error}" leading_trailing_slash: "يجب ألا يبدأ التعبير العادي بشرطة مائلة وينتهي بها." unicode_usernames_avatars: "لا تدعم الصور الرمزية الداخلية للنظام أسماء المستخدمين بترميز Unicode." - list_value_count: "يجب أن تحتوي القائمة على قيم %{count} بالضبط." + list_value_count: "يجب أن تحتوي القائمة على قيم %{count} بالتعيين." markdown_linkify_tlds: "لا يمكنك تضمين قيمة '*'." - google_oauth2_hd_groups: "يجب عليك ضبط جميع إعدادات 'google oauth2 hd' قبل تفعيل هذا الإعداد." + google_oauth2_hd_groups: "يجب عليك تعيين جميع إعدادات 'google oauth2 hd' قبل تفعيل هذا الإعداد." search_tokenize_chinese_enabled: "يجب عليك إيقاف `email editable` قبل تفعيل هذا الإعداد." search_tokenize_japanese_enabled: "يجب عليك إيقاف 'search_tokenize_japanese' قبل تفعيل هذا الإعداد." discourse_connect_cannot_be_enabled_if_second_factor_enforced: "لا يمكنك تفعيل DiscourseConnect في حال فرض المصادقة الثنائية." - delete_rejected_email_after_days: "لا يمكن ضبط هذا الإعداد على أقل من الإعداد delete_email_logs_after_days أو أكثر من %{max}" + delete_rejected_email_after_days: "لا يمكن تعيين هذا الإعداد على أقل من الإعداد delete_email_logs_after_days أو أكثر من %{max}" invalid_uncategorized_category_setting: "لا يمكن تحديد الفئة غير المصنَّفة إذا كان السماح بالموضوعات غير المصنَّفة غير مسموح به" + enable_new_notifications_menu_not_legacy_navigation_menu: "يجب عليك تعيين `navigation_menu` إلى `legacy` قبل تمكين هذا الإعداد." placeholder: discourse_connect_provider_secrets: key: "www.example.com" @@ -2580,7 +2610,7 @@ ar: not_activated: "لا يمكنك تسجيل الدخول بعد. لقد أرسلنا إليك رسالة إلكترونية للتفعيل. يُرجى اتباع التعليمات الواردة في الرسالة الإلكترونية لتفعيل حسابك." not_allowed_from_ip_address: "لا يمكنك تسجيل الدخول باسم %{username} من عنوان IP هذا." admin_not_allowed_from_ip_address: "لا يمكنك تسجيل الدخول كمسؤول من عنوان IP هذا." - reset_not_allowed_from_ip_address: "لا يمكنك طلب إعادة ضبط كلمة المرور من عنوان IP هذا." + reset_not_allowed_from_ip_address: "لا يمكنك طلب إعادة تعيين كلمة المرور من عنوان IP هذا." suspended: "لا يمكنك تسجيل الدخول حتى %{date}." suspended_with_reason: "تم تعليق الحساب حتى %{date}: %{reason}" suspended_with_reason_forever: "تم تعليق الحساب: %{reason}" @@ -2765,7 +2795,7 @@ ar: %{invite_link} invite_password_instructions: title: "تعليمات كلمة المرور للدعوة" - subject_template: "ضبط كلمة المرور لحساب %{site_name}" + subject_template: "تعيين كلمة المرور لحساب %{site_name}" text_body_template: | شكرًا على قبول دعوتك إلى %{site_name} - مرحبًا! @@ -2819,10 +2849,12 @@ ar: text_body_template: "مرحى، يتوفَّر إصدار جديد من [Discourse](https://www.discourse.org)!\n\nإصدارك: %{installed_version}\nالإصدار الجديد: **%{new_version}** \n\n- الترقية بسهولة باستخدام **[ترقية المتصفح بنقرة واحدة](%{base_url}/admin/upgrade)**\n\n - تعرَّف على الجديد في [ملاحظات الإصدار](https://meta.discourse.org/tag/release-notes) أو اعرض [سجل تغييرات GitHub البسيط](https://github.com/discourse/discourse/commits/master)\n\n- انتقل إلى [meta.discourse.org](https://meta.discourse.org) للاطِّلاع على الأخبار والمناقشات ودعم \n\n### ملاحظات الإصدار\n\n%{notes}Discourse\n" flag_reasons: off_topic: "تم الإبلاغ عن منشورك على أنه **خارج الموضوع**: يشعر المجتمع بأنه غير مناسب للموضوع، كما هو محدَّد حاليًا في العنوان وأول منشور." + inappropriate: "تم الإبلاغ عن منشورك على أنه **غير لائق**: يشعر المجتمع بأنه هجومي أو مسيء أو يتضمَّن سلوكًا ينمُّ عن كراهية أو ينتهك [إرشادات مجتمعنا](%{base_path}/guidelines)." spam: "تم الإبلاغ عن منشورك على أنه **غير مرغوب فيه**: يشعر المجتمع بأنه إعلان، أو ذو طبيعة ترويجية بشكلٍ زائد بدلًا من أن يكون مفيدًا أو ذا صلة بالموضوع كما هو متوقَّع." notify_moderators: "تم الإبلاغ عن منشورك على أنه **يستدعي انتباه المشرفين**: يشعر المجتمع بأن المنشور يتطلب تدخلًا يدويًا بواسطة أحد أعضاء فريق العمل." responder: off_topic: "تم الإبلاغ عن المنشور على أنه **خارج الموضوع**: يشعر المجتمع بأنه غير مناسب للموضوع، كما هو محدَّد حاليًا في العنوان وأول منشور." + inappropriate: "تم الإبلاغ عن المنشور على أنه **غير لائق**: يشعر المجتمع بأنه هجومي أو مسيء أو يتضمَّن سلوكًا ينمُّ عن كراهية أو ينتهك [إرشادات مجتمعنا](%{base_path}/guidelines)." spam: "تم الإبلاغ عن المنشور على أنه **غير مرغوب فيه**: يشعر المجتمع بأنه إعلان، أو ذو طبيعة ترويجية بشكلٍ زائد بدلًا من أن يكون مفيدًا أو ذا صلة بالموضوع كما هو متوقَّع." notify_moderators: "تم الإبلاغ عن المنشور على أنه **يستدعي انتباه المشرفين**: يشعر المجتمع بأن المنشور يتطلب تدخلًا يدويًا بواسطة أحد أعضاء فريق العمل." flags_dispositions: @@ -3664,6 +3696,7 @@ ar: %{respond_instructions} user_watching_category_or_tag: + title: "فئة أو وسم مراقبة المستخدم" subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -3735,7 +3768,7 @@ ar: text_body_template: | لقد حاولت للتو إنشاء حساب على %{site_name}، أو حاولت تغيير عنوان البريد الإلكتروني للحساب إلى %{email}، لكن هناك حسابًا موجودًا بالفعل بعنوان البريد الإلكتروني %{email}. - إذا نسيت كلمة مرورك، يمكنك [إعادة ضبطها الآن](%{base_url}/password-reset). + إذا نسيت كلمة مرورك، يمكنك [إعادة تعيينها الآن](%{base_url}/password-reset). إذا لم تحاول إنشاء حساب بعنوان البريد الإلكتروني %{email} أو تغيير عنوان بريدك الإلكتروني، فلا تقلق - يمكنك تجاهل هذه الرسالة بأمان. @@ -3770,9 +3803,9 @@ ar: preheader: "ملخص موجز لزيارتك الأخيرة في %{last_seen_at}" forgot_password: title: "نسيت كلمة المرور" - subject_template: "[%{email_prefix}] إعادة ضبط كلمة المرور" + subject_template: "[%{email_prefix}] إعادة تعيين كلمة المرور" text_body_template: | - طلب شخص ما إعادة ضبط كلمة مرورك على [%{site_name}](%{base_url}). + طلب شخص ما إعادة تعيين كلمة مرورك على [%{site_name}](%{base_url}). إذا لم يكن هذا أنت، يمكنك تجاهل هذه الرسالة الإلكترونية بأمان. @@ -3789,8 +3822,8 @@ ar: انقر على الرابط التالي لتسجيل الدخول: %{base_url}/session/email-login/%{email_token} set_password: - title: "ضبط كلمة المرور" - subject_template: "[%{email_prefix}] ضبط كلمة المرور" + title: "تعيين كلمة المرور" + subject_template: "[%{email_prefix}] تعيين كلمة المرور" text_body_template: | طلب شخص ما إضافة كلمة مرور إلى حسابك على [%{site_name}](%{base_url}). بدلًا من ذلك، يمكنك تسجيل الدخول باستخدام أي خدمة مدعومة عبر الإنترنت (Google ،Facebook، إلى آخره) مرتبطة بعنوان البريد الإلكتروني الذي تم التحقُّق منه. @@ -3931,7 +3964,7 @@ ar: هذه رسالة تلقائية من %{site_name} لإعلامك بأنه قد تمت الموافقة على [منشورك](%{base_url}%{post_url}). page_forbidden: title: "عذرًا! هذه الصفحة خاصة." - site_setting_missing: "يجب ضبط إعداد الموقع `%{name}`." + site_setting_missing: "يجب تعيين إعداد الموقع `%{name}`." page_not_found: page_title: "الصفحة غير موجودة" title: "عذرًا! هذه الصفحة غير موجودة أو خاصة." @@ -4055,6 +4088,7 @@ ar: body: " ## [هذا مكان متحضر للنقاش العام](#civilized)\n\nيُرجى التعامل مع منتدى المناقشة هذا بالاحترام نفسه الذي تتعامل به في الحديقة العامة. نحن أيضًا مورد مجتمعي مشترك — مكان لتبادل المهارات والمعارف والاهتمامات من خلال المحادثة المستمرة.\n\nهذه ليست قواعد صارمة وسريعة. إنها مبادئ توجيهية لمساعدة الحكم الإنساني لمجتمعنا والحفاظ على هذا المكان لطيفًا وودودًا للخطاب العام المتحضر.\n\n\n\n## [تحسين المناقشة](#improve)\n\nساعدنا في جعل هذا مكانًا رائعًا للمناقشة من خلال إضافة شيء إيجابي دائمًا إلى المناقشة، مهما كان صغيرًا. إذا لم تكن متأكدًا من أن منشورك يضيف إلى المحادثة، ففكر فيما تريد قوله وحاول مرة أخرى لاحقًا.\n\nتتمثل إحدى طرق تحسين المناقشة في استكشاف المحادثات الجارية بالفعل. استغرق بعض الوقت في تصفُّح الموضوعات هنا قبل الرد أو البدء في موضوع خاص بك، وستكون لديك فرصة أفضل لمقابلة الآخرين الذين يشاركونك اهتماماتك.\n\nالموضوعات التي تتم مناقشتها هنا تهمنا، ونريدك أن تتصرف كما لو كانت تهمك أيضًا. احترم الموضوعات والأشخاص الذين يناقشونها، حتى لو كنت لا توافق على بعضها.\n\n\n\n## [كُن لطيفًا، حتى عندما لا توافق](#agreeable)\n\nقد ترغب في الرد بمخالفة الرأي. هذا جيد. لكن تذكر أن \"تنتقد الأفكار وليس الأشخاص\". يُرجى تجنُّب:\n\n* التنابز بالألقاب\n* الهجوم الشخصي\n* الرد على نبرة المنشور بدلًا من محتواه الفعلي\n* مخالفة الرأي المتوقعة\n\nبدلًا من ذلك، قدِّم رؤًى عميقة الفكر تعمل على تحسين المحادثة.\n\n\n\n## [مشاركاتك مهمة](#participate)\n\nتحدِّد المحادثات التي لدينا هنا المناخ العام لكل وافد جديد. ساعدنا في التأثير على مستقبل هذا المجتمع من خلال اختيار المشاركة في المناقشات التي تجعل هذا المنتدى مكانًا مثيرًا للاهتمام — وتجنَّب أولئك الذين لا يفعلون ذلك.\n\nيوفِّر Discourse الأدوات التي تتيح للمجتمع تحديد أفضل (وأسوأ) المساهمات بشكلٍ جماعي: الإشارات المرجعية وتسجيلات الإعجاب والبلاغات والردود والتعديلات والمراقبة والكتم وما إلى ذلك. استخدم هذه الأدوات لتحسين تجربتك الخاصة وتجربة الآخرين أيضًا.\n\nلنترك مجتمعنا أفضل مما وجدناه.\n\n\n\n## [إذا رأيت مشكلة، فأبلغ عنها](#flag-problems)\n\nيتمتَّع المشرفون بسلطة خاصة؛ فهم مسؤولون عن هذا المنتدى. وكذلك أنت. فبمساعدتك، يمكن أن يصبح المشرفون ميسرين للمجتمع، وليس مجرد عمال نظافة أو شرطة.\n\nعندما ترى سلوكًا سيئًا، لا ترد. يشجع الرد على السلوك السيئ من خلال الاعتراف به ويستهلك طاقتك ويضيع وقت الجميع. _أبلغ عنه فحسب_. إذا تراكم عدد كافٍ من البلاغات، فسيتم اتخاذ الإجراء، إما تلقائيًا أو عن طريق تدخل المشرف.\n\nللحفاظ على مجتمعنا، يحتفظ المشرفون بالحق في إزالة أي محتوى وأي حساب مستخدم لأي سبب في أي وقت. لا يستعرض المشرفون المنشورات الجديدة؛ ولا يتحمَّل المشرفون ومشغِّلو الموقع أي مسؤولية عن أي محتوى ينشره المجتمع.\n\n\n\n## [كُن متحضرًا دائمًا](#be-civil)\n\nلا شيء يفسد المحادثة الصحية مثل الوقاحة:\n\n* كُن متحضرًا. لا تنشر أي شيء يعتبره أي شخص عاقل مسيئًا أو يحض على الكراهية.\n* حافظ على الأدب. لا تنشر أي شيء فاحش أو جنسي صريح.\n* احترم الآخرين. لا تضايق أو تحزن أي شخص، أو تنتحل صفة الأشخاص، أو تكشف عن معلوماتهم الخاصة.\n* احترم منتدانا. لا تنشر محتوى غير مرغوب فيه أو تفسد المنتدى بأي طريقة أخرى.\n\nهذه ليست مصطلحات محدَّدة ذات تعريفات دقيقة — تجنَّب حتى _ظهور_ أي من هذه الأشياء. إذا لم تكن متأكدًا، فاسأل نفسك كيف ستشعر إذا ظهر منشورك في الصفحة الأولى لموقع إخباري رئيسي.\n\nهذا منتدى عام، وتقوم محركات البحث بفهرسة هذه المناقشات. حافظ على اللغة والروابط والصور آمنة للعائلة والأصدقاء.\n\n\n\n## [حافظ على النظام](#keep-tidy)\n\nابذل جهدًا لوضع الأشياء في مكانها الصحيح؛ حتى نتمكن من قضاء المزيد من الوقت في المناقشة وليس التنظيم. وبالتالي:\n\n* لا تبدأ موضوعًا في فئة خاطئة. يُرجى قراءة تعريفات الفئات.\n* لا تنشر الشيء نفسه في موضوعات متعددة.\n* لا تنشر ردودًا دون وجود محتوى.\n* لا تحوِّل موضوعًا عن طريق تغييره في منتصف الطريق.\n* لا توقِّع منشوراتك — معلومات ملفك الشخصي مرفقة بكل منشور. \n\nبدلًا من نشر \"+1\" أو \"أتفق\"، استخدم الزر \"أعجبني\". بدلًا من أخذ موضوع موجود في اتجاه مختلف جذريًا، استخدم \"الرد في موضوع متربط\".\n\n\n\n## [انشر المحتوى الذي أنشأته بنفسك فقط](#stealing)\n\nلا يجوز لك نشر أي محتوى رقمي يخص شخص آخر دون إذن. لا يجوز لك نشر أوصاف أو روابط أو طرق لسرقة الملكية الفكرية لشخص ما (البرامج أو الفيديوهات أو الملفات الصوتية أو الصور) أو لخرق أي قانون آخر.\n\n\n\n## [بدعمٍ منك](#power)\n\nيتم تشغيل هذا الموقع بواسطة [فريق العمل المحلي](%{base_path}/about) والمستخدمين *أمثالك*؛ أي المجتمع. إذا كانت لديك أي أسئلة أخرى بشأن كيفية عمل الأشياء هنا، فافتح موضوعًا جديدًا في [فئة ملاحظات الموقع](%{base_path}/c/site-feedback) ولنناقشها! إذا كانت هناك مشكلة حرجة أو عاجلة لا يمكن معالجتها من خلال موضوع أو علامة وصفية، فتواصل معنا من خلال [صفحة فريق العمل](%{base_path}/about).\n\n\n\n## [شروط الخدمة](#tos)\n\nنعم، الحديث القانوني ممل، ولكن يجب علينا حماية أنفسنا – وبالتبعية حمايتك أنت وبياناتك – ضد الأشخاص غير الودودين. لدينا [شروط الخدمة](%{base_path}/tos) التي تصف سلوكك (وسلوكنا) والحقوق المتعلقة بالمحتوى والخصوصية والقوانين. لاستخدام هذه الخدمة، يجب أن توافق على الالتزام بشروط الخدمة [TOS](%{base_path}/tos).\n" tos_topic: title: "شروط الخدمة" + body: "تحكم هذه الشروط استخدام منتدى الإنترنت على <%{base_url}>. لاستخدام المنتدى، يجب عليك الموافقة على هذه الشروط مع %{company_name}، وهي الشركة التي تدير المنتدى.\n\nقد تقدِّم الشركة منتجات وخدمات أخرى بشروط مختلفة. تنطبق هذه الشروط على استخدام المنتدى فقط.\n\nانتقل إلى:\n\n- [الشروط المهمة](#heading--important-terms)\n- [أذنك لاستخدام المنتدى](#heading--permission)\n- [شروط استخدام المنتدى](#heading--conditions) - [الاستخدام المقبول](#heading--acceptable-use)\n- [معايير المحتوى](#heading--content-standards)\n- [إنفاذ القانون](#heading--enforcement)\n- [حسابك](#heading--your-account)\n- [محتواك](#heading--your-content)\n- [مسؤوليتك](#heading--responsibility)\n- [إخلاء المسؤولية](#heading--disclaimers)\n- [حدود المسؤولية](#heading--liability)\n- [الملاحظات](#heading--feedback)\n- [الإنهاء](#heading--termination)\n- [النزاعات](#heading--disputes)\n- [الشروط العامة](#heading--general)\n- [التواصل](#heading--contact)\n- [التغييرات](#heading--changes)\n\n

    الشروط المهمة

    \n\n***تتضمَّن هذه الشروط عددًا من الأحكام المهمة التي تؤثر على حقوقك ومسؤولياتك، مثل إخلاء المسؤولية في [إخلاء المسؤولية](#heading--disclaimers)، وحدود مسؤولية الشركة تجاهك في [حدود المسؤولية](#heading--liability)، وموافقتك على تغطية الشركة للأضرار الناجمة عن إساءة استخدامك للمنتدى في [المسؤولية عن استخدامك](#heading--responsibility)، واتفاقية التحكيم في النزاعات في [النزاعات](#heading--disputes).***\n\n

    أذنك لاستخدام المنتدى

    \n\nمع مراعاة هذه الشروط، تمنحك الشركة الإذن باستخدام المنتدى. يجب على الجميع الموافقة على هذه الشروط لاستخدام المنتدى.\n\n

    شروط استخدام المنتدى

    \n\nيخضع إذنك لاستخدام المنتدى للشروط التالية:\n\n1. يجب ألا يقل عمرك عن ثلاثة عشر عامًا.\n\n2. قد لا تتمكن من استخدام المنتدى إذا تواصلت معك الشركة مباشرةً لتخبرك بذلك.\n\n3. يجب عليك استخدام المنتدى وفقًا لسياسة [الاستخدام المقبول](#heading--acceptable-use) و[معايير المحتوى](#heading--content-standards).\n\n

    الاستخدام المقبول

    \n\n1. لا يجوز لك خرق القانون باستخدام المنتدى.\n\n2. لا يجوز لك استخدام أو محاولة استخدام حساب شخص آخر في المنتدى دون الحصول على إذن خاص منه.\n\n3. لا يجوز لك شراء أو بيع أو المتاجرة في أسماء المستخدمين أو غيرها من المعرِّفات الفريدة في المنتدى.\n\n4. لا يجوز لك إرسال إعلانات أو رسائل متسلسلة أو التماسات أخرى من خلال المنتدى، أو استخدام المنتدى لجمع العناوين أو البيانات الشخصية الأخرى لقوائم البريد التجارية أو قواعد البيانات.\n\n5. لا يجوز لك أتمتة الوصول إلى المنتدى أو مراقبة المنتدى، كما هو الحال مع زاحفات الويب أو المكوِّنات الإضافية أو الوظائف الإضافية للمتصفح أو برنامج كمبيوتر آخر ليس متصفح ويب. يمكنك الزحف إلى المنتدى لفهرسته من أجل محرك بحث متاح للجمهور إذا كنت تدير أحدها.\n\n6. لا يجوز لك استخدام المنتدى لإرسال رسائل إلكترونية إلى قوائم التوزيع أو مجموعات الأخبار أو الأسماء المستعارة لبريد جماعي.\n\n7. لا يجوز لك الإيحاء بشكلٍ خاطئ بأنك تابع للشركة أو معتمد منها.\n\n8. لا يجوز لك إنشاء رابط تشعبي للصور أو أي محتوى آخر بلا نص تشعبي على المنتدى ووضعه على صفحات ويب أخرى.\n\n9. لا يجوز لك إزالة أي علامات تُظهِر الملكية الخاصة من المواد التي تقوم بتنزيلها من المنتدى.\n\n10. لا يجوز لك عرض أي جزء من المنتدى على مواقع ويب أخرى داخل \"< iframe >`.\n\n11. لا يجوز لك إيقاف أو تجنُّب أو التحايل على أي قيود أمنية أو قيود على الوصول إلى المنتدى.\n\n12. لا يجوز لك إرهاق البنية التحتية للمنتدى بكمية غير معقولة من الطلبات أو الطلبات المصمَّمة لفرض عبء غير معقول على أنظمة المعلومات التي يقوم عليها المنتدى.\n\n13. لا يجوز لك انتحال شخصية الآخرين من خلال المنتدى.\n\n14. لا يجوز لك تشجيع أو مساعدة أي شخص ينتهك هذه الشروط.\n\n

    معايير المحتوى

    \n\n1. لا يجوز لك إرسال محتوى غير قانوني أو مسيء أو ضار بالآخرين إلى المنتدى. يتضمَّن ذلك المحتوى المزعج أو غير المناسب أو المسيء.\n\n2. لا يجوز لك إرسال محتوى إلى المنتدى ينتهك القانون أو ينتهك حقوق الملكية الفكرية لأي شخص أو ينتهك خصوصية أي شخص أو ينتهك الاتفاقيات التي أبرمتها مع الآخرين.\n\n3. لا يجوز لك إرسال محتوى إلى المنتدى يحتوي على تعليمات برمجية خبيثة للكمبيوتر، مثل فيروسات الكمبيوتر أو برامج التجسس.\n\n4. لا يجوز لك إرسال محتوى إلى المنتدى كعنصر نائب فقط لحجز عنوان أو اسم مستخدم معيَّن أو معرِّف فريد آخر.\n\n5. لا يجوز لك استخدام المنتدى للكشف عن معلومات لا يحق لك الكشف عنها، مثل المعلومات السرية أو الشخصية للآخرين.\n\n

    إنفاذ القانون

    \n\nيجوز للشركة التحقيق ومقاضاة أي انتهاكات لهذه الشروط إلى أقصى حد يسمح به القانون. ويجوز للشركة إخطار سلطات إنفاذ القانون والتعاون معها في مقاضاة انتهاكات القانون وهذه الشروط.\n\nتحتفظ الشركة بالحق في تغيير المحتوى في المنتدى وتنقيحه وحذفه لأي سبب من الأسباب. إذا كنت تعتقد أن شخصًا ما قد أرسل محتوى إلى المنتدى بما ينتهك هذه الشروط، [فتواصل معنا على الفور](#heading--contact).\n\n

    حسابك

    \n\nيجب عليك إنشاء حساب وتسجيل الدخول إليه لاستخدام بعض ميزات المنتدى.\n\nلإنشاء حساب، يجب عليك تقديم بعض المعلومات عن نفسك. إذا أنشأت حسابًا، فإنك توافق على تقديم عنوان بريد إلكتروني صالح على الأقل، وتحديثه أولًا بأول. يمكنك إغلاق حسابك في أي وقت من خلال مراسلة عنوان البريد الإلكتروني <%{contact_email}>. \n\nأنت توافق على أن تكون مسؤولًا عن جميع الإجراءات التي يتم اتخاذها باستخدام حسابك، سواءً كان مصرحًا بها أم لا، حتى تغلق حسابك أو تخطر الشركة بأن حسابك قد تعرض للاختراق. أنت توافق على إخطار الشركة على الفور إذا كنت تشك في تعرُّض حسابك للاختراق. أنت توافق على تحديد كلمة مرور آمنة لحسابك، والحفاظ على سريتها.\n\nيجوز للشركة تقييد حسابك في المنتدى أو تعليقه أو إغلاقه وفقًا لسياستها في التعامل مع طلبات الإزالة المتعلقة بحقوق الطبع والنشر، أو إذا اعتقدت الشركة بشكلٍ معقول أنك انتهكت أي قاعدة في هذه الشروط.\n\n

    محتواك

    \n\nلا شيء في هذه الشروط يمنح الشركة أي حقوق ملكية في الملكية الفكرية التي تشاركها مع المنتدى، مثل معلومات حسابك أو منشوراتك أو أي محتوى آخر تُرسله إلى المنتدى. لا شيء في هذه الشروط يمنحك أي حقوق ملكية في الملكية الفكرية للشركة أيضًا.\n\nبينك وبين الشركة، تظل المسؤول الوحيد عن المحتوى الذي تُرسله إلى المنتدى. أنت توافق على عدم الإيحاء بشكلٍ خاطئ بأن المحتوى الذي تُرسله إلى المنتدى معتمد من الشركة أو برعايتها. لا تُلزِم هذه الشروط الشركة بتخزين نسخ من المحتوى الذي تُرسله أو تحتفظ بها أو تقدِّمها، أو بتغيير المحتوى وفقًا لهذه الشروط.\n\nالمحتوى الذي ترسله إلى المنتدى ملك لك، وأنت تقرر نوع الإذن الذي تمنحه للآخرين. ولكن على الأقل، أنت تمنح الشركة ترخيصًا بتقديم المحتوى الذي تُرسله إلى المنتدى للمستخدمين الآخرين في المنتدى. يسمح هذا الترخيص الخاص للشركة بنسخ المحتوى الذي تُرسله إلى المنتدى ونشره وتحليله.\n\nعندما تتم إزالة المحتوى الذي تُرسله من المنتدى، سواءً بواسطتك أو بواسطة الشركة، ينتهي الترخيص الخاص للشركة عندما تختفي النسخة الأخيرة من النسخ الاحتياطية، وذاكرة التخزين المؤقت، والأنظمة الأخرى للشركة. قد تستمر التراخيص الأخرى التي تطبِّقها على المحتوى الذي تُرسله، مثل تراخيص [Creative Commons](https://creativecommons.org)، بعد إزالة محتواك. قد تمنح هذه التراخيص للآخرين، أو للشركة نفسها، الحق في مشاركة محتواك من خلال المنتدى مرة أخرى.\n\nقد ينتهك الآخرون الذين يتلقون المحتوى الذي تُرسله إلى المنتدى الشروط التي تمنح بموجبها ترخيصًا لمحتواك. أنت توافق على أن الشركة لن تكون مسؤولة تجاهك عن تلك الانتهاكات أو عواقبها.\n\n

    مسؤوليتك

    \n\nأنت توافق على تعويض الشركة بشأن المطالبات القانونية من الآخرين والمتعلقة بخرقك لهذه الشروط، أو خرق هذه الشروط بواسطة آخرين يستخدمون حسابك في المنتدى. توافق أنت والشركة على إخطار الجانب الآخر بأي مطالبات قانونية قد يتعيَّن عليك تعويض الشركة بشأنها في أقرب وقتٍ ممكن. إذا فشلت الشركة في إخطارك بالمطالبة القانونية على الفور، فلن تضطر إلى تعويض الشركة عن الأضرار التي كان بإمكانك الوقوف ضدها أو تخفيفها في حال الإخطار الفوري. أنت توافق على السماح للشركة بقيادة التحقيق والدفاع وتسوية المطالبات القانونية التي يتعيَّن عليك تعويض الشركة بشأنها، والتعاون مع هذه الجهود. توافق الشركة على عدم الموافقة على أي تسوية تقر بخطأك أو تفرض عليك التزامات دون موافقتك المسبقة.\n\n

    إخلاء المسؤولية

    \n\n***أنت تقبل جميع مخاطر استخدام المنتدى والمحتوى الموجود في المنتدى. بقدر ما يسمح به القانون، ستقدِّم الشركة ومورِّدوها المنتدى كما هو، دون أي ضمان على الإطلاق.***\n\nقد يرتبط المنتدى ارتباطًا تشعبيًا بالمنتديات والخدمات التي يديرها الآخرون ويدمجها. لا تقدِّم الشركة أي ضمانات بشأن الخدمات التي يديرها الآخرون، أو المحتوى الذي قد يقدِّمونه. قد يخضع استخدام الخدمات التي يديرها الآخرون لشروط أخرى بينك وبين الخدمة قيد التشغيل.\n\n

    حدود المسؤولية

    \n\n***لن تتحمل الشركة أو موردونها المسؤولية تجاهك عن الأضرار التي تلحق بسبب خرق العقد والتي لم يكن بإمكان موظفيها توقُّعها بشكلٍ معقول عند موافقتك على هذه الشروط.***\n\n***بقدر ما يسمح به القانون، ستقتصر المسؤولية الكاملة تجاهك عن المطالبات من أي نوع والتي تتعلق بالمنتدى أو المحتوى في المنتدى على 50 دولارًا.***\n\n

    الملاحظات

    \n\nترحِّب الشركة بملاحظاتك واقتراحاتك لتحسين المنتدى. راجع قسم [التواصل](#heading--contact) أدناه للتعرُّف على طرق التواصل معنا.\n\nأنت توافق على أن الشركة ستكون حرة في التصرف بناءً على الملاحظات والاقتراحات التي تقدِّمها، وأن الشركة لن تضطر إلى إخطارك باستخدام ملاحظاتك أو الحصول على إذن منك لاستخدامها أو الدفع لك. أنت توافق على عدم إرسال ملاحظات أو اقتراحات تعتقد أنها قد تكون سرية أو مملوكة لك أو للآخرين.\n\n

    الإنهاء

    \n\nيجوز لك أو للشركة إنهاء الاتفاقية المكتوبة في هذه الشروط في أي وقت. وعندما تنتهي اتفاقيتنا، ينتهي إذنك باستخدام المنتدى.\n\nتظل البنود التالية سارية حتى نهاية الاتفاقية: [محتواك](#heading--your-content)، و[الملاحظات](#heading--feedback)، و[مسؤوليتك](#heading--responsibility)، و[إخلاء المسؤولية](#heading--disclaimers)، و[حدود المسؤولية](#heading--liability)، و[الشروط العامة](#heading--general).\n\n

    النزاعات

    \n\nسيحكم %{governing_law} أي نزاع يتعلق بهذه الشروط أو استخدامك للمنتدى.\n\nتوافق أنت والشركة على طلب الإنذارات القضائية المتعلقة بهذه البنود في محكمة الولاية أو المحكمة الفيدرالية في %{city_for_disputes} فقط. ولن تعترض أنت أو الشركة على الاختصاص القضائي أو الساحة أو المكان في تلك المحاكم.\n\n***بخلاف السعي للحصول على أمر قضائي أو المطالبات بموجب قانون الاحتيال وإساءة استخدام الكمبيوتر، ستحل أنت والشركة أي نزاع عن طريق التحكيم المُلزِم الصادر عن جمعية التحكيم الأمريكية. سيتبع التحكيم قواعد التحكيم التجاري والإجراءات التكميلية الخاصة بجمعية التحكيم الأمريكية بشأن المنازعات المتعلقة بالمستهلكين. سيتم إجراء التحكيم في %{city_for_disputes}}. ستقوم بتسوية أي نزاع كفرد، وليس كجزء من دعوى جماعية أو دعوى تمثيلية أخرى، سواءً كنت مدعيًا أو عضوًا في الدعوى. لن يدمج أي محكِّم أي نزاع مع أي تحكيم آخر دون إذن الشركة.***\n\nسيتضمَّن أي قرار تحكيم تكاليف التحكيم وأتعاب المحاماة المعقولة والتكاليف المعقولة للشهود. يجوز لك وللشركة الدخول في قرارات التحكيم في أي محكمة ذات اختصاص.\n\n

    الشروط العامة

    \n\nإذا كان أحد أحكام هذه الشروط غير قابل للتنفيذ كما هو مكتوب، ولكن يمكن تغييره لجعله قابلًا للتنفيذ، فإنه يجب تعديل هذا الحكم إلى الحد الأدنى الضروري لجعله قابلًا للتنفيذ. بخلاف ذلك، ينبغي إزالة هذا الحكم.\n\nلا يجوز لك التنازل عن اتفاقيتك مع الشركة. ويجوز للشركة التنازل عن موافقتك إلى أي شركة تابعة للشركة، أو أي شركة أخرى تستولي على الشركة، أو أي شركة أخرى تشتري أصول الشركة المتعلقة بالمنتدى. ليس هناك أي أثر قانوني للتنازل عن هذه الشروط.\n\nلا تؤدي ممارسة أي حق بموجب هذه الاتفاقية أو التنازل عن أي انتهاك لهذه الاتفاقية إلى التنازل عن أي انتهاك آخر لهذه الاتفاقية.\n\nتجسِّد هذه الشروط جميع شروط الاتفاق بينك وبين الشركة بشأن استخدام المنتدى. وتحل هذه الشروط بالكامل محل أي اتفاقيات أخرى بشأن استخدامك للمنتدى، سواءً كانت مكتوبة أو لا.\n\n

    التواصل

    \n\nيمكنك إخطار الشركة بموجب هذه الشروط، وإرسال الأسئلة إلى الشركة على <%{contact_email}>.\n\nقد تخطرك الشركة بموجب هذه الشروط باستخدام عنوان البريد الإلكتروني الذي تقدِّمه لحسابك في المنتدى، أو عن طريق نشر رسالة على الصفحة الرئيسية للمنتدى أو صفحة حسابك.\n\n

    التغييرات

    \n\nأجرت الشركة آخر تحديث لهذه الشروط في 12 يوليو 2018، وقد تجري تحديثات أخرى عليها. ستنشر الشركة جميع التحديثات في المنتدى. بالنسبة إلى التحديثات التي تتضمَّن تغييرات جوهرية، توافق الشركة على إرسال رسالة إلكترونية إليك، إذا كنت قد أنشأت حسابًا وقدَّمت عنوان بريد إلكتروني صالحًا. قد تعلن الشركة أيضًا عن التحديثات في رسائل خاصة أو تنبيهات على المنتدى.\n\nبمجرد أن تتلقى إشعارًا بتحديث هذه الشروط، يجب أن توافق على الشروط الجديدة لمواصلة استخدام المنتدى.\n" privacy_topic: title: "سياسة الخصوصية" body: " \n \n ## [ما المعلومات التي نجمعها؟](#collect)\n\nإننا نجمع معلومات منك عند تسجيلك على موقعنا ونجمع البيانات عند المشاركة في المنتدى من خلال قراءة المحتوى الذي تمت مشاركته هنا وكتابته وتقييمه.\n\nعند التسجيل على موقعنا، قد يُطلَب منك إدخال اسمك وعنوان بريدك الإلكتروني. ومع ذلك، يمكنك زيارة موقعنا دون التسجيل. سيتم التحقُّق من عنوان بريدك الإلكتروني عن طريق رسالة إلكترونية تحتوي على رابط فريد. إذا تمت زيارة هذا الرابط، فسنعلم أنك تتحكم في عنوان البريد الإلكتروني.\n\nعند التسجيل والنشر، نسجِّل عنوان IP الذي تم إنشاء المنشور من خلاله. قد نحتفظ أيضًا بسجلات الخادم التي تتضمَّن عنوان IP لكل طلب يتم إرساله إلى خادمنا.\n\n \n\n## [كيف نستخدم معلوماتك الخاصة؟](#use)\n\nيمكن استخدام أي من المعلومات التي نجمعها منك بإحدى الطرق التالية:\n\n* لتخصيص تجربتك - تساعدنا معلوماتك على الاستجابة بشكلٍ أفضل لاحتياجاتك الفردية.\n* لتحسين موقعنا - نحن نسعى باستمرار لتحسين عروض موقعنا بناءً على المعلومات والتعليقات التي نتلقاها منك.\n* لتحسين خدمة العملاء - تساعدنا معلوماتك على الاستجابة بشكلٍ أكثر فعالية لطلبات خدمة العملاء واحتياجات الدعم.\n* لإرسال رسائل بريد إلكتروني دورية - يمكن استخدام عنوان البريد الإلكتروني الذي تقدِّمه لإرسال المعلومات والإشعارات التي تطلبها بشأن التغييرات التي تطرأ على الموضوعات أو ردًا على اسم المستخدم الخاص بك أو الاستفسارات أو الطلبات أو الأسئلة الأخرى.\n\n\n\n## [كيف نحمي معلوماتك؟](#protect)\n\nنحن ننفذ مجموعة متنوعة من الإجراءات الأمنية للحفاظ على سلامة المعلومات الشخصية عند دخولك إلى معلوماتك الشخصية أو إرسالها أو الوصول إليها.\n\n \n\n## [ما سياسة الاحتفاظ بالبيانات لديك؟](#data-retention)\n\nسنبذل قصارى جهدنا من أجل:\n\n* الاحتفاظ بسجلات الخادم التي تحتوي على عنوان IP لجميع الطلبات إلى هذا الخادم لمدة لا تزيد عن 90 يومًا.\n* الاحتفاظ بعناوين IP المرتبطة بالمستخدمين المسجَّلين ومنشوراتهم لمدة لا تزيد عن 5 سنوات.\n\n \n\n## [هل نحن استخدام ملفات تعريف الارتباط؟](#cookies)\n\nنعم. ملفات تعريف الارتباط هي ملفات صغيرة ينقلها الموقع أو مقدِّم الخدمة إلى محرك الأقراص الثابتة بجهاز الكمبيوتر الخاص بك من خلال متصفح الويب (إذا سمحت بذلك). تمكِّن ملفات تعريف الارتباط هذا الموقع من التعرُّف على متصفحك، وربطه بحسابك المسجَّل إذا كان لديك واحد.\n\nإننا نستخدم ملفات تعريف الارتباط لفهم تفضيلاتك وحفظها للزيارات المستقبلية وتجميع البيانات المجمَّعة بشأن زيارات الموقع والتفاعل معه حتى نتمكَّن من تقديم تجارب وأدوات أفضل للموقع في المستقبل. بجوز لنا التعاقد مع مقدِّمي خدمة خارجيين لمساعدتنا في فهم زوار موقعنا بشكلٍ أفضل. لا يُسمَح لمقدِّمي الخدمات هؤلاء باستخدام المعلومات التي تم جمعها نيابةً عنا إلا لمساعدتنا في إدارة أعمالنا وتحسينها.\n\n \n\n## [هل نفصح عن أي معلومات لجهات خارجية؟](#disclose)\n\nإننا لا نبيع معلوماتك الشخصية لجهات خارجية أو نتاجر بها أو ننقلها. لا يشمل ذلك الجهات الخارجية الموثوقة التي تساعدنا في تشغيل موقعنا أو إدارة أعمالنا أو تقديم الخدمات لك ما دامت هذه الأطراف توافق على الحفاظ على سرية هذه المعلومات. يجوز لنا أيضًا الإفصاح عن معلوماتك عندما نعتقد أن الكشف عنها هو التصرف الصحيح للامتثال للقانون أو فرض سياسات موقعنا أو حماية حقوقنا أو حقوق الآخرين أو ممتلكاتنا أو سلامتنا. ومع ذلك، يمكن تقديم معلومات الزائر غير الشخصية إلى جهات أخرى للتسويق أو الإعلان أو استخدامات أخرى.\n\n\n\n## [الروابط التابعة لجهات الخارجية](#third-party)\n\nيجوز لنا في بعض الأحيان، ووفقًا لتقديرنا، تضمين أو تقديم منتجات أو خدمات تابعة لجهات خارجية على موقعنا. تملك مواقع الجهات الخارجية هذه سياسات خصوصية منفصلة ومستقلة. لذلك، لا نتحمل أي مسؤولية أو التزام تجاه محتوى هذه المواقع وأنشطتها. ومع ذلك، فإننا نسعى لحماية سلامة موقعنا ونرحب بأي ملاحظات بشأن هذه المواقع.\n\n\n\n## [قانون حماية خصوصية الأطفال على الإنترنت](#coppa)\n\nيتم توجيه موقعنا ومنتجاتنا وخدماتنا جميعها إلى الأشخاص الذين تبلغ أعمارهم 13 عامًا على الأقل أو أكبر. إذا كان هذا الخادم في الولايات المتحدة، وكان عمرك أقل من 13 عامًا، وفقًا لمتطلبات COPPA ([قانون حماية خصوصية الأطفال على الإنترنت](https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act))، فلا تستخدم هذا الموقع.\n\n\n\n## [سياسة الخصوصية على الإنترنت فقط](#online)\n\nتنطبق سياسة الخصوصية على الإنترنت فقط على المعلومات التي يتم جمعها من خلال موقعنا وليس على المعلومات التي يتم جمعها في وضع عدم الاتصال.\n\n\n\n## [موافقتك](#consent)\n\nباستخدام موقعنا، فإنك توافق على سياسة الخصوصية لموقع الويب الخاص بنا.\n\n \n\n## [التغييرات في سياسة الخصوصية لدينا](#changes)\n\nإذا قررنا تغيير سياسة الخصوصية لدينا، فسننشر التغييرات على هذه الصفحة.\n\nهذا المستند هو CC-BY-SA. وتم تحديثه آخر مرة في 31 مايو 2013.\n" @@ -4274,7 +4308,7 @@ ar: name: أول إشارة description: أشرت إلى عضو آخر في المنشور long_description: | - يتم منح هذه الشارة في المرة الأولى التي تشير فيها إلى مستخدم @username في منشورك. تنشئ كل إشارة إشعارًا يتم إرساله إلى ذلك الشخص؛ حتى يعرف بمنشورك. ما عليك سوى البدء في كتابة الرمز @ للإشارة إلى أي مستخدم أو، إذا كان مسموحًا بذلك، مجموعة - إنها الطريقة المناسبة لجذب انتباهه إلى شيء ما. + يتم منح هذه الشارة في المرة الأولى التي تشير فيها إلى مستخدم @اسم المستخدم في منشورك. تنشئ كل إشارة إشعارًا يتم إرساله إلى ذلك الشخص؛ حتى يعرف بمنشورك. ما عليك سوى البدء في كتابة الرمز @ للإشارة إلى أي مستخدم أو، إذا كان مسموحًا بذلك، مجموعة - إنها الطريقة المناسبة لجذب انتباهه إلى شيء ما. first_onebox: name: أول لوحة معاينة description: نشرت رابطًا تم عرضه في لوحة المعاينة @@ -4396,6 +4430,8 @@ ar: title: "إعداد Discourse" step: introduction: + title: "نبذة عن موقعك" + description: "سيتم عرض هذه في صفحة تسجيل الدخول الخاصة بك وفي أي صفحات عامة. يمكنك دائمًا تغييرها لاحقًا." fields: title: label: "اسم المجتمع" @@ -4406,21 +4442,28 @@ ar: default_locale: label: "اللغة" privacy: + title: "تجربة العضو" fields: login_required: placeholder: "خاصة" extra_description: "يمكن للمستخدمين المسجَّلين فقط الوصول إلى هذا المجتمع" invite_only: + placeholder: "بدعوة فقط" extra_description: "يجب دعوة المستخدمين من قِبل المستخدمين الموثوقين أو فريق العمل، وإلا فسيتمكن المستخدمون من التسجيل بأنفسهم." must_approve_users: + placeholder: "يتطلب الموافقة" extra_description: "يجب الموافقة على المستخدمين من قِبل فريق العمل" chat_enabled: - placeholder: "تفعيل الدردشة" + placeholder: "تمكين الدردشة" + extra_description: "تفاعل مع أعضائك في الوقت الفعلي" enable_sidebar: - placeholder: "تفعيل الشريط الجانبي" + placeholder: "تمكين الشريط الجانبي" + extra_description: "تمكَّن من الوصول إلى مساحاتك المفضَّلة بكل سهولة" ready: + title: "موقعك جاهز!" description: "هذا كل شيء! لقد انتهيت من الخطوات الأساسية لإعداد مجتمعك. يمكنك البدء الآن وإلقاء نظرة، وكتابة موضوع ترحيبي، وإرسال الدعوات!

    استمتع بوقتك!" styling: + title: "الشكل والمظهر" fields: color_scheme: label: "نظام الألوان" @@ -4451,19 +4494,38 @@ ar: label: "مربعات الفئات ذات الموضوعات" subcategories_with_featured_topics: label: "الفئات الفرعية ذات الموضوعات المميزة" - corporate: + branding: + title: "شعار الموقع" fields: + logo: + label: "الشعار الرئيسي" + description: "الحجم الموصى به: 600 × 200" + logo_small: + label: "شعار مربع" + description: "الحجم الموصى به: 512 × 512. يُستخدم أيضًا كرمزٍ مفضَّل (رمز الموقع في علامة التبويب) وفي أيقونة التطبيق على الشاشة الرئيسية للجوَّال." + corporate: + title: "مؤسستك" + description: "سيتم استخدام المعلومات التالية في صفحتَي \"شروط الخدمة\" و\"نبذة\". لا تتردد في التخطي إذا لم تكن هناك شركة." + fields: + company_name: + label: "اسم الشركة" + placeholder: "مؤسسة أكمي" governing_law: + label: "القانون الحاكم" placeholder: "قانون ولاية كاليفورنيا" contact_url: + label: "صفحة ويب" placeholder: "https://www.example.com/contact-us" city_for_disputes: + label: "مدينة النزاعات" placeholder: "سان فرانسيسكو، كاليفورنيا" site_contact: + label: "رسائل آلية" description: "سيتم إرسال جميع رسائل Discourse التلقائية الخاصة من هذا المستخدم، مثل التحذيرات بشأن البلاغات وإشعارات إكمال النسخ الاحتياطي." contact_email: label: "نقطة الاتصال" placeholder: "example@user.com" + description: "عنوان البريد الإلكتروني لجهة الاتصال الرئيسية المسؤولة عن هذا الموقع. يتم استخدامه للإشعارات المهمة ويتم إدراجه أيضًا في صفحة نبذة عنك للمسائل العاجلة." invites: title: "دعوة فريق العمل" description: "أنت على وشك الانتهاء! لندعو بعض الأشخاص للمساعدة في تغذية مناقشاتك بموضوعات وردود شيقة لبدء مجتمعك." diff --git a/config/locales/server.be.yml b/config/locales/server.be.yml index 5e9b79a74c..a6b5dd8214 100644 --- a/config/locales/server.be.yml +++ b/config/locales/server.be.yml @@ -974,8 +974,6 @@ be: enable_rich_text_paste: "Ўключыць аўтаматычны HTML для пераўтварэння Markdown пры ўстаўцы тэксту ў кампазітар. (Эксперыментальны)" email_token_valid_hours: "Забыліся на пароль" enable_badges: "Уключыць сістэму жэтона" - blocked_email_domains: "Труба косак спіс паштовых даменаў, якія не могуць рэгістраваць ўліковыя запісы с. Прыклад: mailinator.com | trashmail.net" - allowed_email_domains: "Труба косак спіс паштовых даменаў, якія карыстальнікі павінны зарэгістраваць рахунку. УВАГА: Карыстальнікі з электроннымі, выдатнымі ад пералічаных даменаў не будуць дазволеныя!" log_out_strict: "Пры ўваходзе ў сістэму з, выйдзіце з усіх сеансаў для карыстальніка на ўсіх прыладах" version_checks: "Ping Дыскурс канцэнтратар для абнаўлення версій і паказаць новыя версіі паведамленняў на старонцы substituir robots.txt de manera permanent." - blocked_email_domains: "Llista de dominis de correu delimitada amb barres verticals amb els quals els usuaris no poden registrar comptes. Exemple: mailinator.com|trashmail.net" - allowed_email_domains: "Llista de dominis de correu delimitada amb barres verticals amb els quals els usuaris HAN DE registrar els comptes. ATENCIÓ: Els usuaris amb dominis de correu diferents dels llistats no són autoritzats!" log_out_strict: "Quan es tanca la sessió, tanca TOTES les sessions de l'usuari en tots els dispositius" version_checks: "Fes ping al Discourse Hub per a actualitzacions de versions i per a mostrar missatges de versió nova en el tauler /admin." new_version_emails: "Envia un correu a contact_email address quan hi hagi una nova versió del Discourse disponible." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index d152cf4910..e3dbc3dcbf 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -1404,9 +1404,6 @@ da: email_token_valid_hours: "Glemte password / konto aktiverings muligheder er gyldige i (n) timer." enable_badges: "Aktiver emblem systemet" allow_index_in_robots_txt: "Angiv i robots.txt, at dette websted må indekseres af websøgemaskiner. I undtagelsestilfælde kan du permanent tilsidesætte robots.txt." - blocked_email_domains: "En liste, der er afgrænset via pipe (lodret steg), over e-mail-domæner, som brugere ikke har tilladelse til at registrere konti med. Eksempel: mailinator.com|trashmail.net" - allowed_email_domains: "En pipesepareret liste over e-mail-domæner, som brugerne SKAL registrere konti med. ADVARSEL: Brugere med andre e-mail-domæner end dem, der er angivet, vil ikke være tilladt!" - auto_approve_email_domains: "Brugere med e-mailadresser fra denne liste over domæner vil automatisk blive godkendt." log_out_strict: "Når brugeren logger af, log da ud af alle sessioner, på alle enheder" version_checks: "Ping Discourse Hub for versionopdateringer, og vis meddelelser om ny version på instrumentbrættet /admin" new_version_emails: "Send en email til contact_email adressen når der er en ny version af Discourse tilgængelig." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 2e6affb035..15326b81ad 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -234,6 +234,7 @@ de: not_found_template_link: |

    Diese Einladung zu %{site_name} kann nicht mehr eingelöst werden. Bitte die Person, die dich eingeladen hat, dir eine neue Einladung zu schicken.

    existing_user_cannot_redeem: "Diese Einladung kann nicht eingelöst werden. Bitte die Person, die dich eingeladen hat, darum, dir eine neue Einladung zu schicken." + existing_user_already_redemeed: "Du hast diesen Einladungslink bereits eingelöst." user_exists: "Es ist nicht nötig, %{email} einzuladen. Die Person hat bereits ein Konto!" invite_exists: "Du hast %{email} bereits eingeladen." invalid_email: "%{email} ist keine gültige E-Mail-Adresse." @@ -498,6 +499,7 @@ de: Du kannst deine letzte Antwort bearbeiten, um ein Zitat hinzuzufügen, indem du den Text auswählst und auf die erscheinende Schaltfläche Zitat klickst. Es ist für alle einfacher, Themen zu lesen, die wenige umfassende Antworten statt viele kleine und einzelne Antworten haben. + dominating_topic: Du hast in diesem Thema schon eine Menge gepostet! Gib doch auch anderen die Möglichkeit, hier zu antworten und miteinander zu diskutieren. get_a_room: Du hast @%{reply_username} %{count} Mal geantwortet. Wusstest du, dass du der Person stattdessen eine persönliche Nachricht schicken kannst? too_many_replies: | ### Du hast das Antwort-Limit für dieses Thema erreicht @@ -537,6 +539,7 @@ de: target_user_not_found: "Einer der Empfänger dieser Nachricht konnte nicht gefunden werden." unable_to_update: "Beim Aktualisieren dieses Themas ist ein Fehler aufgetreten." unable_to_tag: "Beim Versehen des Themas mit einem Schlagwort ist ein Fehler aufgetreten." + unable_to_unlist: "Du kannst kein unsichtbares Thema erstellen." featured_link: invalid: "ist ungültig. URL sollte http:// oder https:// enthalten." user: @@ -667,6 +670,7 @@ de: one: "%{count} „Gefällt mir“" other: "%{count} „Gefällt mir“" cannot_permanently_delete: + many_posts: "Dieses Thema hat wiederhergestellte Beiträge. Bitte lösche sie endgültig, bevor du das Thema endgültig löschst." wait_or_different_admin: "Du musst %{time_left} warten, bevor du diesen Beitrag endgültig löschen kannst, oder ein anderer Administrator muss dies tun." rate_limiter: slow_down: "Du hast diese Aktion zu oft durchgeführt. Bitte versuche es später noch einmal." @@ -858,6 +862,7 @@ de: email_body: "%{link}\n\n%{message}" inappropriate: title: "Unangemessen" + description: 'Dieser Beitrag enthält Inhalte, die eine vernünftige Person als anstößig, beleidigend, hasserfüllt oder unsere Community-Richtlinien verletzend auffassen würde.' short_description: 'Ein Verstoß gegen unsere Community-Richtlinien' notify_user: title: "Schreibe @%{username} eine Nachricht" @@ -923,6 +928,7 @@ de: short_description: "Dies ist Werbung" inappropriate: title: "Unangemessen" + description: 'Dieses Thema enthält Inhalte, die eine vernünftige Person als anstößig, beleidigend, hasserfüllt oder unsere Community-Richtlinien verletzend auffassen würde.' long_form: "als unangemessen gemeldet" short_description: 'Ein Verstoß gegen unsere Community-Richtlinien' notify_moderators: @@ -960,6 +966,8 @@ de: mailing_list_mode: "Mailinglisten-Modus ausschalten" all: "Sende mir keine E-Mail mehr von %{sitename}" different_user_description: "Du bist derzeit als ein anderer Benutzer angemeldet als der, dem wir die E-Mail geschickt haben. Bitte melde dich ab oder gehe in den anonymen Modus und versuche es erneut." + not_found_description: "Wir konnten dieses Abonnement leider nicht finden. Möglicherweise ist der Link in deiner E-Mail zu alt und abgelaufen?" + user_not_found_description: "Leider konnten wir keinen Benutzer für dieses Abonnement finden. Du versuchst wahrscheinlich, ein Konto abzumelden, das nicht mehr existiert." log_out: "Abmelden" submit: "Einstellungen speichern" digest_frequency: @@ -996,6 +1004,7 @@ de: write: "Alles schreiben" one_time_password: "Erstelle ein Einmal-Anmelde-Token" bookmarks_calendar: "Lesezeichen-Erinnerungen lesen" + user_status: "Benutzerstatus lesen und aktualisieren" invalid_public_key: "Entschuldige, der öffentliche Schlüssel ist ungültig." invalid_auth_redirect: "Entschuldige, dieser „auth_redirect“-Host ist nicht erlaubt." invalid_token: "Fehlendes, ungültiges oder abgelaufenes Token." @@ -1089,10 +1098,12 @@ de: author: Verfasser edit_reason: Grund consolidated_api_requests: + title: "Konsolidierte API-Anfragen" xaxis: api: "API-Schnittstelle" user_api: "Benutzer-API" yaxis: "Tag" + description: "API-Anfragen für reguläre API-Schlüssel und Benutzer-API-Schlüssel." dau_by_mau: title: "TAB/MAB" xaxis: "Tag" @@ -1415,6 +1426,11 @@ de: allow_duplicate_topic_titles_category: "Themen mit identischen, doppelten Titeln zulassen, wenn die Kategorie unterschiedlich ist. allow_duplicate_topic_titles muss deaktiviert sein." unique_posts_mins: "Minuten, nach denen ein Benutzer denselben Inhalt noch einmal schreiben kann." educate_until_posts: "Zeige das Hilfe-Panel im Editor, wenn ein Benutzer einen seiner ersten (n) Beiträge zu schreiben beginnt." + title: "Der Name dieser Website. Sichtbar für alle Besucher, einschließlich anonymer Benutzer." + site_description: "Beschreibe diese Website in einem Satz. Sichtbar für alle Besucher, auch für anonyme Benutzer." + short_site_description: "Kurze Beschreibung in ein paar Worten. Sichtbar für alle Besucher, auch für anonyme Benutzer." + contact_email: "E-Mail-Adresse einer verantwortlichen Person für diese Website. Wird verwendet für kritische Benachrichtigungen und unter /about angezeigt. Sichtbar für anonyme Benutzer auf öffentlichen Websites." + contact_url: "Kontakt-URL für diese Website. Wenn vorhanden, ersetzt sie die E-Mail-Adresse unter /about und ist für anonyme Benutzer auf öffentlichen Websites sichtbar." crawl_images: "Lade Bilder von fremden URLs herunter, um ihre Höhe und Breite zu bestimmen." download_remote_images_to_local: "Externe (per Hotlink aufgerufene) Bilder in lokale Bilder konvertieren, indem sie heruntergeladen werden; so bleibt der Inhalt erhalten, auch wenn die Bilder in Zukunft von der externen Website entfernt werden." download_remote_images_threshold: "Minimal benötigter freier Festplattenspeicher, um externe Bilder lokal herunterzuladen (in Prozent)" @@ -1430,6 +1446,8 @@ de: edit_history_visible_to_public: "Erlaube jedem, vorherige Versionen eines bearbeiteten Beitrags zu sehen. Wenn deaktiviert, sind diese nur für Team-Mitglieder sichtbar." delete_removed_posts_after: "Beiträge, die deren Verfasser selbst entfernt hat, werden nach (n) Stunden automatisch gelöscht. Die Beiträge werden sofort gelöscht, wenn dieser Wert auf 0 gesetzt wird." notify_users_after_responses_deleted_on_flagged_post: "Wenn ein Beitrag gemeldet und dann entfernt wird, werden alle Benutzer, die auf den Beitrag geantwortet haben und deren Antworten entfernt wurden, benachrichtigt." + max_image_width: "Maximale Breite der Vorschaubilder in einem Beitrag. Bilder mit einer größeren Breite werden verkleinert und mit einem Lightbox versehen." + max_image_height: "Maximale Höhe der Vorschaubilder in einem Beitrag. Bilder mit einer größeren Höhe werden verkleinert und mit einem Lightbox versehen." responsive_post_image_sizes: "Ändere die Größe der Lightbox-Vorschaubilder, um High-DPI-Bildschirme mit den folgenden Pixelverhältnissen zu ermöglichen. Entferne alle Werte, um responsive Bilder zu deaktivieren." fixed_category_positions: "Wenn diese Option aktiviert ist, können Kategorien in einer fest vorgegebenen Reihenfolge angeordnet werden. Andernfalls werden Kategorien nach Aktivität sortiert aufgelistet." fixed_category_positions_on_create: "Wenn diese Option aktiviert ist, wird die Reihenfolge der Kategorien im Dialog zur Themenerstellung beibehalten (erfordert fixed_category_positions)." @@ -1567,10 +1585,7 @@ de: max_favorite_badges: "Maximale Anzahl von Abzeichen, die der Benutzer auswählen kann" whispers_allowed_groups: "Private Kommunikation innerhalb von Themen für Mitglieder bestimmter Gruppen erlauben." allow_index_in_robots_txt: "Lege in der robots.txt fest, dass diese Website von Websuchmaschinen indiziert werden darf. In Ausnahmefällen kannst du die robots.txt dauerhaft überschreiben." - blocked_email_domains: "Eine durch senkrechte Striche getrennte Liste von E-Mail-Domains, die für die Registrierung neuer Konten nicht verwendet werden dürfen. Beispiel: mailinator.com|trashmail.net" - allowed_email_domains: "Eine durch senkrechte Striche („|“) getrennte Liste von E-Mail-Domains, die für die Registrierung neuer Konten verwendet werden können. ACHTUNG: Benutzer mit E-Mail-Adressen anderer Domains werden nicht zugelassen!" normalize_emails: "Überprüfen, ob normalisierte E-Mail-Adressen einzigartig sind. Bei normalisierten E-Mail-Adressen werden alle Punkte aus dem Benutzernamen und alles zwischen + und @ Symbolen entfernt." - auto_approve_email_domains: "Benutzer mit E-Mail-Adressen aus dieser Liste von Domains werden automatisch genehmigt." hide_email_address_taken: "Benutzer beim Registrieren oder Passwort-Zurücksetzen nicht über ein bereits bestehendes Konto mit dieser E-Mail-Adresse informieren. Erfordert die Angabe vollständiger E-Mail-Adressen zum Passwort-Zurücksetzen." log_out_strict: "Beim Abmelden ALLE Sitzungen des Benutzers auf allen Geräten beenden" version_checks: "Kontaktiere den Discourse Hub zur Überprüfung auf neue Versionen und zeige Benachrichtigungen über neue Versionen im /admin-Dashboard an." @@ -1634,6 +1649,7 @@ de: discord_trusted_guilds: 'Das Anmelden per Discord nur Mitgliedern dieser Discord-Gilden gestatten. Verwende die numerische ID der Gilde. Lies die Anleitung hier, um mehr zu erfahren. Leer lassen, um jede Gilde zuzulassen.' enable_backups: "Erlaube Administratoren, Backups des Forums zu erstellen" allow_restore: "Wiederherstellung zulassen, die ALLE Website-Daten ersetzen kann! Lasse dies deaktiviert, es sei denn, du möchtest ein Backup wiederherstellen" + maximum_backups: "Die maximale Anzahl an Backups, die gespeichert werden. Ältere Backups werden automatisch gelöscht." automatic_backups_enabled: "Automatische Backups aktivieren. Die Backups werden im eingestellten Zeitintervall erstellt." backup_frequency: "Die Anzahl von Tagen zwischen Backups." s3_backup_bucket: "Der entfernte Bucket, in dem Backups gespeichert werden. WARNUNG: Achte darauf, dass es ein privater Bucket ist." @@ -1808,6 +1824,7 @@ de: faq_url: "Die vollständige URL zu deiner extern gehosteten FAQ, sofern vorhanden." tos_url: "Die vollständige URL zu deinen extern gehosteten Nutzungsbedingungen, sofern vorhanden." privacy_policy_url: "Die vollständige URL zu deiner extern gehosteten Datenschutzerklärung, sofern vorhanden." + log_anonymizer_details: "Legt fest, ob die Daten eines Benutzers nach Anonymisierung im Log gespeichert werden sollen." newuser_spam_host_threshold: "Wie oft ein neuer Benutzer einen Link zum selben Host innerhalb seiner `newuser_spam_host_threshold` Beiträge veröffentlichen kann, bevor dies als Spam angesehen wird." allowed_spam_host_domains: "Liste von Domains, die keinem Spam-Host-Test unterzogen werden. Neue Benutzer werden niemals daran gehindert, Beiträge mit Links zu diesen Domains zu erstellen." staff_like_weight: "Wie viel Gewicht „Gefällt mir“-Angaben des Teams haben sollen („Gefällt mir“-Angaben von Nicht-Team-Mitgliedern haben ein Gewicht von 1.)" @@ -1974,6 +1991,7 @@ de: display_name_on_posts: "Zeige zusätzlich zum @Benutzernamen auch den vollständigen Namen des Benutzers bei seinen Beiträgen." show_time_gap_days: "Wenn zwei Beiträge so viele Tage auseinanderliegen, dann wird die Zeitspanne im Thema angezeigt." short_progress_text_threshold: "Sobald die Anzahl an Beiträgen in einem Thema diese Nummer übersteigt, zeigt der Fortschrittsbalken nur noch die aktuelle Beitragsnummer. Dieser Wert sollte angepasst werden, falls die Breite des Fortschrittsbalkens verändert wird." + default_code_lang: "Standardmäßige Syntaxhervorhebung für Programmiersprachen, die auf Codeblöcke angewendet wird (auto, text, ruby, python usw.). Dieser Wert muss auch in der Website-Einstellung `highlighted languages` enthalten sein." warn_reviving_old_topic_age: "Wenn jemand beginnt, auf ein Thema zu antworten, dessen letzte Antwort älter als diese Anzahl an Tagen ist, wird eine Warnung angezeigt. Zum Deaktivieren auf 0 setzen." autohighlight_all_code: "Erzwinge Syntaxhervorhebung für alle Quellcode-Blöcke auch dann, wenn keine Sprache angegeben wurde." highlighted_languages: "Aktivierte Regeln für die Syntaxhervorhebung. (Warnung: Das Aktivieren zu vieler Sprachen kann die Leistung beeinträchtigen.) Siehe https://highlightjs.org/static/demo für eine Demonstration" @@ -2090,6 +2108,8 @@ de: shared_drafts_min_trust_level: "Erlaube Benutzern, gemeinsame Entwürfe einzusehen und zu bearbeiten." push_notifications_prompt: "Zeige eine Aufforderung zur Benutzerzustimmung an." push_notifications_icon: "Das Abzeichensymbol, das in der Benachrichtigungsecke angezeigt wird. Ein monochromatisches PNG mit Transparenz (96 × 96) wird empfohlen." + enable_desktop_push_notifications: "Desktop-Push-Benachrichtigungen aktivieren" + push_notification_time_window_mins: "(n) Minuten warten, bevor eine Push-Benachrichtigung gesendet wird. Hilft zu verhindern, dass Push-Benachrichtigungen an einen aktiven Online-Benutzer gesendet werden." base_font: "Basisschriftart für die meisten Texte auf der Website. Themes können diese Schriftart mit der benutzerdefinierten CSS-Eigenschaft `--font-family` überschreiben." heading_font: "Schriftart für Überschriften auf der Website. Themes können diese Schriftart mit der benutzerdefinierten CSS-Eigenschaft `--heading-font-family` überschreiben." enable_sitemap: "Generiere eine Sitemap für deine Website und füge sie in die robots.txt-Datei ein." @@ -2111,9 +2131,12 @@ de: use_name_for_username_suggestions: "Beim Vorschlagen von Benutzernamen den vollen Namen eines Benutzers verwenden." suggest_weekends_in_date_pickers: "Wochenenden (Samstag und Sonntag) in die Datumsvorschläge einbeziehen (deaktiviere dies, wenn du Discourse nur an Wochentagen von Montag bis Freitag benutzt)." splash_screen: "Zeigt einen temporären Ladebildschirm an, während Website-Assets geladen werden" + navigation_menu: "Legt fest, welches Navigationsmenü verwendet werden soll. Die Seitenleisten- und Kopfzeilennavigation kann von den Benutzern angepasst werden. Die Legacy-Option ist zur Abwärtskompatibilität verfügbar." default_sidebar_categories: "Ausgewählte Kategorien werden standardmäßig im Abschnitt „Kategorien“ der Seitenleiste angezeigt." default_sidebar_tags: "Ausgewählte Schlagwörter werden standardmäßig im Abschnitt „Schlagwörter“ der Seitenleiste angezeigt." enable_new_user_profile_nav_groups: "EXPERIMENTELL: Benutzern der ausgewählten Gruppen wird das neue Benutzerprofil-Navigationsmenü angezeigt" + enable_experimental_topic_timeline_groups: "EXPERIMENTELL: Benutzern der ausgewählten Gruppen wird die umgestaltete Themenzeitleiste angezeigt" + enable_experimental_hashtag_autocomplete: "EXPERIMENTELL: Das neue #hashtag-Autovervollständigungssystem für Kategorien und Schlagwörter, das das ausgewählte Element anders darstellt und eine bessere Suche aufweist, verwenden" errors: invalid_css_color: "Ungültige Farbe. Gib einen Farbnamen oder einen Hexadezimalwert ein." invalid_email: "Ungültige E-Mail-Adresse." @@ -2561,10 +2584,12 @@ de: %{notes} flag_reasons: off_topic: "Dein Beitrag wurde als **Thema verfehlt** gemeldet: Die Community denkt, dass er nicht zum Thema passt, wie es durch den Titel und den ersten Beitrag definiert wurde." + inappropriate: "Dein Beitrag wurde als **unangemessen** gemeldet: Die Community denkt, dass er anstößig, beleidigend oder hasserfüllt ist oder einen Verstoß gegen [die Community-Richtlinien](%{base_path}/guidelines) darstellt." spam: "Dein Beitrag wurde als **Spam** gemeldet: Die Community denkt, dass es sich um Werbung handelt, zu werblich in seiner Art und nicht nützlich oder für das Diskussionsthema relevant ist." notify_moderators: "Dein Beitrag wurde **den Moderatoren gemeldet**: Die Community denkt, dass etwas an deinem Beitrag das Eingreifen eines Team-Mitglieds erfordert." responder: off_topic: "Der Beitrag wurde als **Thema verfehlt** gemeldet: Die Community denkt, dass er nicht zum Thema passt, wie es durch den Titel und den ersten Beitrag definiert wurde." + inappropriate: "Der Beitrag wurde als **unangemessen** gemeldet: Die Community denkt, dass er anstößig, beleidigend oder hasserfüllt ist oder einen Verstoß gegen [die Community-Richtlinien](%{base_path}/guidelines) darstellt." spam: "Der Beitrag wurde als **Spam** gemeldet: Die Community denkt, dass es sich um Werbung handelt oder der Beitrag zu werblich in seiner Art und nicht nützlich oder für das Diskussionsthema relevant ist." notify_moderators: "Der Beitrag wurde **den Moderatoren gemeldet**: Die Community denkt, dass etwas an dem Beitrag das Eingreifen eines Team-Mitglieds erfordert." flags_dispositions: @@ -3391,6 +3416,7 @@ de: %{respond_instructions} user_watching_category_or_tag: + title: "Benutzer beobachtet Kategorie oder Schlagwort" subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -4459,6 +4485,8 @@ de: title: "Discourse einrichten" step: introduction: + title: "Über deine Website" + description: "Diese werden bei deiner Anmeldung und auf allen öffentlichen Seiten angezeigt. Du kannst sie später jederzeit ändern." fields: title: label: "Name der Community" @@ -4469,21 +4497,28 @@ de: default_locale: label: "Sprache" privacy: + title: "Mitgliedererfahrung" fields: login_required: placeholder: "Privat" extra_description: "Nur angemeldete Benutzer können auf diese Community zugreifen" invite_only: + placeholder: "Nur auf Einladung" extra_description: "Benutzer müssen von vertrauenswürdigen Benutzern oder Team-Mitgliedern eingeladen werden, andernfalls können sich Benutzer selbst registrieren" must_approve_users: + placeholder: "Genehmigung erfordern" extra_description: "Benutzer müssen von den Team-Mitgliedern genehmigt werden" chat_enabled: placeholder: "Chat aktivieren" + extra_description: "Interagiere in Echtzeit mit deinen Mitgliedern" enable_sidebar: placeholder: "Seitenleiste aktivieren" + extra_description: "Einfacher Zugriff auf deine Lieblingsplätze" ready: + title: "Deine Website ist bereit!" description: "Das war's! Du hast die Grundlagen für die Einrichtung deiner Community geschaffen. Jetzt kannst du dich umsehen, ein Willkommensthema schreiben und Einladungen verschicken!

    Viel Spaß!" styling: + title: "Look and Feel" fields: color_scheme: label: "Farbschema" @@ -4514,19 +4549,38 @@ de: label: "Kategorie-Boxen mit Themen" subcategories_with_featured_topics: label: "Unterkategorien mit hervorgehobenen Themen" - corporate: + branding: + title: "Website-Logo" fields: + logo: + label: "Hauptlogo" + description: "Empfohlene Größe: 600 x 200" + logo_small: + label: "Quadratisches Logo" + description: "Empfohlene Größe: 512 x 512. Wird auch als Favicon und App-Symbol für den Home-Bildschirm von Mobilgeräten verwendet." + corporate: + title: "Deine Organisation" + description: "Die folgenden Informationen werden in deinen Nutzungsbedingungen und auf deinen „Über uns“-Seiten verwendet. Falls es keine Firma gibt, kannst du dies überspringen." + fields: + company_name: + label: "Firmenname" + placeholder: "Acme-Organisation" governing_law: + label: "Anzuwendendes Recht" placeholder: "Deutsches Recht" contact_url: + label: "Webseite" placeholder: "https://www.example.com/contact-us" city_for_disputes: + label: "Stadt für Rechtsstreitigkeiten" placeholder: "Berlin" site_contact: + label: "Automatische Nachrichten" description: "Alle automatischen persönlichen Discourse-Nachrichten werden von diesem Benutzer geschickt, z. B. Warnungen zu Meldungen und Benachrichtigungen über den Abschluss von Backups." contact_email: label: "Ansprechpartner" placeholder: "example@user.com" + description: "E-Mail-Adresse einer verantwortlichen Person für diese Website. Wird verwendet für kritische Benachrichtigungen und auf deiner „Über uns“-Seite für dringende Anfragen angezeigt." invites: title: "Team einladen" description: "Du hast es fast geschafft! Lass uns ein paar Leute einladen, die dabei helfen, Diskussionen anzuregen – mit interessanten Themen und Beiträgen, um deine Community in Schwung zu bringen." diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index a1afb75b4b..06d58e84b2 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -982,8 +982,6 @@ el: prioritize_username_in_ux: "Δίξε το όνομα χρήστη πρώτα στη σελίδα του χρήστη, την κάρτα χρήστη και στις δημοσιεύσεις (όταν είναι ανενεργό, φαίνεται πρώτα το όνομα)" email_token_valid_hours: "Τα tokens ανάκτησης κωδικού / ενεργοποίησης λογαριασμού παραμένουν έγκυρα για (ν) ώρες." enable_badges: "Ενεργοποίηση συστήματος παρασήμων" - blocked_email_domains: "Μία λίστα με διευθύνσεις email τις οποίες οι χρήστες δεν μπορούν να χρησιμοποιήσουν για να δημιουργήσουν λογαριασμό. Πχ: mailinator.com|trashmail.net" - allowed_email_domains: "Μία λίστα με διευθύνσεις email τις οποίες οι χρήστες ΘΑ ΠΡΕΠΕΙ να χρησιμοποιήσουν για να δημιουργήσουν λογαριασμό. ΠΡΟΣΟΧΗ: οι χρήστες με διευθύνσεις email οι οποίες δεν βρίσκονται σε αυτή τη λίστα δεν θα μπορούν να δημιουργήσουν λογαριασμό." log_out_strict: "Όταν αποσυνδεθείτε, ΟΛΕΣ οι δραστηριότητες σας σε ΟΛΕΣ τις συσκευές θα αποσυνδεθούν" new_version_emails: "Αποστολή email στην contact_email διεύθυνση όταν μια νέα έκδοση του Discourse είναι διαθέσιμη." invite_expiry_days: "Για πόσο καιρό οι κύριες προσκλήσεις ισχύουν, σε μέρες " diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 6244bd62c7..b46bbf1cf7 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1651,6 +1651,7 @@ en: content_security_policy_frame_ancestors: "Restrict who can embed this site in iframes via CSP. Control allowed hosts on Embedding" content_security_policy_script_src: "Additional allowlisted script sources. The current host and CDN are included by default. See Mitigate XSS Attacks with Content Security Policy." invalidate_inactive_admin_email_after_days: "Admin accounts that have not visited the site in this number of days will need to re-validate their email address before logging in. Set to 0 to disable." + include_secure_categories_in_tag_counts: "When enabled, count of topics for a tag will include topics that are in read restricted categories for all users. When disabled, normal users are only shown a count of topics for a tag where all the topics are in public categories." top_menu: "Determine which items appear in the homepage navigation, and in what order. Example latest|new|unread|categories|top|read|posted|bookmarks" post_menu: "Determine which items appear on the post menu, and in what order. Example like|edit|flag|delete|share|bookmark|reply" post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." @@ -1682,10 +1683,10 @@ en: whispers_allowed_groups: "Allow private communication within topics for members of specified groups." allow_index_in_robots_txt: "Specify in robots.txt that this site is allowed to be indexed by web search engines. In exceptional cases you can permanently override robots.txt." - blocked_email_domains: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Example: mailinator.com|trashmail.net" - allowed_email_domains: "A pipe-delimited list of email domains that users MUST register accounts with. WARNING: Users with email domains other than those listed will not be allowed!" + blocked_email_domains: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Subdomains are automatically handled for the specified domains. Wildcard symbols * and ? are not supported. Example: mailinator.com|trashmail.net" + allowed_email_domains: "A pipe-delimited list of email domains that users MUST register accounts with. Subdomains are automatically handled for the specified domains. Wildcard symbols * and ? are not supported. WARNING: Users with email domains other than those listed will not be allowed!" normalize_emails: "Check if normalized email is unique. Normalized email removes all dots from the username and everything between + and @ symbols." - auto_approve_email_domains: "Users with email addresses from this list of domains will be automatically approved." + auto_approve_email_domains: "Users with email addresses from this list of domains will be automatically approved. Subdomains are automatically handled for the specified domains. Wildcard symbols * and ? are not supported." hide_email_address_taken: "Don't inform users that an account exists with a given email address during signup or during forgot password flow. Require full email for 'forgotten password' requests." log_out_strict: "When logging out, log out ALL sessions for the user on all devices" version_checks: "Ping the Discourse Hub for version updates and show new version messages on the /admin dashboard" @@ -1893,6 +1894,7 @@ en: tl3_requires_likes_given: "The minimum number of likes that must be given in the last (tl3 time period) days to qualify for promotion to trust level 3." tl3_requires_likes_received: "The minimum number of likes that must be received in the last (tl3 time period) days to qualify for promotion to trust level 3." tl3_links_no_follow: "Do not remove rel=nofollow from links posted by trust level 3 users." + tl4_delete_posts_and_topics: "Allow TL4 users to delete posts and topics created by other users. TL4 users will also be able to see deleted topics and posts." trusted_users_can_edit_others: "Allow users with high trust levels to edit content from other users" min_trust_to_create_topic: "The minimum trust level required to create a new topic." @@ -2040,6 +2042,11 @@ en: block_auto_generated_emails: "Block incoming emails identified as being auto generated." ignore_by_title: "Ignore incoming emails based on their title." mailgun_api_key: "Mailgun Secret API key used to verify webhook messages." + sendgrid_verification_key: "Sendgrid verification key used to verify webhook messages." + mailjet_webhook_token: "Token used to verify webhook payload. It must be passed as the 't' query parameter of the webhook, for example: https://example.com/webhook/mailjet?t=supersecret" + mandrill_authentication_key: "Mandrill authentication key used to verify webhook messages." + postmark_webhook_token: "Token used to verify webhook payload. It must be passed as the 't' query parameter of the webhook, for example: https://example.com/webhook/postmark?t=supersecret" + sparkpost_webhook_token: "Token used to verify webhook payload. It must be passed as the 't' query parameter of the webhook, for example: https://example.com/webhook/sparkpost?t=supersecret" soft_bounce_score: "Bounce score added to the user when a temporary bounce happens." hard_bounce_score: "Bounce score added to the user when a permanent bounce happens." @@ -2388,6 +2395,7 @@ en: navigation_menu: "Determine which navigation menu to use. Sidebar and header navigation are customizable by users. Legacy option is available for backward compatibility." default_sidebar_categories: "Selected categories will be displayed under Sidebar's Categories section by default." default_sidebar_tags: "Selected tags will be displayed under Sidebar's Tags section by default." + enable_new_notifications_menu: "Enables the new notifications menu for the legacy navigation menu." enable_new_user_profile_nav_groups: "EXPERIMENTAL: Users of the selected groups will be shown the new user profile navigation menu" enable_experimental_topic_timeline_groups: "EXPERIMENTAL: Users of the selected groups will be shown the refactored topic timeline" enable_experimental_hashtag_autocomplete: "EXPERIMENTAL: Use the new #hashtag autocompletion system for categories and tags that renders the selected item differently and has improved search" @@ -2449,6 +2457,7 @@ en: discourse_connect_cannot_be_enabled_if_second_factor_enforced: "You cannot enable DiscourseConnect if 2FA is enforced." delete_rejected_email_after_days: "This setting cannot be set lower than the delete_email_logs_after_days setting or greater than %{max}" invalid_uncategorized_category_setting: "The Uncategorized category cannot be selected if allow uncategorized topics is not allowed" + enable_new_notifications_menu_not_legacy_navigation_menu: "You must set `navigation_menu` to `legacy` before enabling this setting." placeholder: discourse_connect_provider_secrets: @@ -2671,6 +2680,7 @@ en: must_not_contain_two_special_chars_in_seq: "must not contain a sequence of 2 or more special chars (.-_)" must_not_end_with_confusing_suffix: "must not end with a confusing suffix like .json or .png etc." email: + blank: "can't be blank." invalid: "is invalid." not_allowed: "is not allowed from that email provider. Please use another email address." blocked: "is not allowed." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index a79d6b31c1..d2b1ebe287 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -234,6 +234,7 @@ es: not_found_template_link: |

    Esta invitación a %{site_name} ya no se puede usar. Por favor, pídele a la persona que te invitó que te vuelva a enviar una nueva invitación.

    existing_user_cannot_redeem: "Esta invitación no se puede canjear. Pide a la persona que te invitó que te envíe una nueva invitación." + existing_user_already_redemeed: "Ya has canjeado este enlace de invitación." user_exists: "No hace falta que invites a %{email}, ¡ya tienen una cuenta!" invite_exists: "Ya has invitado a %{email}." invalid_email: "%{email} no es un correo electrónico válido." @@ -498,6 +499,7 @@ es: Puedes editar tu anterior respuesta para añadir una cita. Para ello, selecciona el texto que quieras citar y pulsa el botón citar respuesta que aparecerá. Es más fácil leer temas que tengan menos respuestas (aunque más profundas), que tener que leer muchas respuestas individuales. + dominating_topic: '¡Has publicado mucho en este tema! Considera la posibilidad de dar a los demás la oportunidad de responder aquí y debatir también entre ellos.' get_a_room: Has respondido a @%{reply_username} %{count} veces, ¿sabías que también puedes enviarle un mensaje personal directamente? too_many_replies: | ### Has llegado al límite de respuestas en este tema @@ -537,6 +539,7 @@ es: target_user_not_found: "No se pudo encontrar a uno de los usuarios a los que envías este mensaje." unable_to_update: "Hubo un error actualizando ese tema." unable_to_tag: "Hubo un error al etiquetar el tema." + unable_to_unlist: "Lo sentimos, no puedes crear un tema no listado." featured_link: invalid: "no es válido. La URL debería incluir http:// o https://." user: @@ -667,6 +670,7 @@ es: one: "%{count} me gusta" other: "%{count} Me gusta" cannot_permanently_delete: + many_posts: "Este tema tiene mensajes sin eliminar. Elimínalos definitivamente antes de eliminar definitivamente el tema." wait_or_different_admin: "Debes esperar %{time_left} antes de eliminar para siempre esta publicación, pero otra persona con permisos de administrador puede eliminarla inmediatamente." rate_limiter: slow_down: "Has realizado esta acción demasiadas veces. Inténtalo de nuevo más tarde." @@ -858,6 +862,7 @@ es: email_body: "%{link}\n\n%{message}" inappropriate: title: "Inapropiado" + description: 'Esta publicación tiene contenido que una persona razonable podría considerar ofensivo, abusivo, ser una conducta de odio o una violación de nuestras directrices de la comunidad.' short_description: 'Infringe nuestras directrices de la comunidad' notify_user: title: "Enviar un mensaje a @%{username}" @@ -923,6 +928,7 @@ es: short_description: "Esto es un anuncio" inappropriate: title: "Inapropiado" + description: 'Este tema tiene contenido que una persona razonable podría considerar ofensivo, abusivo, ser una conducta de odio o una violación de nuestras directrices de la comunidad.' long_form: "marcado como inapropiado" short_description: 'Infringe nuestras directrices de la comunidad' notify_moderators: @@ -960,6 +966,8 @@ es: mailing_list_mode: "Desactivar el modo lista de correo" all: "No enviarme ningún correo electrónico desde %{sitename}" different_user_description: "Actualmente has iniciado sesión con un usuario diferente al que te hemos enviado el correo electrónico. Cierra sesión o entra en modo anónimo antes de intentarlo de nuevo." + not_found_description: "Lo sentimos, no hemos podido encontrar esa suscripción. ¿Es posible que el enlace del correo electrónico sea demasiado antiguo y haya caducado?" + user_not_found_description: "Lo sentimos, no hemos podido encontrar un usuario para esta suscripción. Probablemente estás intentando dar de baja una cuenta que ya no existe." log_out: "Cerrar sesión" submit: "Guardar preferencias" digest_frequency: @@ -996,6 +1004,7 @@ es: write: "Escribir todo" one_time_password: "Crear un código de inicio de sesión de un único uso" bookmarks_calendar: "Leer recordatorios de marcadores" + user_status: "Leer y actualizar el estado del usuario" invalid_public_key: "Lo sentimos, la clave pública no es válida." invalid_auth_redirect: "Lo sentimos, este host auth_redirect no está permitido." invalid_token: "El token no es válido, ha caducado o no se encuentra" @@ -1089,10 +1098,12 @@ es: author: Autor edit_reason: Motivo consolidated_api_requests: + title: "Solicitudes de API consolidadas" xaxis: api: "API" user_api: "API de usuario" yaxis: "Día" + description: "Solicitudes de API para claves de API normales y claves de API de usuario." dau_by_mau: title: "UAD/UAM" xaxis: "Día" @@ -1415,6 +1426,11 @@ es: allow_duplicate_topic_titles_category: "Permitir temas con títulos iguales si la categoría es distinta. Hay que desactivar el ajuste allow_duplicate_topic_titles." unique_posts_mins: "Cantidad de minutos que deben pasar antes de que un usuario pueda publicar el mismo contenido de nuevo" educate_until_posts: "Cuando el usuario comienza a escribir sus primeras (n) publicaciones, mostrar el panel emergente de consejos para usuarios nuevos en el editor." + title: "El nombre de este sitio. Visible para todos los visitantes, incluidos los usuarios anónimos." + site_description: "Describe este sitio en una frase. Visible para todos los visitantes, incluidos los usuarios anónimos." + short_site_description: "Breve descripción en pocas palabras. Visible para todos los visitantes, incluidos los usuarios anónimos." + contact_email: "Dirección de correo electrónico del contacto clave responsable de este sitio. Se utiliza para notificaciones críticas, y también se muestra en /about. Visible para usuarios anónimos en sitios públicos." + contact_url: "URL de contacto para este sitio. Cuando está presente, sustituye a la dirección de correo electrónico en /about y es visible para los usuarios anónimos en los sitios públicos." crawl_images: "Recuperar imágenes desde URLs remotas para insertarlas con las dimensiones correctas de ancho y de largo." download_remote_images_to_local: "Convertir imágenes remotas (hotlinked) a imágenes locales descargándolas. Esto preserva el contenido incluso si las imágenes son eliminadas de su sitio remoto original en el futuro." download_remote_images_threshold: "Espacio mínimo en el disco necesario para descargar imágenes remotas de forma local (porcentaje)" @@ -1430,6 +1446,8 @@ es: edit_history_visible_to_public: "Permitir a todos ver las versiones previas de una publicación editada. Si se desactiva, solo los miembros del equipo podrán verlas." delete_removed_posts_after: "Las publicaciones eliminadas por su autor se eliminarán automáticamente después de (n) horas. Si se establece este valor a 0, las publicaciones se eliminarán inmediatamente." notify_users_after_responses_deleted_on_flagged_post: "Al borrar una publicación reportada, notificar a los usuarios que hayan respondido y se vaya a borrar su respuesta." + max_image_width: "Anchura máxima de las miniaturas de las imágenes de una publicación. Las imágenes con una anchura mayor se redimensionarán y se mostrarán en lightbox." + max_image_height: "Altura máxima de las miniaturas de las imágenes de una entrada. Las imágenes con una altura mayor se redimensionarán y se mostrarán en lightbox." responsive_post_image_sizes: "Cambiar el tamaño de las imágenes de vista previa de lightbox para permitir su visualización en pantallas de alto DPI de las siguientes proporciones de píxeles. Eliminar todos los valores para desactivar las imágenes responsivas." fixed_category_positions: "Si está marcada, podrás organizar las categorías en un orden determinado. Si no, las categorías se mostrarán según su actividad reciente." fixed_category_positions_on_create: "Si está marcada, el orden de las categorías se mantendrá en el diálogo de creación de temas (requiere fixed_category_positions)." @@ -1567,10 +1585,7 @@ es: max_favorite_badges: "Número máximo de insignias que el usuario puede seleccionar" whispers_allowed_groups: "Permitir la comunicación privada dentro de los temas para los miembros de los grupos especificados." allow_index_in_robots_txt: "Especificar en robots.txt que se permite que este sitio sea indexado por los motores de búsqueda en Internet. En casos excepcionales, puedes sobrescribir robots.txt permanentemente." - blocked_email_domains: "Lista de dominios de correo electrónico que no pueden ser utilizados para registrarse. Ejemplo: mailinator.com|trashmail.net" - allowed_email_domains: "Una lista separada con barras verticales de dominios de correo electrónico que los usuarios DEBEN usar para registrar sus cuentas. ¡CUIDADO! No se permitirá el registro de usuarios cuyo correo electrónico sea de un dominio diferente a los aquí listados" normalize_emails: "Comprobar si la dirección de correo normalizada es única. Para normalizar las direcciones se eliminan los puntos y todo lo que haya entre los caracteres + y @." - auto_approve_email_domains: "Los usuarios con direcciones de correo electrónico de esta lista de dominios serán aprobados automáticamente." hide_email_address_taken: "No informar a los usuarios si ya existe una cuenta con un correo electrónico al registrarse o al restablecer la contraseña. Pedir la dirección de correo completa para solicitudes de restablecimiento de contraseña." log_out_strict: "Al cerrar sesión, cierra TODAS las sesiones del usuario en todos los dispositivos" version_checks: "Ping el Discourse Hub para actualizaciones de versión y mostrar mensajes del número de versión en el dashboard /admin' target='_blank'>/admin." @@ -1634,6 +1649,7 @@ es: discord_trusted_guilds: 'Solo permitir iniciar sesión con Discord a miembros de estas guilds de Discord. Usa la ID numérica del guild. Para más información, mira las instrucciones aquí. Deja en blanco para permitir cualquier guild.' enable_backups: "Permitir a los administradores crear respaldos del foro" allow_restore: "Activar la restauración. ¡Esto puede sobrescribir TODOS los datos! Déjalo desactivado en todo momento, y actívalo solo para restaurar una copia de seguridad." + maximum_backups: "La cantidad máxima de copias de seguridad que se mantendrán. Las copias de seguridad más antiguas se eliminan automáticamente" automatic_backups_enabled: "Ejecutar respaldos automáticos definidos por la opción de frecuencia de respaldos" backup_frequency: "El número de días entre respaldos." s3_backup_bucket: "El bucket remoto para mantener copias de seguridad. AVISO: Asegúrate de que es un bucket privado." @@ -1808,6 +1824,7 @@ es: faq_url: "Si tienes un documento de preguntas frecuentes alojado en algún otro sitio y que quieras utilizar, introduce la URL completa aquí." tos_url: "Si tienes un documento de condiciones de servicio alojado en algún otro sitio y que quieras utilizar, introduce la URL completa aquí." privacy_policy_url: "Si tienes un documento de política de privacidad alojado en algún otro sitio y que quieras utilizar, introduce la URL completa aquí." + log_anonymizer_details: "Ya sea para mantener los detalles de un usuario en el registro después de ser anonimizados." newuser_spam_host_threshold: "Cantidad de veces que un usuario nuevo puede publicar un enlace al mismo host dentro del «newuser_spam_host_threshold» de sus publicaciones antes de ser considerado spam." allowed_spam_host_domains: "Una lista de dominios que se excluyen de las pruebas de spam. A los usuarios nuevos no se les restringirá nunca la posibilidad de crear publicaciones con enlaces a estos dominios." staff_like_weight: "Peso que dar a los me gusta del personal (los «Me gusta» de los demás pesan 1)." @@ -1974,6 +1991,7 @@ es: display_name_on_posts: "Mostrar el nombre completo de un usuario en sus publicaciones, además de su @usuario." show_time_gap_days: "Si entre dos publicaciones han pasado este número de días, mostrar el lapso de tiempo en el tema." short_progress_text_threshold: "Después de que el número de publicaciones en un tema alcance esta cifra, la barra de progreso solo mostrará el número de publicaciones actual. Si cambias la anchura de la barra de progreso, deberías revisar este valor." + default_code_lang: "Lenguaje de programación predeterminado para resaltado de sintaxis en bloques de código (auto, text, ruby, python, etc.) Este valor también debe estar presente en el ajuste «highlighted languages»." warn_reviving_old_topic_age: "Cuando alguien publica en un tema cuya última respuesta fue hace más tiempo que este número de días, se le mostrará un aviso para desalentar el hecho de revivir una antigua discusión. Establece el valor en 0 para desactivar." autohighlight_all_code: "Forzar el resaltado de código a los bloques de código preformateado cuando no se especifique el lenguaje del código." highlighted_languages: "Reglas de resaltado de sintaxis incluidas. (Aviso: incluir demasiados lenguajes puede afectar al rendimiento) Consulta: https://highlightjs.org/static/demo para un demo." @@ -2090,6 +2108,8 @@ es: shared_drafts_min_trust_level: "Permitir a los usuarios ver y editar borradores compartidos." push_notifications_prompt: "Mostrar aviso del consentimiento del usuario." push_notifications_icon: "El icono de la insignia que aparece en el menú de notificaciones. Se recomienda un PNG monocromático de 96 × 96 con transparencia." + enable_desktop_push_notifications: "Activar las notificaciones push de escritorio" + push_notification_time_window_mins: "Espera (n) minutos antes de enviar la notificación automática. Ayuda a evitar que se envíen notificaciones automáticas a un usuario en línea activo." base_font: "Fuente base para la mayoría de texto en el sitio. Los temas lo pueden sobrescribir a través de la propiedad personalizada de CSS «--font-family»." heading_font: "Fuente a usar para los encabezados del sitio. Los temas lo pueden sobrescribir a través de la propiedad personalizada de CSS «--heading-font-family»." enable_sitemap: "Generar un sitemap para el sitio e incluirlo en el archivo robots.txt." @@ -2111,9 +2131,12 @@ es: use_name_for_username_suggestions: "Usar el nombre completo para sugerir nombres de usuario" suggest_weekends_in_date_pickers: "Incluir fines de semana (sábados y domingos) en las sugerencias de los selectores de fecha (desactiva esto si solo usas Discourse de lunes a viernes)" splash_screen: "Muestra una pantalla de carga temporal mientras los archivos de la página se cargan" + navigation_menu: "Determina qué menú de navegación utilizar. La barra lateral y la navegación del encabezado son personalizables por los usuarios. La opción heredada está disponible para compatibilidad con versiones anteriores." default_sidebar_categories: "Las categorías seleccionadas aparecerán por defecto en la sección de categorías de la barra lateral." default_sidebar_tags: "Las etiquetas seleccionadas se mostrarán por defecto en la sección Etiquetas de la barra lateral." enable_new_user_profile_nav_groups: "EXPERIMENTAL: Los usuarios de los grupos seleccionados verán el nuevo menú de navegación del perfil de usuario" + enable_experimental_topic_timeline_groups: "EXPERIMENTAL: a los usuarios de los grupos seleccionados se les mostrará la línea de tiempo del tema refactorizado" + enable_experimental_hashtag_autocomplete: "EXPERIMENTAL: Utiliza el nuevo sistema de autocompletado #hashtag para categorías y etiquetas que muestra el elemento seleccionado de forma diferente y ha mejorado la búsqueda" errors: invalid_css_color: "Color no válido. Introduce el nombre de un color o su valor hexadecimal." invalid_email: "Dirección de correo electrónico no válida." @@ -2560,10 +2583,12 @@ es: %{notes} flag_reasons: off_topic: "Tu publicación fue denunciada como **sin relación con el tema**: la comunidad piensa que no se ajusta debidamente al tema, definido por el título o la primera publicación." + inappropriate: "Tu publicación fue denunciada como **inapropiada**: la comunidad piensa que es ofensiva, abusiva, una conducta de odio o que vulnera alguna de las [directrices de la comunidad](%{base_path}/guidelines)." spam: "Tu publicación fue denunciada como **spam**: la comunidad piensa que se trata de un anuncio, algo de naturaleza promocional, en vez de algo útil o relevante para lo que se espera del tema." notify_moderators: "Tu publicación fue denunciada para la **atención de un moderador**: la comunidad piensa que algo de esta publicación requiere la intervención manual de un miembro del equipo." responder: off_topic: "La publicación ha sido denunciada por **no tener relación con el tema**: la comunidad cree que no encaja con el tema, título y primer mensaje." + inappropriate: "La publicación fue denunciada como **inapropiada**: la comunidad piensa que es ofensiva, abusiva, una conducta de odio o que vulnera alguna de las [directrices de la comunidad](%{base_path}/guidelines)." spam: "La publicación ha sido denunciada por ser **spam**: la comunidad cree que es un anuncio o algo de naturaleza promocional en vez de buscar ser útil o relevante para el tema." notify_moderators: "La publicación ha sido denunciada para que sea **revisada por un moderador**: la comunidad cree que algo relacionado con ella requiere de la intervención de un miembro del equipo." flags_dispositions: @@ -3392,6 +3417,7 @@ es: %{respond_instructions} user_watching_category_or_tag: + title: "Usuario vigilando la categoría o etiqueta" subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -3889,6 +3915,178 @@ es: Sí, la jerga legal es aburrida, pero debemos protegernos a nosotros mismos – y por extensión, a ti y a tus datos – contra gente poco amistosa. Tenemos unas [Condiciones de servicio](%{base_path}/tos) que describen tu (y nuestro) comportamiento y derechos relacionados con el contenido, la privacidad y las leyes. Para utilizar este servicio, debes aceptar respetar nuestras [TOS](%{base_path}/tos). tos_topic: title: "Términos del servicio" + body: | + Estas condiciones rigen el uso del foro de Internet en <%{base_url}>. Para utilizar el foro, debes aceptar estas condiciones con %{company_name}, la empresa que gestiona el foro. + + La empresa puede ofrecer otros productos y servicios, con condiciones diferentes. Estas condiciones se aplican solo al uso del foro. + + Saltar a: + + - [Condiciones importantes](#heading--important-terms) + - [Tu permiso para utilizar el foro](#heading--permission) + - [Condiciones de uso del foro](#heading--conditions) + - [Uso aceptable](#heading--acceptable-use) + - [Normas de contenido](#heading--content-standards) + - [Aplicación](#heading--enforcement) + - [Tu cuenta](#heading--your-account) + - [Tu contenido](#heading--your-content) + - [Tu responsabilidad](#heading--responsibility) + - [Renuncias](#heading--disclaimers) + - [Límites de responsabilidad](#heading--liability) + - [Comentarios](#heading--feedback) + - [Terminación](#heading--termination) + - [Disputas](#heading--disputes) + - [Condiciones generales](#heading--general) + - [Contacto](#heading--contact) + - [Cambios](#heading--changes) + +

    Condiciones importantes

    + + ***Estas condiciones incluyen una serie de disposiciones importantes que afectan a tus derechos y responsabilidades, como las cláusulas de exención de responsabilidad en [Cláusulas de exención de responsabilidad](#heading--disclaimers), los límites de la responsabilidad de la empresa hacia ti en [Límites de responsabilidad](#heading--liability), tu acuerdo de cubrir a la empresa por los daños causados por tu mal uso del foro en [Responsabilidad por tu uso](#heading--responsibility), y un acuerdo para arbitrar disputas en [Disputas](#heading--disputes). + +

    Tu permiso para utilizar el Foro

    + + Sujeto a estos términos, la empresa te da permiso para utilizar el foro. Todo el mundo debe aceptar estas condiciones para utilizar el foro. + +

    Condiciones de uso del foro

    + + Tu permiso para utilizar el foro está sujeto a las siguientes condiciones: + + 1. Debes tener al menos trece años. + + 2. No podrás seguir utilizando el foro si la empresa se pone en contacto contigo directamente para decirte que no puedes hacerlo. + + 3. Debes utilizar el foro de acuerdo con las normas de [Uso aceptable](#heading--acceptable-use) y las [Normas de contenido](#heading--content-standards). + +

    Uso Aceptable

    + + 1. No puedes infringir la ley utilizando el foro. + + 2. 2. No puedes utilizar o intentar utilizar la cuenta de otra persona en el foro sin su permiso específico. + + 3. 3. No puedes comprar, vender o comerciar con nombres de usuario u otros identificadores únicos en el foro. + + 4. No puedes enviar publicidad, cartas en cadena u otras solicitudes a través del foro, ni utilizar el foro para recopilar direcciones u otros datos personales para listas de correo o bases de datos comerciales. + + 5. 5. No puedes automatizar el acceso al foro ni monitorizarlo, por ejemplo, con un rastreador web, un complemento o add-on del navegador u otro programa informático que no sea un navegador web. Puedes rastrear el foro para indexarlo para un motor de búsqueda disponible públicamente, si gestionas uno. + + 6. No puedes utilizar el foro para enviar correo electrónico a listas de distribución, grupos de noticias o alias de correo de grupo. + + 7. No puedes dar a entender falsamente que estás afiliado o respaldado por la empresa. + + 8. No puedes poner hipervínculos a imágenes u otros contenidos no hipertextuales del foro en otras páginas web. + + 9. No puedes eliminar ninguna marca que muestre la propiedad de los materiales que descargues del foro. + + 10. No puedes mostrar ninguna parte del foro en otras páginas web con ` + HTML + end + + private + + def oembed_data + @oembed_data = get_oembed + end + + def get_oembed_url + "https://www.tiktok.com/oembed?url=#{url}" + end + end + end +end diff --git a/lib/onebox/layout.rb b/lib/onebox/layout.rb index e6e31daa6f..53c63bb834 100644 --- a/lib/onebox/layout.rb +++ b/lib/onebox/layout.rb @@ -15,7 +15,7 @@ module Onebox @record = Onebox::Helpers.symbolize_keys(record) # Fix any relative paths - if @record[:image] && @record[:image] =~ %r{^/[^/]} + if @record[:image] && @record[:image] =~ %r{\A/[^/]} @record[:image] = "#{uri.scheme}://#{uri.host}/#{@record[:image]}" end @@ -40,7 +40,7 @@ module Onebox link: record[:link], title: record[:title], favicon: record[:favicon], - domain: record[:domain] || uri.host.to_s.sub(/^www\./, ""), + domain: record[:domain] || uri.host.to_s.sub(/\Awww\./, ""), article_published_time: record[:article_published_time], article_published_time_title: record[:article_published_time_title], metadata_1_label: record[:metadata_1_label], diff --git a/lib/onebox/mixins/git_blob_onebox.rb b/lib/onebox/mixins/git_blob_onebox.rb index 511a6b6084..713606d687 100644 --- a/lib/onebox/mixins/git_blob_onebox.rb +++ b/lib/onebox/mixins/git_blob_onebox.rb @@ -119,7 +119,7 @@ module Onebox a_lines = str.lines a_lines.each do |l| l = l.chomp("\n") # remove new line - m = l.match(/^[ ]*/) # find leading spaces 0 or more + m = l.match(/\A[ ]*/) # find leading spaces 0 or more unless m.nil? || l.size == m[0].size || l.size == 0 # no match | only spaces in line | empty line m_str_length = m[0].size if m_str_length <= 1 # minimum space is 1 or nothing we can break we found our minimum @@ -166,7 +166,7 @@ module Onebox @file = m[:file] @lang = Onebox::FileTypeFinder.from_file_name(m[:file]) - if @lang == "stl" && link.match?(%r{^https?://(www\.)?github\.com.*/blob/}) + if @lang == "stl" && link.match?(%r{\Ahttps?://(www\.)?github\.com.*/blob/}) @model_file = @lang.dup @raw = "https://render.githubusercontent.com/view/solid?url=" + self.raw_template(m) else diff --git a/lib/onebox/normalizer.rb b/lib/onebox/normalizer.rb index ac4c26b541..ecd0b509f1 100644 --- a/lib/onebox/normalizer.rb +++ b/lib/onebox/normalizer.rb @@ -4,19 +4,11 @@ module Onebox class Normalizer attr_reader :data - def get(attr, length = nil, sanitize = true) - return nil if Onebox::Helpers.blank?(data) - + def get(attr, *args) value = data[attr] - - return nil if Onebox::Helpers.blank?(value) - - value = html_entities.decode(value) - value = Sanitize.fragment(value) if sanitize - value.strip! - value = Onebox::Helpers.truncate(value, length) unless length.nil? - - value + return if value.blank? + return value.map { |v| sanitize_value(v, *args) } if value.is_a?(Array) + sanitize_value(value, *args) end def method_missing(attr, *args, &block) @@ -48,5 +40,13 @@ module Onebox def html_entities @html_entities ||= HTMLEntities.new end + + def sanitize_value(value, length = nil, sanitize = true) + value = html_entities.decode(value) + value = Sanitize.fragment(value) if sanitize + value.strip! + value = Onebox::Helpers.truncate(value, length) if length + value + end end end diff --git a/lib/onebox/open_graph.rb b/lib/onebox/open_graph.rb index 4fb2579347..10c1f165d9 100644 --- a/lib/onebox/open_graph.rb +++ b/lib/onebox/open_graph.rb @@ -22,6 +22,8 @@ module Onebox private + COLLECTIONS = %i[article_section article_section_color article_tag] + def extract(doc) return {} if Onebox::Helpers.blank?(doc) @@ -30,10 +32,17 @@ module Onebox doc .css("meta") .each do |m| - if (m["property"] && m["property"][/^(?:og|article|product):(.+)$/i]) || - (m["name"] && m["name"][/^(?:og|article|product):(.+)$/i]) + if (m["property"] && m["property"][/\A(?:og|article|product):(.+)\z/i]) || + (m["name"] && m["name"][/\A(?:og|article|product):(.+)\z/i]) value = (m["content"] || m["value"]).to_s - data[$1.tr("-:", "_").to_sym] ||= value unless Onebox::Helpers.blank?(value) + next if Onebox::Helpers.blank?(value) + key = $1.tr("-:", "_").to_sym + data[key] ||= value + if key.in?(COLLECTIONS) + collection_name = "#{key}s".to_sym + data[collection_name] ||= [] + data[collection_name] << value + end end end diff --git a/lib/onebox/sanitize_config.rb b/lib/onebox/sanitize_config.rb index 49c552f05b..77f97ee925 100644 --- a/lib/onebox/sanitize_config.rb +++ b/lib/onebox/sanitize_config.rb @@ -10,7 +10,7 @@ module Onebox Sanitize::Config::RELAXED, elements: Sanitize::Config::RELAXED[:elements] + - %w[audio details embed iframe source video svg path], + %w[audio details embed iframe source video svg path use], attributes: { "a" => Sanitize::Config::RELAXED[:attributes]["a"] + %w[target], "audio" => %w[controls controlslist], @@ -40,7 +40,8 @@ module Onebox "path" => %w[d fill-rule], "svg" => %w[aria-hidden width height viewbox], "div" => [:data], # any data-* attributes, - "span" => [:data], # any data-* attributes + "span" => [:data], # any data-* attributes, + "use" => %w[href], }, add_attributes: { "iframe" => { @@ -57,7 +58,7 @@ module Onebox next unless env[:node_name] == "a" a_tag = env[:node] a_tag["href"] ||= "#" - if a_tag["href"] =~ %r{^(?:[a-z]+:)?//} + if a_tag["href"] =~ %r{\A(?:[a-z]+:)?//} a_tag["rel"] = "nofollow ugc noopener" else a_tag.remove_attribute("target") @@ -89,6 +90,9 @@ module Onebox "source" => { "src" => HTTP_PROTOCOLS, }, + "use" => { + "href" => [:relative], + }, }, css: { properties: Sanitize::Config::RELAXED[:css][:properties] + %w[--aspect-ratio], diff --git a/lib/onebox/templates/discoursetopic.mustache b/lib/onebox/templates/discoursetopic.mustache new file mode 100644 index 0000000000..706b91b9fb --- /dev/null +++ b/lib/onebox/templates/discoursetopic.mustache @@ -0,0 +1,42 @@ +{{#image}}{{/image}} + +
    +

    {{title}}

    + {{#render_category_block?}} +
    + {{#categories}} + + + + {{name}} + + + {{/categories}} + {{#render_tags?}} +
    +
    +
    + + {{#article_tags}} + {{.}} + {{/article_tags}} +
    +
    +
    + {{/render_tags?}} +
    + {{/render_category_block?}} +
    + +{{#description}} +

    {{description}}

    +{{/description}} + +{{#data1}} +

    + {{label1}}: {{data1}} + {{#data2}} + {{label2}}: {{data2}} + {{/data2}} +

    +{{/data1}} diff --git a/lib/oneboxer.rb b/lib/oneboxer.rb index 2427b9b9b3..eb36ebad21 100644 --- a/lib/oneboxer.rb +++ b/lib/oneboxer.rb @@ -6,8 +6,8 @@ Dir["#{Rails.root}/lib/onebox/engine/*_onebox.rb"].sort.each { |f| require f } module Oneboxer ONEBOX_CSS_CLASS = "onebox" - AUDIO_REGEX = /^\.(mp3|og[ga]|opus|wav|m4[abpr]|aac|flac)$/i - VIDEO_REGEX = /^\.(mov|mp4|webm|m4v|3gp|ogv|avi|mpeg|ogv)$/i + AUDIO_REGEX = /\A\.(mp3|og[ga]|opus|wav|m4[abpr]|aac|flac)\z/i + VIDEO_REGEX = /\A\.(mov|mp4|webm|m4v|3gp|ogv|avi|mpeg|ogv)\z/i # keep reloaders happy unless defined?(Oneboxer::Result) @@ -343,7 +343,8 @@ module Oneboxer end end - html = html.presence || "#{URI(url).to_s}" + normalized_url = ::Onebox::Helpers.normalize_url_for_output(URI(url).to_s) + html = html.presence || "#{normalized_url}" { onebox: html, preview: html } end @@ -355,18 +356,28 @@ module Oneboxer "" end + normalized_url = ::Onebox::Helpers.normalize_url_for_output(url) case File.extname(URI(url).path || "") when VIDEO_REGEX <<~HTML HTML when AUDIO_REGEX - "" + <<~HTML + + HTML end end diff --git a/lib/plain_text_to_markdown.rb b/lib/plain_text_to_markdown.rb index 8e582e6f36..d914cd867e 100644 --- a/lib/plain_text_to_markdown.rb +++ b/lib/plain_text_to_markdown.rb @@ -100,7 +100,7 @@ class PlainTextToMarkdown # @param line [Line] def remove_quote_level_indicators!(line) - match_data = line.text.match(/^(?>+)\s?(?.*)/) + match_data = line.text.match(/\A(?>+)\s?(?.*)/) if match_data line.text = match_data[:text] @@ -128,7 +128,7 @@ class PlainTextToMarkdown def classify_line_as_code!(line, previous_line) line.code_block = previous_line.code_block unless previous_line.nil? || previous_line.valid_code_block? - return unless line.text =~ /^\s{0,3}```/ + return unless line.text =~ /\A\s{0,3}```/ if line.code_block.present? line.code_block.end_line = line @@ -173,7 +173,7 @@ class PlainTextToMarkdown end def indent_with_non_breaking_spaces(text) - text.sub(/^\s+/) do |s| + text.sub(/\A\s+/) do |s| # replace tabs with 2 spaces s.gsub!("\t", " ") diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index fc69c10476..77f9e4ff2c 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -675,17 +675,17 @@ class Plugin::Instance DiscoursePluginRegistry.register_glob(admin_path, "hbr", admin: true) DiscourseJsProcessor.plugin_transpile_paths << root_path.sub(Rails.root.to_s, "").sub( - %r{^/*}, + %r{\A/*}, "", ) DiscourseJsProcessor.plugin_transpile_paths << admin_path.sub(Rails.root.to_s, "").sub( - %r{^/*}, + %r{\A/*}, "", ) test_path = "#{root_dir_name}/test/javascripts" DiscourseJsProcessor.plugin_transpile_paths << test_path.sub(Rails.root.to_s, "").sub( - %r{^/*}, + %r{\A/*}, "", ) end @@ -1292,10 +1292,14 @@ class Plugin::Instance DiscoursePluginRegistry.register_topic_preloader_association(fields, self) end + def register_search_group_query_callback(callback) + DiscoursePluginRegistry.register_search_groups_set_query_callback(callback, self) + end + private def validate_directory_column_name(column_name) - match = /^[_a-z]+$/.match(column_name) + match = /\A[_a-z]+\z/.match(column_name) unless match raise "Invalid directory column name '#{column_name}'. Can only contain a-z and underscores" end diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 5dc04721a5..a7cd9a11b9 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -110,6 +110,7 @@ class PostDestroyer UserActionManager.post_created(@post) DiscourseEvent.trigger(:post_recovered, @post, @opts, @user) Jobs.enqueue(:sync_topic_user_bookmarked, topic_id: topic.id) if topic + Jobs.enqueue(:notify_mailing_list_subscribers, post_id: @post.id) if @post.is_first_post? UserActionManager.topic_created(topic) diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 56a4f0349f..05df311c5b 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -48,7 +48,7 @@ module PrettyText filename = find_file(root_path, part_name) if filename source = File.read("#{root_path}#{filename}") - source = ERB.new(source).result(binding) if filename =~ /\.erb$/ + source = ERB.new(source).result(binding) if filename =~ /\.erb\z/ transpiler = DiscourseJsProcessor::Transpiler.new transpiled = transpiler.perform(source, "#{Rails.root}/app/assets/javascripts/", part_name) @@ -64,7 +64,7 @@ module PrettyText def self.ctx_load_directory(ctx, path) root_path = "#{Rails.root}/app/assets/javascripts/" Dir["#{root_path}#{path}/**/*"].sort.each do |f| - apply_es6_file(ctx, root_path, f.sub(root_path, "").sub(/\.js(.es6)?$/, "")) + apply_es6_file(ctx, root_path, f.sub(root_path, "").sub(/\.js(.es6)?\z/, "")) end end @@ -116,9 +116,9 @@ module PrettyText to_load << a if File.file?(a) && a =~ /discourse-markdown/ end to_load.uniq.each do |f| - if f =~ %r{^.+assets/javascripts/} + if f =~ %r{\A.+assets/javascripts/} root = Regexp.last_match[0] - apply_es6_file(ctx, root, f.sub(root, "").sub(/\.js(\.es6)?$/, "")) + apply_es6_file(ctx, root, f.sub(root, "").sub(/\.js(\.es6)?\z/, "")) end end @@ -469,10 +469,21 @@ module PrettyText DiscourseEvent.trigger(:reduce_excerpt, doc, options) strip_image_wrapping(doc) strip_oneboxed_media(doc) + + if SiteSetting.enable_experimental_hashtag_autocomplete && options[:plain_hashtags] + convert_hashtag_links_to_plaintext(doc) + end + html = doc.to_html ExcerptParser.get_excerpt(html, max_length, options) end + def self.convert_hashtag_links_to_plaintext(doc) + doc + .css("a.hashtag-cooked") + .each { |hashtag| hashtag.replace("##{hashtag.attributes["data-slug"]}") } + end + def self.strip_links(string) return string if string.blank? diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index 621f59d7f8..2ca864fb4a 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -102,7 +102,7 @@ module PrettyText # TODO (martin) Remove this when everything is using hashtag_lookup # after enable_experimental_hashtag_autocomplete is default. def category_tag_hashtag_lookup(text) - is_tag = text =~ /#{TAG_HASHTAG_POSTFIX}$/ + is_tag = text =~ /#{TAG_HASHTAG_POSTFIX}\z/ if !is_tag && category = Category.query_from_hashtag_slug(text) [category.url, text] diff --git a/lib/require_dependency_backward_compatibility.rb b/lib/require_dependency_backward_compatibility.rb index 5b550e45d3..b5e3e0248d 100644 --- a/lib/require_dependency_backward_compatibility.rb +++ b/lib/require_dependency_backward_compatibility.rb @@ -14,7 +14,7 @@ module RequireDependencyBackwardCompatibility def require_dependency(filename) name = filename.to_s return if name == "jobs/base" - return super(name.sub(%r{^lib/}, "")) if name.start_with?("lib/") + return super(name.sub(%r{\Alib/}, "")) if name.start_with?("lib/") super end diff --git a/lib/retrieve_title.rb b/lib/retrieve_title.rb index 826792650f..ddbeb62838 100644 --- a/lib/retrieve_title.rb +++ b/lib/retrieve_title.rb @@ -32,7 +32,7 @@ module RetrieveTitle # A horrible hack - YouTube uses `document.title` to populate the title # for some reason. For any other site than YouTube this wouldn't be worth it. if title == "YouTube" && html =~ /document\.title *= *"(.*)";/ - title = Regexp.last_match[1].sub(/ - YouTube$/, "") + title = Regexp.last_match[1].sub(/ - YouTube\z/, "") end if !title && node = doc.at('meta[property="og:title"]') @@ -53,11 +53,12 @@ module RetrieveTitle def self.max_chunk_size(uri) # Exception for sites that leave the title until very late. - if uri.host =~ /(^|\.)amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)$/ + if uri.host =~ + /(^|\.)amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)\z/ return 500 end - return 300 if uri.host =~ /(^|\.)youtube\.com$/ || uri.host =~ /(^|\.)youtu\.be$/ - return 50 if uri.host =~ /(^|\.)github\.com$/ + return 300 if uri.host =~ /(^|\.)youtube\.com\z/ || uri.host =~ /(^|\.)youtu\.be\z/ + return 50 if uri.host =~ /(^|\.)github\.com\z/ # default is 20k 20 diff --git a/lib/route_matcher.rb b/lib/route_matcher.rb index 6512f7942f..2c46605555 100644 --- a/lib/route_matcher.rb +++ b/lib/route_matcher.rb @@ -46,7 +46,7 @@ class RouteMatcher return true if actions.nil? # actions are unrestricted # message_bus is not a rails route, special handling - return true if actions.include?("message_bus") && request.fullpath =~ %r{^/message-bus/.*/poll} + return true if actions.include?("message_bus") && request.fullpath =~ %r{\A/message-bus/.*/poll} path_params = path_params_from_request(request) actions.include? "#{path_params[:controller]}##{path_params[:action]}" @@ -59,7 +59,7 @@ class RouteMatcher params.all? do |param| param_alias = aliases&.[](param) - allowed_values = [allowed_param_values[param.to_s]].flatten + allowed_values = [allowed_param_values.fetch(param.to_s, [])].flatten value = requested_params[param.to_s] alias_value = requested_params[param_alias.to_s] diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb index f8082f04ca..b8dc762c84 100644 --- a/lib/s3_helper.rb +++ b/lib/s3_helper.rb @@ -175,12 +175,9 @@ class S3Helper cors_rules: final_rules, }, ) - rescue Aws::S3::Errors::AccessDenied => err - # TODO (martin) Remove this warning log level once we are sure this new - # ensure_cors! rule is functioning correctly. - Discourse.warn_exception( - err, - message: "Could not PutBucketCors rules for #{@s3_bucket_name}, rules: #{final_rules}", + rescue Aws::S3::Errors::AccessDenied + Rails.logger.info( + "Could not PutBucketCors rules for #{@s3_bucket_name}, rules: #{final_rules}", ) return false end diff --git a/lib/s3_inventory.rb b/lib/s3_inventory.rb index 91da944b92..3b9f4391e7 100644 --- a/lib/s3_inventory.rb +++ b/lib/s3_inventory.rb @@ -334,7 +334,7 @@ class S3Inventory objects = [] hive_path = File.join(inventory_path, bucket_name, inventory_id, "hive") - @s3_helper.list(hive_path).each { |obj| objects << obj if obj.key.match?(/symlink\.txt$/i) } + @s3_helper.list(hive_path).each { |obj| objects << obj if obj.key.match?(/symlink\.txt\z/i) } objects rescue Aws::Errors::ServiceError => e diff --git a/lib/scheduler/defer.rb b/lib/scheduler/defer.rb index 0afdcfbaa0..176ae1a62a 100644 --- a/lib/scheduler/defer.rb +++ b/lib/scheduler/defer.rb @@ -4,15 +4,18 @@ require "weakref" module Scheduler module Deferrable DEFAULT_TIMEOUT ||= 90 + STATS_CACHE_SIZE ||= 100 def initialize @async = !Rails.env.test? @queue = Queue.new @mutex = Mutex.new + @stats_mutex = Mutex.new @paused = false @thread = nil @reactor = nil @timeout = DEFAULT_TIMEOUT + @stats = LruRedux::ThreadSafeCache.new(STATS_CACHE_SIZE) end def timeout=(t) @@ -23,6 +26,10 @@ module Scheduler @queue.length end + def stats + @stats_mutex.synchronize { @stats.to_a } + end + def pause stop! @paused = true @@ -38,6 +45,11 @@ module Scheduler end def later(desc = nil, db = RailsMultisite::ConnectionManagement.current_db, &blk) + @stats_mutex.synchronize do + stats = (@stats[desc] ||= { queued: 0, finished: 0, duration: 0, errors: 0 }) + stats[:queued] += 1 + end + if @async start_thread if !@thread&.alive? && !@paused @queue << [db, blk, desc] @@ -67,13 +79,18 @@ module Scheduler def start_thread @mutex.synchronize do @reactor = MessageBus::TimerThread.new if !@reactor - @thread = Thread.new { do_work while true } if !@thread&.alive? + @thread = + Thread.new do + @thread.abort_on_exception = true if Rails.env.test? + do_work while true + end if !@thread&.alive? end end # using non_block to match Ruby #deq def do_work(non_block = false) db, job, desc = @queue.deq(non_block) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) db ||= RailsMultisite::ConnectionManagement::DEFAULT RailsMultisite::ConnectionManagement.with_connection(db) do @@ -84,6 +101,10 @@ module Scheduler end if !non_block job.call rescue => ex + @stats_mutex.synchronize do + stats = @stats[desc] + stats[:errors] += 1 if stats + end Discourse.handle_job_exception(ex, message: "Running deferred code '#{desc}'") ensure warning_job&.cancel @@ -93,6 +114,15 @@ module Scheduler Discourse.handle_job_exception(ex, message: "Processing deferred code queue") ensure ActiveRecord::Base.connection_handler.clear_active_connections! + if start + @stats_mutex.synchronize do + stats = @stats[desc] + if stats + stats[:finished] += 1 + stats[:duration] += Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + end + end + end end end diff --git a/lib/search.rb b/lib/search.rb index e81b7ab0d9..0429afe2bb 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -128,7 +128,7 @@ class Search end data.gsub!(/\S+/) do |str| - if str =~ %r{^["]?((https?://)[\S]+)["]?$} + if str =~ %r{\A["]?((https?://)[\S]+)["]?\z} begin uri = URI.parse(Regexp.last_match[1]) uri.query = nil @@ -145,9 +145,9 @@ class Search end def self.word_to_date(str) - return Time.zone.now.beginning_of_day.days_ago(str.to_i) if str =~ /^[0-9]{1,3}$/ + return Time.zone.now.beginning_of_day.days_ago(str.to_i) if str =~ /\A[0-9]{1,3}\z/ - if str =~ /^([12][0-9]{3})(-([0-1]?[0-9]))?(-([0-3]?[0-9]))?$/ + if str =~ /\A([12][0-9]{3})(-([0-1]?[0-9]))?(-([0-3]?[0-9]))?\z/ year = $1.to_i month = $2 ? $3.to_i : 1 day = $4 ? $5.to_i : 1 @@ -307,7 +307,7 @@ class Search # If the term is a number or url to a topic, just include that topic if @opts[:search_for_id] && %w[topic private_messages all_topics].include?(@results.type_filter) - if @term =~ /^\d+$/ + if @term =~ /\A\d+\z/ single_topic(@term.to_i) else if route = Discourse.route_for(@term) @@ -355,7 +355,7 @@ class Search Array.wrap(@custom_topic_eager_loads) end - advanced_filter(/^in:personal-direct$/i) do |posts| + advanced_filter(/\Ain:personal-direct\z/i) do |posts| if @guardian.user posts.joins("LEFT JOIN topic_allowed_groups tg ON posts.topic_id = tg.topic_id").where( <<~SQL, @@ -376,60 +376,60 @@ class Search end end - advanced_filter(/^in:all-pms$/i) { |posts| posts.private_posts if @guardian.is_admin? } + advanced_filter(/\Ain:all-pms\z/i) { |posts| posts.private_posts if @guardian.is_admin? } - advanced_filter(/^in:tagged$/i) do |posts| + advanced_filter(/\Ain:tagged\z/i) do |posts| posts.where("EXISTS (SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = posts.topic_id)") end - advanced_filter(/^in:untagged$/i) do |posts| + advanced_filter(/\Ain:untagged\z/i) do |posts| posts.joins( "LEFT JOIN topic_tags ON topic_tags.topic_id = posts.topic_id", ).where("topic_tags.id IS NULL") end - advanced_filter(/^status:open$/i) do |posts| + advanced_filter(/\Astatus:open\z/i) do |posts| posts.where("NOT topics.closed AND NOT topics.archived") end - advanced_filter(/^status:closed$/i) { |posts| posts.where("topics.closed") } + advanced_filter(/\Astatus:closed\z/i) { |posts| posts.where("topics.closed") } - advanced_filter(/^status:public$/i) do |posts| + advanced_filter(/\Astatus:public\z/i) do |posts| category_ids = Category.where(read_restricted: false).pluck(:id) posts.where("topics.category_id in (?)", category_ids) end - advanced_filter(/^status:archived$/i) { |posts| posts.where("topics.archived") } + advanced_filter(/\Astatus:archived\z/i) { |posts| posts.where("topics.archived") } - advanced_filter(/^status:noreplies$/i) { |posts| posts.where("topics.posts_count = 1") } + advanced_filter(/\Astatus:noreplies\z/i) { |posts| posts.where("topics.posts_count = 1") } - advanced_filter(/^status:single_user$/i) { |posts| posts.where("topics.participant_count = 1") } + advanced_filter(/\Astatus:single_user\z/i) { |posts| posts.where("topics.participant_count = 1") } - advanced_filter(/^posts_count:(\d+)$/i) do |posts, match| + advanced_filter(/\Aposts_count:(\d+)\z/i) do |posts, match| posts.where("topics.posts_count = ?", match.to_i) end - advanced_filter(/^min_post_count:(\d+)$/i) do |posts, match| + advanced_filter(/\Amin_post_count:(\d+)\z/i) do |posts, match| posts.where("topics.posts_count >= ?", match.to_i) end - advanced_filter(/^min_posts:(\d+)$/i) do |posts, match| + advanced_filter(/\Amin_posts:(\d+)\z/i) do |posts, match| posts.where("topics.posts_count >= ?", match.to_i) end - advanced_filter(/^max_posts:(\d+)$/i) do |posts, match| + advanced_filter(/\Amax_posts:(\d+)\z/i) do |posts, match| posts.where("topics.posts_count <= ?", match.to_i) end - advanced_filter(/^in:first|^f$/i) { |posts| posts.where("posts.post_number = 1") } + advanced_filter(/\Ain:first|^f\z/i) { |posts| posts.where("posts.post_number = 1") } - advanced_filter(/^in:pinned$/i) { |posts| posts.where("topics.pinned_at IS NOT NULL") } + advanced_filter(/\Ain:pinned\z/i) { |posts| posts.where("topics.pinned_at IS NOT NULL") } - advanced_filter(/^in:wiki$/i) { |posts, match| posts.where(wiki: true) } + advanced_filter(/\Ain:wiki\z/i) { |posts, match| posts.where(wiki: true) } - advanced_filter(/^badge:(.*)$/i) do |posts, match| + advanced_filter(/\Abadge:(.*)\z/i) do |posts, match| badge_id = Badge.where("name ilike ? OR id = ?", match, match.to_i).pluck_first(:id) if badge_id posts.where( @@ -454,7 +454,7 @@ class Search ) end - advanced_filter(/^in:(likes)$/i) do |posts, match| + advanced_filter(/\Ain:(likes)\z/i) do |posts, match| post_action_type_filter(posts, PostActionType.types[:like]) if @guardian.user end @@ -462,7 +462,7 @@ class Search # this at some point, as it only acts on posts at the moment. On the other # hand, this may not be necessary, as the user bookmark list has advanced # search based on a RegisteredBookmarkable's #search_query method. - advanced_filter(/^in:(bookmarks)$/i) do |posts, match| + advanced_filter(/\Ain:(bookmarks)\z/i) do |posts, match| posts.where(<<~SQL, @guardian.user.id) if @guardian.user posts.id IN ( SELECT bookmarkable_id FROM bookmarks @@ -471,20 +471,20 @@ class Search SQL end - advanced_filter(/^in:posted$/i) do |posts| + advanced_filter(/\Ain:posted\z/i) do |posts| posts.where("posts.user_id = ?", @guardian.user.id) if @guardian.user end - advanced_filter(/^in:(created|mine)$/i) do |posts| + advanced_filter(/\Ain:(created|mine)\z/i) do |posts| posts.where(user_id: @guardian.user.id, post_number: 1) if @guardian.user end - advanced_filter(/^created:@(.*)$/i) do |posts, match| + advanced_filter(/\Acreated:@(.*)\z/i) do |posts, match| user_id = User.where(username: match.downcase).pluck_first(:id) posts.where(user_id: user_id, post_number: 1) end - advanced_filter(/^in:(watching|tracking)$/i) do |posts, match| + advanced_filter(/\Ain:(watching|tracking)\z/i) do |posts, match| if @guardian.user level = TopicUser.notification_levels[match.downcase.to_sym] posts.where( @@ -499,7 +499,7 @@ class Search end end - advanced_filter(/^in:seen$/i) do |posts| + advanced_filter(/\Ain:seen\z/i) do |posts| if @guardian.user posts.joins( "INNER JOIN post_timings ON @@ -511,7 +511,7 @@ class Search end end - advanced_filter(/^in:unseen$/i) do |posts| + advanced_filter(/\Ain:unseen\z/i) do |posts| if @guardian.user posts.joins( "LEFT JOIN post_timings ON @@ -523,9 +523,9 @@ class Search end end - advanced_filter(/^with:images$/i) { |posts| posts.where("posts.image_upload_id IS NOT NULL") } + advanced_filter(/\Awith:images\z/i) { |posts| posts.where("posts.image_upload_id IS NOT NULL") } - advanced_filter(/^category:(.+)$/i) do |posts, match| + advanced_filter(/\Acategory:(.+)\z/i) do |posts, match| exact = false if match[0] == "=" @@ -544,7 +544,7 @@ class Search end end - advanced_filter(/^\#([\p{L}\p{M}0-9\-:=]+)$/i) do |posts, match| + advanced_filter(/\A\#([\p{L}\p{M}0-9\-:=]+)\z/i) do |posts, match| category_slug, subcategory_slug = match.to_s.split(":") next unless category_slug @@ -614,13 +614,18 @@ class Search end end - advanced_filter(/^group:(.+)$/i) do |posts, match| - group_id = + advanced_filter(/\Agroup:(.+)\z/i) do |posts, match| + group_query = Group .visible_groups(@guardian.user) .members_visible_groups(@guardian.user) - .where("name ilike ? OR (id = ? AND id > 0)", match, match.to_i) - .pluck_first(:id) + .where("groups.name ILIKE ? OR (id = ? AND id > 0)", match, match.to_i) + + DiscoursePluginRegistry.search_groups_set_query_callbacks.each do |cb| + group_query = cb.call(group_query, @term, @guardian) + end + + group_id = group_query.pluck_first(:id) if group_id posts.where( @@ -632,7 +637,7 @@ class Search end end - advanced_filter(/^group_messages:(.+)$/i) do |posts, match| + advanced_filter(/\Agroup_messages:(.+)\z/i) do |posts, match| group_id = Group .visible_groups(@guardian.user) @@ -651,7 +656,7 @@ class Search end end - advanced_filter(/^user:(.+)$/i) do |posts, match| + advanced_filter(/\Auser:(.+)\z/i) do |posts, match| user_id = User .where(staged: false) @@ -664,7 +669,7 @@ class Search end end - advanced_filter(/^\@(\S+)$/i) do |posts, match| + advanced_filter(/\A\@(\S+)\z/i) do |posts, match| username = User.normalize_username(match) user_id = User.not_staged.where(username_lower: username).pluck_first(:id) @@ -678,7 +683,7 @@ class Search end end - advanced_filter(/^before:(.*)$/i) do |posts, match| + advanced_filter(/\Abefore:(.*)\z/i) do |posts, match| if date = Search.word_to_date(match) posts.where("posts.created_at < ?", date) else @@ -686,7 +691,7 @@ class Search end end - advanced_filter(/^after:(.*)$/i) do |posts, match| + advanced_filter(/\Aafter:(.*)\z/i) do |posts, match| if date = Search.word_to_date(match) posts.where("posts.created_at > ?", date) else @@ -694,15 +699,15 @@ class Search end end - advanced_filter(/^tags?:([\p{L}\p{M}0-9,\-_+]+)$/i) do |posts, match| + advanced_filter(/\Atags?:([\p{L}\p{M}0-9,\-_+]+)\z/i) do |posts, match| search_tags(posts, match, positive: true) end - advanced_filter(/^\-tags?:([\p{L}\p{M}0-9,\-_+]+)$/i) do |posts, match| + advanced_filter(/\A\-tags?:([\p{L}\p{M}0-9,\-_+]+)\z/i) do |posts, match| search_tags(posts, match, positive: false) end - advanced_filter(/^filetypes?:([a-zA-Z0-9,\-_]+)$/i) do |posts, match| + advanced_filter(/\Afiletypes?:([a-zA-Z0-9,\-_]+)\z/i) do |posts, match| file_extensions = match.split(",").map(&:downcase) posts.where( "posts.id IN ( @@ -721,11 +726,11 @@ class Search ) end - advanced_filter(/^min_views:(\d+)$/i) do |posts, match| + advanced_filter(/\Amin_views:(\d+)\z/i) do |posts, match| posts.where("topics.views >= ?", match.to_i) end - advanced_filter(/^max_views:(\d+)$/i) do |posts, match| + advanced_filter(/\Amax_views:(\d+)\z/i) do |posts, match| posts.where("topics.views <= ?", match.to_i) end @@ -784,38 +789,38 @@ class Search if word == "l" @order = :latest nil - elsif word =~ /^order:\w+$/i + elsif word =~ /\Aorder:\w+\z/i @order = word.downcase.gsub("order:", "").to_sym nil - elsif word =~ /^in:title$/i || word == "t" + elsif word =~ /\Ain:title\z/i || word == "t" @in_title = true nil - elsif word =~ /^topic:(\d+)$/i + elsif word =~ /\Atopic:(\d+)\z/i topic_id = $1.to_i if topic_id > 1 topic = Topic.find_by(id: topic_id) @search_context = topic if @guardian.can_see?(topic) end nil - elsif word =~ /^in:all$/i + elsif word =~ /\Ain:all\z/i @search_all_topics = true nil - elsif word =~ /^in:personal$/i + elsif word =~ /\Ain:personal\z/i @search_pms = true nil - elsif word =~ /^in:messages$/i + elsif word =~ /\Ain:messages\z/i @search_pms = true nil - elsif word =~ /^in:personal-direct$/i + elsif word =~ /\Ain:personal-direct\z/i @search_pms = true nil - elsif word =~ /^in:all-pms$/i + elsif word =~ /\Ain:all-pms\z/i @search_all_pms = true nil - elsif word =~ /^group_messages:(.+)$/i + elsif word =~ /\Agroup_messages:(.+)\z/i @search_pms = true nil - elsif word =~ /^personal_messages:(.+)$/i + elsif word =~ /\Apersonal_messages:(.+)\z/i if user = User.find_by_username($1) @search_pms = true @search_context = user @@ -944,11 +949,17 @@ class Search end def groups_search - groups = - Group - .visible_groups(@guardian.user, "name ASC", include_everyone: false) - .where("name ILIKE :term OR full_name ILIKE :term", term: "%#{@term}%") - .limit(limit) + group_query = + Group.visible_groups(@guardian.user, "groups.name ASC", include_everyone: false).where( + "groups.name ILIKE :term OR groups.full_name ILIKE :term", + term: "%#{@term}%", + ) + + DiscoursePluginRegistry.search_groups_set_query_callbacks.each do |cb| + group_query = cb.call(group_query, @term, @guardian) + end + + groups = group_query.limit(limit) groups.each { |group| @results.add(group) } end diff --git a/lib/shrink_uploaded_image.rb b/lib/shrink_uploaded_image.rb index d1a7f14f01..f46a3b7207 100644 --- a/lib/shrink_uploaded_image.rb +++ b/lib/shrink_uploaded_image.rb @@ -98,7 +98,7 @@ class ShrinkUploadedImage elsif !post.topic || post.topic.trashed? log "A deleted topic" elsif post.cooked.include?(original_upload.sha1) - if post.raw.include?("#{Discourse.base_url.sub(%r{^https?://}i, "")}/t/") + if post.raw.include?("#{Discourse.base_url.sub(%r{\Ahttps?://}i, "")}/t/") log "Updating a topic onebox" else log "Updating an external onebox" diff --git a/lib/site_settings/validations.rb b/lib/site_settings/validations.rb index fdc36ff57e..b8ac030964 100644 --- a/lib/site_settings/validations.rb +++ b/lib/site_settings/validations.rb @@ -243,7 +243,7 @@ module SiteSettings::Validations def validate_cors_origins(new_val) return if new_val.blank? - return unless new_val.split("|").any?(%r{/$}) + return unless new_val.split("|").any?(%r{/\z}) validate_error :cors_origins_should_not_have_trailing_slash end diff --git a/lib/site_settings/yaml_loader.rb b/lib/site_settings/yaml_loader.rb index cfd44db56a..6d303d726c 100644 --- a/lib/site_settings/yaml_loader.rb +++ b/lib/site_settings/yaml_loader.rb @@ -38,10 +38,6 @@ class SiteSettings::YamlLoader private def load_yaml(path) - if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.1.0") - YAML.load_file(path, aliases: true) - else - YAML.load_file(path) - end + YAML.load_file(path, aliases: true) end end diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb index 217bed7737..a9d636b6c3 100644 --- a/lib/stylesheet/manager.rb +++ b/lib/stylesheet/manager.rb @@ -11,7 +11,7 @@ class Stylesheet::Manager CACHE_PATH ||= "tmp/stylesheet-cache" MANIFEST_DIR ||= "#{Rails.root}/tmp/cache/assets/#{Rails.env}" - THEME_REGEX ||= /_theme$/ + THEME_REGEX ||= /_theme\z/ COLOR_SCHEME_STYLESHEET ||= "color_definitions" @@lock = Mutex.new diff --git a/lib/stylesheet/manager/builder.rb b/lib/stylesheet/manager/builder.rb index 47dbae0650..9e022b8d59 100644 --- a/lib/stylesheet/manager/builder.rb +++ b/lib/stylesheet/manager/builder.rb @@ -35,7 +35,7 @@ class Stylesheet::Manager::Builder end end - rtl = @target.to_s =~ /_rtl$/ + rtl = @target.to_s =~ /_rtl\z/ css, source_map = with_load_paths do |load_paths| Stylesheet::Compiler.compile_asset( diff --git a/lib/stylesheet/watcher.rb b/lib/stylesheet/watcher.rb index a2c8189e83..8e41e6832a 100644 --- a/lib/stylesheet/watcher.rb +++ b/lib/stylesheet/watcher.rb @@ -21,7 +21,7 @@ module Stylesheet @default_paths = ["app/assets/stylesheets"] Discourse.plugins.each do |plugin| if plugin.path.to_s.include?(Rails.root.to_s) - @default_paths << File.dirname(plugin.path).sub(Rails.root.to_s, "").sub(%r{^/}, "") + @default_paths << File.dirname(plugin.path).sub(Rails.root.to_s, "").sub(%r{\A/}, "") else # if plugin doesn’t seem to be in our app, consider it as outside of the app # and ignore it @@ -41,7 +41,7 @@ module Stylesheet end end - listener_opts = { ignore: /xxxx/, only: /\.(css|scss)$/ } + listener_opts = { ignore: /xxxx/, only: /\.(css|scss)\z/ } listener_opts[:force_polling] = true if ENV["FORCE_POLLING"] Thread.new do diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index f00ab3068e..1c9b3a3311 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -307,7 +307,7 @@ task "assets:precompile" => "assets:precompile:before" do concurrent? do |proc| manifest .files - .select { |k, v| k =~ /\.js$/ } + .select { |k, v| k =~ /\.js\z/ } .each do |file, info| path = "#{assets_path}/#{file}" _file = (d = File.dirname(file)) == "." ? "_#{file}" : "#{d}/_#{File.basename(file)}" diff --git a/lib/tasks/cdn.rake b/lib/tasks/cdn.rake index ee2b0c14ee..559144bf9d 100644 --- a/lib/tasks/cdn.rake +++ b/lib/tasks/cdn.rake @@ -10,7 +10,7 @@ task "assets:prestage" => :environment do |t| def get_assets(path) Dir .glob("#{Rails.root}/public/assets/#{path}*") - .map { |f| "/assets/#{path}#{f.split("/")[-1]}" if f =~ /[a-f0-9]{16}\.(css|js)$/ } + .map { |f| "/assets/#{path}#{f.split("/")[-1]}" if f =~ /[a-f0-9]{16}\.(css|js)\z/ } .compact end diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index 403fd6749c..68fb2d50f7 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -109,7 +109,7 @@ class SeedHelper def self.filter # Allows a plugin to exclude any specified seed data files from running if DiscoursePluginRegistry.seedfu_filter.any? - /^(?!.*(#{DiscoursePluginRegistry.seedfu_filter.to_a.join("|")})).*$/ + /\A(?!.*(#{DiscoursePluginRegistry.seedfu_filter.to_a.join("|")})).*\z/ else nil end diff --git a/lib/tasks/emoji.rake b/lib/tasks/emoji.rake index fe049fc550..e9bce247dc 100644 --- a/lib/tasks/emoji.rake +++ b/lib/tasks/emoji.rake @@ -459,7 +459,7 @@ end def codepoints_to_code(codepoints, fitzpatrick_scale) codepoints = codepoints.map { |c| c.to_s(16).rjust(4, "0") }.join("_").downcase - codepoints.gsub!(/_fe0f$/, "") if !fitzpatrick_scale + codepoints.gsub!(/_fe0f\z/, "") if !fitzpatrick_scale codepoints end diff --git a/lib/tasks/hashtags.rake b/lib/tasks/hashtags.rake new file mode 100644 index 0000000000..9810e653b7 --- /dev/null +++ b/lib/tasks/hashtags.rake @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +desc "Mark posts with the old hashtag cooked format (pre enable_experimental_hashtag_autocomplete) for rebake" +task "hashtags:mark_old_format_for_rebake" => :environment do + # See Post#rebake_old, which is called via the PeriodicalUpdates job + # on a schedule. + puts "Finding posts matching old format, this could take some time..." + posts_to_rebake = Post.where("cooked like '%class=\"hashtag\"%'") + STDOUT.puts( + "[!] You are about to mark #{posts_to_rebake.count} posts containing hashtags in the old format to rebake. [CTRL+c] to cancel, [ENTER] to continue", + ) + STDIN.gets.chomp if !Rails.env.test? + posts_to_rebake.update_all(baked_version: 0) + puts "Done, rebakes will happen when periodal updates job runs." +end diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake index 32255f2f30..6f4d67fe12 100644 --- a/lib/tasks/plugin.rake +++ b/lib/tasks/plugin.rake @@ -86,7 +86,7 @@ task "plugin:update", :plugin do |t, args| upstream_branch = `git -C '#{plugin_path}' for-each-ref --format='%(upstream:short)' $(git -C '#{plugin_path}' symbolic-ref -q HEAD)`.strip - has_origin_main = `git -C '#{plugin_path}' branch -a`.match?(%r{remotes/origin/main$}) + has_origin_main = `git -C '#{plugin_path}' branch -a`.match?(%r{remotes/origin/main\z}) has_local_main = `git -C '#{plugin_path}' show-ref refs/heads/main`.present? if upstream_branch == "origin/master" && has_origin_main diff --git a/lib/tasks/release_note.rake b/lib/tasks/release_note.rake index 8f665d6eba..e4bbe63f3d 100644 --- a/lib/tasks/release_note.rake +++ b/lib/tasks/release_note.rake @@ -3,12 +3,12 @@ DATE_REGEX ||= /\A\d{4}-\d{2}-\d{2}/ CHANGE_TYPES ||= [ - { pattern: /^FEATURE:/, heading: "New Features" }, - { pattern: /^FIX:/, heading: "Bug Fixes" }, - { pattern: /^UX:/, heading: "UX Changes" }, - { pattern: /^SECURITY:/, heading: "Security Changes" }, - { pattern: /^PERF:/, heading: "Performance" }, - { pattern: /^A11Y:/, heading: "Accessibility" }, + { pattern: /\AFEATURE:/, heading: "New Features" }, + { pattern: /\AFIX:/, heading: "Bug Fixes" }, + { pattern: /\AUX:/, heading: "UX Changes" }, + { pattern: /\ASECURITY:/, heading: "Security Changes" }, + { pattern: /\APERF:/, heading: "Performance" }, + { pattern: /\AA11Y:/, heading: "Accessibility" }, ] desc "generate a release note from the important commits" @@ -83,7 +83,7 @@ def find_changes(repo, from, to) CHANGE_TYPES.each { |ct| changes[ct] = Set.new } out.each_line do |comment| - next if comment =~ /^\s*Revert/ + next if comment =~ /\A\s*Revert/ split_comments(comment).each do |line| ct = CHANGE_TYPES.find { |t| line =~ t[:pattern] } changes[ct] << better(line) if ct @@ -122,7 +122,7 @@ def better(line) end def remove_prefix(line) - line.gsub(/^(FIX|FEATURE|UX|SECURITY|PERF|A11Y):/, "").strip + line.gsub(/\A(FIX|FEATURE|UX|SECURITY|PERF|A11Y):/, "").strip end def escape_brackets(line) @@ -130,7 +130,7 @@ def escape_brackets(line) end def remove_pull_request(line) - line.gsub(/ \(\#\d+\)$/, "") + line.gsub(/ \(\#\d+\)\z/, "") end def split_comments(text) diff --git a/lib/tasks/typepad.thor b/lib/tasks/typepad.thor index 649f12db52..bbef98037e 100644 --- a/lib/tasks/typepad.thor +++ b/lib/tasks/typepad.thor @@ -34,7 +34,7 @@ class Typepad < Thor File.open(options[:file]).each_line do |l| l = l.scrub - if l =~ /^--------$/ + if l =~ /\A--------\z/ parsed_entry = process_entry(input) if parsed_entry puts "Parsed #{parsed_entry[:title]}" @@ -119,7 +119,7 @@ class Typepad < Thor def parse_meta_data(section) result = {} section.split(/\n/).each do |l| - if l =~ /^([A-Z\ ]+)\: (.*)$/ + if l =~ /\A([A-Z\ ]+)\: (.*)\z/ key, value = Regexp.last_match[1], Regexp.last_match[2] clean_type!(key) value.strip! @@ -134,7 +134,7 @@ class Typepad < Thor def parse_section(section) section.strip! - if section =~ /^([^:]+):/ + if section =~ /\A([^:]+):/ type = clean_type!(Regexp.last_match[1]) value = section.split("\n")[1..-1].join("\n") value.strip! @@ -195,8 +195,8 @@ class Typepad < Thor comment[:name] = comment[:author] if comment[:author] - comment[:author].gsub!(/^[_\.]+/, '') - comment[:author].gsub!(/[_\.]+$/, '') + comment[:author].gsub!(/\A[_\.]+/, '') + comment[:author].gsub!(/[_\.]+\z/, '') if comment[:author].size < 12 comment[:author].gsub!(/ /, '_') diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 8410987d8e..1104a9f3a7 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -35,7 +35,7 @@ def gather_uploads .where("url !~ ?", "^\/uploads\/#{current_db}") .find_each do |upload| begin - old_db = upload.url[%r{^/uploads/([^/]+)/}, 1] + old_db = upload.url[%r{\A/uploads/([^/]+)/}, 1] from = upload.url.dup to = upload.url.sub("/uploads/#{old_db}/", "/uploads/#{current_db}/") source = "#{public_directory}#{from}" @@ -321,8 +321,8 @@ def regenerate_missing_optimized scope.find_each do |optimized_image| upload = optimized_image.upload - next unless optimized_image.url =~ %r{^/[^/]} - next unless upload.url =~ %r{^/[^/]} + next unless optimized_image.url =~ %r{\A/[^/]} + next unless upload.url =~ %r{\A/[^/]} thumbnail = "#{public_directory}#{optimized_image.url}" original = "#{public_directory}#{upload.url}" @@ -537,6 +537,10 @@ task "uploads:sync_s3_acls" => :environment do end end +# +# TODO (martin) Update this rake task to use the _first_ UploadReference +# record for each upload to determine security, and do not mark things +# as secure if the first record is something public e.g. a site setting. task "uploads:disable_secure_uploads" => :environment do RailsMultisite::ConnectionManagement.each_connection do |db| unless Discourse.store.external? @@ -584,6 +588,10 @@ end # the upload secure flag and S3 upload ACLs. Any uploads that # have their secure status changed will have all associated posts # rebaked. +# +# TODO (martin) Update this rake task to use the _first_ UploadReference +# record for each upload to determine security, and do not mark things +# as secure if the first record is something public e.g. a site setting. task "uploads:secure_upload_analyse_and_update" => :environment do RailsMultisite::ConnectionManagement.each_connection do |db| unless Discourse.store.external? diff --git a/lib/theme_javascript_compiler.rb b/lib/theme_javascript_compiler.rb index 65d10d96fe..f67009bec3 100644 --- a/lib/theme_javascript_compiler.rb +++ b/lib/theme_javascript_compiler.rb @@ -200,7 +200,7 @@ class ThemeJavascriptCompiler end def raw_template_name(name) - name = name.sub(/\.(raw|hbr)$/, "") + name = name.sub(/\.(raw|hbr)\z/, "") name.inspect end @@ -228,7 +228,7 @@ class ThemeJavascriptCompiler def append_module(script, name, include_variables: true) original_filename = name - name = "discourse/theme-#{@theme_id}/#{name.gsub(%r{^discourse/}, "")}" + name = "discourse/theme-#{@theme_id}/#{name.gsub(%r{\Adiscourse/}, "")}" script = "#{theme_settings}#{script}" if include_variables transpiler = DiscourseJsProcessor::Transpiler.new diff --git a/lib/topic_creator.rb b/lib/topic_creator.rb index 084c71584a..f9cdc99b44 100644 --- a/lib/topic_creator.rb +++ b/lib/topic_creator.rb @@ -181,7 +181,7 @@ class TopicCreator return Category.find(SiteSetting.shared_drafts_category) if @opts[:shared_draft] - if (@opts[:category].is_a? Integer) || (@opts[:category] =~ /^\d+$/) + if (@opts[:category].is_a? Integer) || (@opts[:category] =~ /\A\d+\z/) Category.find_by(id: @opts[:category]) end end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 5b33a15fbe..a7049b008e 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -14,11 +14,16 @@ class TopicQuery def self.validators @validators ||= begin - int = lambda { |x| Integer === x || (String === x && x.match?(/^-?[0-9]+$/)) } - + int = lambda { |x| Integer === x || (String === x && x.match?(/\A-?[0-9]+\z/)) } zero_up_to_max_int = lambda { |x| int.call(x) && x.to_i.between?(0, PG_MAX_INT) } + array_or_string = lambda { |x| Array === x || String === x } - { max_posts: zero_up_to_max_int, min_posts: zero_up_to_max_int, page: zero_up_to_max_int } + { + max_posts: zero_up_to_max_int, + min_posts: zero_up_to_max_int, + page: zero_up_to_max_int, + tags: array_or_string, + } end end @@ -637,9 +642,13 @@ class TopicQuery options.reverse_merge!(@options) options.reverse_merge!(per_page: per_page_setting) unless options[:limit] == false - # Whether to return visible topics - options[:visible] = true if @user.nil? || @user.regular? - options[:visible] = false if @user && @user.id == options[:filtered_to_user] + # Whether to include unlisted (visible = false) topics + viewing_own_topics = @user && @user.id == options[:filtered_to_user] + + if options[:visible].nil? + options[:visible] = true if @user.nil? || @user.regular? + options[:visible] = false if @guardian.can_see_unlisted_topics? || viewing_own_topics + end # Start with a list of all topics result = Topic.unscoped.includes(:category) @@ -726,14 +735,17 @@ class TopicQuery result = result.where.not(id: TopicTag.distinct.pluck(:topic_id)) end - result = result.where(<<~SQL, name: @options[:exclude_tag]) if @options[:exclude_tag].present? - topics.id NOT IN ( - SELECT topic_tags.topic_id - FROM topic_tags - INNER JOIN tags ON tags.id = topic_tags.tag_id - WHERE tags.name = :name - ) - SQL + if @options[:exclude_tag].present? && + !DiscourseTagging.hidden_tag_names(@guardian).include?(@options[:exclude_tag]) + result = result.where(<<~SQL, name: @options[:exclude_tag]) + topics.id NOT IN ( + SELECT topic_tags.topic_id + FROM topic_tags + INNER JOIN tags ON tags.id = topic_tags.tag_id + WHERE tags.name = :name + ) + SQL + end end result = apply_ordering(result, options) diff --git a/lib/topic_view.rb b/lib/topic_view.rb index a6431e392e..576b6d3c5c 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -31,8 +31,6 @@ class TopicView :personal_message, :can_review_topic, :page, - :mentioned_users, - :mentions, ) alias queued_posts_enabled? queued_posts_enabled @@ -144,9 +142,6 @@ class TopicView end end - parse_mentions - load_mentioned_users - TopicView.preload(self) @draft_key = @topic.draft_key @@ -250,7 +245,8 @@ class TopicView @topic.category title += " - #{@topic.category.name}" elsif SiteSetting.tagging_enabled && @topic.tags.exists? - title += " - #{@topic.tags.order("tags.topic_count DESC").first.name}" + title += + " - #{@topic.tags.order("tags.#{Tag.topic_count_column(@guardian)} DESC").first.name}" end end title @@ -700,17 +696,25 @@ class TopicView @topic.published_page end - def parse_mentions - @mentions = @posts.to_h { |p| [p.id, p.mentions] }.reject { |_, v| v.empty? } + def mentioned_users + @mentioned_users ||= + begin + mentions = @posts.to_h { |p| [p.id, p.mentions] }.reject { |_, v| v.empty? } + usernames = mentions.values + usernames.flatten! + usernames.uniq! + + users = User.where(username: usernames).includes(:user_status).index_by(&:username) + + mentions.reduce({}) do |hash, (post_id, post_mentioned_usernames)| + hash[post_id] = post_mentioned_usernames.map { |username| users[username] }.compact + hash + end + end end - def load_mentioned_users - usernames = @mentions.values.flatten.uniq - mentioned_users = User.where(username: usernames) - - mentioned_users = mentioned_users.includes(:user_status) if SiteSetting.enable_user_status - - @mentioned_users = mentioned_users.to_h { |u| [u.username, u] } + def tags + @topic.tags.map(&:name) end protected @@ -814,13 +818,9 @@ class TopicView end def find_topic(topic_or_topic_id) - if topic_or_topic_id.is_a?(Topic) - topic_or_topic_id - else - # with_deleted covered in #check_and_raise_exceptions - finder = Topic.with_deleted.where(id: topic_or_topic_id).includes(:category) - finder.first - end + return topic_or_topic_id if topic_or_topic_id.is_a?(Topic) + # with_deleted covered in #check_and_raise_exceptions + Topic.with_deleted.includes(:category, :tags).find_by(id: topic_or_topic_id) end def unfiltered_posts diff --git a/lib/upload_creator.rb b/lib/upload_creator.rb index d83af7461c..c826a4b906 100644 --- a/lib/upload_creator.rb +++ b/lib/upload_creator.rb @@ -57,9 +57,6 @@ class UploadCreator true end ) - - # TODO (martin) Validate @opts[:type] to make sure only blessed types are passed - # in, since the clientside can pass any type it wants. end def create_for(user_id) @@ -78,13 +75,13 @@ class UploadCreator is_image ||= @image_info && FileHelper.is_supported_image?("test.#{@image_info.type}") is_image = false if @opts[:for_theme] - # if this is present then it means we are creating an upload record from + # If this is present then it means we are creating an upload record from # an external_upload_stub and the file is > ExternalUploadManager::DOWNLOAD_LIMIT, # so we have not downloaded it to a tempfile. no modifications can be made to the # file in this case because it does not exist; we simply move it to its new location # in S3 # - # TODO (martin) I've added a bunch of external_upload_too_big checks littered + # FIXME: I've added a bunch of external_upload_too_big checks littered # throughout the UploadCreator code. It would be better to have two seperate # classes with shared methods, rather than doing all these checks all over the # place. Needs a refactor. @@ -389,7 +386,7 @@ class UploadCreator end def convert_heif_to_jpeg? - File.extname(@filename).downcase.match?(/\.hei(f|c)$/) + File.extname(@filename).downcase.match?(/\.hei(f|c)\z/) end def convert_heif! @@ -596,7 +593,7 @@ class UploadCreator def should_optimize? # GIF is too slow (plus, we'll soon be converting them to MP4) # Optimizing SVG is useless - return false if @file.path =~ /\.(gif|svg)$/i + return false if @file.path =~ /\.(gif|svg)\z/i # Safeguard for large PNGs return pixels < 2_000_000 if @file.path =~ /\.png/i # Everything else is fine! diff --git a/lib/upload_security.rb b/lib/upload_security.rb index ccaf86f1ed..c00c1376ec 100644 --- a/lib/upload_security.rb +++ b/lib/upload_security.rb @@ -13,8 +13,12 @@ # original post the upload is linked to has far more bearing on its security context # post-upload. If the access_control_post_id does not exist then we just rely # on the current secure? status, otherwise there would be a lot of additional -# complex queries and joins to perform. Over time more of these specific -# queries will be implemented. +# complex queries and joins to perform. +# +# These queries will be performed only if the @creating option is false. So if +# an upload is included in a post, and it's an upload from a different source +# (e.g. a category logo, site setting upload) then we will determine secure +# state _based on the first place the upload was referenced_. # # NOTE: When updating this to add more cases where uploads will be marked # secure, consider uploads:secure_upload_analyse_and_update as well, which @@ -35,6 +39,18 @@ class UploadSecurity badge_image ] + PUBLIC_UPLOAD_REFERENCE_TYPES = %w[ + Badge + Category + CustomEmoji + Group + SiteSetting + ThemeField + User + UserAvatar + UserProfile + ] + def self.register_custom_public_type(type) @@custom_public_types << type if !@@custom_public_types.include?(type) end @@ -65,6 +81,46 @@ class UploadSecurity [false, "no checks satisfied"] end + private + + def access_control_post + @access_control_post ||= + @upload.access_control_post_id.present? ? @upload.access_control_post : nil + end + + def insecure_context_checks + { + secure_uploads_disabled: "secure uploads is disabled", + insecure_creation_for_modifiers: "one or more creation for_modifiers was satisfied", + public_type: "upload is public type", + regular_emoji: "upload is used for regular emoji", + publicly_referenced_first: "upload was publicly referenced when it was first created", + } + end + + def secure_context_checks + { + login_required: "login is required", + access_control_post_has_secure_uploads: "access control post dictates security", + secure_creation_for_modifiers: "one or more creation for_modifiers was satisfied", + uploading_in_composer: "uploading via the composer", + already_secure: "upload is already secure", + } + end + + # The access control check is important because that is the truest indicator + # of whether an upload should be secure or not, and thus should be returned + # immediately if there is an access control post. + def priority_check?(check) + check == :access_control_post_has_secure_uploads && access_control_post + end + + def perform_check(check) + send("#{check}_check") + end + + #### START PUBLIC CHECKS #### + def secure_uploads_disabled_check !SiteSetting.secure_uploads? end @@ -78,8 +134,19 @@ class UploadSecurity PUBLIC_TYPES.include?(@upload_type) || @@custom_public_types.include?(@upload_type) end - def custom_emoji_check - @upload.id.present? && CustomEmoji.exists?(upload_id: @upload.id) + def publicly_referenced_first_check + return false if @creating + first_reference = + @upload + .upload_references + .joins(<<~SQL) + LEFT JOIN posts ON upload_references.target_type = 'Post' AND upload_references.target_id = posts.id + SQL + .where("posts.deleted_at IS NULL") + .order(created_at: :asc) + .first + return false if first_reference.blank? + PUBLIC_UPLOAD_REFERENCE_TYPES.include?(first_reference.target_type) end def regular_emoji_check @@ -89,18 +156,26 @@ class UploadSecurity uri.path.include?("images/emoji") end + #### END PUBLIC CHECKS #### + + #--------------------------# + + #### START PRIVATE CHECKS #### + def login_required_check SiteSetting.login_required? end - # whether the upload should remain secure or not after posting depends on its context, + # Whether the upload should remain secure or not after posting depends on its context, # which is based on the post it is linked to via access_control_post_id. - # if that post is with_secure_uploads? then the upload should also be secure. - # this may change to false if the upload was set to secure on upload e.g. in - # a post composer then it turned out that the post itself was not in a secure context # - # a post is with secure uploads if it is a private message or in a read restricted - # category + # If that post is with_secure_uploads? then the upload should also be secure. + # + # This may change to false if the upload was set to secure on upload e.g. in + # a post composer then it turned out that the post itself was not in a secure context. + # + # A post is with secure uploads if it is a private message or in a read restricted + # category. See `Post#with_secure_uploads?` for the full definition. def access_control_post_has_secure_uploads_check access_control_post&.with_secure_uploads? end @@ -118,41 +193,5 @@ class UploadSecurity @upload.secure? end - private - - def access_control_post - @access_control_post ||= - @upload.access_control_post_id.present? ? @upload.access_control_post : nil - end - - def insecure_context_checks - { - secure_uploads_disabled: "secure uploads is disabled", - insecure_creation_for_modifiers: "one or more creation for_modifiers was satisfied", - public_type: "upload is public type", - custom_emoji: "upload is used for custom emoji", - regular_emoji: "upload is used for regular emoji", - } - end - - def secure_context_checks - { - login_required: "login is required", - access_control_post_has_secure_uploads: "access control post dictates security", - secure_creation_for_modifiers: "one or more creation for_modifiers was satisfied", - uploading_in_composer: "uploading via the composer", - already_secure: "upload is already secure", - } - end - - # the access control check is important because that is the truest indicator - # of whether an upload should be secure or not, and thus should be returned - # immediately if there is an access control post - def priority_check?(check) - check == :access_control_post_has_secure_uploads && access_control_post - end - - def perform_check(check) - send("#{check}_check") - end + #### END PRIVATE CHECKS #### end diff --git a/lib/url_helper.rb b/lib/url_helper.rb index 097b3866db..320f3babd2 100644 --- a/lib/url_helper.rb +++ b/lib/url_helper.rb @@ -48,8 +48,8 @@ class UrlHelper end def self.absolute(url, cdn = Discourse.asset_host) - cdn = "https:#{cdn}" if cdn && cdn =~ %r{^//} - url =~ %r{^/[^/]} ? (cdn || Discourse.base_url_no_prefix) + url : url + cdn = "https:#{cdn}" if cdn && cdn =~ %r{\A//} + url =~ %r{\A/[^/]} ? (cdn || Discourse.base_url_no_prefix) + url : url end def self.absolute_without_cdn(url) @@ -57,7 +57,7 @@ class UrlHelper end def self.schemaless(url) - url.sub(/^http:/i, "") + url.sub(/\Ahttp:/i, "") end def self.secure_proxy_without_cdn(url) diff --git a/lib/validators/css_color_validator.rb b/lib/validators/css_color_validator.rb index fdc1fe8f28..6085ab49aa 100644 --- a/lib/validators/css_color_validator.rb +++ b/lib/validators/css_color_validator.rb @@ -156,7 +156,7 @@ class CssColorValidator end def valid_value?(val) - !!(val =~ /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/ || COLORS.include?(val&.downcase)) + !!(val =~ /\A#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\z/ || COLORS.include?(val&.downcase)) end def error_message diff --git a/lib/validators/email_validator.rb b/lib/validators/email_validator.rb index aca5a06950..81badf8e85 100644 --- a/lib/validators/email_validator.rb +++ b/lib/validators/email_validator.rb @@ -2,7 +2,10 @@ class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - if !EmailAddressValidator.valid_value?(value) + if value.blank? + record.errors.add(attribute, I18n.t(:"user.email.blank")) + invalid = true + elsif !EmailAddressValidator.valid_value?(value) if Invite === record && attribute == :email record.errors.add(:base, I18n.t(:"invite.invalid_email", email: CGI.escapeHTML(value))) else diff --git a/lib/validators/enable_new_notifications_menu_validator.rb b/lib/validators/enable_new_notifications_menu_validator.rb new file mode 100644 index 0000000000..30de97d1ba --- /dev/null +++ b/lib/validators/enable_new_notifications_menu_validator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class EnableNewNotificationsMenuValidator + def initialize(opts = {}) + end + + def valid_value?(value) + return true if value == "f" + SiteSetting.navigation_menu == "legacy" + end + + def error_message + I18n.t("site_settings.errors.enable_new_notifications_menu_not_legacy_navigation_menu") + end +end diff --git a/lib/validators/unicode_username_allowlist_validator.rb b/lib/validators/unicode_username_allowlist_validator.rb index 824e5e4344..f60c161707 100644 --- a/lib/validators/unicode_username_allowlist_validator.rb +++ b/lib/validators/unicode_username_allowlist_validator.rb @@ -9,7 +9,7 @@ class UnicodeUsernameAllowlistValidator @error_message = nil return true if value.blank? - if value.match?(%r{^/.*/[imxo]*$}) + if value.match?(%r{\A/.*/[imxo]*\z}) @error_message = I18n.t("site_settings.errors.allowed_unicode_usernames.leading_trailing_slash") else diff --git a/lib/version.rb b/lib/version.rb index e634b8f3ff..b234035d1a 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -10,7 +10,7 @@ module Discourse MAJOR = 3 MINOR = 1 TINY = 0 - PRE = "beta1" + PRE = "beta2" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/package.json b/package.json index d39655d49c..922906ef92 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "chrome-launcher": "^0.15.1", "chrome-remote-interface": "^0.31.3", "eslint-config-discourse": "^3.3.0", + "jsdoc-to-markdown": "^8.0.0", "lefthook": "^1.2.0", "puppeteer-core": "^13.7.0" }, diff --git a/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb b/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb index 759348ef4e..ca5640e992 100644 --- a/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb +++ b/plugins/chat/app/controllers/api/chat_channels_archives_controller.rb @@ -8,35 +8,48 @@ class Chat::Api::ChatChannelsArchivesController < Chat::Api::ChatChannelsControl guardian.ensure_can_change_channel_status!(channel_from_params, :archived) raise Discourse::InvalidAccess if !existing_archive.failed? Chat::ChatChannelArchiveService.retry_archive_process(chat_channel: channel_from_params) - else - archive_params = - params - .require(:archive) - .tap do |ca| - ca.require(:type) - ca.permit(:title, :topic_id, :category_id, tags: []) - end + return render json: success_json + end - new_topic = archive_params[:type] == "new_topic" - raise Discourse::InvalidParameters if new_topic && archive_params[:title].blank? - raise Discourse::InvalidParameters if !new_topic && archive_params[:topic_id].blank? + new_topic = archive_params[:type] == "new_topic" + raise Discourse::InvalidParameters if new_topic && archive_params[:title].blank? + raise Discourse::InvalidParameters if !new_topic && archive_params[:topic_id].blank? - if !guardian.can_change_channel_status?(channel_from_params, :read_only) - raise Discourse::InvalidAccess.new(I18n.t("chat.errors.channel_cannot_be_archived")) - end + if !guardian.can_change_channel_status?(channel_from_params, :read_only) + raise Discourse::InvalidAccess.new(I18n.t("chat.errors.channel_cannot_be_archived")) + end - Chat::ChatChannelArchiveService.begin_archive_process( + begin + Chat::ChatChannelArchiveService.create_archive_process( chat_channel: channel_from_params, acting_user: current_user, - topic_params: { - topic_id: archive_params[:topic_id], - topic_title: archive_params[:title], - category_id: archive_params[:category_id], - tags: archive_params[:tags], - }, + topic_params: topic_params, ) + rescue Chat::ChatChannelArchiveService::ArchiveValidationError => err + return render json: failed_json.merge(errors: err.errors), status: 400 end render json: success_json end + + private + + def archive_params + @archive_params ||= + params + .require(:archive) + .tap do |ca| + ca.require(:type) + ca.permit(:title, :topic_id, :category_id, tags: []) + end + end + + def topic_params + @topic_params ||= { + topic_id: archive_params[:topic_id], + topic_title: archive_params[:title], + category_id: archive_params[:category_id], + tags: archive_params[:tags], + } + end end diff --git a/plugins/chat/app/controllers/api/chat_channels_controller.rb b/plugins/chat/app/controllers/api/chat_channels_controller.rb index f61107037a..1a618b9678 100644 --- a/plugins/chat/app/controllers/api/chat_channels_controller.rb +++ b/plugins/chat/app/controllers/api/chat_channels_controller.rb @@ -38,6 +38,13 @@ class Chat::Api::ChatChannelsController < Chat::Api begin ChatChannel.transaction do + channel_from_params.update!( + slug: + "#{Time.now.strftime("%Y%m%d-%H%M")}-#{channel_from_params.slug}-deleted".truncate( + SiteSetting.max_topic_title_length, + omission: "", + ), + ) channel_from_params.trash!(current_user) StaffActionLogger.new(current_user).log_custom( "chat_channel_delete", @@ -57,7 +64,7 @@ class Chat::Api::ChatChannelsController < Chat::Api def create channel_params = - params.require(:channel).permit(:chatable_id, :name, :description, :auto_join_users) + params.require(:channel).permit(:chatable_id, :name, :slug, :description, :auto_join_users) guardian.ensure_can_create_chat_channel! if channel_params[:name].length > SiteSetting.max_topic_title_length @@ -81,6 +88,7 @@ class Chat::Api::ChatChannelsController < Chat::Api channel = chatable.create_chat_channel!( name: channel_params[:name], + slug: channel_params[:slug], description: channel_params[:description], user_count: 1, auto_join_users: auto_join_users, diff --git a/plugins/chat/app/controllers/chat_controller.rb b/plugins/chat/app/controllers/chat_controller.rb index 9849ffc290..31ad38e02a 100644 --- a/plugins/chat/app/controllers/chat_controller.rb +++ b/plugins/chat/app/controllers/chat_controller.rb @@ -432,9 +432,10 @@ class Chat::ChatController < Chat::ChatBaseController def set_draft if params[:data].present? - ChatDraft.find_or_initialize_by(user: current_user, chat_channel_id: @chat_channel.id).update( - data: params[:data], - ) + ChatDraft.find_or_initialize_by( + user: current_user, + chat_channel_id: @chat_channel.id, + ).update!(data: params[:data]) else ChatDraft.where(user: current_user, chat_channel_id: @chat_channel.id).destroy_all end diff --git a/plugins/chat/app/controllers/incoming_chat_webhooks_controller.rb b/plugins/chat/app/controllers/incoming_chat_webhooks_controller.rb index f528e59513..58d730cbc6 100644 --- a/plugins/chat/app/controllers/incoming_chat_webhooks_controller.rb +++ b/plugins/chat/app/controllers/incoming_chat_webhooks_controller.rb @@ -85,12 +85,15 @@ class Chat::IncomingChatWebhooksController < ApplicationController ) end + # The webhook POST body can be in 3 different formats: + # + # * { text: "message text" }, which is the most basic method, and also mirrors Slack payloads + # * { attachments: [ text: "message text" ] }, which is a variant of Slack payloads using legacy attachments + # * { payload: "", attachments: null, text: null }, where JSON STRING can look + # like the `attachments` example above (along with other attributes), which is fired by OpsGenie def validate_payload - params.require([:key]) + params.require(:key) - # TODO (martin) It is not clear whether the :payload key is actually - # present in the webhooks sent from OpsGenie, so once it is confirmed - # in production what we are actually getting then we can remove this. if !params[:text] && !params[:payload] && !params[:attachments] raise Discourse::InvalidParameters end @@ -99,7 +102,7 @@ class Chat::IncomingChatWebhooksController < ApplicationController def debug_payload return if !SiteSetting.chat_debug_webhook_payloads Rails.logger.warn( - "Debugging chat webhook payload: " + + "Debugging chat webhook payload for endpoint #{params[:key]}: " + JSON.dump( { payload: params[:payload], attachments: params[:attachments], text: params[:text] }, ), diff --git a/plugins/chat/app/jobs/regular/chat_channel_delete.rb b/plugins/chat/app/jobs/regular/chat_channel_delete.rb index ac89be4db9..3d407caf9c 100644 --- a/plugins/chat/app/jobs/regular/chat_channel_delete.rb +++ b/plugins/chat/app/jobs/regular/chat_channel_delete.rb @@ -28,24 +28,30 @@ module Jobs Rails.logger.debug( "Deleting chat messages, mentions, revisions, and uploads for channel #{chat_channel.id}", ) - ChatMessage.transaction do - chat_messages = ChatMessage.where(chat_channel: chat_channel) - message_ids = chat_messages.select(:id) - ChatMention.where(chat_message_id: message_ids).delete_all - ChatMessageRevision.where(chat_message_id: message_ids).delete_all - ChatMessageReaction.where(chat_message_id: message_ids).delete_all + chat_messages = ChatMessage.where(chat_channel: chat_channel) + delete_messages_and_related_records(chat_channel, chat_messages) if chat_messages.any? + end + end - # if the uploads are not used anywhere else they will be deleted - # by the CleanUpUploads job in core - ChatUpload.where(chat_message_id: message_ids).delete_all + def delete_messages_and_related_records(chat_channel, chat_messages) + message_ids = chat_messages.pluck(:id) - # only the messages and the channel are Trashable, everything else gets - # permanently destroyed - chat_messages.update_all( - deleted_by_id: chat_channel.deleted_by_id, - deleted_at: Time.zone.now, - ) - end + ChatMessage.transaction do + ChatMention.where(chat_message_id: message_ids).delete_all + ChatMessageRevision.where(chat_message_id: message_ids).delete_all + ChatMessageReaction.where(chat_message_id: message_ids).delete_all + + # if the uploads are not used anywhere else they will be deleted + # by the CleanUpUploads job in core + DB.exec("DELETE FROM chat_uploads WHERE chat_message_id IN (#{message_ids.join(",")})") + UploadReference.where(target_id: message_ids, target_type: "ChatMessage").delete_all + + # only the messages and the channel are Trashable, everything else gets + # permanently destroyed + chat_messages.update_all( + deleted_by_id: chat_channel.deleted_by_id, + deleted_at: Time.zone.now, + ) end end end diff --git a/plugins/chat/app/models/chat_channel_archive.rb b/plugins/chat/app/models/chat_channel_archive.rb index e84cdb35e3..057af4e5bf 100644 --- a/plugins/chat/app/models/chat_channel_archive.rb +++ b/plugins/chat/app/models/chat_channel_archive.rb @@ -13,6 +13,10 @@ class ChatChannelArchive < ActiveRecord::Base def failed? !complete? && self.archive_error.present? end + + def new_topic? + self.destination_topic_title.present? + end end # == Schema Information diff --git a/plugins/chat/app/models/chat_draft.rb b/plugins/chat/app/models/chat_draft.rb index 1d3781fa82..7dc1b7feeb 100644 --- a/plugins/chat/app/models/chat_draft.rb +++ b/plugins/chat/app/models/chat_draft.rb @@ -3,6 +3,13 @@ class ChatDraft < ActiveRecord::Base belongs_to :user belongs_to :chat_channel + + validate :data_length + def data_length + if self.data && self.data.length > SiteSetting.max_chat_draft_length + self.errors.add(:base, I18n.t("chat.errors.draft_too_long")) + end + end end # == Schema Information diff --git a/plugins/chat/app/models/chat_message.rb b/plugins/chat/app/models/chat_message.rb index f7688a8a13..79e938b55b 100644 --- a/plugins/chat/app/models/chat_message.rb +++ b/plugins/chat/app/models/chat_message.rb @@ -14,8 +14,11 @@ class ChatMessage < ActiveRecord::Base has_many :revisions, class_name: "ChatMessageRevision", dependent: :destroy has_many :reactions, class_name: "ChatMessageReaction", dependent: :destroy has_many :bookmarks, as: :bookmarkable, dependent: :destroy + has_many :upload_references, as: :target, dependent: :destroy + has_many :uploads, through: :upload_references + + # TODO (martin) Remove this when we drop the ChatUpload table has_many :chat_uploads, dependent: :destroy - has_many :uploads, through: :chat_uploads has_one :chat_webhook_event, dependent: :destroy has_one :chat_mention, dependent: :destroy @@ -61,14 +64,20 @@ class ChatMessage < ActiveRecord::Base end def attach_uploads(uploads) - return if uploads.blank? + return if uploads.blank? || self.new_record? now = Time.now - record_attrs = + ref_record_attrs = uploads.map do |upload| - { upload_id: upload.id, chat_message_id: self.id, created_at: now, updated_at: now } + { + upload_id: upload.id, + target_id: self.id, + target_type: "ChatMessage", + created_at: now, + updated_at: now, + } end - ChatUpload.insert_all!(record_attrs) + UploadReference.insert_all!(ref_record_attrs) end def excerpt @@ -91,20 +100,19 @@ class ChatMessage < ActiveRecord::Base end def to_markdown - markdown = [] + upload_markdown = + self + .upload_references + .includes(:upload) + .order(:created_at) + .map(&:to_markdown) + .reject(&:empty?) - if self.message.present? - msg = self.message + return self.message if upload_markdown.empty? - self.chat_uploads.any? ? markdown << msg + "\n" : markdown << msg - end + return ["#{self.message}\n"].concat(upload_markdown).join("\n") if self.message.present? - self - .chat_uploads - .order(:created_at) - .each { |chat_upload| markdown << UploadMarkdown.new(chat_upload.upload).to_markdown } - - markdown.reject(&:empty?).join("\n") + upload_markdown.join("\n") end def cook diff --git a/plugins/chat/app/models/chat_upload.rb b/plugins/chat/app/models/chat_upload.rb index 3382328bfd..f9d969c40a 100644 --- a/plugins/chat/app/models/chat_upload.rb +++ b/plugins/chat/app/models/chat_upload.rb @@ -1,8 +1,15 @@ # frozen_string_literal: true +# TODO (martin) DEPRECATED: Remove this model once UploadReference has been +# in place for a couple of months, 2023-04-01 +# +# NOTE: Do not use this model anymore, chat messages are linked to uploads via +# the UploadReference table now, just like everything else. class ChatUpload < ActiveRecord::Base belongs_to :chat_message belongs_to :upload + + deprecate *public_instance_methods(false) end # == Schema Information diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs index 29707eb757..05ebec3b06 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.hbs @@ -18,22 +18,22 @@
    -
    +
    {{replace-emoji this.channel.escapedTitle}}
    @@ -90,4 +90,4 @@ leaveIcon="sign-out-alt" }} /> -
    \ No newline at end of file +
    diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js index 0537ff51bd..c7e1aa5b65 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js @@ -5,7 +5,7 @@ export default class ChatChannelAboutView extends Component { @service chat; tagName = ""; channel = null; - onEditChatChannelTitle = null; + onEditChatChannelName = null; onEditChatChannelDescription = null; isLoading = false; } diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js index 94d27ed5a6..0f23a725e3 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js @@ -19,8 +19,11 @@ export default Component.extend({ "channel.archive_failed" ) channelArchiveFailedMessage() { + const translationKey = !this.channel.archive_topic_id + ? "chat.channel_status.archive_failed_no_topic" + : "chat.channel_status.archive_failed"; return htmlSafe( - I18n.t("chat.channel_status.archive_failed", { + I18n.t(translationKey, { completed: this.channel.archived_messages, total: this.channel.total_messages, topic_url: this._getTopicUrl(), @@ -50,6 +53,9 @@ export default Component.extend({ }, _getTopicUrl() { + if (!this.channel.archive_topic_id) { + return ""; + } return getURL(`/t/-/${this.channel.archive_topic_id}`); }, }); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js index 4943ae1e34..9a34dd80e5 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js @@ -40,9 +40,7 @@ export default Component.extend(ModalFunctionality, { this.set("deleting", true); return this.chatApi - .destroyChannel(this.chatChannel.id, { - name_confirmation: this.channelNameConfirmation, - }) + .destroyChannel(this.chatChannel.id, this.channelNameConfirmation) .then(() => { this.set("confirmed", true); this.flash(I18n.t("chat.channel_delete.process_started"), "success"); diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js index 5367f4af96..7ce1a4b497 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js @@ -87,10 +87,7 @@ export default class ChatChannelSettingsView extends Component { const settings = {}; settings[key] = value; return this.chatApi - .updateCurrentUserChatChannelNotificationsSettings( - this.channel.id, - settings - ) + .updateCurrentUserChannelNotificationsSettings(this.channel.id, settings) .then((result) => { [ "muted", diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-desktop.js b/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-desktop.js index e6ad42af9e..d88a31d814 100644 --- a/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-desktop.js +++ b/plugins/chat/assets/javascripts/discourse/components/chat-message-actions-desktop.js @@ -4,8 +4,7 @@ import { createPopper } from "@popperjs/core"; import { schedule } from "@ember/runloop"; import { inject as service } from "@ember/service"; -const MSG_ACTIONS_HORIZONTAL_PADDING = 2; -const MSG_ACTIONS_VERTICAL_PADDING = -28; +const MSG_ACTIONS_VERTICAL_PADDING = -10; export default Component.extend({ tagName: "", @@ -28,27 +27,13 @@ export default Component.extend({ `.chat-message-actions-container[data-id="${this.message.id}"] .chat-message-actions` ), { - placement: "right-start", + placement: "top-end", modifiers: [ { name: "hide", enabled: true }, - { - name: "eventListeners", - options: { - scroll: false, - }, - }, + { name: "eventListeners", options: { scroll: false } }, { name: "offset", - options: { - offset: ({ popper, placement }) => { - return [ - MSG_ACTIONS_VERTICAL_PADDING, - -(placement.includes("left") || placement.includes("right") - ? popper.width + MSG_ACTIONS_HORIZONTAL_PADDING - : popper.height), - ]; - }, - }, + options: { offset: [-2, MSG_ACTIONS_VERTICAL_PADDING] }, }, ], } diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name.js similarity index 68% rename from plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js rename to plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name.js index d57ad3f6ce..fcba0d7711 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-title.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-edit-name.js @@ -6,30 +6,30 @@ export default class ChatChannelEditTitleController extends Controller.extend( ModalFunctionality ) { @service chatApi; - editedTitle = ""; + editedName = ""; - @computed("model.title", "editedTitle") + @computed("model.title", "editedName") get isSaveDisabled() { return ( - this.model.title === this.editedTitle || - this.editedTitle?.length > this.siteSettings.max_topic_title_length + this.model.title === this.editedName || + this.editedName?.length > this.siteSettings.max_topic_title_length ); } onShow() { - this.set("editedTitle", this.model.title || ""); + this.set("editedName", this.model.title || ""); } onClose() { - this.set("editedTitle", ""); + this.set("editedName", ""); this.clearFlash(); } @action - onSaveChatChannelTitle() { + onSaveChatChannelName() { return this.chatApi .updateChannel(this.model.id, { - name: this.editedTitle, + name: this.editedName, }) .then((result) => { this.model.set("title", result.channel.title); @@ -43,8 +43,8 @@ export default class ChatChannelEditTitleController extends Controller.extend( } @action - onChangeChatChannelTitle(title) { + onChangeChatChannelName(title) { this.clearFlash(); - this.set("editedTitle", title); + this.set("editedName", title); } } diff --git a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js index d33ec8fd22..73514a7bd6 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/chat-channel-info-about.js @@ -7,8 +7,8 @@ export default class ChatChannelInfoAboutController extends Controller.extend( ModalFunctionality ) { @action - onEditChatChannelTitle() { - showModal("chat-channel-edit-title", { model: this.model }); + onEditChatChannelName() { + showModal("chat-channel-edit-name", { model: this.model }); } @action diff --git a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js index ae2ed12f48..062a3c7664 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/create-channel.js @@ -1,11 +1,14 @@ import { escapeExpression } from "discourse/lib/utilities"; +import { ajax } from "discourse/lib/ajax"; +import { cancel } from "@ember/runloop"; +import discourseDebounce from "discourse-common/lib/debounce"; import Controller from "@ember/controller"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { action, computed } from "@ember/object"; import { gt, notEmpty } from "@ember/object/computed"; import { inject as service } from "@ember/service"; -import { isBlank } from "@ember/utils"; +import { isBlank, isPresent } from "@ember/utils"; import { htmlSafe } from "@ember/template"; const DEFAULT_HINT = htmlSafe( @@ -26,6 +29,8 @@ export default class CreateChannelController extends Controller.extend( category = null; categoryId = null; name = ""; + slug = ""; + autoGeneratedSlug = ""; description = ""; categoryPermissionsHint = null; autoJoinUsers = null; @@ -39,16 +44,26 @@ export default class CreateChannelController extends Controller.extend( return !this.categorySelected || isBlank(this.name); } + @computed("categorySelected", "name") + get categoryName() { + return this.categorySelected && isPresent(this.name) + ? escapeExpression(this.name) + : null; + } + onShow() { this.set("categoryPermissionsHint", DEFAULT_HINT); } onClose() { + cancel(this.generateSlugHandler); this.setProperties({ categoryId: null, category: null, name: "", description: "", + slug: "", + autoGeneratedSlug: "", categoryPermissionsHint: DEFAULT_HINT, autoJoinWarning: "", }); @@ -58,6 +73,7 @@ export default class CreateChannelController extends Controller.extend( const data = { chatable_id: this.categoryId, name: this.name, + slug: this.slug || this.autoGeneratedSlug, description: this.description, auto_join_users: this.autoJoinUsers, }; @@ -140,17 +156,53 @@ export default class CreateChannelController extends Controller.extend( } } + // intentionally not showing AJAX error for this, we will autogenerate + // the slug server-side if they leave it blank + _generateSlug(name) { + ajax("/slugs/generate.json", { type: "GET", data: { name } }).then( + (response) => { + this.set("autoGeneratedSlug", response.slug); + } + ); + } + + _debouncedGenerateSlug(name) { + cancel(this.generateSlugHandler); + this._clearAutoGeneratedSlug(); + if (!name) { + return; + } + this.generateSlugHandler = discourseDebounce( + this, + this._generateSlug, + name, + 300 + ); + } + + _clearAutoGeneratedSlug() { + this.set("autoGeneratedSlug", ""); + } + @action onCategoryChange(categoryId) { let category = categoryId ? this.site.categories.findBy("id", categoryId) : null; this._updatePermissionsHint(category); + + const name = this.name || category?.name || ""; this.setProperties({ categoryId, category, - name: category?.name || "", + name, }); + this._debouncedGenerateSlug(name); + } + + @action + onNameChange(name) { + this._debouncedGenerateSlug(name); } @action diff --git a/plugins/chat/assets/javascripts/discourse/controllers/preferences-chat.js b/plugins/chat/assets/javascripts/discourse/controllers/preferences-chat.js index bf4ef8a8ae..5774f44b4b 100644 --- a/plugins/chat/assets/javascripts/discourse/controllers/preferences-chat.js +++ b/plugins/chat/assets/javascripts/discourse/controllers/preferences-chat.js @@ -22,6 +22,7 @@ const EMAIL_FREQUENCY_OPTIONS = [ export default class PreferencesChatController extends Controller { @service chatAudioManager; + subpageTitle = I18n.t("chat.admin.title"); emailFrequencyOptions = EMAIL_FREQUENCY_OPTIONS; diff --git a/plugins/chat/assets/javascripts/discourse/lib/collection.js b/plugins/chat/assets/javascripts/discourse/lib/collection.js new file mode 100644 index 0000000000..a001121e2d --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/lib/collection.js @@ -0,0 +1,128 @@ +/** @module Collection */ + +import { ajax } from "discourse/lib/ajax"; +import { tracked } from "@glimmer/tracking"; +import { bind } from "discourse-common/utils/decorators"; +import { Promise } from "rsvp"; + +/** + * Handles a paginated API response. + * + * @class + */ +export default class Collection { + @tracked items = []; + @tracked meta = {}; + @tracked loading = false; + + /** + * Create a Collection instance + * @param {string} resourceURL - the API endpoint to call + * @param {callback} handler - anonymous function used to handle the response + */ + constructor(resourceURL, handler) { + this._resourceURL = resourceURL; + this._handler = handler; + this._fetchedAll = false; + } + + get loadMoreURL() { + return this.meta.load_more_url; + } + + get totalRows() { + return this.meta.total_rows; + } + + get length() { + return this.items.length; + } + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols + [Symbol.iterator]() { + let index = 0; + + return { + next: () => { + if (index < this.items.length) { + return { value: this.items[index++], done: false }; + } else { + return { done: true }; + } + }, + }; + } + + /** + * Loads first batch of results + * @returns {Promise} + */ + @bind + load(params = {}) { + this._fetchedAll = false; + + if (this.loading) { + return Promise.resolve(); + } + + this.loading = true; + + const filteredQueryParams = Object.entries(params).filter( + ([, v]) => v !== undefined + ); + const queryString = new URLSearchParams(filteredQueryParams).toString(); + + const endpoint = this._resourceURL + (queryString ? `?${queryString}` : ""); + return this.#fetch(endpoint) + .then((result) => { + this.items = this._handler(result); + this.meta = result.meta; + }) + .finally(() => { + this.loading = false; + }); + } + + /** + * Attempts to load more results + * @returns {Promise} + */ + @bind + loadMore() { + let promise = Promise.resolve(); + + if (this.loading) { + return promise; + } + + if ( + this._fetchedAll || + (this.totalRows && this.items.length >= this.totalRows) + ) { + return promise; + } + + this.loading = true; + + if (this.loadMoreURL) { + promise = this.#fetch(this.loadMoreURL).then((result) => { + const newItems = this._handler(result); + + if (newItems.length) { + this.items = this.items.concat(newItems); + } else { + this._fetchedAll = true; + } + this.meta = result.meta; + }); + } + + return promise.finally(() => { + this.loading = false; + }); + } + + #fetch(url) { + return ajax(url, { type: "GET" }); + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js index e88cb954cc..e0157f0f61 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js @@ -1,123 +1,42 @@ +/** @module ChatApi */ + import Service, { inject as service } from "@ember/service"; import { ajax } from "discourse/lib/ajax"; import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership"; -import { tracked } from "@glimmer/tracking"; -import { bind } from "discourse-common/utils/decorators"; -import { Promise } from "rsvp"; - -class Collection { - @tracked items = []; - @tracked meta = {}; - @tracked loading = false; - - constructor(resourceURL, handler) { - this._resourceURL = resourceURL; - this._handler = handler; - this._fetchedAll = false; - } - - get loadMoreURL() { - return this.meta.load_more_url; - } - - get totalRows() { - return this.meta.total_rows; - } - - get length() { - return this.items.length; - } - - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols - [Symbol.iterator]() { - let index = 0; - - return { - next: () => { - if (index < this.items.length) { - return { value: this.items[index++], done: false }; - } else { - return { done: true }; - } - }, - }; - } - - @bind - load(params = {}) { - this._fetchedAll = false; - - if (this.loading) { - return; - } - - this.loading = true; - - const filteredQueryParams = Object.entries(params).filter( - ([, v]) => v !== undefined - ); - const queryString = new URLSearchParams(filteredQueryParams).toString(); - - const endpoint = this._resourceURL + (queryString ? `?${queryString}` : ""); - return this.#fetch(endpoint) - .then((result) => { - this.items = this._handler(result); - this.meta = result.meta; - }) - .finally(() => { - this.loading = false; - }); - } - - @bind - loadMore() { - let promise = Promise.resolve(); - - if (this.loading) { - return promise; - } - - if ( - this._fetchedAll || - (this.totalRows && this.items.length >= this.totalRows) - ) { - return promise; - } - - this.loading = true; - - if (this.loadMoreURL) { - promise = this.#fetch(this.loadMoreURL).then((result) => { - const newItems = this._handler(result); - - if (newItems.length) { - this.items = this.items.concat(newItems); - } else { - this._fetchedAll = true; - } - this.meta = result.meta; - }); - } - - return promise.finally(() => { - this.loading = false; - }); - } - - #fetch(url) { - return ajax(url, { type: "GET" }); - } -} +import Collection from "../lib/collection"; +/** + * Chat API service. Provides methods to interact with the chat API. + * + * @class + * @implements {@ember/service} + */ export default class ChatApi extends Service { @service chatChannelsManager; - getChannel(channelId) { + /** + * Get a channel by its ID. + * @param {number} channelId - The ID of the channel. + * @returns {Promise} + * + * @example + * + * this.chatApi.channel(1).then(channel => { ... }) + */ + channel(channelId) { return this.#getRequest(`/channels/${channelId}`).then((result) => this.chatChannelsManager.store(result.channel) ); } + /** + * List all accessible category channels of the current user. + * @returns {module:Collection} + * + * @example + * + * this.chatApi.channels.then(channels => { ... }) + */ channels() { return new Collection(`${this.#basePath}/channels`, (response) => { return response.channels.map((channel) => @@ -126,26 +45,85 @@ export default class ChatApi extends Service { }); } + /** + * Moves messages from one channel to another. + * @param {number} channelId - The ID of the original channel. + * @param {object} data - Params of the move. + * @param {Array.} data.message_ids - IDs of the moved messages. + * @param {number} data.destination_channel_id - ID of the channel where the messages are moved to. + * @returns {Promise} + * + * @example + * + * this.chatApi + * .moveChannelMessages(1, { + * message_ids: [2, 3], + * destination_channel_id: 4, + * }).then(() => { ... }) + */ moveChannelMessages(channelId, data = {}) { return this.#postRequest(`/channels/${channelId}/messages/moves`, { move: data, }); } - destroyChannel(channelId, data = {}) { - return this.#deleteRequest(`/channels/${channelId}`, { channel: data }); + /** + * Destroys a channel. + * @param {number} channelId - The ID of the channel. + * @param {string} channelName - The name of the channel to be destroyed, used as confirmation. + * @returns {Promise} + * + * @example + * + * this.chatApi.destroyChannel(1, "foo").then(() => { ... }) + */ + destroyChannel(channelId, channelName) { + return this.#deleteRequest(`/channels/${channelId}`, { + channel: { name_confirmation: channelName }, + }); } + /** + * Creates a channel. + * @param {object} data - Params of the channel. + * @param {string} data.name - The name of the channel. + * @param {string} data.chatable_id - The category of the channel. + * @param {string} data.description - The description of the channel. + * @param {boolean} [data.auto_join_users] - Should users join this channel automatically. + * @returns {Promise} + * + * @example + * + * this.chatApi + * .createChannel({ name: "foo", chatable_id: 1, description "bar" }) + * .then((channel) => { ... }) + */ createChannel(data = {}) { return this.#postRequest("/channels", { channel: data }).then((response) => this.chatChannelsManager.store(response.channel) ); } + /** + * Lists chat permissions for a category. + * @param {number} categoryId - ID of the category. + * @returns {Promise} + */ categoryPermissions(categoryId) { - return ajax(`/chat/api/category-chatables/${categoryId}/permissions`); + return this.#getRequest(`/category-chatables/${categoryId}/permissions`); } + /** + * Sends a message. + * @param {number} channelId - ID of the channel. + * @param {object} data - Params of the message. + * @param {string} data.message - The raw content of the message in markdown. + * @param {string} data.cooked - The cooked content of the message. + * @param {number} [data.in_reply_to_id] - The ID of the replied-to message. + * @param {number} [data.staged_id] - The staged ID of the message before it was persisted. + * @param {Array.} [data.upload_ids] - Array of upload ids linked to the message. + * @returns {Promise} + */ sendMessage(channelId, data = {}) { return ajax(`/chat/${channelId}`, { ignoreUnsent: false, @@ -154,20 +132,50 @@ export default class ChatApi extends Service { }); } + /** + * Creates a channel archive. + * @param {number} channelId - The ID of the channel. + * @param {object} data - Params of the archive. + * @param {string} data.selection - "new_topic" or "existing_topic". + * @param {string} [data.title] - Title of the topic when creating a new topic. + * @param {string} [data.category_id] - ID of the category used when creating a new topic. + * @param {Array.} [data.tags] - tags used when creating a new topic. + * @param {string} [data.topic_id] - ID of the topic when using an existing topic. + * @returns {Promise} + */ createChannelArchive(channelId, data = {}) { return this.#postRequest(`/channels/${channelId}/archives`, { archive: data, }); } + /** + * Updates a channel. + * @param {number} channelId - The ID of the channel. + * @param {object} data - Params of the archive. + * @param {string} [data.description] - Description of the channel. + * @param {string} [data.name] - Name of the channel. + * @returns {Promise} + */ updateChannel(channelId, data = {}) { return this.#putRequest(`/channels/${channelId}`, { channel: data }); } + /** + * Updates the status of a channel. + * @param {number} channelId - The ID of the channel. + * @param {string} status - The new status, can be "open" or "closed". + * @returns {Promise} + */ updateChannelStatus(channelId, status) { return this.#putRequest(`/channels/${channelId}/status`, { status }); } + /** + * Lists members of a channel. + * @param {number} channelId - The ID of the channel. + * @returns {module:Collection} + */ listChannelMemberships(channelId) { return new Collection( `${this.#basePath}/channels/${channelId}/memberships`, @@ -179,27 +187,50 @@ export default class ChatApi extends Service { ); } + /** + * Lists public and direct message channels of the current user. + * @returns {Promise} + */ listCurrentUserChannels() { - return this.#getRequest(`/channels/me`).then((result) => { + return this.#getRequest("/channels/me").then((result) => { return (result?.channels || []).map((channel) => this.chatChannelsManager.store(channel) ); }); } + /** + * Makes current user follow a channel. + * @param {number} channelId - The ID of the channel. + * @returns {Promise} + */ followChannel(channelId) { return this.#postRequest(`/channels/${channelId}/memberships/me`).then( (result) => UserChatChannelMembership.create(result.membership) ); } + /** + * Makes current user unfollow a channel. + * @param {number} channelId - The ID of the channel. + * @returns {Promise} + */ unfollowChannel(channelId) { return this.#deleteRequest(`/channels/${channelId}/memberships/me`).then( (result) => UserChatChannelMembership.create(result.membership) ); } - updateCurrentUserChatChannelNotificationsSettings(channelId, data = {}) { + /** + * Update notifications settings of current user for a channel. + * @param {number} channelId - The ID of the channel. + * @param {object} data - The settings to modify. + * @param {boolean} [data.muted] - Mutes the channel. + * @param {string} [data.desktop_notification_level] - Notifications level on desktop: never, mention or always. + * @param {string} [data.mobile_notification_level] - Notifications level on mobile: never, mention or always. + * @returns {Promise} + */ + updateCurrentUserChannelNotificationsSettings(channelId, data = {}) { return this.#putRequest( `/channels/${channelId}/notifications-settings/me`, { notifications_settings: data } diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js index 1a73752a07..9d2cae5539 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js @@ -117,7 +117,7 @@ export default class ChatChannelsManager extends Service { async #find(id) { return this.chatApi - .getChannel(id) + .channel(id) .catch(popupAjaxError) .then((channel) => { this.#cache(channel); diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js index 0cb1c56ace..931f480e43 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat.js @@ -373,11 +373,13 @@ export default class Chat extends Service { data.data = JSON.stringify(draft); } - ajax("/chat/drafts", { type: "POST", data, ignoreUnsent: false }) + ajax("/chat/drafts.json", { type: "POST", data, ignoreUnsent: false }) .then(() => { this.markNetworkAsReliable(); }) .catch((error) => { + // we ignore a draft which can't be saved because it's too big + // and only deal with network error for now if (!error.jqXHR?.responseJSON?.errors?.length) { this.markNetworkAsUnreliable(); } diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs index 5f5d5a8aa1..e7c3b8d6a4 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/chat-channel-info-about.hbs @@ -1,5 +1,5 @@ \ No newline at end of file +/> diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-name.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-name.hbs new file mode 100644 index 0000000000..00492a2521 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-name.hbs @@ -0,0 +1,22 @@ + + + + {{i18n "chat.channel_edit_name_modal.description"}} + + + + diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-title.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-title.hbs deleted file mode 100644 index 99fb42dcbc..0000000000 --- a/plugins/chat/assets/javascripts/discourse/templates/modal/chat-channel-edit-title.hbs +++ /dev/null @@ -1,20 +0,0 @@ - - - - {{i18n "chat.channel_edit_title_modal.description"}} - - - - \ No newline at end of file diff --git a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs index 13b2ff45e2..c25dc4e63b 100644 --- a/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs +++ b/plugins/chat/assets/javascripts/discourse/templates/modal/create-channel.hbs @@ -1,45 +1,81 @@ - - +
    + + +
    - {{#if this.categoryPermissionsHint}} -
    - {{this.categoryPermissionsHint}} -
    - {{/if}} +
    + + +
    + +
    + + +
    + +
    + + + + {{#if this.categoryPermissionsHint}} +
    + {{this.categoryPermissionsHint}} +
    + {{/if}} +
    {{#if this.autoJoinAvailable}} - +
    + +
    {{/if}} - - - - - -
    \ No newline at end of file +
    diff --git a/plugins/chat/assets/stylesheets/common/chat-channel-info.scss b/plugins/chat/assets/stylesheets/common/chat-channel-info.scss index 29f3cb30cd..091ea9366d 100644 --- a/plugins/chat/assets/stylesheets/common/chat-channel-info.scss +++ b/plugins/chat/assets/stylesheets/common/chat-channel-info.scss @@ -117,13 +117,14 @@ input.channel-members-view__search-input { } } -// Channel info edit title modal -.chat-channel-edit-title-modal__title-input { +// Channel info edit name modal +.chat-channel-edit-name-modal__name-input { display: flex; margin: 0; + width: 100%; } -.chat-channel-edit-title-modal__description { +.chat-channel-edit-name-modal__description { display: flex; padding: 0.5rem 0; color: var(--primary-medium); diff --git a/plugins/chat/assets/stylesheets/common/common.scss b/plugins/chat/assets/stylesheets/common/common.scss index 4290dda413..d985227338 100644 --- a/plugins/chat/assets/stylesheets/common/common.scss +++ b/plugins/chat/assets/stylesheets/common/common.scss @@ -525,43 +525,6 @@ body.has-full-page-chat { margin-bottom: 0; } -.create-channel-modal { - .modal-inner-container { - width: 500px; - } - .choose-topic-results-list { - max-height: 200px; - overflow-y: scroll; - } - .select-kit.combo-box, - .create-channel-name-input, - .create-channel-description-input, - #choose-topic-title { - width: 100%; - margin-bottom: 0; - } - .category-chooser { - .select-kit-selected-name.selected-name.choice { - color: var( - --primary-high - ); // Make consistent with color of placeholder text when choosing topic - } - } - - .create-channel-hint { - font-size: 0.8em; - margin-top: 0.2em; - } - - .create-channel-label, - label[for="choose-topic-title"] { - margin: 1em 0 0.35em; - } - .chat-channel-title { - margin: 1em 0 0 0; - } -} - .chat-message-collapser, .chat-message-text { > p { @@ -677,8 +640,7 @@ html.has-full-page-chat { ); } } -} - -[data-popper-reference-hidden] { - visibility: hidden; + [data-popper-reference-hidden] { + visibility: hidden; + } } diff --git a/plugins/chat/assets/stylesheets/common/create-channel-modal.scss b/plugins/chat/assets/stylesheets/common/create-channel-modal.scss new file mode 100644 index 0000000000..bfd8a7735e --- /dev/null +++ b/plugins/chat/assets/stylesheets/common/create-channel-modal.scss @@ -0,0 +1,45 @@ +.create-channel-modal { + .modal-inner-container { + width: 500px; + } + + .choose-topic-results-list { + max-height: 200px; + overflow-y: scroll; + } + + .select-kit.combo-box, + .create-channel-name-input, + .create-channel-slug-input, + .create-channel-description-input, + #choose-topic-title { + width: 100%; + margin-bottom: 0; + } + .category-chooser { + .select-kit-selected-name.selected-name.choice { + color: var( + --primary-high + ); // Make consistent with color of placeholder text when choosing topic + } + } + + .create-channel-hint { + font-size: var(--font-down-1); + padding-top: 0.25rem; + color: var(--secondary-low); + } + + .create-channel-control { + margin-bottom: 1rem; + } + + .auto-join-channel { + &__description { + margin: 0; + padding-top: 0.25rem; + color: var(--secondary-low); + font-size: var(--font-down-1) !important; + } + } +} diff --git a/plugins/chat/config/locales/client.ar.yml b/plugins/chat/config/locales/client.ar.yml index aa061c3d69..3bf2e22710 100644 --- a/plugins/chat/config/locales/client.ar.yml +++ b/plugins/chat/config/locales/client.ar.yml @@ -113,6 +113,13 @@ ar: new_messages: "رسائل جديدة" mention_warning: dismiss: "تجاهل" + cannot_see: + zero: "لا يمكن للمستخدم %{username} و%{others} مستخدم آخر الوصول إلى هذه القناة ولن يتلقوا إشعارات." + one: "لا يمكن للمستخدم %{username} ومستخدم (%{others}) آخر الوصول إلى هذه القناة ولن يتلقيا إشعارات." + two: "لا يمكن للمستخدم %{username} ومستخدمَين (%{others}) آخرين الوصول إلى هذه القناة ولن يتلقوا إشعارات." + few: "لا يمكن للمستخدم %{username} و%{others} مستخدمين آخر الوصول إلى هذه القناة ولن يتلقوا إشعارات." + many: "لا يمكن للمستخدم %{username} و%{others} مستخدمًا آخر الوصول إلى هذه القناة ولن يتلقوا إشعارات." + other: "لا يمكن للمستخدم %{username} و%{others} مستخدم آخر الوصول إلى هذه القناة ولن يتلقوا إشعارات." invitations_sent: zero: "تم إرسال دعوات" one: "تم إرسال دعوة" @@ -121,14 +128,70 @@ ar: many: "تم إرسال دعوات" other: "تم إرسال دعوات" invite: "دعوة إلى القناة" + without_membership: + zero: "لم ينضم %{username} إلى هذه القناة." + one: "لم ينضم %{username} و%{others} مستخدم آخر إلى هذه القناة." + two: "لم ينضم %{username} ومستخدمان (%{others}) آخران إلى هذه القناة." + few: "لم ينضم %{username} و%{others} مستخدمين آخرين إلى هذه القناة." + many: "لم ينضم %{username} و%{others} مستخدمًا آخر إلى هذه القناة." + other: "لم ينضم %{username} و%{others} مستخدم آخر إلى هذه القناة." + group_mentions_disabled: + zero: "لا تسمح المجموعة %{group_name} و%{others} مجموعة أخرى بالإشارات." + one: "لا تسمح المجموعة %{group_name} بالإشارات." + two: "لا تسمح المجموعة %{group_name} ومجموعتان (%{others}) أخرتان بالإشارات." + few: "لا تسمح المجموعة %{group_name} و%{others} مجموعات أخرى بالإشارات." + many: "لا تسمح المجموعة %{group_name} و%{others} مجموعةً أخرى بالإشارات." + other: "لا تسمح المجموعة %{group_name} و%{others} مجموعة أخرى بالإشارات." + too_many_members: + zero: "تحتوي المجموعة %{group_name} و%{others} مجموعة أخرى على أعضاء كثيرين. لم يتلقَّ أحد الإشعارات" + one: "تحتوي المجموعة %{group_name} ومجموعة (%{others}) أخرى على أعضاء كثيرين. لم يتلقَّ أحد الإشعارات" + two: "تحتوي المجموعة %{group_name} ومجموعتان (%{others}) أخرتان على أعضاء كثيرين. لم يتلقَّ أحد الإشعارات" + few: "تحتوي المجموعة %{group_name} و%{others} مجموعات أخرى على أعضاء كثيرين. لم يتلقَّ أحد الإشعارات" + many: "تحتوي المجموعة %{group_name} و%{others} مجموعةً أخرى على أعضاء كثيرين. لم يتلقَّ أحد الإشعارات" + other: "تحتوي المجموعة %{group_name} و%{others} مجموعة أخرى على أعضاء كثيرين. لم يتلقَّ أحد الإشعارات" + warning_multiple: + zero: "%{count} آخر" + one: "%{count} آخر" + two: "%{count} آخران" + few: "%{count} آخرين" + many: "%{count} آخرين" + other: "%{count} آخرين" groups: + header: + some: "لن يتلقى بعض المستخدمين الإشعارات" + all: "لن يتلقى أحد الإشعارات" + unreachable: + zero: "لا تسمح المجموعة @%{group} و@%{group_2} بالإشارات" + one: "لا تسمح المجموعة @%{group} بالإشارات" + two: "لا تسمح المجموعة @%{group} و@%{group_2} بالإشارات" + few: "لا تسمح المجموعة @%{group} و@%{group_2} بالإشارات" + many: "لا تسمح المجموعة @%{group} و@%{group_2} بالإشارات" + other: "لا تسمح المجموعة @%{group} و@%{group_2} بالإشارات" + unreachable_multiple: "لا تسمح المجموعة @%{group} و%{count} من المجموعات الأخرى بالإشارات" + too_many_members: + zero: "تتجاوز الإشارة إلى @%{group} أو @%{group_2} حد الإشعارات البالغ %{notification_limit} من %{limit}" + one: "تتجاوز الإشارة إلى @%{group} حد الإشعارات البالغ %{notification_limit} من %{limit}" + two: "تتجاوز الإشارة إلى @%{group} أو @%{group_2} حد الإشعارات البالغ %{notification_limit} من %{limit}" + few: "تتجاوز الإشارة إلى @%{group} أو @%{group_2} حد الإشعارات البالغ %{notification_limit} من %{limit}" + many: "تتجاوز الإشارة إلى @%{group} أو @%{group_2} حد الإشعارات البالغ %{notification_limit} من %{limit}" + other: "تتجاوز الإشارة إلى @%{group} أو @%{group_2} حد الإشعارات البالغ %{notification_limit} من %{limit}" + too_many_members_multiple: "تتجاوز هذه المجموعات البالغ عددها %{count} حد الإشعارات البالغ %{notification_limit} من %{limit}" users_limit: - zero: "%{count} مستخدمًا" + zero: "%{count} مستخدم" one: "مستخدم واحد (%{count})" two: "مستخدمان (%{count})" few: "%{count} مستخدمين" many: "%{count} مستخدمًا" other: "%{count} مستخدم" + notification_limit: "حد الإشعارات" + too_many_mentions: "تتجاوز هذه الرسالة حد الإشعارات البالغ %{notification_limit} من %{limit}" + mentions_limit: + zero: "%{count} إشارة" + one: "إشارة واحدة (%{count})" + two: "إشارتان (%{count})" + few: "%{count} إشارات" + many: "%{count} إشارةً" + other: "%{count} إشارة" aria_roles: header: "رأس الدردشة" composer: "أداة إنشاء الدردشة" @@ -191,7 +254,8 @@ ar: read_only: "للقراءة فقط" archived_header: "القناة مؤرشفة" archived: "مؤرشفة" - archive_failed: "فشلت أرشفة القناة. تمت أرشفة %{completed}/%{total} من الرسائل في الموضوع المستهدف. اضغط على إعادة المحاولة لمحاولة إكمال الأرشيف." + archive_failed: "فشلت أرشفة القناة. تمت أرشفة %{completed}/%{total} من الرسائل. موضوع الوجهة. اضغط على \"إعادة المحاولة\" لمحاولة إكمال الأرشيف." + archive_failed_no_topic: "فشلت أرشفة القناة. تمت أرشفة %{completed}/%{total} من الرسائل، ولم يتم إنشاء موضوع الوجهة. اضغط على \"إعادة المحاولة\" لمحاولة إكمال الأرشيف." archive_completed: "راجع موضوع الأرشيف" closed_header: "القناة مغلقة" closed: "مغلقة" @@ -214,6 +278,7 @@ ar: associated_topic: الموضوع المرتبط associated_category: الفئة المرتبطة title: العنوان + name: الاسم description: الوصف channel_info: back_to_all_channels: "كل القنوات" @@ -222,10 +287,6 @@ ar: about: نبذة members: الأعضاء settings: الإعدادات - channel_edit_title_modal: - title: تعديل العنوان - input_placeholder: إضافة عنوان - description: أعط عنوانًا وصفيًا قصيرًا لقناتك channel_edit_description_modal: title: تعديل الوصف input_placeholder: أضِف وصفًا @@ -300,7 +361,9 @@ ar: mention: "للإشارات فقط" always: "للنشاط بأكمله" settings: - enable_auto_join_users: "إضافة كل المستخدمين النشطين مؤخرًا تلقائيًا" + channel_wide_mentions_label: "السماح بإشارات @الكل و@هنا" + channel_wide_mentions_description: "السماح للمستخدمين بإرسال إشعارات إلى جميع أعضاء #%{channel} باستخدام @الكل، أو المستخدمين النشطين في الوقت الحالي فقط باستخدام @هنا" + auto_join_users_label: "إضافة المستخدمين تلقائيًا" auto_join_users_warning: "سينضم كل المستخدمين الذين ليسوا أعضاءً في هذه القناة ولديهم وصول إلى فئة %{category}. هل أنت متأكد؟" desktop_notification_level: "إشعارات سطح المكتب" follow: "الانضمام" @@ -315,10 +378,12 @@ ar: saved: "تم الحفظ" unfollow: "مغادرة" admin_title: "مسؤول" + retention_info: "سيتم الاحتفاظ بسجل الدردشة لمدة %{days} من الأيام." admin: title: "الدردشة" direct_messages: title: "الدردشة الشخصية" + new: "إنشاء دردشة شخصية" create: "إنشاء" leave: "مغادرة هذه الدردشة الشخصية" cannot_create: "عذرًا، لا يمكنك إرسال الرسائل المباشرة." @@ -337,7 +402,7 @@ ar: none: "لم يتم إنشاء خطافات ويب واردة حالية." no_emoji: "لم يتم تحديد رمز تعبيري" post_to: "نشر إلى" - reset_emoji: "إعادة ضبط الرمز التعبيري" + reset_emoji: "إعادة تعيين الرمز التعبيري" save: "حفظ" edit: "تعديل" select_emoji: "اختيار الرمز التعبيري" @@ -346,7 +411,7 @@ ar: url: "عنوان URL" url_instructions: "يحتوي عنوان URL هذا على قيمة سرية - احتفظ بها في مكانٍ آمن." username: "اسم المستخدم" - username_instructions: "اسم المستخدم لبرنامج الروبوت الذي ينشر على القناة. يتم ضبطه افتراضيًا على \"النظام\" عند تركه فارغة." + username_instructions: "اسم المستخدم لبرنامج الروبوت الذي ينشر على القناة. يتم تعيينه افتراضيًا إلى \"النظام\" عند تركه فارغة." instructions: "يمكن استخدام خطافات الويب الواردة بواسطة أنظمة خارجية لنشر الرسائل في قناة دردشة مخصَّصة كمستخدم برنامج روبوت عبر نقطة النهاية /hooks/:key. يتألف الحمل من معلمة نصية فردية، وهي مقيَّدة إلى 2000 حرف.

    نحن ندعم أيضًا المعلمات النصية بتنسيق Slack، مع استخراج الروابط والإشارات بناءً على التنسيق في https://api.slack.com/reference/surfaces/formatting، لكن يجب استخدام نقطة النهاية /hooks/:key/slack من أجل ذلك." selection: cancel: "إلغاء" @@ -457,7 +522,7 @@ ar: label: رسالة sender: label: المرسل - description: يتم ضبطه افتراضيًا على النظام + description: يتم تعيينه افتراضيًا إلى النظام review: transcript: view: "عرض نص الرسائل السابقة" @@ -489,7 +554,7 @@ ar: user_menu: no_chat_notifications_title: "ليس لديك أي إشعارات دردشة حتى الآن" no_chat_notifications_body: > - سيتم إرسال إشعار إليك في هذه اللوحة عندما يراسلك أحدهم مباشرةً أو يشير إليك @mention في الدردشة. سيتم أيضًا إرسال الإشعارات إلى بريدك الإلكتروني في حال عدم تسجيلك الدخول لفترة من الوقت.

    انقر على العنوان الموجود أعلى أي قناة دردشة لضبط التنبيهات التي تتلقاها في تلك القناة. للمزيد من المعلومات، راجع تفضيلات الإشعارات. + سيتم إرسال إشعار إليك في هذه اللوحة عندما يراسلك أحدهم مباشرةً أو يشير إليك @mention في الدردشة. سيتم أيضًا إرسال الإشعارات إلى بريدك الإلكتروني في حال عدم تسجيلك الدخول لفترة من الوقت.

    انقر على العنوان الموجود أعلى أي قناة دردشة لتعيين التنبيهات التي تتلقاها في تلك القناة. للمزيد من المعلومات، راجع تفضيلات الإشعارات. tabs: chat_notifications: "إشعارات الدردشة" chat_notifications_with_unread: diff --git a/plugins/chat/config/locales/client.be.yml b/plugins/chat/config/locales/client.be.yml index 3511bc87e8..5403bb14a7 100644 --- a/plugins/chat/config/locales/client.be.yml +++ b/plugins/chat/config/locales/client.be.yml @@ -35,6 +35,7 @@ be: yesterday: учора about_view: title: Загаловак + name: імя description: Апісанне channel_info: back_to_channel: "Назад" diff --git a/plugins/chat/config/locales/client.bg.yml b/plugins/chat/config/locales/client.bg.yml index 46d8437cf6..e7463e97f4 100644 --- a/plugins/chat/config/locales/client.bg.yml +++ b/plugins/chat/config/locales/client.bg.yml @@ -48,6 +48,7 @@ bg: yesterday: Вчера about_view: title: Заглавие + name: Име description: Описание channel_info: back_to_channel: "Назад" diff --git a/plugins/chat/config/locales/client.bs_BA.yml b/plugins/chat/config/locales/client.bs_BA.yml index af9bc26169..414d00793b 100644 --- a/plugins/chat/config/locales/client.bs_BA.yml +++ b/plugins/chat/config/locales/client.bs_BA.yml @@ -50,6 +50,7 @@ bs_BA: yesterday: Yesterday about_view: title: Naslov + name: Ime description: Opis channel_info: back_to_channel: "Prethodno" diff --git a/plugins/chat/config/locales/client.ca.yml b/plugins/chat/config/locales/client.ca.yml index 4e070701a0..79adffade5 100644 --- a/plugins/chat/config/locales/client.ca.yml +++ b/plugins/chat/config/locales/client.ca.yml @@ -50,6 +50,7 @@ ca: yesterday: Ahir about_view: title: Títol + name: Nom description: Descripció channel_info: back_to_channel: "Enrere" diff --git a/plugins/chat/config/locales/client.cs.yml b/plugins/chat/config/locales/client.cs.yml index 2c80cabf77..f9b87ae1aa 100644 --- a/plugins/chat/config/locales/client.cs.yml +++ b/plugins/chat/config/locales/client.cs.yml @@ -51,6 +51,7 @@ cs: yesterday: Včera about_view: title: Nadpis + name: Jméno description: Popis channel_info: back_to_channel: "Zpět" diff --git a/plugins/chat/config/locales/client.da.yml b/plugins/chat/config/locales/client.da.yml index 02eda5f370..ea0d9ddfa6 100644 --- a/plugins/chat/config/locales/client.da.yml +++ b/plugins/chat/config/locales/client.da.yml @@ -126,6 +126,7 @@ da: yesterday: I går about_view: title: Titel + name: Navn description: Beskrivelse channel_info: back_to_channel: "Tilbage" diff --git a/plugins/chat/config/locales/client.de.yml b/plugins/chat/config/locales/client.de.yml index 2284011584..98330dcbf0 100644 --- a/plugins/chat/config/locales/client.de.yml +++ b/plugins/chat/config/locales/client.de.yml @@ -109,14 +109,45 @@ de: new_messages: "neue Nachrichten" mention_warning: dismiss: "verwerfen" + cannot_see: + one: "%{username} kann nicht auf diesen Kanal zugreifen und wurde nicht benachrichtigt." + other: "%{username} und %{others} können nicht auf diesen Kanal zugreifen und wurden nicht benachrichtigt." invitations_sent: one: "Einladung gesendet" other: "Einladungen gesendet" invite: "Zum Kanal einladen" + without_membership: + one: "%{username} ist diesem Kanal nicht beigetreten." + other: "%{username} und %{others} sind diesem Kanal nicht beigetreten." + group_mentions_disabled: + one: "%{group_name} erlaubt keine Erwähnungen" + other: "%{group_name} und %{others} erlauben keine Erwähnungen" + too_many_members: + one: "%{group_name} hat zu viele Mitglieder. Niemand wurde benachrichtigt" + other: "%{group_name} und %{others} haben zu viele Mitglieder. Niemand wurde benachrichtigt" + warning_multiple: + one: "%{count} anderer" + other: "%{count} andere" groups: + header: + some: "Einige Benutzer werden nicht benachrichtigt" + all: "Niemand wird benachrichtigt" + unreachable: + one: "@%{group} erlaubt keine Erwähnungen" + other: "@%{group} und @%{group_2} erlauben keine Erwähnungen" + unreachable_multiple: "@%{group} und %{count} andere erlauben keine Erwähnungen" + too_many_members: + one: "Die Erwähnung von @%{group} übersteigt das %{notification_limit} von %{limit}" + other: "Die Erwähnung von @%{group} oder @%{group_2} übersteigt das %{notification_limit} von %{limit}" + too_many_members_multiple: "Diese %{count} Gruppen übersteigen das %{notification_limit} von %{limit}" users_limit: one: "%{count} Benutzer" other: "%{count} Benutzer" + notification_limit: "Benachrichtigungslimit" + too_many_mentions: "Diese Nachricht überschreitet das %{notification_limit} von %{limit}" + mentions_limit: + one: "%{count} Erwähnung" + other: "%{count} Erwähnungen" aria_roles: header: "Chat-Kopfzeile" composer: "Chat-Composer" @@ -175,7 +206,6 @@ de: read_only: "Schreibgeschützt" archived_header: "Kanal ist archiviert" archived: "Archiviert" - archive_failed: "Archivieren des Kanals fehlgeschlagen. %{completed}/%{total} Nachrichten wurden im Zielthema archiviert. Drücke auf „Erneut versuchen“, um zu versuchen, die Archivierung abzuschließen." archive_completed: "Archivthema anzeigen" closed_header: "Kanal ist geschlossen" closed: "Geschlossen" @@ -198,6 +228,7 @@ de: associated_topic: Verknüpftes Thema associated_category: Verknüpfte Kategorie title: Titel + name: Name description: Beschreibung channel_info: back_to_all_channels: "Alle Kanäle" @@ -206,10 +237,6 @@ de: about: Über members: Mitglieder settings: Einstellungen - channel_edit_title_modal: - title: Titel bearbeiten - input_placeholder: Titel hinzufügen - description: Gib deinem Kanal einen kurzen aussagekräftigen Titel channel_edit_description_modal: title: Beschreibung bearbeiten input_placeholder: Beschreibung hinzufügen @@ -272,9 +299,9 @@ de: mention: "Nur für Erwähnungen" always: "Für alle Aktivitäten" settings: + channel_wide_mentions_label: "@all- und @here-Erwähnungen zulassen" + channel_wide_mentions_description: "Erlaube Benutzern, alle Mitglieder von #%{channel} mit @all zu benachrichtigen oder nur diejenigen, die gerade aktiv sind, mit @here" auto_join_users_label: "Benutzer automatisch hinzufügen" - auto_join_users_info: "Stündlich prüfen, welche Benutzer in den letzten 3 Monaten aktiv waren, und zu diesem Kanal hinzufügen, falls sie Zugriff auf die Kategorie %{category} haben." - enable_auto_join_users: "Automatisch alle kürzlich aktiven Benutzer hinzufügen" auto_join_users_warning: "Jeder Benutzer, der kein Mitglied dieses Kanals ist und Zugriff auf die Kategorie %{category} hat, wird beitreten. Bist du dir sicher?" desktop_notification_level: "Desktop-Benachrichtigungen" follow: "Beitreten" @@ -294,6 +321,7 @@ de: title: "Chat" direct_messages: title: "Persönlicher Chat" + new: "Persönlichen Chat erstellen" create: "Los" leave: "Diesen persönlichen Chat verlassen" cannot_create: "Du kannst leider keine Direktnachrichten senden." diff --git a/plugins/chat/config/locales/client.el.yml b/plugins/chat/config/locales/client.el.yml index affaf2d6c7..9886d4f572 100644 --- a/plugins/chat/config/locales/client.el.yml +++ b/plugins/chat/config/locales/client.el.yml @@ -49,6 +49,7 @@ el: yesterday: Χτες about_view: title: Τίτλος + name: Όνομα description: Περιγραφή channel_info: back_to_channel: "Πίσω" diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml index 9f912699a8..bda7d953a3 100644 --- a/plugins/chat/config/locales/client.en.yml +++ b/plugins/chat/config/locales/client.en.yml @@ -205,7 +205,8 @@ en: read_only: "Read Only" archived_header: "Channel is archived" archived: "Archived" - archive_failed: "Archive channel failed. %{completed}/%{total} messages have been archived in the destination topic. Press retry to attempt to complete the archive." + archive_failed: "Archive channel failed. %{completed}/%{total} messages have been archived. the destination topic. Press retry to attempt to complete the archive." + archive_failed_no_topic: "Archive channel failed. %{completed}/%{total} messages have been archived, the destination topic was not created. Press retry to attempt to complete the archive." archive_completed: "See the archive topic" closed_header: "Channel is closed" closed: "Closed" @@ -232,6 +233,7 @@ en: associated_topic: Linked topic associated_category: Linked category title: Title + name: Name description: Description channel_info: @@ -242,10 +244,10 @@ en: members: Members settings: Settings - channel_edit_title_modal: - title: Edit title - input_placeholder: Add a title - description: Give a short descriptive title to your channel + channel_edit_name_modal: + title: Edit name + input_placeholder: Add a name + description: Give a short descriptive name to your channel channel_edit_description_modal: title: Edit description @@ -287,6 +289,7 @@ en: create: "Create channel" description: "Description (optional)" name: "Channel name" + slug: "Channel slug (optional)" title: "New channel" type: "Type" types: @@ -322,8 +325,8 @@ en: channel_wide_mentions_label: "Allow @all and @here mentions" channel_wide_mentions_description: "Allow users to notify all members of #%{channel} with @all or only those who are active in the moment with @here" auto_join_users_label: "Automatically add users" - auto_join_users_info: "Check hourly which users have been active in the last 3 months and, if they have access to the %{category} category, add them to this channel." - enable_auto_join_users: "Automatically add all recently active users" + auto_join_users_info: "Check hourly which users have been active in the last 3 months. Add them to this channel if they have access to the %{category} category." + auto_join_users_info_no_category: "Check hourly which users have been active in the last 3 months. Add them to this channel if they have access to the selected category." auto_join_users_warning: "Every user who isn't a member of this channel and has access to the %{category} category will join. Are you sure?" desktop_notification_level: "Desktop notifications" follow: "Join" diff --git a/plugins/chat/config/locales/client.es.yml b/plugins/chat/config/locales/client.es.yml index 72205d9223..66e0159c3a 100644 --- a/plugins/chat/config/locales/client.es.yml +++ b/plugins/chat/config/locales/client.es.yml @@ -109,14 +109,45 @@ es: new_messages: "nuevos mensajes" mention_warning: dismiss: "descartar" + cannot_see: + one: "%{username} no puede acceder a este canal y no fue notificado." + other: "%{username} y %{others} no pueden acceder a este canal y no fueron notificados." invitations_sent: one: "Invitación enviada" other: "Invitaciones enviadas" invite: "Invitar al canal" + without_membership: + one: "%{username} no se ha unido a este canal." + other: "%{username} y %{others} no se han unido a este canal." + group_mentions_disabled: + one: "%{group_name} no permite menciones" + other: "%{group_name} y %{others} no permiten menciones" + too_many_members: + one: "%{group_name} tiene demasiados miembros. No se notificó a nadie" + other: "%{group_name} y %{others} tienen demasiados miembros. No se notificó a nadie" + warning_multiple: + one: "%{count} otro" + other: "%{count} otros" groups: + header: + some: "Algunos usuarios no serán notificados" + all: "No se notificará a nadie" + unreachable: + one: "@%{group} no permite menciones" + other: "@%{group} y @%{group_2} no permiten menciones" + unreachable_multiple: "@%{group} y otros %{count} no permiten menciones" + too_many_members: + one: "Mencionar a @%{grupo} supera el %{notification_limit} de %{limit}" + other: "Mencionar tanto a @%{grupo} como a @%{group_2} supera el %{notification_limit} de %{limit}" + too_many_members_multiple: "Estos %{count} grupos superan el %{notification_limit} de %{limit}" users_limit: one: "%{count} usuario" other: "%{count} usuarios" + notification_limit: "límite de notificaciones" + too_many_mentions: "Este mensaje supera el %{notification_limit} de %{limit}" + mentions_limit: + one: "%{count} mención" + other: "%{count} menciones" aria_roles: header: "Encabezado del chat" composer: "Compositor del chat" @@ -175,7 +206,6 @@ es: read_only: "Solo lectura" archived_header: "El canal está archivado" archived: "Archivado" - archive_failed: "La archivación del canal ha fallado. Se han archivado %{completed}/%{total} mensajes en el tema de destino. Pulsa reintentar para intentar completar el archivo." archive_completed: "Ver el tema del archivo" closed_header: "El canal está cerrado" closed: "Cerrado" @@ -198,6 +228,7 @@ es: associated_topic: Tema vinculado associated_category: Categoría vinculada title: Título + name: Nombre description: Descripción channel_info: back_to_all_channels: "Todos los canales" @@ -206,10 +237,6 @@ es: about: Acerca de members: Miembros settings: Ajustes - channel_edit_title_modal: - title: Editar título - input_placeholder: Añade un título - description: Pon un título corto y descriptivo a tu canal channel_edit_description_modal: title: Editar descripción input_placeholder: Añade una descripción @@ -272,9 +299,9 @@ es: mention: "Solo para menciones" always: "Para toda actividad" settings: + channel_wide_mentions_label: "Permitir menciones @all y @here" + channel_wide_mentions_description: "Permite a los usuarios notificar a todos los miembros de #%{channel} con @all o solo a los que estén activos en ese momento con @here" auto_join_users_label: "Añadir usuarios automáticamente" - auto_join_users_info: "Consulta cada hora qué usuarios han estado activos en los últimos 3 meses y, si tienen acceso a la categoría %{category} , añádelos a este canal." - enable_auto_join_users: "Añade automáticamente todos los usuarios activos recientemente" auto_join_users_warning: "Todos los usuarios que no sean miembros de este canal y tengan acceso a la categoría %{category} se unirán. ¿Estás seguro/a?" desktop_notification_level: "Notificaciones de escritorio" follow: "Unirse" @@ -294,6 +321,7 @@ es: title: "Chat" direct_messages: title: "Chat personal" + new: "Crear un chat personal" create: "Ir" leave: "Abandonar este chat personal" cannot_create: "Lo sentimos, no puedes enviar mensajes directos." diff --git a/plugins/chat/config/locales/client.et.yml b/plugins/chat/config/locales/client.et.yml index a47d4999af..08ba7f3f3f 100644 --- a/plugins/chat/config/locales/client.et.yml +++ b/plugins/chat/config/locales/client.et.yml @@ -48,6 +48,7 @@ et: yesterday: Eile about_view: title: Pealkiri + name: Nimi description: Kirjeldus channel_info: back_to_channel: "Tagasi" diff --git a/plugins/chat/config/locales/client.fa_IR.yml b/plugins/chat/config/locales/client.fa_IR.yml index 46e293247d..65f3da3891 100644 --- a/plugins/chat/config/locales/client.fa_IR.yml +++ b/plugins/chat/config/locales/client.fa_IR.yml @@ -158,6 +158,7 @@ fa_IR: associated_topic: موضوع مرتبط associated_category: دسته‌بندی مرتبط title: عنوان + name: نام description: توضیح channel_info: back_to_all_channels: "همه کانال‌ها" @@ -166,10 +167,6 @@ fa_IR: about: درباره members: اعضاء settings: تنظیمات - channel_edit_title_modal: - title: ویرایش عنوان - input_placeholder: افزودن عنوان - description: یک عنوان توصیفی کوتاه به کانال خود بدهید channel_edit_description_modal: title: ویرایش توضیحات input_placeholder: افزودن توضیحات @@ -220,8 +217,6 @@ fa_IR: channel_wide_mentions_label: "اجازه داده به استفاده از اشاره @all و @here" channel_wide_mentions_description: "به کاربران اجازه دهید به همه اعضای #%{channel} با @all یا فقط افرادی که در حال حاضر فعال هستند با @here، آگاه‌سازی کنند." auto_join_users_label: "افزودن خودکار کاربران" - auto_join_users_info: "بررسی کنید که کاربران در ۳ ماه گذشته فعال بوده‌اند و در دسته‌بندی %{category} دسترسی داشته باشند، آنها را به این کانال اضافه کنید." - enable_auto_join_users: "به طور خودکار همه کاربران فعال اخیر را اضافه کنید" desktop_notification_level: "آگاه‌سازی‌های دسکتاپ" follow: "عضو شدن" followed: "عضو شده" diff --git a/plugins/chat/config/locales/client.fi.yml b/plugins/chat/config/locales/client.fi.yml index 7a03a90e74..7cfc08d6f4 100644 --- a/plugins/chat/config/locales/client.fi.yml +++ b/plugins/chat/config/locales/client.fi.yml @@ -109,14 +109,45 @@ fi: new_messages: "uusia viestejä" mention_warning: dismiss: "hylkää" + cannot_see: + one: "%{username} ei voi käyttää tätä kanavaa, eikä hänelle ilmoitettu." + other: "%{username} ja %{others} eivät voi käyttää tätä kanavaa, eikä heille ilmoitettu." invitations_sent: one: "Kutsu lähetetty" other: "Kutsut lähetettiin" invite: "Kutsu kanavalle" + without_membership: + one: "%{username} ei ole liittynyt tälle kanavalle." + other: "%{username} ja %{others} eivät ole liittyneet tälle kanavalle." + group_mentions_disabled: + one: "%{group_name} ei salli mainintoja" + other: "%{group_name} ja %{others} eivät salli mainintoja" + too_many_members: + one: "Ryhmässä %{group_name} on liian monta jäsentä. Kukaan ei saanut ilmoitusta." + other: "Ryhmä %{group_name} ja %{others} sisältävät liikaa jäseniä. Kukaan ei saanut ilmoitusta." + warning_multiple: + one: "%{count} muu" + other: "%{count} muuta" groups: + header: + some: "Osa käyttäjistä ei saa ilmoitusta" + all: "Kukaan ei saa ilmoitusta" + unreachable: + one: "@%{group} ei salli mainintoja" + other: "@%{group} ja @%{group_2} eivät salli mainintoja" + unreachable_multiple: "@%{group} ja %{count} muuta eivät salli mainintoja" + too_many_members: + one: "Ryhmän @%{group} mainitseminen ylittää %{notification_limit}: %{limit}" + other: "Ryhmien @%{group} ja @%{group_2} mainitseminen ylittää %{notification_limit}: %{limit}" + too_many_members_multiple: "Nämä %{count} ryhmää ylittää %{notification_limit}: %{limit}" users_limit: one: "%{count} käyttäjä" other: "%{count} käyttäjää" + notification_limit: "ilmoitusrajan" + too_many_mentions: "Tämä viesti ylittää %{notification_limit}: %{limit}" + mentions_limit: + one: "%{count} maininta" + other: "%{count} mainintaa" aria_roles: header: "Chatin ylätunniste" composer: "Chatin tekstieditori" @@ -175,7 +206,6 @@ fi: read_only: "Vain luku" archived_header: "Kanava on arkistoitu" archived: "Arkistoitu" - archive_failed: "Kanavan arkistointi epäonnistui. %{completed}/%{total} viestiä on arkistoitu kohdeketjuun. Yritä arkistoinnin viimeistelyä uudelleen painamalla Yritä uudelleen -painiketta." archive_completed: "Katso arkistoketju" closed_header: "Kanava on suljettu" closed: "Suljettu" @@ -198,6 +228,7 @@ fi: associated_topic: Linkitetty ketju associated_category: Linkitetty alue title: Otsikko + name: Nimi description: Kuvaus channel_info: back_to_all_channels: "Kaikki kanavat" @@ -206,10 +237,6 @@ fi: about: Tietoja members: Jäsenet settings: Asetukset - channel_edit_title_modal: - title: Muokkaa otsikkoa - input_placeholder: Lisää otsikko - description: Anna kanavallesi lyhyt kuvaava otsikko channel_edit_description_modal: title: Muokkaa kuvausta input_placeholder: Lisää kuvaus @@ -272,9 +299,9 @@ fi: mention: "Vain maininnoissa" always: "Kaikessa toiminnassa" settings: + channel_wide_mentions_label: "Salli @all- ja @here-maininnat" + channel_wide_mentions_description: "Salli käyttäjien ilmoittaa kaikille kanavan #%{channel} käyttäjille @all-maininnalla tai niille, jotka ovat aktiivisia kyseisellä hetkellä, @here-maininnalla" auto_join_users_label: "Lisää käyttäjiä automaattisesti" - auto_join_users_info: "Tarkista tunnin välein, ketkä käyttäjistä ovat olleet aktiivisia viimeisten kolmen kuukauden aikana, ja jos heillä on pääsy alueelle %{category}, lisää heidät tälle kanavalle." - enable_auto_join_users: "Lisää automaattisesti kaikki hiljattain aktiiviset käyttäjät" auto_join_users_warning: "Jokainen käyttäjä, joka ei ole tämän kanavan jäsen ja jolla on pääsy alueelle %{category}, liittyy. Oletko varma?" desktop_notification_level: "Työpöytäilmoitukset" follow: "Liity" @@ -294,6 +321,7 @@ fi: title: "Chat" direct_messages: title: "Henkilökohtainen chat" + new: "Luo yksityis-chat" create: "Siirry" leave: "Poistu tästä henkilökohtaisesta chatista" cannot_create: "Valitettavasti et voi lähettää yksityisviestejä." diff --git a/plugins/chat/config/locales/client.fr.yml b/plugins/chat/config/locales/client.fr.yml index aeea6c8817..89542e513a 100644 --- a/plugins/chat/config/locales/client.fr.yml +++ b/plugins/chat/config/locales/client.fr.yml @@ -109,14 +109,45 @@ fr: new_messages: "nouveaux messages" mention_warning: dismiss: "rejeter" + cannot_see: + one: "%{username} ne peut pas accéder à ce canal et n'a pas été averti(e)." + other: "%{username} et %{others} ne peuvent pas accéder à ce canal et n'ont pas été avertis." invitations_sent: one: "Invitation envoyée" other: "Invitations envoyées" invite: "Inviter à rejoindre le canal" + without_membership: + one: "%{username} n'a pas rejoint ce canal." + other: "%{username} et %{others} n'ont pas rejoint ce canal." + group_mentions_disabled: + one: "%{group_name} n'autorise pas les mentions" + other: "%{group_name} et %{others} n'autorisent pas les mentions" + too_many_members: + one: "%{group_name} a trop de membres. Personne n'a reçu de notification" + other: "%{group_name} et %{others} ont trop de membres. Personne n'a reçu de notification" + warning_multiple: + one: "%{count} autre" + other: "%{count} autres" groups: + header: + some: "Certains utilisateurs ne recevront pas de notification" + all: "Personne ne recevra de notification" + unreachable: + one: "@%{group} n'autorise pas les mentions" + other: "@%{group} et @%{group_2} n'autorisent pas les mentions" + unreachable_multiple: "@%{group} et %{count} autres groupes n'autorisent pas les mentions" + too_many_members: + one: "Mentionner @%{group} dépasse la %{notification_limit} de %{limit}" + other: "Mentionner @%{group} et @%{group_2} dépasse la %{notification_limit} de %{limit}" + too_many_members_multiple: "Ces %{count} groupes dépassent la %{notification_limit} de %{limit}" users_limit: one: "%{count} utilisateur" other: "%{count} utilisateurs" + notification_limit: "limite de notification" + too_many_mentions: "Ce message dépasse la %{notification_limit} de %{limit}" + mentions_limit: + one: "%{count} mention" + other: "%{count} mentions" aria_roles: header: "En-tête de discussion" composer: "Compositeur de discussion" @@ -176,6 +207,7 @@ fr: archived_header: "Le canal est archivé" archived: "Archivé" archive_failed: "Échec de l'archivage du canal. %{completed}/%{total} messages ont été archivés dans le sujet de destination. Appuyez sur Réessayer pour tenter de terminer l'archivage." + archive_failed_no_topic: "Échec de l'archivage du canal. %{completed}/%{total} messages ont été archivés, le sujet de destination n'a pas été créé. Appuyez sur Réessayer pour tenter de terminer l'archivage." archive_completed: "Voir le sujet de l'archive" closed_header: "Le canal est fermé" closed: "Fermé" @@ -198,6 +230,7 @@ fr: associated_topic: Sujet lié associated_category: Catégorie liée title: Titre + name: Nom description: Description channel_info: back_to_all_channels: "Tous les canaux" @@ -206,10 +239,6 @@ fr: about: À propos members: Membres settings: Paramètres - channel_edit_title_modal: - title: Modifier le titre - input_placeholder: Ajouter un titre - description: Ajoutez un court titre descriptif à votre canal channel_edit_description_modal: title: Modifier la description input_placeholder: Ajouter une description @@ -272,9 +301,9 @@ fr: mention: "Seulement pour les mentions" always: "Pour toutes les activités" settings: + channel_wide_mentions_label: "Autoriser les mentions @all et @here" + channel_wide_mentions_description: "Autoriser les utilisateurs à notifier tous les membres de #%{channel} avec @all ou uniquement ceux qui sont actifs en ce moment avec @here" auto_join_users_label: "Ajouter automatiquement des utilisateurs" - auto_join_users_info: "Vérifier toutes les heures quels utilisateurs ont été actifs au cours des 3 derniers mois et, s'ils ont accès à la catégorie %{category}, les ajouter à ce canal." - enable_auto_join_users: "Ajouter automatiquement tous les utilisateurs récemment actifs" auto_join_users_warning: "Chaque utilisateur qui n'est pas membre de ce canal et qui a accès à la catégorie %{category} le rejoindra. Voulez-vous continuer ?" desktop_notification_level: "Notifications sur le bureau" follow: "Rejoindre" @@ -294,6 +323,7 @@ fr: title: "Discussion" direct_messages: title: "Discussion privée" + new: "Créer une conversation personnelle" create: "Valider" leave: "Quitter cette discussion privée" cannot_create: "Nous sommes désolés, vous ne pouvez pas envoyer de messages privés." diff --git a/plugins/chat/config/locales/client.gl.yml b/plugins/chat/config/locales/client.gl.yml index 2876f6445d..e0c19ffc21 100644 --- a/plugins/chat/config/locales/client.gl.yml +++ b/plugins/chat/config/locales/client.gl.yml @@ -49,6 +49,7 @@ gl: yesterday: Onte about_view: title: Título + name: Nome description: Descrición channel_info: back_to_channel: "Volver" diff --git a/plugins/chat/config/locales/client.he.yml b/plugins/chat/config/locales/client.he.yml index 5938459677..600738e1ed 100644 --- a/plugins/chat/config/locales/client.he.yml +++ b/plugins/chat/config/locales/client.he.yml @@ -215,7 +215,6 @@ he: read_only: "לקריאה בלבד" archived_header: "הערוץ בארכיון" archived: "בארכיון" - archive_failed: "העברת הערוץ לארכיון נכשלה. %{completed}/%{total} הודעות הועברו לארכיון תחת נושא היעד. נא לנסות להשלים את ההעברה לארכיון פעם נוספת." archive_completed: "אפשר לעיין בארכיון הנושא" closed_header: "הערוץ סגור" closed: "סגורה" @@ -238,6 +237,7 @@ he: associated_topic: נושא מקושר associated_category: קטגוריה מקושרת title: כותרת + name: שם description: תיאור channel_info: back_to_all_channels: "כל הערוצים" @@ -246,10 +246,6 @@ he: about: אודות members: חברים settings: הגדרות - channel_edit_title_modal: - title: עריכת כותרת - input_placeholder: הוספת כותרת - description: נא לספק כותרת ברורה לערוץ שלך channel_edit_description_modal: title: עריכת תיאור input_placeholder: הוספת תיאור @@ -320,8 +316,6 @@ he: settings: channel_wide_mentions_label: "לאפשר אזכורים של ‎@all (כולם) ו־‎@here (כאן)" auto_join_users_label: "להוסיף משתמשים אוטומטית" - auto_join_users_info: "לבדוק כל שעה אילו משתמשים היו פעילים ב־3 החודשים האחרונים ואם יש להם גישה לקטגוריה %{category}, להוסיף אותם לערוץ הזה." - enable_auto_join_users: "להוסיף אוטומטית את כל המשתמשים שהיו פעילים לאחרונה" auto_join_users_warning: "כל משתמש שאינו חבר בערוץ הזה ויש לו גישה לקטגוריה %{category} יצטרף. זה בסדר?" desktop_notification_level: "התראות שולחן עבודה" follow: "הצטרף" diff --git a/plugins/chat/config/locales/client.hr.yml b/plugins/chat/config/locales/client.hr.yml index 7ae25a592b..2bd64eca17 100644 --- a/plugins/chat/config/locales/client.hr.yml +++ b/plugins/chat/config/locales/client.hr.yml @@ -111,6 +111,8 @@ hr: title_capitalized: "Čet" exit: "natrag" channel_status: + archive_failed: "Arhiviranje kanala nije uspjelo. %{completed}/%{total} poruke su arhivirane. tema odredišta. Pritisnite ponovno za pokušaj dovršetka arhiviranja." + archive_failed_no_topic: "Arhiviranje kanala nije uspjelo. %{completed}/%{total} poruka je arhivirano, odredišna tema nije kreirana. Pritisnite ponovno za pokušaj dovršetka arhiviranja." closed: "Zatvoreno" open: "Otvori" browse: @@ -123,6 +125,7 @@ hr: yesterday: Jučer about_view: title: Naslov + name: Ime description: Opis channel_info: back_to_channel: "Natrag" diff --git a/plugins/chat/config/locales/client.hu.yml b/plugins/chat/config/locales/client.hu.yml index bde8e8a596..01eb56ac58 100644 --- a/plugins/chat/config/locales/client.hu.yml +++ b/plugins/chat/config/locales/client.hu.yml @@ -168,7 +168,6 @@ hu: read_only: "Csak olvasható" archived_header: "A csatorna archiválva van" archived: "Archivált" - archive_failed: "A csatorna archiválása nem sikerült.%{completed}/%{total} üzenet archiválva lett a céltémában . Nyomja meg az Újra gombot az archiválás befejezéséhez." archive_completed: "Lásd az archív témát" closed_header: "A csatorna zárolt" closed: "Zárt" @@ -191,6 +190,7 @@ hu: associated_topic: Kapcsolódó téma associated_category: Kapcsolódó kategória title: Cím + name: Név description: Leírás channel_info: back_to_all_channels: "Összes csatorna" @@ -199,10 +199,6 @@ hu: about: Névjegy members: Tagok settings: Beállítások - channel_edit_title_modal: - title: Cím szerkesztése - input_placeholder: Cím hozzáadása - description: Adjon egy rövid, leíró címet a csatornájának channel_edit_description_modal: title: Leírás szerkesztése input_placeholder: Leírás hozzáadása @@ -262,7 +258,6 @@ hu: settings: channel_wide_mentions_label: "Engedélyezve az @all és @here említések" auto_join_users_label: "Felhasználók automatikus hozzáadása" - enable_auto_join_users: "Az összes nemrégiben aktív felhasználó automatikus hozzáadása" desktop_notification_level: "Asztali értesítések" follow: "Belépés" followed: "Csatlakozott" diff --git a/plugins/chat/config/locales/client.hy.yml b/plugins/chat/config/locales/client.hy.yml index bbf4043102..7e631debd6 100644 --- a/plugins/chat/config/locales/client.hy.yml +++ b/plugins/chat/config/locales/client.hy.yml @@ -49,6 +49,7 @@ hy: yesterday: Երեկվա about_view: title: Վերնագիր + name: Անուն description: Նկարագրությունը channel_info: back_to_channel: "Ետ" diff --git a/plugins/chat/config/locales/client.id.yml b/plugins/chat/config/locales/client.id.yml index b160a2ea18..08c1befefa 100644 --- a/plugins/chat/config/locales/client.id.yml +++ b/plugins/chat/config/locales/client.id.yml @@ -40,6 +40,7 @@ id: filter_closed: Tertutup about_view: title: Judul + name: Nama channel_info: tabs: about: Tentang diff --git a/plugins/chat/config/locales/client.it.yml b/plugins/chat/config/locales/client.it.yml index b23f8b1474..574c41f43c 100644 --- a/plugins/chat/config/locales/client.it.yml +++ b/plugins/chat/config/locales/client.it.yml @@ -109,14 +109,45 @@ it: new_messages: "nuovi messaggi" mention_warning: dismiss: "ignora" + cannot_see: + one: "%{username} non può accedere a questo canale e non è stato avvisato." + other: "%{username} e %{others} non possono accedere a questo canale e non sono stati avvisati." invitations_sent: one: "Invito inviato" other: "Inviti inviati" invite: "Invita al canale" + without_membership: + one: "%{username} non ha partecipato a questo canale." + other: "%{username} e %{others} non hanno partecipato a questo canale." + group_mentions_disabled: + one: "%{group_name} non consente menzioni" + other: "%{group_name} e %{others} non consentono menzioni" + too_many_members: + one: "%{group_name} ha troppi membri. Nessuno è stato avvisato" + other: "%{group_name} e %{others} hanno troppi membri. Nessuno è stato avvisato" + warning_multiple: + one: "%{count} altro" + other: "%{count} altri" groups: + header: + some: "Alcuni utenti non riceveranno notifiche" + all: "Nessuno verrà avvisato" + unreachable: + one: "@%{group} non consente menzioni" + other: "@%{group} e @%{group_2} non consentono menzioni" + unreachable_multiple: "@%{group} e altri %{count} non consentono menzioni" + too_many_members: + one: "La menzione di @%{group} supera il %{notification_limit} di %{limit}" + other: "Menzionando sia @%{group} che @%{group_2} si supera il %{notification_limit} di %{limit}" + too_many_members_multiple: "Questi %{count} gruppi superano il %{notification_limit} di %{limit}" users_limit: one: "%{count} utente" other: "%{count} utenti" + notification_limit: "limite di notifica" + too_many_mentions: "Questo messaggio supera il %{notification_limit} di %{limit}" + mentions_limit: + one: "%{count} menzione" + other: "%{count} menzioni" aria_roles: header: "Intestazione della chat" composer: "Compositore di chat" @@ -175,7 +206,6 @@ it: read_only: "Sola lettura" archived_header: "Il canale è archiviato" archived: "Archiviati" - archive_failed: "Archiviazione del canale non riuscita. %{completed}/%{total} messaggi sono stati archiviati nell'argomento di destinazione. Premi Riprova per tentare di completare l'archiviazione." archive_completed: "Vedi l'argomento di archiviazione" closed_header: "Il canale è chiuso" closed: "Chiusi" @@ -198,6 +228,7 @@ it: associated_topic: Argomento collegato associated_category: Categoria collegata title: Titolo + name: Nome description: Descrizione channel_info: back_to_all_channels: "Tutti i canali" @@ -206,10 +237,6 @@ it: about: Informazioni members: Membri settings: Impostazioni - channel_edit_title_modal: - title: Modifica titolo - input_placeholder: Aggiungi un titolo - description: Assegna un breve titolo descrittivo al tuo canale channel_edit_description_modal: title: Modifica descrizione input_placeholder: Aggiungi una descrizione @@ -272,7 +299,9 @@ it: mention: "Solo per le menzioni" always: "Per tutte le attività" settings: - enable_auto_join_users: "Aggiungi automaticamente tutti gli utenti attivi di recente" + channel_wide_mentions_label: "Consenti le menzioni @all e @here" + channel_wide_mentions_description: "Consenti agli utenti di avvisare tutti i membri di #%{channel} con @all o solo quelli che sono attivi al momento con @here" + auto_join_users_label: "Aggiunta automatica utenti" auto_join_users_warning: "Tutti gli utenti che non sono membri di questo canale e hanno accesso alla categoria %{category} parteciperanno. Vuoi procedere?" desktop_notification_level: "Notifiche sul desktop" follow: "Partecipa" @@ -286,11 +315,13 @@ it: save: "Salva" saved: "Salvato" unfollow: "Esci" - admin_title: "Amministrazione" + admin_title: "Amministratore" + retention_info: "La cronologia delle chat verrà salvata per %{days} giorni." admin: title: "Chat" direct_messages: title: "Chat personale" + new: "Crea una chat personale" create: "Vai" leave: "Lascia questa chat personale" cannot_create: "Spiacenti, non puoi inviare messaggi diretti." diff --git a/plugins/chat/config/locales/client.ja.yml b/plugins/chat/config/locales/client.ja.yml index c7cc4e40ca..87e16b34ca 100644 --- a/plugins/chat/config/locales/client.ja.yml +++ b/plugins/chat/config/locales/client.ja.yml @@ -115,7 +115,7 @@ ja: other: "他 %{count} 件" groups: users_limit: - other: "ユーザー: %{count} 人" + other: "%{count} 人のユーザー" aria_roles: header: "チャットヘッダー" composer: "チャット作成ツール" @@ -173,7 +173,6 @@ ja: read_only: "読み取り専用" archived_header: "チャンネルはアーカイブされています" archived: "アーカイブ済み" - archive_failed: "チャンネルのアーカイブに失敗しました。%{completed} / 全 %{total} 件のメッセージがアーカイブ先のトピックにアーカイブされました。再試行を押して、アーカイブの完了を試してください。" archive_completed: "アーカイブ済みのトピックを見る" closed_header: "チャンネルは閉鎖されています" closed: "閉鎖" @@ -196,6 +195,7 @@ ja: associated_topic: リンクされたトピック associated_category: リンクされたカテゴリ title: タイトル + name: 名前 description: 説明 channel_info: back_to_all_channels: "すべてのチャンネル" @@ -204,10 +204,6 @@ ja: about: 紹介 members: メンバー settings: 設定 - channel_edit_title_modal: - title: タイトルを編集する - input_placeholder: タイトルを追加する - description: チャンネルを説明する短いタイトルを付けます channel_edit_description_modal: title: 説明を編集する input_placeholder: 説明を追加する @@ -268,8 +264,6 @@ ja: always: "すべてのアクティビティ" settings: auto_join_users_label: "自動的にユーザーを追加する" - auto_join_users_info: "過去 3 か月にアクティブだったユーザーを 1 時間ごとに確認し、それらのユーザーが %{category} カテゴリにアクセスできる場合はこのチャンネルに追加します。" - enable_auto_join_users: "最近アクティブなユーザーをすべて自動的に追加する" auto_join_users_warning: "このチャンネルのメンバーでなく、%{category} カテゴリにアクセスできるすべてのユーザーが参加します。よろしいですか?" desktop_notification_level: "デスクトップ通知" follow: "参加" diff --git a/plugins/chat/config/locales/client.ko.yml b/plugins/chat/config/locales/client.ko.yml index 5ebf3e3a8a..9ebb61d50e 100644 --- a/plugins/chat/config/locales/client.ko.yml +++ b/plugins/chat/config/locales/client.ko.yml @@ -129,6 +129,7 @@ ko: filter_placeholder: 회원 찾기 about_view: title: 제목 + name: 그룹명 description: 내용 channel_info: back_to_all_channels: "모든 채널" @@ -137,9 +138,6 @@ ko: about: 소개 members: 회원 settings: 설정 - channel_edit_title_modal: - title: 제목 수정 - input_placeholder: 제목 추가 channel_edit_description_modal: title: 설명 수정 input_placeholder: 설명 추가 diff --git a/plugins/chat/config/locales/client.lt.yml b/plugins/chat/config/locales/client.lt.yml index dca6422a07..4543446058 100644 --- a/plugins/chat/config/locales/client.lt.yml +++ b/plugins/chat/config/locales/client.lt.yml @@ -52,6 +52,7 @@ lt: yesterday: Vakar about_view: title: Antraštė + name: Vardas description: Aprašymas channel_info: back_to_channel: "Atgal" diff --git a/plugins/chat/config/locales/client.lv.yml b/plugins/chat/config/locales/client.lv.yml index 8ff08c6a4c..4e11375693 100644 --- a/plugins/chat/config/locales/client.lv.yml +++ b/plugins/chat/config/locales/client.lv.yml @@ -51,6 +51,7 @@ lv: yesterday: Vakar about_view: title: Virsraksts + name: Vārds description: Apraksts channel_info: back_to_channel: "Atpakaļ" diff --git a/plugins/chat/config/locales/client.nb_NO.yml b/plugins/chat/config/locales/client.nb_NO.yml index 1c54185fe6..5f413a287e 100644 --- a/plugins/chat/config/locales/client.nb_NO.yml +++ b/plugins/chat/config/locales/client.nb_NO.yml @@ -50,6 +50,7 @@ nb_NO: yesterday: I går about_view: title: Tittel + name: Navn description: Beskrivelse channel_info: back_to_channel: "Forrige" diff --git a/plugins/chat/config/locales/client.nl.yml b/plugins/chat/config/locales/client.nl.yml index 3ee38c4f15..e2251cb237 100644 --- a/plugins/chat/config/locales/client.nl.yml +++ b/plugins/chat/config/locales/client.nl.yml @@ -175,7 +175,6 @@ nl: read_only: "Alleen-lezen" archived_header: "Kanaal is gearchiveerd" archived: "Gearchiveerd" - archive_failed: "Archiveren van kanaal mislukt. %{completed}/%{total} berichten zijn gearchiveerd in het bestemmingstopic. Druk op Opnieuw proberen om te proberen de archivering te voltooien." archive_completed: "Zie het archieftopic" closed_header: "Kanaal is gesloten" closed: "Gesloten" @@ -198,6 +197,7 @@ nl: associated_topic: Gelinkt topic associated_category: Gelinkte categorie title: Titel + name: Naam description: Beschrijving channel_info: back_to_all_channels: "Alle kanalen" @@ -206,10 +206,6 @@ nl: about: Over members: Leden settings: Instellingen - channel_edit_title_modal: - title: Titel bewerken - input_placeholder: Voeg een titel toe - description: Geef een korte beschrijvende titel aan je kanaal channel_edit_description_modal: title: Beschrijving bewerken input_placeholder: Voeg een beschrijving toe @@ -273,8 +269,6 @@ nl: always: "Voor alle activiteit" settings: auto_join_users_label: "Gebruikers automatisch toevoegen" - auto_join_users_info: "Controleer elk uur welke gebruikers de afgelopen 3 maanden actief zijn geweest en voeg ze toe aan dit kanaal als ze toegang hebben tot de categorie %{category}." - enable_auto_join_users: "Automatisch alle recent actieve gebruikers toevoegen" auto_join_users_warning: "Elke gebruiker die geen lid is van dit kanaal en toegang heeft tot de categorie %{category} wordt lid. Weet je het zeker?" desktop_notification_level: "Bureaubladmeldingen" follow: "Deelnemen" diff --git a/plugins/chat/config/locales/client.pl_PL.yml b/plugins/chat/config/locales/client.pl_PL.yml index 84fb41b375..4c8306f14d 100644 --- a/plugins/chat/config/locales/client.pl_PL.yml +++ b/plugins/chat/config/locales/client.pl_PL.yml @@ -181,7 +181,6 @@ pl_PL: read_only: "Tylko do odczytu" archived_header: "Kanał został zarchiwizowany" archived: "Zarchiwizowany" - archive_failed: "Archiwizacja kanału nie powiodła się. %{completed}/%{total} wiadomości zostało zarchiwizowanych w temacie docelowym. Naciśnij przycisk ponów, aby spróbować zakończyć archiwizację." closed_header: "Kanał jest zamknięty" closed: "Zamknięta" open_header: "Kanał jest otwarty" @@ -203,6 +202,7 @@ pl_PL: associated_topic: Powiązany temat associated_category: Połączona kategoria title: Tytuł + name: Nazwa description: Opis channel_info: back_to_all_channels: "Wszystkie kanały" @@ -211,10 +211,6 @@ pl_PL: about: O stronie members: Członkowie settings: Ustawienia - channel_edit_title_modal: - title: Edytuj tytuł - input_placeholder: Dodaj tytuł - description: Nadaj swojemu kanałowi krótki opisowy tytuł channel_edit_description_modal: title: Edytuj opis input_placeholder: Dodaj opis @@ -269,7 +265,6 @@ pl_PL: settings: channel_wide_mentions_description: "Zezwalaj użytkownikom na powiadamianie wszystkich członków #%{channel} za pomocą @all lub tylko tych, którzy są aktywni w danym momencie za pomocą @here" auto_join_users_label: "Automatycznie dodawaj użytkowników" - enable_auto_join_users: "Automatycznie dodawaj wszystkich ostatnio aktywnych użytkowników" desktop_notification_level: "Powiadomienia na pulpicie" follow: "Dołącz" followed: "Dołączył" diff --git a/plugins/chat/config/locales/client.pt.yml b/plugins/chat/config/locales/client.pt.yml index c216d02ae2..2f8736a5bd 100644 --- a/plugins/chat/config/locales/client.pt.yml +++ b/plugins/chat/config/locales/client.pt.yml @@ -52,6 +52,7 @@ pt: yesterday: Ontem about_view: title: Título + name: Nome description: Descrição channel_info: back_to_channel: "Retroceder" diff --git a/plugins/chat/config/locales/client.pt_BR.yml b/plugins/chat/config/locales/client.pt_BR.yml index 04b1c05651..51a5851beb 100644 --- a/plugins/chat/config/locales/client.pt_BR.yml +++ b/plugins/chat/config/locales/client.pt_BR.yml @@ -109,14 +109,45 @@ pt_BR: new_messages: "novas mensagens" mention_warning: dismiss: "ignorar" + cannot_see: + one: "%{username} não pode acessar este canal e não recebeu notificação." + other: "%{username} e %{others} não podem acessar este canal e não receberam notificação." invitations_sent: one: "Convite enviado" other: "Convites enviados" invite: "convidar para canal" + without_membership: + one: "%{username} não entrou neste canal." + other: "%{username} e %{others} não se entraram neste canal." + group_mentions_disabled: + one: "%{group_name} não permite menções" + other: "%{group_name} e %{others} não permitem menções" + too_many_members: + one: "%{group_name} tem membros demais. Ninguém foi notificado" + other: "%{group_name} e %{others} têm membros demais. Ninguém foi notificado" + warning_multiple: + one: "mais %{count}" + other: "mais %{count}" groups: + header: + some: "Alguns usuários não serão notificados" + all: "Ninguém será notificado" + unreachable: + one: "@%{group} não permite menções" + other: "@%{group} e @%{group_2} não permitem menções" + unreachable_multiple: "@%{group} e outros %{count} não permitem menções" + too_many_members: + one: "Mencionar @%{group} excede o %{notification_limit} de %{limit}" + other: "Mencionar ambos @%{group} ou @%{group_2} excede o %{notification_limit} de %{limit}" + too_many_members_multiple: "Esses %{count} grupos excedem o %{notification_limit} de %{limit}" users_limit: one: "%{count} usuário" other: "%{count} usuários" + notification_limit: "limite de notificações" + too_many_mentions: "Esta mensagem excede o %{notification_limit} de %{limit}" + mentions_limit: + one: "%{count} menção" + other: "%{count} menções" aria_roles: header: "Cabeçalho do chat" composer: "Compositor de chat" @@ -175,7 +206,8 @@ pt_BR: read_only: "Somente leitura" archived_header: "O canal está arquivado" archived: "Arquivados" - archive_failed: "Falha no canal de arquivamento. %{completed}/%{total} mensagens foram arquivadas no tópico de destino. Pressione repetir para tentar concluir o arquivo." + archive_failed: "Falha no canal de arquivamento. %{completed}/%{total} mensagens foram arquivadas. tópico de destino. Pressione repetir para tentar concluir o arquivo." + archive_failed_no_topic: "Falha no canal de arquivamento. %{completed}/%{total} mensagens foram arquivadas. O tópico de destino não foi criado. Pressione repetir para tentar concluir o arquivo." archive_completed: "Veja o tópico de arquivo" closed_header: "Canal fechado" closed: "Fechados" @@ -198,6 +230,7 @@ pt_BR: associated_topic: Tópico vinculado associated_category: Categoria vinculada title: Título + name: Nome description: Descrição channel_info: back_to_all_channels: "Todos os canais" @@ -206,10 +239,6 @@ pt_BR: about: Sobre members: Membros settings: Definições - channel_edit_title_modal: - title: Editar título - input_placeholder: Adicione um título - description: Dê um breve título descritivo ao seu canal channel_edit_description_modal: title: Editar descrição input_placeholder: Adicione uma descrição @@ -272,9 +301,9 @@ pt_BR: mention: "Apenas para menções" always: "Para todas as atividades" settings: + channel_wide_mentions_label: "Permitir menções @all e @here" + channel_wide_mentions_description: "Permitir que os usuários notifiquem todos os membros de #%{channel} com @all ou apenas aqueles que estão ativos no momento com @here" auto_join_users_label: "Adicionar usuários(as) automaticamente" - auto_join_users_info: "Verifique a cada hora quais usuários(as) estiveram ativos(as) nos últimos três meses e, se tiverem acesso à categria %{category}, adicione a este canal." - enable_auto_join_users: "Adicionar automaticamente todos os usuários ativos recentemente" auto_join_users_warning: "Todos(as) os(as) usuários(as) que não são membros deste canal e têm acesso à categoria %{category} participarão. Tem certeza?" desktop_notification_level: "Notificações do desktop" follow: "Participar" @@ -294,6 +323,7 @@ pt_BR: title: "Chat" direct_messages: title: "Chat pessoal" + new: "Criar um chat pessoal" create: "Ir" leave: "Sair deste chat pessoal" cannot_create: "Desculpe, você não pode enviar mensagens diretas." diff --git a/plugins/chat/config/locales/client.ro.yml b/plugins/chat/config/locales/client.ro.yml index ba0beba97d..2f1ee31bdb 100644 --- a/plugins/chat/config/locales/client.ro.yml +++ b/plugins/chat/config/locales/client.ro.yml @@ -50,6 +50,7 @@ ro: yesterday: Ieri about_view: title: Titlu + name: Nume description: Descriere channel_info: back_to_channel: "Înapoi" diff --git a/plugins/chat/config/locales/client.ru.yml b/plugins/chat/config/locales/client.ru.yml index 9270a8a218..0cada296ac 100644 --- a/plugins/chat/config/locales/client.ru.yml +++ b/plugins/chat/config/locales/client.ru.yml @@ -179,7 +179,6 @@ ru: read_only: "Только для чтения" archived_header: "Канал заархивирован" archived: "Архивные" - archive_failed: "Во время архивации поизошла ошибка. %{completed}/%{total} сообщений были заархивированы в целевой теме. Нажмите «Повторить», чтобы попытаться завершить архивирование." archive_completed: "См. архивную тему." closed_header: "Канал закрыт" closed: "Закрытые" @@ -202,6 +201,7 @@ ru: associated_topic: Связанная тема associated_category: Связанная категория title: Название + name: Название description: Описание channel_info: back_to_all_channels: "Все каналы" @@ -210,10 +210,6 @@ ru: about: Информация members: Участники settings: Настройки - channel_edit_title_modal: - title: Изменить название - input_placeholder: Добавьте название - description: Дайте короткое описательное название вашему каналу channel_edit_description_modal: title: Изменить описание input_placeholder: Добавить описание @@ -282,7 +278,6 @@ ru: mention: "Только для упоминаний" always: "Для всех действий" settings: - enable_auto_join_users: "Автоматически добавлять всех активных пользователей" auto_join_users_warning: "Любой пользователь, не являющийся участником этого канала, но имеющий доступ к разделу %{category}, будет автоматически подключён. Продолжить?" desktop_notification_level: "Уведомления на рабочем столе" follow: "Подписаться" diff --git a/plugins/chat/config/locales/client.sk.yml b/plugins/chat/config/locales/client.sk.yml index 354e7eb8f7..8eb983411a 100644 --- a/plugins/chat/config/locales/client.sk.yml +++ b/plugins/chat/config/locales/client.sk.yml @@ -50,6 +50,7 @@ sk: yesterday: Včera about_view: title: Názov + name: Meno description: Popis channel_info: back_to_channel: "Späť" diff --git a/plugins/chat/config/locales/client.sl.yml b/plugins/chat/config/locales/client.sl.yml index 9ca7c6746d..bea0885798 100644 --- a/plugins/chat/config/locales/client.sl.yml +++ b/plugins/chat/config/locales/client.sl.yml @@ -51,6 +51,7 @@ sl: yesterday: Včeraj about_view: title: Naziv + name: Ime description: Opis channel_info: back_to_channel: "Nazaj" diff --git a/plugins/chat/config/locales/client.sq.yml b/plugins/chat/config/locales/client.sq.yml index 1e52f4381b..1bf5824832 100644 --- a/plugins/chat/config/locales/client.sq.yml +++ b/plugins/chat/config/locales/client.sq.yml @@ -42,6 +42,7 @@ sq: yesterday: Dje about_view: title: Titulli + name: Emri description: Përshkrimi channel_info: back_to_channel: "Kthehu mbrapa" diff --git a/plugins/chat/config/locales/client.sr.yml b/plugins/chat/config/locales/client.sr.yml index a874c603a2..888b9273bc 100644 --- a/plugins/chat/config/locales/client.sr.yml +++ b/plugins/chat/config/locales/client.sr.yml @@ -46,6 +46,7 @@ sr: yesterday: Juče about_view: title: Naslov + name: Ime foruma description: Opis channel_info: back_to_channel: "Nazad" diff --git a/plugins/chat/config/locales/client.sv.yml b/plugins/chat/config/locales/client.sv.yml index 5a93df4ef8..0b8561ead5 100644 --- a/plugins/chat/config/locales/client.sv.yml +++ b/plugins/chat/config/locales/client.sv.yml @@ -206,7 +206,8 @@ sv: read_only: "Endast läsning" archived_header: "Kanalen är arkiverad" archived: "Arkiverad" - archive_failed: "Arkivering av kanalen misslyckades. %{completed}/%{total} meddelanden har arkiverats i destinationsämnet. Tryck på försök igen för att försöka slutföra arkivet." + archive_failed: "Arkivkanalen misslyckades. %{completed}/%{total} meddelanden har arkiverats. målämnet. Tryck på försök igen för att försöka slutföra arkiveringen." + archive_failed_no_topic: "Arkivkanalen misslyckades. %{completed}/%{total} meddelanden har arkiverats. Målämnet skapades inte. Tryck på försök igen för att försöka slutföra arkiveringen." archive_completed: "Se det arkiverade ämnet" closed_header: "Kanalen är stängd" closed: "Stängd" @@ -229,6 +230,7 @@ sv: associated_topic: Länkat ämne associated_category: Länkad kategori title: Rubrik + name: Namn description: Beskrivning channel_info: back_to_all_channels: "Alla kanaler" @@ -237,10 +239,6 @@ sv: about: Om members: Medlemmar settings: Inställningar - channel_edit_title_modal: - title: Redigera titel - input_placeholder: Lägg till en titel - description: Ge en kort beskrivande titel till din kanal channel_edit_description_modal: title: Redigera beskrivning input_placeholder: Lägg till en beskrivning @@ -306,8 +304,6 @@ sv: channel_wide_mentions_label: "Tillåt @all och @here omnämnanden" channel_wide_mentions_description: "Tillåt användare att avisera alla #%{channel}-medlemmar med @all eller bara de som är aktiva för tillfället med @here" auto_join_users_label: "Lägg till användare automatiskt" - auto_join_users_info: "Kontrollera varje timme vilka användare som har varit aktiva under de senaste 3 månaderna och, om de har tillgång till kategorin %{category}, lägg till dem i den här kanalen." - enable_auto_join_users: "Lägg automatiskt till alla nyligen aktiva användare" auto_join_users_warning: "Varje användare som inte är medlem i den här kanalen och har tillgång till kategorin %{category} kommer att gå med. Är du säker?" desktop_notification_level: "Skrivbordsaviseringar" follow: "Gå med" diff --git a/plugins/chat/config/locales/client.sw.yml b/plugins/chat/config/locales/client.sw.yml index 564dbb925a..eafb4beb24 100644 --- a/plugins/chat/config/locales/client.sw.yml +++ b/plugins/chat/config/locales/client.sw.yml @@ -48,6 +48,7 @@ sw: yesterday: Jana about_view: title: Kichwa cha Habari + name: Jina description: Elezo channel_info: back_to_channel: "Iliyopita" diff --git a/plugins/chat/config/locales/client.te.yml b/plugins/chat/config/locales/client.te.yml index 07add2434a..8740839c87 100644 --- a/plugins/chat/config/locales/client.te.yml +++ b/plugins/chat/config/locales/client.te.yml @@ -30,6 +30,7 @@ te: yesterday: నిన్న about_view: title: శీర్షిక + name: పేరు description: వివరణ channel_info: back_to_channel: "వెనుకకు" diff --git a/plugins/chat/config/locales/client.th.yml b/plugins/chat/config/locales/client.th.yml index fb8eb0936a..738d06e551 100644 --- a/plugins/chat/config/locales/client.th.yml +++ b/plugins/chat/config/locales/client.th.yml @@ -48,6 +48,7 @@ th: yesterday: เมื่อวาน about_view: title: ชื่อเรื่อง + name: ชื่อ description: รายละเอียด channel_info: back_to_channel: "กลับ" diff --git a/plugins/chat/config/locales/client.tr_TR.yml b/plugins/chat/config/locales/client.tr_TR.yml index 945dc66c86..457195215b 100644 --- a/plugins/chat/config/locales/client.tr_TR.yml +++ b/plugins/chat/config/locales/client.tr_TR.yml @@ -206,7 +206,6 @@ tr_TR: read_only: "Salt Okunur" archived_header: "Kanal arşivlendi" archived: "Arşivlendi" - archive_failed: "Kanal arşivlemesi başarısız oldu. %{completed}/%{total} mesaj hedef konuya arşivlendi. Arşivi tamamlamayı denemek için tekrar deneye basın." archive_completed: "Arşiv konusuna bakın" closed_header: "Kanal kapalı" closed: "Kapalı" @@ -229,6 +228,7 @@ tr_TR: associated_topic: Bağlantılı konu associated_category: Bağlantılı kategori title: Başlık + name: Ad description: Açıklama channel_info: back_to_all_channels: "Tüm kanallar" @@ -237,10 +237,6 @@ tr_TR: about: Hakkında members: Üyeler settings: Ayarlar - channel_edit_title_modal: - title: Başlığı düzenle - input_placeholder: Başlık ekle - description: Kanalınıza kısa bir açıklayıcı başlık ekleyin channel_edit_description_modal: title: Açıklamayı düzenle input_placeholder: Açıklama ekle @@ -306,8 +302,6 @@ tr_TR: channel_wide_mentions_label: "@all ve @here bahsetmelerine izin ver" channel_wide_mentions_description: "Kullanıcıların @all ile #%{channel} kanalının tüm üyelerine veya @here ile yalnızca o anda aktif olanlara bildirim göndermelerine izin verin" auto_join_users_label: "Kullanıcıları otomatik olarak ekle" - auto_join_users_info: "Son 3 ayda hangi kullanıcıların aktif olduğunu saatlik olarak kontrol edin ve %{category} adlı kategoriye erişimleri varsa bu kanala ekleyin." - enable_auto_join_users: "Son zamanlarda aktif olan tüm kullanıcıları otomatik olarak ekle" auto_join_users_warning: "Bu kanala üye olmayan ve %{category} adlı kategoriye erişimi olan her kullanıcı katılacak. Emin misiniz?" desktop_notification_level: "Masaüstü bildirimleri" follow: "Katıl" @@ -327,6 +321,7 @@ tr_TR: title: "Sohbet" direct_messages: title: "Kişisel sohbet" + new: "Kişisel sohbet oluşturun" create: "Git" leave: "Bu kişisel sohbetten ayrıl" cannot_create: "Üzgünüz, doğrudan mesaj gönderemezsiniz." diff --git a/plugins/chat/config/locales/client.uk.yml b/plugins/chat/config/locales/client.uk.yml index 84f6942b0c..dfff1062da 100644 --- a/plugins/chat/config/locales/client.uk.yml +++ b/plugins/chat/config/locales/client.uk.yml @@ -60,6 +60,7 @@ uk: yesterday: Вчора about_view: title: Назва + name: Імʼя description: Опис channel_info: back_to_channel: "Назад" diff --git a/plugins/chat/config/locales/client.ur.yml b/plugins/chat/config/locales/client.ur.yml index 4e03a55cf3..3f7f16269e 100644 --- a/plugins/chat/config/locales/client.ur.yml +++ b/plugins/chat/config/locales/client.ur.yml @@ -50,6 +50,7 @@ ur: yesterday: کَل about_view: title: عنوان + name: نام description: تفصیل channel_info: back_to_channel: "واپس" diff --git a/plugins/chat/config/locales/client.vi.yml b/plugins/chat/config/locales/client.vi.yml index 49738130d0..4c1039356d 100644 --- a/plugins/chat/config/locales/client.vi.yml +++ b/plugins/chat/config/locales/client.vi.yml @@ -70,6 +70,7 @@ vi: yesterday: Hôm qua about_view: title: Tiêu đề + name: Tên description: Mô tả channel_info: back_to_channel: "Quay lại" diff --git a/plugins/chat/config/locales/client.zh_CN.yml b/plugins/chat/config/locales/client.zh_CN.yml index f3c76d2cd5..670ff1908e 100644 --- a/plugins/chat/config/locales/client.zh_CN.yml +++ b/plugins/chat/config/locales/client.zh_CN.yml @@ -108,14 +108,35 @@ zh_CN: new_messages: "新消息" mention_warning: dismiss: "忽略" + cannot_see: + other: "%{username} 和 %{others} 无法访问此频道且未收到通知。" invitations_sent: other: "已发送邀请" invite: "邀请加入频道" + without_membership: + other: "%{username} 和 %{others} 尚未加入此频道。" + group_mentions_disabled: + other: "%{group_name} 和 %{others} 不允许提及" + too_many_members: + other: "%{group_name} 和 %{others} 的成员过多。任何人都不会收到通知" warning_multiple: other: "其他 %{count} 个" groups: + header: + some: "某些用户不会收到通知" + all: "任何人都不会收到通知" + unreachable: + other: "@%{group} 和 @%{group_2} 不允许提及" + unreachable_multiple: "@%{group} 和其他 %{count} 个群组不允许提及" + too_many_members: + other: "提及 @%{group}或 @%{group_2}超出 %{limit}的%{notification_limit}" + too_many_members_multiple: "这 %{count} 个群组超出了 %{limit}的%{notification_limit}" users_limit: other: "%{count} 位用户" + notification_limit: "通知限制" + too_many_mentions: "此消息超出 %{limit}的%{notification_limit}" + mentions_limit: + other: "%{count} 个提及" aria_roles: header: "聊天标题" composer: "聊天输入框" @@ -173,7 +194,6 @@ zh_CN: read_only: "只读" archived_header: "频道已被归档" archived: "已归档" - archive_failed: "频道归档失败。%{completed} 条(共 %{total} 条)消息已被归档到目标话题。按“重试”,尝试完成存档。" archive_completed: "请参阅归档话题" closed_header: "频道已被关闭" closed: "已关闭" @@ -196,6 +216,7 @@ zh_CN: associated_topic: 链接的话题 associated_category: 链接的类别 title: 标题 + name: 名称 description: 描述 channel_info: back_to_all_channels: "所有频道" @@ -204,10 +225,6 @@ zh_CN: about: 关于 members: 成员 settings: 设置 - channel_edit_title_modal: - title: 编辑标题 - input_placeholder: 添加标题 - description: 为您的频道提供简短的描述性标题 channel_edit_description_modal: title: 编辑描述 input_placeholder: 添加描述 @@ -267,9 +284,9 @@ zh_CN: mention: "仅限提及" always: "所有活动" settings: + channel_wide_mentions_label: "允许 @all 和 @here 提及" + channel_wide_mentions_description: "允许用户使用 @all 通知#%{channel} 的所有成员,或仅使用 @here 通知当前活跃的成员" auto_join_users_label: "自动添加用户" - auto_join_users_info: "每小时检查哪些用户在过去 3 个月内处于活跃状态,如果他们有权访问“%{category}”类别,则将他们添加到此频道。" - enable_auto_join_users: "自动添加所有最近活跃的用户" auto_join_users_warning: "所有不是此频道成员且有权访问“%{category}”类别的用户都将加入。确定吗?" desktop_notification_level: "桌面通知" follow: "加入" @@ -289,6 +306,7 @@ zh_CN: title: "聊天" direct_messages: title: "个人聊天" + new: "创建个人聊天" create: "开始" leave: "离开此个人聊天" cannot_create: "抱歉,您无法发送直接消息。" diff --git a/plugins/chat/config/locales/client.zh_TW.yml b/plugins/chat/config/locales/client.zh_TW.yml index 63f5cb67a0..19a7aa958b 100644 --- a/plugins/chat/config/locales/client.zh_TW.yml +++ b/plugins/chat/config/locales/client.zh_TW.yml @@ -123,6 +123,7 @@ zh_TW: associated_topic: 連結的主題 associated_category: 連結的類別 title: 標題 + name: 名字 description: 簡述 channel_info: back_to_all_channels: "所有頻道" @@ -131,9 +132,6 @@ zh_TW: about: 關於 members: 成員 settings: 設定 - channel_edit_title_modal: - title: 編輯標題 - input_placeholder: 新增標題 channel_edit_description_modal: title: 編輯描述 input_placeholder: 新增描述 diff --git a/plugins/chat/config/locales/server.ar.yml b/plugins/chat/config/locales/server.ar.yml index e18a7d3120..eb1401fe74 100644 --- a/plugins/chat/config/locales/server.ar.yml +++ b/plugins/chat/config/locales/server.ar.yml @@ -8,22 +8,24 @@ ar: site_settings: chat_enabled: "تفعيل المكوِّن الإضافي للدردشة." chat_allowed_groups: "يمكن للمستخدمين في هذه المجموعات الدردشة. لاحظ أن أعضاء فريق العمل يمكنهم دائمًا الوصول إلى الدردشة." - chat_channel_retention_days: "سيتم الاحتفاظ برسائل الدردشة في القنوات العادية لهذا العدد من الأيام. اضبط القيمة على '0' للاحتفاظ بالرسائل إلى الأبد." - chat_dm_retention_days: "سيتم الاحتفاظ برسائل الدردشة في قنوات الدردشة الشخصية لهذا العدد من الأيام. اضبط القيمة على '0' للاحتفاظ بالرسائل إلى الأبد." - chat_auto_silence_duration: "عدد الدقائق التي سيتم فيها كتم المستخدمين عندما يتجاوزون حد معدل إنشاء رسائل الدردشة. اضبط القيمة على '0' لإيقاف الكتم التلقائي." - chat_allowed_messages_for_trust_level_0: "عدد الرسائل المسموح للمستخدمين من مستوى الثقة 0 بإرسالها خلال 30 ثانية. اضبط القيمة على '0' لإيقاف الحد." - chat_allowed_messages_for_other_trust_levels: "عدد الرسائل المسموح للمستخدمين من مستوى الثقة 1-4 بإرسالها خلال 30 ثانية. اضبط القيمة على '0' لإيقاف الحد." + chat_channel_retention_days: "سيتم الاحتفاظ برسائل الدردشة في القنوات العادية لهذا العدد من الأيام. اتعيين القيمة على '0' للاحتفاظ بالرسائل إلى الأبد." + chat_dm_retention_days: "سيتم الاحتفاظ برسائل الدردشة في قنوات الدردشة الشخصية لهذا العدد من الأيام. اتعيين القيمة على '0' للاحتفاظ بالرسائل إلى الأبد." + chat_auto_silence_duration: "عدد الدقائق التي سيتم فيها كتم المستخدمين عندما يتجاوزون حد معدل إنشاء رسائل الدردشة. اتعيين القيمة على '0' لإيقاف الكتم التلقائي." + chat_allowed_messages_for_trust_level_0: "عدد الرسائل المسموح للمستخدمين من مستوى الثقة 0 بإرسالها خلال 30 ثانية. اتعيين القيمة على '0' لإيقاف الحد." + chat_allowed_messages_for_other_trust_levels: "عدد الرسائل المسموح للمستخدمين من مستوى الثقة 1-4 بإرسالها خلال 30 ثانية. اتعيين القيمة على '0' لإيقاف الحد." chat_silence_user_sensitivity: "احتمالية أن يتم كتم المستخدم الذي تم الإبلاغ عنه في الدردشة تلقائيًا." chat_auto_silence_from_flags_duration: "عدد الدقائق التي سيتم كتم المستخدمين تلقائيًا خلالها بسبب رسائل الدردشة التي تم الإبلاغ عنها." chat_default_channel_id: "قناة الدردشة التي سيتم فتحها بشكلٍ افتراضي عندما لا يكون لدى المستخدم رسائل أو إشارات غير مقروءة في قنوات أخرى." - chat_duplicate_message_sensitivity: "احتمالية حظر الرسائل المكررة خلال فترة قصيرة من المُرسل نفسه. رقم عشري بين 0 و1.0، مع كون 1.0 هو أعلى إعداد (يحظر الرسائل بشكلٍ أكثر تكرارًا في فترة زمنية أقصر). اضبط القيمة على `0` للسماح بالرسائل المكررة." + chat_duplicate_message_sensitivity: "احتمالية حظر الرسائل المكررة خلال فترة قصيرة من المُرسل نفسه. رقم عشري بين 0 و1.0، مع كون 1.0 هو أعلى إعداد (يحظر الرسائل بشكلٍ أكثر تكرارًا في فترة زمنية أقصر). اتعيين القيمة على `0` للسماح بالرسائل المكررة." chat_minimum_message_length: "الحد الأدنى لعدد الأحرف لرسالة دردشة." chat_allow_uploads: "السماح بالتحميلات في قنوات الدردشة العامة وقنوات الرسائل المباشرة." chat_archive_destination_topic_status: "الحالة التي يجب أن يكون عليها الموضوع المستهدف بعد اكتمال أرشيف القناة. ينطبق ذلك فقط عندما يكون الموضوع المستهدف موضوعًا جديدًا وليس موضوعًا موجودًا." default_emoji_reactions: "تفاعلات الرموز التعبيرية الافتراضية لرسائل الدردشة. أضِف ما يصل إلى 5 رموز تعبيرية للتفاعل السريع." direct_message_enabled_groups: "السماح للمستخدمين في تلك المجموعات بإنشاء دردشات شخصية بين مستخدم وآخر. ملاحظة: يمكن للموظفين دائمًا إنشاء دردشات شخصية، وسيتمكن المستخدمون من الرد على الدردشات الشخصية التي بدأها مستخدمون لديهم إذن بإنشائها." chat_message_flag_allowed_groups: "السماح للمستخدمين في تلك المجموعات بالإبلاغ عن رسائل الدردشة." - chat_max_direct_message_users: "لا يمكن للمستخدمين إضافة أكثر من هذا العدد من المستخدمين الآخرين عند إنشاء رسالة مباشرة جديدة. اضبط القيمة على 0 للسماح بإرسال الرسائل إلى نفسك فقط. الموظفون معفيون من هذا الإعداد." + max_mentions_per_chat_message: "الحد الأقصى لعدد إشعارات @الاسم التي يمكن لمستخدم استخدامها في رسالة دردشة." + chat_max_direct_message_users: "لا يمكن للمستخدمين إضافة أكثر من هذا العدد من المستخدمين الآخرين عند إنشاء رسالة مباشرة جديدة. اتعيين القيمة على 0 للسماح بإرسال الرسائل إلى نفسك فقط. الموظفون معفيون من هذا الإعداد." + chat_allow_archiving_channels: "السماح لفريق العمل بأرشفة الرسائل في أحد الموضوعات عند إغلاق القناة." errors: chat_default_channel: "يجب أن تكون قناة الدردشة الافتراضية قناةً عامة." direct_message_enabled_groups_invalid: "يجب عليك تحديد مجموعة واحدة على الأقل لهذا الإعداد. إذا كنت لا تريد أن يقوم أي شخص باستثناء فريق العمل بإرسال رسائل مباشرة، فاختر مجموعة فريق العمل." @@ -33,12 +35,21 @@ ar: title: "اكتمل أرشيف قناة الدردشة" subject_template: "اكتمل أرشيف قناة الدردشة بنجاح" text_body_template: | - اكتملت أرشفة قناة الدردشة **\#%{channel_name}** بنجاح. وتم نسخ الرسائل إلى الموضوع [%{topic_title}](%{topic_url}). + اكتملت أرشفة قناة الدردشة %{channel_hashtag_or_name} بنجاح. تم نسخ الرسائل في الموضوع [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "فشلت أرشفة قناة الدردشة" subject_template: "فشلت أرشفة قناة الدردشة" text_body_template: | - فشلت أرشفة قناة الدردشة **\#%{channel_name}**. تم وضع الرسائل %{messages_archived} في الأرشيف. تم نسخ الرسائل المؤرشفة جزئيًا في الموضوع [%{topic_title}](%{topic_url}). انتقل إلى القناة في %{channel_url} لإعادة المحاولة. + فشلت أرشفة قناة الدردشة %{channel_hashtag_or_name}. تمت أرشفة %{messages_archived} من الرسائل. تم نسخ الرسائل المؤرشفة جزئيًا في الموضوع [%{topic_title}](%{topic_url}). انتقل إلى القناة في %{channel_url} لإعادة المحاولة. + chat_channel_archive_failed_no_topic: + title: "فشلت أرشفة قناة الدردشة" + subject_template: "فشلت أرشفة قناة الدردشة" + text_body_template: | + فشلت أرشفة قناة الدردشة %{channel_hashtag_or_name}. لم تتم أرشفة أي رسائل. لم يتم إنشاء الموضوع بنجاح للأسباب التالية: + + %{topic_validation_errors} + + انتقل إلى القناة في %{channel_url} لإعادة المحاولة. chat: deleted_chat_username: تم الحذف errors: @@ -52,6 +63,7 @@ ar: duplicate_message: "لقد نشرت رسالة مماثلة مؤخرًا." delete_channel_failed: "فشل حذف القناة، يُرجى إعادة المحاولة." minimum_length_not_met: "الرسالة قصيرة جدًا، ويجب ألا تقل عن %{minimum} من الأحرف." + message_too_long: "الرسالة طويلة جدًا، يجب ألا يزيد عدد أحرف الرسائل عن %{maximum} من الأحرف." max_reactions_limit_reached: "غير مسموح بتفاعلات جديدة على هذه الرسالة." message_move_invalid_channel: "يجب أن تكون القناة المصدر والمستهدفة قناتين عامتين." message_move_no_messages_found: "لم يتم العثور على رسائل بمعرِّفات الرسائل المقدَّمة." @@ -187,7 +199,24 @@ ar: from: "%{site_name}" subject: direct_message_from_1: "[%{email_prefix}] رسالة جديدة من %{username}" + direct_message_from_2: "[%{email_prefix}] رسالة جديدة من %{username1} و%{username2}" + direct_message_from_more: + zero: "[%{email_prefix}] رسالة جديدة من %{username} و%{count} مستخدم آخر" + one: "[%{email_prefix}] رسالة جديدة من %{username} ومستخدم (%{count}) آخر" + two: "[%{email_prefix}] رسالة جديدة من %{username} ومستخدمَين (%{count}) آخرين" + few: "[%{email_prefix}] رسالة جديدة من %{username} و%{count} مستخدمين آخرين" + many: "[%{email_prefix}] رسالة جديدة من %{username} و%{count} مستخدمًا آخر" + other: "[%{email_prefix}] رسالة جديدة من %{username} و%{count} مستخدم آخر" chat_channel_1: "[%{email_prefix}] رسالة جديدة في %{channel}" + chat_channel_2: "[%{email_prefix}] رسالة جديدة في %{channel1} و%{channel2}" + chat_channel_more: + zero: "[%{email_prefix}] رسالة جديدة في %{channel} و%{count} قناة أخرى" + one: "[%{email_prefix}] رسالة جديدة في %{channel} وقناة (%{count}) أخرى" + two: "[%{email_prefix}] رسالة جديدة في %{channel} وقناتين (%{count}) آخرتين" + few: "[%{email_prefix}] رسالة جديدة في %{channel} و%{count} قنوات أخرى" + many: "[%{email_prefix}] رسالة جديدة في %{channel} و%{count} قناةً أخرى" + other: "[%{email_prefix}] رسالة جديدة في %{channel} و%{count} قناة أخرى" + chat_channel_and_direct_message: "[%{email_prefix}] رسالة جديدة في %{channel} ومن %{username}" unsubscribe: "يتم إرسال ملخص الدردشة هذا من %{site_link} عندما تكون غائبًا. غيِّر %{email_preferences_link} أو %{unsubscribe_link} لإلغاء الاشتراك." unsubscribe_no_link: "يتم إرسال ملخص الدردشة هذا من %{site_link} عندما تكون غائبًا. غيِّر %{email_preferences_link} لديك." view_messages: diff --git a/plugins/chat/config/locales/server.de.yml b/plugins/chat/config/locales/server.de.yml index 27cc0bccc6..2870f37aa1 100644 --- a/plugins/chat/config/locales/server.de.yml +++ b/plugins/chat/config/locales/server.de.yml @@ -23,7 +23,9 @@ de: default_emoji_reactions: "Standard-Emoji-Reaktionen für Chat-Nachrichten. Füge bis zu 5 Emojis für schnelle Reaktionen hinzu." direct_message_enabled_groups: "Benutzern in diesen Gruppen erlauben, persönliche Benutzer-zu-Benutzer-Chats zu erstellen. Hinweis: Teammitglieder können immer persönliche Chats erstellen und Benutzer können auf persönliche Chats antworten, die von Benutzern initiiert wurden, die die Berechtigung haben, sie zu erstellen." chat_message_flag_allowed_groups: "Benutzer in diesen Gruppen dürfen Chat-Nachrichten markieren." + max_mentions_per_chat_message: "Maximale Anzahl von @name-Benachrichtigungen, die ein Benutzer in einer Chat-Nachricht verwenden kann." chat_max_direct_message_users: "Benutzer können nicht mehr als diese Anzahl anderer Benutzer hinzufügen, wenn sie eine neue Direktnachricht erstellen. Auf 0 setzen, um nur Nachrichten an sich selbst zuzulassen. Teammitglieder sind von dieser Einstellung ausgenommen." + chat_allow_archiving_channels: "Dem Team erlauben, Nachrichten zu einem Thema zu archivieren, wenn sie einen Kanal schließen." errors: chat_default_channel: "Der Standard-Chat-Kanal muss ein öffentlicher Kanal sein." direct_message_enabled_groups_invalid: "Du musst mindestens eine Gruppe für diese Einstellung angeben. Wenn du nicht möchtest, dass andere Personen als Teammitglieder Direktnachrichten senden, wähle die Gruppe für Teammitglieder aus." @@ -32,13 +34,12 @@ de: chat_channel_archive_complete: title: "Chat-Kanal-Archivierung abgeschlossen" subject_template: "Chat-Kanal-Archivierung erfolgreich abgeschlossen" - text_body_template: | - Die Archivierung des Chat-Kanals **\#%{channel_name}** wurde erfolgreich abgeschlossen. Die Nachrichten wurden in das Thema [%{topic_title}](%{topic_url}) kopiert. chat_channel_archive_failed: title: "Chat-Kanal-Archivierung fehlgeschlagen" subject_template: "Chat-Kanal-Archivierung fehlgeschlagen" - text_body_template: | - Die Archivierung des Chat-Kanals **\#%{channel_name}** ist fehlgeschlagen. %{messages_archived} Nachrichten wurden archiviert. Teilweise archivierte Nachrichten wurden in das Thema [%{topic_title}](%{topic_url}) kopiert. Besuche den Kanal unter %{channel_url}, um es erneut zu versuchen. + chat_channel_archive_failed_no_topic: + title: "Chat-Kanal-Archivierung fehlgeschlagen" + subject_template: "Chat-Kanal-Archivierung fehlgeschlagen" chat: deleted_chat_username: gelöscht errors: @@ -52,6 +53,7 @@ de: duplicate_message: "Du hast vor Kurzem eine identische Nachricht gepostet." delete_channel_failed: "Löschen des Kanals fehlgeschlagen, bitte versuche es erneut." minimum_length_not_met: "Die Nachricht ist zu kurz, sie muss mindestens %{minimum} Zeichen lang sein." + message_too_long: "Nachricht ist zu lang. Nachrichten dürfen maximal %{maximum} Zeichen lang sein." max_reactions_limit_reached: "Neue Reaktionen auf diese Nachricht sind nicht erlaubt." message_move_invalid_channel: "Quell- und Zielkanal müssen öffentliche Kanäle sein." message_move_no_messages_found: "Es wurden keine Nachrichten mit den angegebenen Nachrichten-IDs gefunden." @@ -167,7 +169,16 @@ de: from: "%{site_name}" subject: direct_message_from_1: "[%{email_prefix}] Neue Nachricht von %{username}" + direct_message_from_2: "[%{email_prefix}] Neue Nachricht von %{username1} und %{username2}" + direct_message_from_more: + one: "[%{email_prefix}] Neue Nachricht von %{username} und %{count} anderem Benutzer" + other: "[%{email_prefix}] Neue Nachricht von %{username} und %{count} anderen" chat_channel_1: "[%{email_prefix}] Neue Nachricht in %{channel}" + chat_channel_2: "[%{email_prefix}] Neue Nachricht in %{channel1} und %{channel2}" + chat_channel_more: + one: "[%{email_prefix}] Neue Nachricht in %{channel} und %{count} anderem Kanal" + other: "[%{email_prefix}] Neue Nachricht in %{channel} und %{count} anderen" + chat_channel_and_direct_message: "[%{email_prefix}] Neue Nachricht in %{channel} und von %{username}" unsubscribe: "Diese Chat-Zusammenfassung wird dir von %{site_link} gesendet, wenn du abwesend bist. Ändere deine %{email_preferences_link} oder %{unsubscribe_link}, um dich abzumelden." unsubscribe_no_link: "Diese Chat-Zusammenfassung wird dir von %{site_link} gesendet, wenn du abwesend bist. Ändere deine %{email_preferences_link}." view_messages: diff --git a/plugins/chat/config/locales/server.en.yml b/plugins/chat/config/locales/server.en.yml index 62c2229208..372eda5e4a 100644 --- a/plugins/chat/config/locales/server.en.yml +++ b/plugins/chat/config/locales/server.en.yml @@ -29,12 +29,21 @@ en: title: "Chat Channel Archive Complete" subject_template: "Chat channel archive completed successfully" text_body_template: | - Archiving the chat channel **\#%{channel_name}** has been completed successfully. The messages were copied into the topic [%{topic_title}](%{topic_url}). + Archiving the chat channel %{channel_hashtag_or_name} has been completed successfully. The messages were copied into the topic [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "Chat Channel Archive Failed" subject_template: "Chat channel archive failed" text_body_template: | - Archiving the chat channel **\#%{channel_name}** has failed. %{messages_archived} messages have been archived. Partially archived messages were copied into the topic [%{topic_title}](%{topic_url}). Visit the channel at %{channel_url} to retry. + Archiving the chat channel %{channel_hashtag_or_name} has failed. %{messages_archived} messages have been archived. Partially archived messages were copied into the topic [%{topic_title}](%{topic_url}). Visit the channel at %{channel_url} to retry. + chat_channel_archive_failed_no_topic: + title: "Chat Channel Archive Failed" + subject_template: "Chat channel archive failed" + text_body_template: | + Archiving the chat channel %{channel_hashtag_or_name} has failed. No messages have been archived. The topic was not created successfully for the following reasons: + + %{topic_validation_errors} + + Visit the channel at %{channel_url} to retry. chat: deleted_chat_username: deleted @@ -50,6 +59,7 @@ en: delete_channel_failed: "Delete channel failed, please try again." minimum_length_not_met: "Message is too short, must have a minimum of %{minimum} characters." message_too_long: "Message is too long, messages must be a maximum of %{maximum} characters." + draft_too_long: "Draft is too long." max_reactions_limit_reached: "New reactions are not allowed on this message." message_move_invalid_channel: "The source and destination channel must be public channels." message_move_no_messages_found: "No messages were found with the provided message IDs." diff --git a/plugins/chat/config/locales/server.es.yml b/plugins/chat/config/locales/server.es.yml index 86164dbcc6..fe0cb10978 100644 --- a/plugins/chat/config/locales/server.es.yml +++ b/plugins/chat/config/locales/server.es.yml @@ -23,7 +23,9 @@ es: default_emoji_reactions: "Reacciones emoji por defecto para los mensajes de chat. Añade hasta 5 emojis para una reacción rápida." direct_message_enabled_groups: "Permite a los usuarios de estos grupos crear Chats Personales de usuario a usuario. Nota: el personal siempre puede crear Chats Personales, y los usuarios podrán responder a los Chats Personales iniciados por los usuarios que tienen permiso para crearlos." chat_message_flag_allowed_groups: "Los usuarios de estos grupos pueden marcar los mensajes del chat." + max_mentions_per_chat_message: "Número máximo de notificaciones de @name que un usuario puede usar en un mensaje de chat." chat_max_direct_message_users: "Los usuarios no pueden añadir más de este número de otros usuarios al crear un nuevo mensaje directo. Establece el valor 0 para permitir solo los mensajes a uno mismo. El personal está exento de este ajuste." + chat_allow_archiving_channels: "Permitir al personal archivar mensajes en un tema al cerrar un canal." errors: chat_default_channel: "El canal de chat por defecto debe ser un canal público." direct_message_enabled_groups_invalid: "Debes especificar al menos un grupo para esta configuración. Si no quieres que nadie, excepto el personal, envíe mensajes directos, elige el grupo del personal." @@ -32,13 +34,12 @@ es: chat_channel_archive_complete: title: "Archivado del canal de chat completado" subject_template: "El archivado del canal de chat se ha completado con éxito" - text_body_template: | - El archivado del canal de chat **\#%{channel_name}** se completó con éxito. Los mensajes se copiaron en el tema [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "No se pudo archivar el canal" subject_template: "No se pudo archivar el canal" - text_body_template: | - El archivo del canal de chat **\#%{channel_name}** ha fallado. Se han archivado los mensajes de %{messages_archived}. Los mensajes parcialmente archivados se han copiado en el tema [%{topic_title}](%{topic_url}). Visita el canal en %{channel_url} para volver a intentarlo. + chat_channel_archive_failed_no_topic: + title: "No se pudo archivar el canal" + subject_template: "No se pudo archivar el canal" chat: deleted_chat_username: eliminado errors: @@ -52,6 +53,7 @@ es: duplicate_message: "Tú también publicaste un mensaje idéntico hace poco." delete_channel_failed: "No se pudo eliminar el canal, inténtalo de nuevo." minimum_length_not_met: "El mensaje es demasiado corto, debe tener un mínimo de %{minimum} caracteres." + message_too_long: "El mensaje es demasiado largo, los mensajes deben tener un máximo de %{maximum} caracteres." max_reactions_limit_reached: "No se permiten nuevas reacciones en este mensaje." message_move_invalid_channel: "El canal de origen y el de destino deben ser canales públicos." message_move_no_messages_found: "No se ha encontrado ningún mensaje con los identificadores de mensaje proporcionados." @@ -167,7 +169,16 @@ es: from: "%{site_name}" subject: direct_message_from_1: "[%{email_prefix}] Nuevo mensaje de %{username}" + direct_message_from_2: "[%{email_prefix}] Nuevo mensaje de %{username1} y %{username2}" + direct_message_from_more: + one: "[%{email_prefix}] Nuevo mensaje de %{username} y %{count}" + other: "[%{email_prefix}] Nuevo mensaje de %{username} y otros %{count}" chat_channel_1: "[%{email_prefix}] Nuevo mensaje en %{channel}" + chat_channel_2: "[%{email_prefix}] Nuevo mensaje en %{channel1} y %{channel2}" + chat_channel_more: + one: "[%{email_prefix}] Nuevo mensaje en %{channel} y %{count}" + other: "[%{email_prefix}] Nuevo mensaje en %{channel} y otros %{count}" + chat_channel_and_direct_message: "[%{email_prefix}] Nuevo mensaje en %{channel} y de %{username}" unsubscribe: "Este resumen del chat se envía desde %{site_link} cuando estás fuera. Cambia tu %{email_preferences_link}, o %{unsubscribe_link} para darte de baja." unsubscribe_no_link: "Este resumen del chat se envía desde %{site_link} cuando estás fuera. Cambia tu %{email_preferences_link}." view_messages: diff --git a/plugins/chat/config/locales/server.fa_IR.yml b/plugins/chat/config/locales/server.fa_IR.yml index 3d3731e2e7..d8d0cf7981 100644 --- a/plugins/chat/config/locales/server.fa_IR.yml +++ b/plugins/chat/config/locales/server.fa_IR.yml @@ -12,6 +12,10 @@ fa_IR: chat_message_flag_allowed_groups: "کاربران در این گروه‌ها مجاز به گزارش دادن، پیام‌های گفتگو هستند." errors: chat_upload_not_allowed_secure_uploads: "وقتی که در تنظیمات سایت آپلودهای ایمن فعال باشد، آپلود گفتگو مجاز نیست." + system_messages: + chat_channel_archive_complete: + text_body_template: | + بایگانی کانال گفتگو %{channel_hashtag_or_name} با موفقیت انجام شد. پیام‌ها در موضوع [%{topic_title}](%{topic_url}) کپی شده‌اند. chat: deleted_chat_username: حذف شد errors: diff --git a/plugins/chat/config/locales/server.fi.yml b/plugins/chat/config/locales/server.fi.yml index 4d00a82532..822200ae87 100644 --- a/plugins/chat/config/locales/server.fi.yml +++ b/plugins/chat/config/locales/server.fi.yml @@ -23,7 +23,9 @@ fi: default_emoji_reactions: "Chat-viestien oletusarvoiset emoji-reaktiot. Lisää enintään 5 emojia nopeaa reagointia varten." direct_message_enabled_groups: "Salli näiden ryhmien käyttäjien luoda käyttäjien välisiä henkilökohtaisia chat-keskusteluja. Huomautus: henkilökunta voi aina luoda henkilökohtaisia chat-keskusteluja, ja käyttäjät voivat vastata henkilökohtaisiin chat-keskusteluihin, jotka on aloittanut käyttäjä, jolla on oikeus luoda niitä." chat_message_flag_allowed_groups: "Näiden ryhmien käyttäjät voivat liputtaa chat-viestejä." + max_mentions_per_chat_message: "Kuinka monta @nimi-ilmoitusta käyttäjä voi lisätä chat-viestiin." chat_max_direct_message_users: "Käyttäjät eivät voi lisätä tätä määrää enempää muita käyttäjiä luodessaan uutta yksityisviestiä. Aseta arvoksi 0, jos haluat sallia ainostaan viestit itselle. Tämä asetus ei koske henkilökuntaa." + chat_allow_archiving_channels: "Salli henkilökunnan arkistoida viestejä ketjuun suljettaessa kanavaa." errors: chat_default_channel: "Oletus-chat-kanavan täytyy olla julkinen kanava." direct_message_enabled_groups_invalid: "Sinun täytyy määrittää vähintään yksi ryhmä tälle asetukselle. Jos et halua muiden kuin henkilökunnan lähettävän yksityisviestejä, valitse henkilökunnan ryhmä." @@ -32,13 +34,12 @@ fi: chat_channel_archive_complete: title: "Chat-kanavan arkistointi on valmis" subject_template: "Chat-kanavan arkistointi on valmis" - text_body_template: | - Chat-kanavan **\#%{channel_name}** arkistointi on valmis. Viestit kopioitiin ketjuun [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "Chat-kanavan arkistointi epäonnistui" subject_template: "Chat-kanavan arkistointi epäonnistui" - text_body_template: | - Chat-kanavan **\#%{channel_name}** arkistointi epäonnistui. %{messages_archived} viestiä on arkistoitu. Osittain arkistoidut viestit kopioitiin ketjuun [%{topic_title}](%{topic_url}). Yritä uudelleen vierailemalla kanavalla osoitteessa %{channel_url}. + chat_channel_archive_failed_no_topic: + title: "Chat-kanavan arkistointi epäonnistui" + subject_template: "Chat-kanavan arkistointi epäonnistui" chat: deleted_chat_username: poistettu errors: @@ -52,6 +53,7 @@ fi: duplicate_message: "Lähetit identtisen viestin liian äskettäin." delete_channel_failed: "Kanavan poistaminen epäonnistui, yritä uudelleen." minimum_length_not_met: "Viesti on liian lyhyt, siinä täytyy olla vähintään %{minimum} merkkiä." + message_too_long: "Viesti on liian pitkä, viesteissä täytyy olla enintään %{maximum} merkkiä." max_reactions_limit_reached: "Uusia reaktioita ei sallita tässä viestissä." message_move_invalid_channel: "Lähde- ja kohdekanavan täytyy olla julkisia kanavia." message_move_no_messages_found: "Annetuilla viestitunnuksilla ei löytynyt viestejä." @@ -167,7 +169,16 @@ fi: from: "%{site_name}" subject: direct_message_from_1: "[%{email_prefix}] Uusi viesti käyttäjältä %{username}" + direct_message_from_2: "[%{email_prefix}] Uusi viesti käyttäjältä %{username1} ja %{username2}" + direct_message_from_more: + one: "[%{email_prefix}] Uusi viesti käyttäjältä %{username} ja %{count} muulta" + other: "[%{email_prefix}] Uusi viesti käyttäjältä %{username} ja %{count} muulta" chat_channel_1: "[%{email_prefix}] Uusi viesti kanavalta %{channel}" + chat_channel_2: "[%{email_prefix}] Uusi viesti kanavalla %{channel1} ja %{channel2}" + chat_channel_more: + one: "[%{email_prefix}] Uusi viesti kanavalla %{channel} ja %{count} muulla" + other: "[%{email_prefix}] Uusi viesti kanavalla %{channel} ja %{count} muulla" + chat_channel_and_direct_message: "[%{email_prefix}] Uusi viesti kanavalla %{channel} ja käyttäjältä %{username}" unsubscribe: "Tämä chat-yhteenveto lähetetään sivustolta %{site_link}, kun olet poissa. Määritä %{email_preferences_link} tai %{unsubscribe_link} peruuttaaksesi tilauksen." unsubscribe_no_link: "Tämä chat-yhteenveto lähetetään sivustolta %{site_link}, kun olet poissa. Määritä %{email_preferences_link}." view_messages: diff --git a/plugins/chat/config/locales/server.fr.yml b/plugins/chat/config/locales/server.fr.yml index f97b61f5ea..76ae88a4de 100644 --- a/plugins/chat/config/locales/server.fr.yml +++ b/plugins/chat/config/locales/server.fr.yml @@ -23,7 +23,9 @@ fr: default_emoji_reactions: "Réactions émoji par défaut pour les messages de discussion. Ajoutez jusqu'à 5 émojis pour une réaction rapide." direct_message_enabled_groups: "Permettre aux utilisateurs de ces groupes de créer des discussions privées entre utilisateurs. Remarque : les responsables peuvent toujours créer des conversations privées et les utilisateurs pourront répondre aux conversations privées initiées par les utilisateurs qui sont autorisés à les créer." chat_message_flag_allowed_groups: "Les utilisateurs de ces groupes sont autorisés à signaler les messages de discussion." + max_mentions_per_chat_message: "Nombre maximum de notifications @name qu'un utilisateur peut utiliser dans un message de discussion." chat_max_direct_message_users: "Les utilisateurs ne peuvent pas ajouter plus que ce nombre d'autres utilisateurs lors de la création d'un nouveau message privé. Fixez cette valeur à 0 pour n'autoriser les messages que pour vous-même. Les responsables ne sont pas soumis à ce paramètre." + chat_allow_archiving_channels: "Autoriser le personnel à archiver les messages dans un sujet lors de la fermeture d'un canal." errors: chat_default_channel: "Le canal de discussion par défaut doit être un canal public." direct_message_enabled_groups_invalid: "Vous devez spécifier au moins un groupe pour ce paramètre. Si vous ne souhaitez pas que quiconque, à l'exception des responsables, envoie des messages privés, choisissez le groupe des responsables." @@ -33,12 +35,21 @@ fr: title: "Archivage du canal de discussion terminé" subject_template: "L'archivage du canal de discussion est terminé" text_body_template: | - L'archivage du canal de discussion **\#%{channel_name}** a bien été effectué. Les messages ont été copiés dans le sujet [%{topic_title}](%{topic_url}). + L'archivage du canal de discussion %{channel_hashtag_or_name} a bien été effectué. Les messages ont été copiés dans le sujet [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "Échec de l'archivage du canal de discussion" subject_template: "Échec de l'archivage du canal de discussion" text_body_template: | - L'archivage du canal de discussion **\#%{channel_name}** a échoué. Les messages de %{messages_archived} ont été archivés. Les messages partiellement archivés ont été copiés dans le sujet [%{topic_title}](%{topic_url}). Visitez le canal à l'adresse %{channel_url} pour réessayer. + L'archivage du canal de discussion %{channel_hashtag_or_name} a échoué. Les messages de %{messages_archived} ont été archivés. Les messages partiellement archivés ont été copiés dans le sujet [%{topic_title}](%{topic_url}). Visitez le canal à l'adresse %{channel_url} pour réessayer. + chat_channel_archive_failed_no_topic: + title: "Échec de l'archivage du canal de discussion" + subject_template: "Échec de l'archivage du canal de discussion" + text_body_template: | + L'archivage du canal de discussion %{channel_hashtag_or_name} a échoué. Aucun message n'a été archivé. Le sujet n'a pas été créé pour les raisons suivantes : + + %{topic_validation_errors} + + Visitez le canal à l'adresse %{channel_url} pour réessayer. chat: deleted_chat_username: supprimé errors: @@ -52,6 +63,7 @@ fr: duplicate_message: "Vous avez publié un message identique trop récemment." delete_channel_failed: "Échec de la suppression du canal, veuillez réessayer." minimum_length_not_met: "Le message est trop court. Il doit comporter au moins %{minimum} caractères." + message_too_long: "Le message est trop long, les messages doivent comporter au maximum %{maximum} caractères." max_reactions_limit_reached: "Les nouvelles réactions ne sont pas autorisées sur ce message." message_move_invalid_channel: "Le canal source et le canal de destination doivent être des canaux publics." message_move_no_messages_found: "Aucun message n'a été trouvé avec les ID de message fournis." @@ -167,7 +179,16 @@ fr: from: "%{site_name}" subject: direct_message_from_1: "[%{email_prefix}] Nouveau message de %{username}" + direct_message_from_2: "[%{email_prefix}] Nouveaux messages de %{username1} et de %{username2}" + direct_message_from_more: + one: "[%{email_prefix}] Nouveaux messages de %{username} et de %{count} autre utilisateur" + other: "[%{email_prefix}] Nouveaux messages de %{username} et de %{count} autres utilisateurs" chat_channel_1: "[%{email_prefix}] Nouveau message dans %{channel}" + chat_channel_2: "[%{email_prefix}] Nouveaux messages dans %{channel1} et %{channel2}" + chat_channel_more: + one: "[%{email_prefix}] Nouveaux messages dans %{channel} et %{count} autre canal" + other: "[%{email_prefix}] Nouveaux messages dans %{channel} et %{count} autres canaux" + chat_channel_and_direct_message: "[%{email_prefix}] Nouveau message dans %{channel} de la part de %{username}" unsubscribe: "Ce résumé de discussion est envoyé à partir de %{site_link} lorsque vous vous absentez. Changez vos %{email_preferences_link} ou %{unsubscribe_link} pour vous désabonner." unsubscribe_no_link: "Ce résumé de discussion est envoyé à partir de %{site_link} lorsque vous vous absentez. Changez vos %{email_preferences_link}." view_messages: diff --git a/plugins/chat/config/locales/server.he.yml b/plugins/chat/config/locales/server.he.yml index 2857b27aba..dfa31f74c4 100644 --- a/plugins/chat/config/locales/server.he.yml +++ b/plugins/chat/config/locales/server.he.yml @@ -23,6 +23,7 @@ he: default_emoji_reactions: "רגשות אמוג׳י כברירת מחדל להודעות צ׳אט. ניתן להוסיף עד 5 אמוג׳ים לתגובה מהירה." direct_message_enabled_groups: "לאפשר למשתמשים בקבוצות אלה ליצור צ׳אטים אישיים בין המשתמשים לבין עצמם. הערה: הסגל תמיד יכול ליצור צ׳אטים אישיים, ומשתמשים יוכלו להשיב לצ׳אטים אישיים שיזמו משתמשים שיש להם הרשאה ליצור אותם." chat_message_flag_allowed_groups: "משתמשים בקבוצות אלו רשאים לסמן הודעות צ׳אט בדגל." + max_mentions_per_chat_message: "מספר התראות מרבי של @שם בהן יכול להשתמש משתמש בהודעת צ׳אט." chat_max_direct_message_users: "משתמשים לא יכולים להוסיף יותר מכמות זו של משתמשים אחרים בעת יצירת הודעה ישירה חדשה. יש להגדיר ל־0 כדי לאפשר הודעות עצמיות. הסגל מוחרג מההגדרה הזאת." chat_allow_archiving_channels: "לאפשר לסגל להעביר הודעות לארכיון בנושא בעת סגירת ערוץ." errors: @@ -34,12 +35,21 @@ he: title: "העברת ערוץ הצ׳אט לארכיון הושלמה" subject_template: "העברת ערוץ הצ׳אט לארכיון הושלמה בהצלחה" text_body_template: | - העברת ערוץ הצ׳אט **‎\#%{channel_name}** לארכיון הושלמה בהצלחה. ההודעות הועתקו לנושא [%{topic_title}](%{topic_url}). + העברת הערוץ %{channel_hashtag_or_name} לארכיון הושלמה בהצלחה. ההודעות הועתקו לנושא [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "העברת הצ׳אט לארכיון נכשלה" subject_template: "העברת הצ׳אט לארכיון נכשלה" text_body_template: | - העברת ערוץ הצ׳אט **‎\#%{channel_name}** לארכיון נכשלה. %{messages_archived} הודעות הועברו לארכיון. הודעות שהועברו לארכיון באופן חלקי הועתקו לנושא [%{topic_title}](%{topic_url}). יש לבקר בכתובת הערוץ %{channel_url} כדי לנסות שוב. + העברת הערוץ %{channel_hashtag_or_name} לארכיון נכשלה. %{messages_archived} הודעות הועברו לארכיון. הודעות שהועברו לארכיון למחצה הועתקו לנושא [%{topic_title}](%{topic_url}). יש לבקר בערוץ דרך %{channel_url} כדי לנסות שוב. + chat_channel_archive_failed_no_topic: + title: "העברת הצ׳אט לארכיון נכשלה" + subject_template: "העברת הצ׳אט לארכיון נכשלה" + text_body_template: | + העברת ערוץ הצ׳אט %{channel_hashtag_or_name} לארכיון נכשלה. אף הודעה לא הועברה לארכיון. הנושא לא נוצר כראוי מהסיבות הבאות: + + %{topic_validation_errors} + + ניתן לבקר בערוץ דרך %{channel_url} כדי לנסות שוב. chat: deleted_chat_username: נמחק errors: diff --git a/plugins/chat/config/locales/server.hr.yml b/plugins/chat/config/locales/server.hr.yml index 0b008b6962..4c1336053b 100644 --- a/plugins/chat/config/locales/server.hr.yml +++ b/plugins/chat/config/locales/server.hr.yml @@ -5,6 +5,10 @@ # https://translate.discourse.org/ hr: + system_messages: + chat_channel_archive_failed_no_topic: + title: "Arhiva Chat kanala nije uspjela" + subject_template: "Arhiva kanala za chat nije uspjela" chat: deleted_chat_username: izbrisao errors: diff --git a/plugins/chat/config/locales/server.hu.yml b/plugins/chat/config/locales/server.hu.yml index 2b4c59fd9a..946223ee3f 100644 --- a/plugins/chat/config/locales/server.hu.yml +++ b/plugins/chat/config/locales/server.hu.yml @@ -23,13 +23,12 @@ hu: chat_channel_archive_complete: title: "A csevegőcsatorna archiválása kész" subject_template: "A csevegőcsatorna archiválása sikeresen befejeződött" - text_body_template: | - A(z) **\#%{channel_name}** csevegőcsatorna archiválása sikeresen befejeződött. Az üzenetek átmásolásra kerültek a(z) [ <%{topic_title}](%{topic_url}) témába. chat_channel_archive_failed: title: "A csevegőcsatorna archiválása sikertelen" subject_template: "A csevegőcsatorna archiválása sikertelen" - text_body_template: | - A(z) **\#%{channel_name}** csevegőcsatorna archiválása nem sikerült. %{messages_archived} üzenet archiválásra került. A részben archivált üzeneteket a(z) [%{topic_title}](%{topic_url}) témába lettek másolva. Keresse fel a(z) %{channel_url} csatornát az újbóli próbálkozáshoz. + chat_channel_archive_failed_no_topic: + title: "A csevegőcsatorna archiválása sikertelen" + subject_template: "A csevegőcsatorna archiválása sikertelen" chat: deleted_chat_username: törölt errors: diff --git a/plugins/chat/config/locales/server.it.yml b/plugins/chat/config/locales/server.it.yml index d4d7e27912..ff1f25f59c 100644 --- a/plugins/chat/config/locales/server.it.yml +++ b/plugins/chat/config/locales/server.it.yml @@ -23,7 +23,9 @@ it: default_emoji_reactions: "Reazioni emoji predefinite per i messaggi di chat. Aggiungi fino a 5 emoji per una reazione rapida." direct_message_enabled_groups: "Consenti agli utenti all'interno di questi gruppi di creare chat personali da utente a utente. Nota: lo staff può sempre creare chat personali e gli utenti potranno rispondere alle chat personali avviate da utenti che dispongono dell'autorizzazione per crearle." chat_message_flag_allowed_groups: "Gli utenti di questi gruppi possono contrassegnare i messaggi di chat." + max_mentions_per_chat_message: "Numero massimo di notifiche @name che un utente può utilizzare in un messaggio di chat." chat_max_direct_message_users: "Gli utenti non possono aggiungere più utenti di quelli indicati da questa opzione durante la creazione di un nuovo messaggio diretto. Impostare l'opzione a 0 per consentire solo i messaggi a se stessi. Questa impostazione non si applica allo staff." + chat_allow_archiving_channels: "Consenti allo staff di archiviare i messaggi in un argomento alla chiusura di un canale." errors: chat_default_channel: "Il canale di chat predefinito deve essere un canale pubblico." direct_message_enabled_groups_invalid: "Devi specificare almeno un gruppo per questa impostazione. Se non vuoi che nessuno al di fuori dello staff possa inviare messaggi diretti, scegli il gruppo dello staff." @@ -32,13 +34,12 @@ it: chat_channel_archive_complete: title: "Archiviazione canale chat completata" subject_template: "Archiviazione del canale di chat completata correttamente" - text_body_template: | - L'archiviazione del canale di chat **\#%{channel_name}** è stata completata con successo. I messaggi sono stati copiati nell'argomento [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "Archiviazione canale chat non riuscita" subject_template: "Archiviazione canale chat non riuscita" - text_body_template: | - L'archiviazione del canale di chat **\#%{channel_name}** non è riuscita. %{messages_archived} messaggi sono stati archiviati. I messaggi parzialmente archiviati sono stati copiati nell'argomento [%{topic_title}](%{topic_url}). Visita il canale in %{channel_url} per riprovare. + chat_channel_archive_failed_no_topic: + title: "Archiviazione canale chat non riuscita" + subject_template: "Archiviazione canale chat non riuscita" chat: deleted_chat_username: eliminato errors: @@ -52,6 +53,7 @@ it: duplicate_message: "Hai pubblicato un messaggio identico troppo di recente." delete_channel_failed: "Eliminazione del canale non riuscita, riprova." minimum_length_not_met: "Il messaggio è troppo breve, deve contenere almeno %{minimum} caratteri." + message_too_long: "Il messaggio è troppo lungo. I messaggi devono contenere al massimo %{maximum} caratteri." max_reactions_limit_reached: "Non sono consentite nuove reazioni su questo messaggio." message_move_invalid_channel: "I canali di origine e di destinazione devono essere canali pubblici." message_move_no_messages_found: "Nessun messaggio è stato trovato con gli ID messaggio forniti." @@ -167,7 +169,16 @@ it: from: "%{site_name}" subject: direct_message_from_1: "[%{email_prefix}] Nuovo messaggio da %{username}" + direct_message_from_2: "[%{email_prefix}] Nuovi messaggi da %{username1} e %{username2}" + direct_message_from_more: + one: "[%{email_prefix}] Nuovo messaggio da %{username} e %{count} altro" + other: "[%{email_prefix}] Nuovo messaggio da %{username} e altri %{count}" chat_channel_1: "[%{email_prefix}] Nuovo messaggio in %{channel}" + chat_channel_2: "[%{email_prefix}] Nuovo messaggio in %{channel1} e %{channel2}" + chat_channel_more: + one: "[%{email_prefix}] Nuovo messaggio in %{channel} e %{count} altro" + other: "[%{email_prefix}] Nuovo messaggio in %{channel} e altri %{count}" + chat_channel_and_direct_message: "[%{email_prefix}] Nuovo messaggio in %{channel} e da %{username}" unsubscribe: "Questo riepilogo della chat viene inviato da %{site_link} quando non ci sei. Modifica %{email_preferences_link} o %{unsubscribe_link} per annullare l'iscrizione." unsubscribe_no_link: "Questo riepilogo della chat viene inviato da %{site_link} quando non ci sei. Modifica %{email_preferences_link}." view_messages: diff --git a/plugins/chat/config/locales/server.ja.yml b/plugins/chat/config/locales/server.ja.yml index 1947d4b2a8..74cccff6b5 100644 --- a/plugins/chat/config/locales/server.ja.yml +++ b/plugins/chat/config/locales/server.ja.yml @@ -33,13 +33,12 @@ ja: chat_channel_archive_complete: title: "チャットチャンネルのアーカイブ完了" subject_template: "チャットチャンネルのアーカイブが正常に完了しました" - text_body_template: | - チャットチャンネル **\#%{channel_name}** のアーカイブが正常に完了しました。メッセージはトピック [%{topic_title}](%{topic_url}) にコピーされました。 chat_channel_archive_failed: title: "チャットチャンネルのアーカイブ失敗" subject_template: "チャットチャンネルのアーカイブに失敗しました" - text_body_template: | - チャットチャンネル **\#%{channel_name}** のアーカイブに失敗しました。%{messages_archived} 件のメッセージがアーカイブされました。部分的にアーカイブされたメッセージは、トピック [%{topic_title}](%{topic_url}) にコピーされました。%{channel_url} よりチャンネルにアクセスして、再試行してください。 + chat_channel_archive_failed_no_topic: + title: "チャットチャンネルのアーカイブ失敗" + subject_template: "チャットチャンネルのアーカイブに失敗しました" chat: deleted_chat_username: 削除済み errors: diff --git a/plugins/chat/config/locales/server.nl.yml b/plugins/chat/config/locales/server.nl.yml index b57936113e..391e00314a 100644 --- a/plugins/chat/config/locales/server.nl.yml +++ b/plugins/chat/config/locales/server.nl.yml @@ -33,13 +33,12 @@ nl: chat_channel_archive_complete: title: "Archiveren van chatkanaal voltooid" subject_template: "Archiveren van chatkanaal voltooid" - text_body_template: | - Het archiveren van het chatkanaal **\#%{channel_name}** is voltooid. De berichten zijn gekopieerd naar het topic [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "Archiveren van chatkanaal mislukt" subject_template: "Archiveren van chatkanaal mislukt" - text_body_template: | - Het archiveren van het chatkanaal **\#%{channel_name}** is mislukt. %{messages_archived} berichten zijn gearchiveerd. Gedeeltelijk gearchiveerde berichten zijn gekopieerd naar het topic [%{topic_title}](%{topic_url}). Ga naar het kanaal op %{channel_url} om het opnieuw te proberen. + chat_channel_archive_failed_no_topic: + title: "Archiveren van kanaal mislukt" + subject_template: "Chatkanaal archiveren mislukt" chat: deleted_chat_username: verwijderd errors: diff --git a/plugins/chat/config/locales/server.pt_BR.yml b/plugins/chat/config/locales/server.pt_BR.yml index 68de3506f5..9768013a03 100644 --- a/plugins/chat/config/locales/server.pt_BR.yml +++ b/plugins/chat/config/locales/server.pt_BR.yml @@ -23,6 +23,7 @@ pt_BR: default_emoji_reactions: "Reações de emoji padrão para mensagens do chat. Adicione até cinco emojis para uma reação rápida." direct_message_enabled_groups: "Permitir que os(as) usuários(as) desses grupos criem chats pessoais de usuário para usuário(a). Observação: a equipe sempre pode criar chats pessoais e os(as) usuários(as) poderão responder aos chats pessoais iniciados por usuários que tenham permissão para criá-los." chat_message_flag_allowed_groups: "Os(as) usuários(as) desses grupos podem sinalizar mensagens do chat." + max_mentions_per_chat_message: "Quantidade máxima de notificações @nome que um(a) usuário(a) pode usar numa mensagem de chat." chat_max_direct_message_users: "Os(as) usuários(as) não poderão adicionar mais do que essa quantidade de usuários(as) ao criar uma nova mensagem direta. Defina como 0 para apenas mostrar mensagens para si. A equipe está isenta desta configuração." chat_allow_archiving_channels: "Permita que a equipe arquive mensagens em um tópico ao encerrar um canal." errors: @@ -34,12 +35,21 @@ pt_BR: title: "Arquivamento do Canal de Chat Concluído" subject_template: "Arquivo do canal de chat concluído com sucesso" text_body_template: | - O arquivamento do canal de chat **\#%{channel_name}** foi concluído com êxito. As mensagens foram copiadas para o tópico [%{topic_title}](%{topic_url}). + O arquivamento do canal de chat %{channel_hashtag_or_name} foi concluído com êxito. As mensagens foram copiadas para o tópico [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "Falha no Arquivamento do Canal de Chat" subject_template: "Falha no arquivamento do canal de chat" text_body_template: | - Falha ao realizar o arquivamento do canal de chat **\#%{channel_name}**. %{messages_archived} mensagens foram arquivadas. As mensagens parcialmente arquivadas foram copiadas para o tópico [%{topic_title}](%{topic_url}). Visite o canal em %{channel_url} para tentar novamente. + Falha ao realizar o arquivamento do canal de chat %{channel_hashtag_or_name}. %{messages_archived} mensagens foram arquivadas. As mensagens parcialmente arquivadas foram copiadas para o tópico [%{topic_title}](%{topic_url}). Visite o canal em %{channel_url} para tentar novamente. + chat_channel_archive_failed_no_topic: + title: "Falha no Arquivamento do Canal de Chat" + subject_template: "Falha no arquivamento do canal de chat" + text_body_template: | + Falha ao arquivar o canal de chat %{channel_hashtag_or_name} . Nenhuma mensagem foi arquivada. O tópico não foi criado com sucesso pelas seguintes razões: + + %{topic_validation_errors} + + Visite o canal em %{channel_url} para tentar novamente. chat: deleted_chat_username: excluído errors: @@ -53,6 +63,7 @@ pt_BR: duplicate_message: "Você postou uma mensagem idêntica muito recentemente." delete_channel_failed: "Falha ao excluir canal. Tente novamente." minimum_length_not_met: "A mensagem é muito curta, deve ter no mínimo %{minimum} caracteres." + message_too_long: "A mensagem é muito longa; as mensagens devem ter no máximo %{maximum} caracteres." max_reactions_limit_reached: "Novas reações não são permitidas nesta mensagem." message_move_invalid_channel: "O canal de origem e de destino deve ser canais públicos." message_move_no_messages_found: "Nenhuma mensagem foi encontrada com os IDs de mensagem fornecidos." @@ -168,7 +179,16 @@ pt_BR: from: "%{site_name}" subject: direct_message_from_1: "[%{email_prefix}] Nova mensagem de %{username}" + direct_message_from_2: "[%{email_prefix}] Nova mensagem de %{username1} e %{username2}" + direct_message_from_more: + one: "[%{email_prefix}] Nova mensagem de %{username} e %{count} outro" + other: "[%{email_prefix}] Nova mensagem de %{username} e %{count} outros" chat_channel_1: "[%{email_prefix}] Nova mensagem de %{channel}" + chat_channel_2: "[%{email_prefix}] Nova mensagem em %{channel1} e %{channel2}" + chat_channel_more: + one: "[%{email_prefix}] Nova mensagem em %{channel} e %{count} outro" + other: "[%{email_prefix}] Nova mensagem em %{channel} e %{count} outros" + chat_channel_and_direct_message: "[%{email_prefix}] Nova mensagem em %{channel} e de %{username}" unsubscribe: "Este resumo do chat será enviado de %{site_link} quando você estiver ausente. Altere seu %{email_preferences_link}, ou %{unsubscribe_link} para cancelar a inscrição." unsubscribe_no_link: "Este resumo do chat será enviado de %{site_link} quando você estiver ausente. Altere seu %{email_preferences_link}." view_messages: diff --git a/plugins/chat/config/locales/server.ru.yml b/plugins/chat/config/locales/server.ru.yml index 61b76202e5..ad56d2df82 100644 --- a/plugins/chat/config/locales/server.ru.yml +++ b/plugins/chat/config/locales/server.ru.yml @@ -30,13 +30,12 @@ ru: chat_channel_archive_complete: title: "Архивация канала завершена" subject_template: "Архивация канала успешно завершена" - text_body_template: | - Архивация канала **\#%{channel_name}** успешно завершена. Сообщения были скопированы в тему [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "Не удалось заархивировать канал" subject_template: "Не удалось заархивировать канал" - text_body_template: | - Не удалось заархивировать канал **\#%{channel_name}**. Сообщения %{messages_archived} были заархивированы. Частично заархивированные сообщения были скопированы в тему [%{topic_title}](%{topic_url}). Посетите канал %{channel_url} и повторите попытку. + chat_channel_archive_failed_no_topic: + title: "Не удалось заархивировать канал" + subject_template: "Не удалось заархивировать канал" chat: deleted_chat_username: удалён errors: @@ -114,7 +113,7 @@ ru: multi_user_truncated: "%{users} и ещё %{leftover}" category_channel: errors: - slug_contains_non_ascii_chars: "Содержит символы не в ascii-кодировке" + slug_contains_non_ascii_chars: "содержит символы не в ascii-кодировке" bookmarkable: notification_title: "Сообщение в канале %{channel_name}" personal_chat: "личный чат" @@ -126,7 +125,7 @@ ru: one: "%{count} участник" few: "%{count} участника" many: "%{count} участников" - other: "%{count} участников" + other: "%{count} участника" and_x_others: one: "и ещё %{count}" few: "и ещё %{count}" @@ -169,7 +168,7 @@ ru: from: "%{site_name}" subject: direct_message_from_1: "[%{email_prefix}] Новое сообщение от %{username}" - chat_channel_1: "[%{email_prefix}] Новое сообщение в %{channel}" + chat_channel_1: "[%{email_prefix}] Новое сообщение в канале «%{channel}»" unsubscribe: "Этот дайджест чата рассылается с сайта %{site_link} в период вашего отсутствия на форуме. Для отмены подписки измените %{email_preferences_link} или %{unsubscribe_link}." unsubscribe_no_link: "Этот дайджест чата рассылается с сайта %{site_link} в период вашего отсутствия на форуме. Настройка рассылки: %{email_preferences_link}." view_messages: diff --git a/plugins/chat/config/locales/server.sv.yml b/plugins/chat/config/locales/server.sv.yml index f26863f495..b979616ca0 100644 --- a/plugins/chat/config/locales/server.sv.yml +++ b/plugins/chat/config/locales/server.sv.yml @@ -35,12 +35,21 @@ sv: title: "Arkivering av chattkanalen är färdigt" subject_template: "Arkivering av chattkanalen slutfördes framgångsrikt" text_body_template: | - Arkivering av chattkanalen **\#%{channel_name}** har slutförts. Meddelandena har kopierats till ämnet [%{topic_title}](%{topic_url}). + Arkiveringen av chattkanalen %{channel_hashtag_or_name} har slutförts. Meddelanden kopierades till ämnet [%{topic_title}](%{topic_url}). chat_channel_archive_failed: title: "Arkivering av chattkanalen misslyckades" subject_template: "Arkivering av chattkanalen misslyckades" text_body_template: | - Arkivering av chatt kanalen **\#%{channel_name}** misslyckades. %{messages_archived} meddelanden har arkiverats. Delvis arkiverade meddelanden kopierades till ämnet [%{topic_title}](%{topic_url}). Besök kanalen på %{channel_url} för att försöka igen. + Arkivering av chattkanalen %{channel_hashtag_or_name} misslyckades. %{messages_archived} meddelanden har arkiverats. Delvis arkiverade meddelanden kopierades till ämnet [%{topic_title}](%{topic_url}). Besök kanalen kl. %{channel_url} för att försöka igen. + chat_channel_archive_failed_no_topic: + title: "Arkivering av chattkanalen misslyckades" + subject_template: "Arkivering av chattkanalen misslyckades" + text_body_template: | + Arkivering av chattkanalen %{channel_hashtag_or_name} misslyckades. Inga meddelanden har arkiverats. Ämnet kunde inte skapas, av följande skäl: + + %{topic_validation_errors} + + Besök kanalen på %{channel_url} för att försöka igen. chat: deleted_chat_username: raderat errors: diff --git a/plugins/chat/config/locales/server.tr_TR.yml b/plugins/chat/config/locales/server.tr_TR.yml index 0cdb8d7ca6..6e61df1109 100644 --- a/plugins/chat/config/locales/server.tr_TR.yml +++ b/plugins/chat/config/locales/server.tr_TR.yml @@ -34,13 +34,12 @@ tr_TR: chat_channel_archive_complete: title: "Sohbet Kanalı Arşivlemesi Tamamlandı" subject_template: "Sohbet kanalı arşivlemesi başarıyla tamamlandı" - text_body_template: | - **\#%{channel_name}** sohbet kanalının arşivlenmesi başarıyla tamamlandı. Mesajlar [%{topic_title}](%{topic_url}) konusuna kopyalandı. chat_channel_archive_failed: title: "Sohbet Kanalı Arşivleme Başarısız" subject_template: "Sohbet kanalı arşivleme başarısız" - text_body_template: | - **\#%{channel_name}** sohbet kanalının arşivlenmesi başarısız oldu. %{messages_archived} mesaj arşivlendi. Kısmen arşivlenen mesajlar [%{topic_title}](%{topic_url}) konusuna kopyalandı. Yeniden denemek için %{channel_url} adresinden kanalı ziyaret edin. + chat_channel_archive_failed_no_topic: + title: "Sohbet Kanalı Arşivlenemedi" + subject_template: "Sohbet kanalı arşivlenemedi" chat: deleted_chat_username: silindi errors: diff --git a/plugins/chat/config/locales/server.zh_CN.yml b/plugins/chat/config/locales/server.zh_CN.yml index fa3c06623e..8ea370ee00 100644 --- a/plugins/chat/config/locales/server.zh_CN.yml +++ b/plugins/chat/config/locales/server.zh_CN.yml @@ -23,6 +23,7 @@ zh_CN: default_emoji_reactions: "聊天消息的默认表情符号回应。最多可添加 5 个表情符号进行快速回应。" direct_message_enabled_groups: "允许这些群组中的用户创建用户间的个人聊天。请注意:管理人员始终可以创建个人聊天,用户将能够回复有权创建个人聊天的用户发起的个人聊天。" chat_message_flag_allowed_groups: "这些群组中的用户可以举报聊天消息。" + max_mentions_per_chat_message: "用户可以在聊天消息中使用的 @name 通知的最大数量。" chat_max_direct_message_users: "在创建新的直接消息时,用户无法添加超过此数量的其他用户。设置为 0 只允许给自己发送消息。管理人员不受此设置的影响。" chat_allow_archiving_channels: "允许管理人员在关闭频道时将消息归档到某个话题。" errors: @@ -33,13 +34,12 @@ zh_CN: chat_channel_archive_complete: title: "聊天频道归档完成" subject_template: "聊天频道归档成功完成" - text_body_template: | - 聊天频道**\#%{channel_name}**归档已成功完成。消息已被复制到话题[%{topic_title}](%{topic_url})中。 chat_channel_archive_failed: title: "聊天频道归档失败" subject_template: "聊天频道归档失败" - text_body_template: | - 聊天频道**#%{channel_name}**归档失败。%{messages_archived} 条消息已被归档。部分归档的消息已被复制到话题[%{topic_title}](%{topic_url})。请访问 %{channel_url} 下的频道以重试。 + chat_channel_archive_failed_no_topic: + title: "聊天频道归档失败" + subject_template: "聊天频道归档失败" chat: deleted_chat_username: 已删除 errors: @@ -53,6 +53,7 @@ zh_CN: duplicate_message: "您在短时间内发布了一条相同的消息。" delete_channel_failed: "删除频道失败,请重试。" minimum_length_not_met: "消息太短,必须至少有 %{minimum} 个字符。" + message_too_long: "消息过长,最多只能包含 %{maximum} 个字符。" max_reactions_limit_reached: "此消息不允许有新的回应。" message_move_invalid_channel: "源频道和目标频道必须是公共频道。" message_move_no_messages_found: "找不到带有提供的消息 ID 的消息。" @@ -161,6 +162,16 @@ zh_CN: description: other: "您有新的聊天消息" from: "%{site_name}" + subject: + direct_message_from_1: "[%{email_prefix}] 来自 %{username} 的新消息" + direct_message_from_2: "[%{email_prefix}] 来自 %{username1} 和 %{username2} 的新消息" + direct_message_from_more: + other: "[%{email_prefix}] 来自 %{username} 和其他 %{count} 位用户的新消息" + chat_channel_1: "[%{email_prefix}] %{channel} 中的新消息" + chat_channel_2: "[%{email_prefix}] %{channel1} 和 %{channel2} 中的新消息" + chat_channel_more: + other: "[%{email_prefix}] %{channel} 和其他 %{count} 个频道的新消息" + chat_channel_and_direct_message: "[%{email_prefix}] %{channel} 中和来自 %{username} 的新消息" unsubscribe: "此聊天摘要在您离开时从%{site_link}发送。更改您的%{email_preferences_link},或者%{unsubscribe_link}以退订。" unsubscribe_no_link: "此聊天摘要在您离开时从%{site_link}发送。更改您的%{email_preferences_link}。" view_messages: diff --git a/plugins/chat/config/settings.yml b/plugins/chat/config/settings.yml index 072e9d9792..015477dc03 100644 --- a/plugins/chat/config/settings.yml +++ b/plugins/chat/config/settings.yml @@ -110,3 +110,6 @@ chat: default: 5 max: 10 min: 0 + max_chat_draft_length: + default: 50_000 + hidden: true diff --git a/plugins/chat/db/migrate/20230123020036_move_chat_uploads_to_upload_references.rb b/plugins/chat/db/migrate/20230123020036_move_chat_uploads_to_upload_references.rb new file mode 100644 index 0000000000..22dafbcff2 --- /dev/null +++ b/plugins/chat/db/migrate/20230123020036_move_chat_uploads_to_upload_references.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class MoveChatUploadsToUploadReferences < ActiveRecord::Migration[7.0] + def up + execute <<~SQL + INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at) + SELECT chat_uploads.upload_id, 'ChatMessage', chat_uploads.chat_message_id, chat_uploads.created_at, chat_uploads.updated_at + FROM chat_uploads + INNER JOIN uploads ON uploads.id = chat_uploads.upload_id + ON CONFLICT DO NOTHING + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/plugins/chat/db/post_migrate/20230116090324_drop_chat_drafts_over_max_length.rb b/plugins/chat/db/post_migrate/20230116090324_drop_chat_drafts_over_max_length.rb new file mode 100644 index 0000000000..1d6913086f --- /dev/null +++ b/plugins/chat/db/post_migrate/20230116090324_drop_chat_drafts_over_max_length.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class DropChatDraftsOverMaxLength < ActiveRecord::Migration[7.0] + def up + if table_exists?(:chat_drafts) + # Delete abusive drafts + execute <<~SQL + DELETE FROM chat_drafts + WHERE LENGTH(data) > 50000 + SQL + end + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/plugins/chat/db/post_migrate/20230123025112_move_chat_uploads_to_upload_references_post.rb b/plugins/chat/db/post_migrate/20230123025112_move_chat_uploads_to_upload_references_post.rb new file mode 100644 index 0000000000..02ec2dfbff --- /dev/null +++ b/plugins/chat/db/post_migrate/20230123025112_move_chat_uploads_to_upload_references_post.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class MoveChatUploadsToUploadReferencesPost < ActiveRecord::Migration[7.0] + def up + execute <<~SQL + INSERT INTO upload_references(upload_id, target_type, target_id, created_at, updated_at) + SELECT chat_uploads.upload_id, 'ChatMessage', chat_uploads.chat_message_id, chat_uploads.created_at, chat_uploads.updated_at + FROM chat_uploads + INNER JOIN uploads ON uploads.id = chat_uploads.upload_id + ON CONFLICT DO NOTHING + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/plugins/chat/docs/FRONTEND.md b/plugins/chat/docs/FRONTEND.md new file mode 100644 index 0000000000..8d81aafc50 --- /dev/null +++ b/plugins/chat/docs/FRONTEND.md @@ -0,0 +1,352 @@ +## Modules + +
    +
    Collection
    +
    +
    ChatApi
    +
    +
    + + + +## Collection + +* [Collection](#module_Collection) + * [module.exports](#exp_module_Collection--module.exports) ⏏ + * [new module.exports(resourceURL, handler)](#new_module_Collection--module.exports_new) + * [.load()](#module_Collection--module.exports+load) ⇒ Promise + * [.loadMore()](#module_Collection--module.exports+loadMore) ⇒ Promise + + +* * * + + + +### module.exports ⏏ +Handles a paginated API response. + +**Kind**: Exported class + +* * * + + + +#### new module.exports(resourceURL, handler) +Create a Collection instance + + +| Param | Type | Description | +| --- | --- | --- | +| resourceURL | string | the API endpoint to call | +| handler | callback | anonymous function used to handle the response | + + +* * * + + + +#### module.exports.load() ⇒ Promise +Loads first batch of results + +**Kind**: instance method of [module.exports](#exp_module_Collection--module.exports) + +* * * + + + +#### module.exports.loadMore() ⇒ Promise +Attempts to load more results + +**Kind**: instance method of [module.exports](#exp_module_Collection--module.exports) + +* * * + + + +## ChatApi + +* [ChatApi](#module_ChatApi) + * [module.exports](#exp_module_ChatApi--module.exports) ⏏ + * [.channel(channelId)](#module_ChatApi--module.exports+channel) ⇒ Promise + * [.channels()](#module_ChatApi--module.exports+channels) ⇒ [module.exports](#exp_module_Collection--module.exports) + * [.moveChannelMessages(channelId, data)](#module_ChatApi--module.exports+moveChannelMessages) ⇒ Promise + * [.destroyChannel(channelId, channelName)](#module_ChatApi--module.exports+destroyChannel) ⇒ Promise + * [.createChannel(data)](#module_ChatApi--module.exports+createChannel) ⇒ Promise + * [.categoryPermissions(categoryId)](#module_ChatApi--module.exports+categoryPermissions) ⇒ Promise + * [.sendMessage(channelId, data)](#module_ChatApi--module.exports+sendMessage) ⇒ Promise + * [.createChannelArchive(channelId, data)](#module_ChatApi--module.exports+createChannelArchive) ⇒ Promise + * [.updateChannel(channelId, data)](#module_ChatApi--module.exports+updateChannel) ⇒ Promise + * [.updateChannelStatus(channelId, status)](#module_ChatApi--module.exports+updateChannelStatus) ⇒ Promise + * [.listChannelMemberships(channelId)](#module_ChatApi--module.exports+listChannelMemberships) ⇒ [module.exports](#exp_module_Collection--module.exports) + * [.listCurrentUserChannels()](#module_ChatApi--module.exports+listCurrentUserChannels) ⇒ Promise + * [.followChannel(channelId)](#module_ChatApi--module.exports+followChannel) ⇒ Promise + * [.unfollowChannel(channelId)](#module_ChatApi--module.exports+unfollowChannel) ⇒ Promise + * [.updateCurrentUserChannelNotificationsSettings(channelId, data)](#module_ChatApi--module.exports+updateCurrentUserChannelNotificationsSettings) ⇒ Promise + + +* * * + + + +### module.exports ⏏ +Chat API service. Provides methods to interact with the chat API. + +**Kind**: Exported class +**Implements**: {@ember/service} + +* * * + + + +#### module.exports.channel(channelId) ⇒ Promise +Get a channel by its ID. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | + +**Example** +```js +this.chatApi.channel(1).then(channel => { ... }) +``` + +* * * + + + +#### module.exports.channels() ⇒ [module.exports](#exp_module_Collection--module.exports) +List all accessible category channels of the current user. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) +**Example** +```js +this.chatApi.channels.then(channels => { ... }) +``` + +* * * + + + +#### module.exports.moveChannelMessages(channelId, data) ⇒ Promise +Moves messages from one channel to another. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the original channel. | +| data | object | Params of the move. | +| data.message_ids | Array.<number> | IDs of the moved messages. | +| data.destination_channel_id | number | ID of the channel where the messages are moved to. | + +**Example** +```js +this.chatApi + .moveChannelMessages(1, { + message_ids: [2, 3], + destination_channel_id: 4, + }).then(() => { ... }) +``` + +* * * + + + +#### module.exports.destroyChannel(channelId, channelName) ⇒ Promise +Destroys a channel. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | +| channelName | string | The name of the channel to be destroyed, used as confirmation. | + +**Example** +```js +this.chatApi.destroyChannel(1, "foo").then(() => { ... }) +``` + +* * * + + + +#### module.exports.createChannel(data) ⇒ Promise +Creates a channel. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| data | object | Params of the channel. | +| data.name | string | The name of the channel. | +| data.chatable_id | string | The category of the channel. | +| data.description | string | The description of the channel. | +| [data.auto_join_users] | boolean | Should users join this channel automatically. | + +**Example** +```js +this.chatApi + .createChannel({ name: "foo", chatable_id: 1, description "bar" }) + .then((channel) => { ... }) +``` + +* * * + + + +#### module.exports.categoryPermissions(categoryId) ⇒ Promise +Lists chat permissions for a category. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| categoryId | number | ID of the category. | + + +* * * + + + +#### module.exports.sendMessage(channelId, data) ⇒ Promise +Sends a message. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | ID of the channel. | +| data | object | Params of the message. | +| data.message | string | The raw content of the message in markdown. | +| data.cooked | string | The cooked content of the message. | +| [data.in_reply_to_id] | number | The ID of the replied-to message. | +| [data.staged_id] | number | The staged ID of the message before it was persisted. | +| [data.upload_ids] | Array.<number> | Array of upload ids linked to the message. | + + +* * * + + + +#### module.exports.createChannelArchive(channelId, data) ⇒ Promise +Creates a channel archive. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | +| data | object | Params of the archive. | +| data.selection | string | "new_topic" or "existing_topic". | +| [data.title] | string | Title of the topic when creating a new topic. | +| [data.category_id] | string | ID of the category used when creating a new topic. | +| [data.tags] | Array.<string> | tags used when creating a new topic. | +| [data.topic_id] | string | ID of the topic when using an existing topic. | + + +* * * + + + +#### module.exports.updateChannel(channelId, data) ⇒ Promise +Updates a channel. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | +| data | object | Params of the archive. | +| [data.description] | string | Description of the channel. | +| [data.name] | string | Name of the channel. | + + +* * * + + + +#### module.exports.updateChannelStatus(channelId, status) ⇒ Promise +Updates the status of a channel. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | +| status | string | The new status, can be "open" or "closed". | + + +* * * + + + +#### module.exports.listChannelMemberships(channelId) ⇒ [module.exports](#exp_module_Collection--module.exports) +Lists members of a channel. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | + + +* * * + + + +#### module.exports.listCurrentUserChannels() ⇒ Promise +Lists public and direct message channels of the current user. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +* * * + + + +#### module.exports.followChannel(channelId) ⇒ Promise +Makes current user follow a channel. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | + + +* * * + + + +#### module.exports.unfollowChannel(channelId) ⇒ Promise +Makes current user unfollow a channel. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | + + +* * * + + + +#### module.exports.updateCurrentUserChannelNotificationsSettings(channelId, data) ⇒ Promise +Update notifications settings of current user for a channel. + +**Kind**: instance method of [module.exports](#exp_module_ChatApi--module.exports) + +| Param | Type | Description | +| --- | --- | --- | +| channelId | number | The ID of the channel. | +| data | object | The settings to modify. | +| [data.muted] | boolean | Mutes the channel. | +| [data.desktop_notification_level] | string | Notifications level on desktop: never, mention or always. | +| [data.mobile_notification_level] | string | Notifications level on mobile: never, mention or always. | + + +* * * + diff --git a/plugins/chat/lib/chat_channel_archive_service.rb b/plugins/chat/lib/chat_channel_archive_service.rb index 45196c1f93..cf656ef4ca 100644 --- a/plugins/chat/lib/chat_channel_archive_service.rb +++ b/plugins/chat/lib/chat_channel_archive_service.rb @@ -2,9 +2,7 @@ ## # From time to time, site admins may choose to sunset a chat channel and archive -# the messages within. The main use case for this is a topic-based channel, but -# it can be used for category channels just fine. It cannot be used for DM channels -# in its current iteration. +# the messages within. It cannot be used for DM channels in its current iteration. # # To archive a channel, we mark it read_only first to prevent any further message # additions or changes, and create a record to track whether the archive topic @@ -17,9 +15,29 @@ class Chat::ChatChannelArchiveService ARCHIVED_MESSAGES_PER_POST = 100 - def self.begin_archive_process(chat_channel:, acting_user:, topic_params:) + class ArchiveValidationError < StandardError + attr_reader :errors + + def initialize(errors: []) + super + @errors = errors + end + end + + def self.create_archive_process(chat_channel:, acting_user:, topic_params:) return if ChatChannelArchive.exists?(chat_channel: chat_channel) + # Only need to validate topic params for a new topic, not an existing one. + if topic_params[:topic_id].blank? + valid, errors = + Chat::ChatChannelArchiveService.validate_topic_params( + Guardian.new(acting_user), + topic_params, + ) + + raise ArchiveValidationError.new(errors: errors) if !valid + end + ChatChannelArchive.transaction do chat_channel.read_only!(acting_user) @@ -48,6 +66,21 @@ class Chat::ChatChannelArchiveService chat_channel.chat_channel_archive end + def self.validate_topic_params(guardian, topic_params) + topic_creator = + TopicCreator.new( + Discourse.system_user, + guardian, + { + title: topic_params[:topic_title], + category: topic_params[:category_id], + tags: topic_params[:tags], + import_mode: true, + }, + ) + [topic_creator.valid?, topic_creator.errors.full_messages] + end + attr_reader :chat_channel_archive, :chat_channel, :chat_channel_title def initialize(chat_channel_archive) @@ -60,22 +93,22 @@ class Chat::ChatChannelArchiveService chat_channel_archive.update(archive_error: nil) begin - ensure_destination_topic_exists! + return if !ensure_destination_topic_exists! Rails.logger.info( "Creating posts from message batches for #{chat_channel_title} archive, #{chat_channel_archive.total_messages} messages to archive (#{chat_channel_archive.total_messages / ARCHIVED_MESSAGES_PER_POST} posts).", ) - # a batch should be idempotent, either the post is created and the + # A batch should be idempotent, either the post is created and the # messages are deleted or we roll back the whole thing. # - # at some point we may want to reconsider disabling post validations, + # At some point we may want to reconsider disabling post validations, # and add in things like dynamic resizing of the number of messages per - # post based on post length, but that can be done later + # post based on post length, but that can be done later. # - # another future improvement is to send a MessageBus message for each + # Another future improvement is to send a MessageBus message for each # completed batch, so the UI can receive updates and show a progress - # bar or something similar + # bar or something similar. chat_channel .chat_messages .find_in_batches(batch_size: ARCHIVED_MESSAGES_PER_POST) do |chat_messages| @@ -95,7 +128,7 @@ class Chat::ChatChannelArchiveService kick_all_users complete_archive rescue => err - notify_archiver(:failed, error: err) + notify_archiver(:failed, error_message: err.message) raise err end end @@ -144,29 +177,44 @@ class Chat::ChatChannelArchiveService }, ) - chat_channel_archive.update!(destination_topic: topic_creator.create) + if topic_creator.valid? + chat_channel_archive.update!(destination_topic: topic_creator.create) + else + Rails.logger.info("Destination topic for #{chat_channel_title} archive was not valid.") + notify_archiver( + :failed_no_topic, + error_message: topic_creator.errors.full_messages.join("\n"), + ) + end end - Rails.logger.info("Creating first post for #{chat_channel_title} archive.") - create_post( - I18n.t( - "chat.channel.archive.first_post_raw", - channel_name: chat_channel_title, - channel_url: chat_channel.url, - ), - ) + if chat_channel_archive.destination_topic.present? + Rails.logger.info("Creating first post for #{chat_channel_title} archive.") + create_post( + I18n.t( + "chat.channel.archive.first_post_raw", + channel_name: chat_channel_title, + channel_url: chat_channel.url, + ), + ) + end else Rails.logger.info("Topic already exists for #{chat_channel_title} archive.") end - update_destination_topic_status + if chat_channel_archive.destination_topic.present? + update_destination_topic_status + return true + end + + false end def update_destination_topic_status - # we only want to do this when the destination topic is new, not an + # We only want to do this when the destination topic is new, not an # existing topic, because we don't want to update the status unexpectedly # on an existing topic - if chat_channel_archive.destination_topic_title.present? + if chat_channel_archive.new_topic? if SiteSetting.chat_archive_destination_topic_status == "archived" chat_channel_archive.destination_topic.update!(archived: true) elsif SiteSetting.chat_archive_destination_topic_status == "closed" @@ -198,16 +246,17 @@ class Chat::ChatChannelArchiveService notify_archiver(:success) end - def notify_archiver(result, error: nil) + def notify_archiver(result, error_message: nil) base_translation_params = { - channel_name: chat_channel_title, - topic_title: chat_channel_archive.destination_topic.title, - topic_url: chat_channel_archive.destination_topic.url, + channel_hashtag_or_name: channel_hashtag_or_name, + topic_title: chat_channel_archive.destination_topic&.title, + topic_url: chat_channel_archive.destination_topic&.url, + topic_validation_errors: result == :failed_no_topic ? error_message : nil, } - if result == :failed + if result == :failed || result == :failed_no_topic Discourse.warn_exception( - error, + error_message, message: "Error when archiving chat channel #{chat_channel_title}.", env: { chat_channel_id: chat_channel.id, @@ -219,10 +268,17 @@ class Chat::ChatChannelArchiveService channel_url: chat_channel.url, messages_archived: chat_channel_archive.archived_messages, ) - chat_channel_archive.update(archive_error: error.message) + chat_channel_archive.update(archive_error: error_message) + message_translation_key = + case result + when :failed + :chat_channel_archive_failed + when :failed_no_topic + :chat_channel_archive_failed_no_topic + end SystemMessage.create_from_system_user( chat_channel_archive.archived_by, - :chat_channel_archive_failed, + message_translation_key, error_translation_params, ) else @@ -235,7 +291,7 @@ class Chat::ChatChannelArchiveService ChatPublisher.publish_archive_status( chat_channel, - archive_status: result, + archive_status: result != :success ? :failed : :success, archived_messages: chat_channel_archive.archived_messages, archive_topic_id: chat_channel_archive.destination_topic_id, total_messages: chat_channel_archive.total_messages, @@ -245,4 +301,11 @@ class Chat::ChatChannelArchiveService def kick_all_users Chat::ChatChannelMembershipManager.new(chat_channel).unfollow_all_users end + + def channel_hashtag_or_name + if chat_channel.slug.present? && SiteSetting.enable_experimental_hashtag_autocomplete + return "##{chat_channel.slug}::channel" + end + chat_channel_title + end end diff --git a/plugins/chat/lib/chat_message_updater.rb b/plugins/chat/lib/chat_message_updater.rb index 04e8ae9372..79ecfed4e2 100644 --- a/plugins/chat/lib/chat_message_updater.rb +++ b/plugins/chat/lib/chat_message_updater.rb @@ -83,7 +83,8 @@ class Chat::ChatMessageUpdater def update_uploads(upload_info) return unless upload_info[:changed] - ChatUpload.where(chat_message: @chat_message).destroy_all + DB.exec("DELETE FROM chat_uploads WHERE chat_message_id = #{@chat_message.id}") + UploadReference.where(target: @chat_message).destroy_all @chat_message.attach_uploads(upload_info[:uploads]) end diff --git a/plugins/chat/lib/chat_transcript_service.rb b/plugins/chat/lib/chat_transcript_service.rb index 6326494cdd..f61421d35f 100644 --- a/plugins/chat/lib/chat_transcript_service.rb +++ b/plugins/chat/lib/chat_transcript_service.rb @@ -147,7 +147,7 @@ class ChatTranscriptService def messages @messages ||= ChatMessage - .includes(:user, chat_uploads: :upload) + .includes(:user, upload_references: :upload) .where(id: @message_ids, chat_channel_id: @channel.id) .order(:created_at) end diff --git a/plugins/chat/lib/message_mover.rb b/plugins/chat/lib/message_mover.rb index fc290f757c..11cd84ac33 100644 --- a/plugins/chat/lib/message_mover.rb +++ b/plugins/chat/lib/message_mover.rb @@ -125,10 +125,10 @@ class Chat::MessageMover SQL DB.exec(<<~SQL) - UPDATE chat_uploads cu - SET chat_message_id = mm.new_chat_message_id + UPDATE upload_references uref + SET target_id = mm.new_chat_message_id FROM moved_chat_messages mm - WHERE cu.chat_message_id = mm.old_chat_message_id + WHERE uref.target_id = mm.old_chat_message_id AND uref.target_type = 'ChatMessage' SQL DB.exec(<<~SQL) diff --git a/plugins/chat/lib/tasks/chat_doc.rake b/plugins/chat/lib/tasks/chat_doc.rake new file mode 100644 index 0000000000..98fb9553e3 --- /dev/null +++ b/plugins/chat/lib/tasks/chat_doc.rake @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +task "chat:doc" do + destination = File.join(Rails.root, "plugins/chat/docs/FRONTEND.md") + config = File.join(Rails.root, ".jsdoc") + + files = %w[ + plugins/chat/assets/javascripts/discourse/lib/collection.js + plugins/chat/assets/javascripts/discourse/services/chat-api.js + ] + + `yarn --silent jsdoc2md --separators -c #{config} -f #{files.join(" ")} > #{destination}` +end diff --git a/plugins/chat/plugin.rb b/plugins/chat/plugin.rb index b3b5070a19..10e0cb4d3f 100644 --- a/plugins/chat/plugin.rb +++ b/plugins/chat/plugin.rb @@ -13,6 +13,7 @@ register_asset "stylesheets/mixins/chat-scrollbar.scss" register_asset "stylesheets/common/core-extensions.scss" register_asset "stylesheets/common/chat-emoji-picker.scss" register_asset "stylesheets/common/chat-channel-card.scss" +register_asset "stylesheets/common/create-channel-modal.scss" register_asset "stylesheets/common/dc-filter-input.scss" register_asset "stylesheets/common/common.scss" register_asset "stylesheets/common/chat-browse.scss" @@ -371,14 +372,6 @@ after_initialize do end end - if respond_to?(:register_upload_unused) - register_upload_unused do |uploads| - uploads.joins("LEFT JOIN chat_uploads cu ON cu.upload_id = uploads.id").where( - "cu.upload_id IS NULL", - ) - end - end - if respond_to?(:register_upload_in_use) register_upload_in_use do |upload| ChatMessage.where( @@ -455,6 +448,8 @@ after_initialize do add_to_serializer(:current_user, :chat_drafts) do ChatDraft .where(user_id: object.id) + .order(updated_at: :desc) + .limit(20) .pluck(:chat_channel_id, :data) .map { |row| { channel_id: row[0], data: row[1] } } end diff --git a/plugins/chat/spec/components/chat_message_creator_spec.rb b/plugins/chat/spec/components/chat_message_creator_spec.rb index 45f3a4ad2e..0f6f56a8b9 100644 --- a/plugins/chat/spec/components/chat_message_creator_spec.rb +++ b/plugins/chat/spec/components/chat_message_creator_spec.rb @@ -461,7 +461,9 @@ describe Chat::ChatMessageCreator do content: "Beep boop", upload_ids: [upload1.id], ) - }.to change { ChatUpload.where(upload_id: upload1.id).count }.by(1) + }.to not_change { chat_upload_count([upload1]) }.and change { + UploadReference.where(upload_id: upload1.id).count + }.by(1) end it "can attach multiple uploads to a new message" do @@ -472,9 +474,9 @@ describe Chat::ChatMessageCreator do content: "Beep boop", upload_ids: [upload1.id, upload2.id], ) - }.to change { ChatUpload.where(upload_id: upload1.id).count }.by(1).and change { - ChatUpload.where(upload_id: upload2.id).count - }.by(1) + }.to not_change { chat_upload_count([upload1, upload2]) }.and change { + UploadReference.where(upload_id: [upload1.id, upload2.id]).count + }.by(2) end it "filters out uploads that weren't uploaded by the user" do @@ -485,7 +487,7 @@ describe Chat::ChatMessageCreator do content: "Beep boop", upload_ids: [private_upload.id], ) - }.not_to change { ChatUpload.where(upload_id: private_upload.id).count } + }.not_to change { chat_upload_count([private_upload]) } end it "doesn't attach uploads when `chat_allow_uploads` is false" do @@ -497,7 +499,9 @@ describe Chat::ChatMessageCreator do content: "Beep boop", upload_ids: [upload1.id], ) - }.not_to change { ChatUpload.where(upload_id: upload1.id).count } + }.to not_change { chat_upload_count([upload1]) }.and not_change { + UploadReference.where(upload_id: upload1.id).count + } end end end @@ -605,4 +609,11 @@ describe Chat::ChatMessageCreator do end end end + + # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 + def chat_upload_count(uploads) + DB.query_single( + "SELECT COUNT(*) FROM chat_uploads WHERE upload_id IN (#{uploads.map(&:id).join(",")})", + ).first + end end diff --git a/plugins/chat/spec/components/chat_message_updater_spec.rb b/plugins/chat/spec/components/chat_message_updater_spec.rb index f32f979faa..39c00726bc 100644 --- a/plugins/chat/spec/components/chat_message_updater_spec.rb +++ b/plugins/chat/spec/components/chat_message_updater_spec.rb @@ -329,7 +329,7 @@ describe Chat::ChatMessageUpdater do new_content: "I guess this is different", upload_ids: [upload2.id, upload1.id], ) - }.not_to change { ChatUpload.count } + }.to not_change { chat_upload_count }.and not_change { UploadReference.count } end it "removes uploads that should be removed" do @@ -340,6 +340,16 @@ describe Chat::ChatMessageUpdater do public_chat_channel, upload_ids: [upload1.id, upload2.id], ) + + # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 + DB.exec(<<~SQL) + INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) + VALUES(#{upload1.id}, #{chat_message.id}, NOW(), NOW()) + SQL + DB.exec(<<~SQL) + INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) + VALUES(#{upload2.id}, #{chat_message.id}, NOW(), NOW()) + SQL expect { Chat::ChatMessageUpdater.update( guardian: guardian, @@ -347,7 +357,9 @@ describe Chat::ChatMessageUpdater do new_content: "I guess this is different", upload_ids: [upload1.id], ) - }.to change { ChatUpload.where(upload_id: upload2.id).count }.by(-1) + }.to change { chat_upload_count([upload2]) }.by(-1).and change { + UploadReference.where(upload_id: upload2.id).count + }.by(-1) end it "removes all uploads if they should be removed" do @@ -358,6 +370,16 @@ describe Chat::ChatMessageUpdater do public_chat_channel, upload_ids: [upload1.id, upload2.id], ) + + # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 + DB.exec(<<~SQL) + INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) + VALUES(#{upload1.id}, #{chat_message.id}, NOW(), NOW()) + SQL + DB.exec(<<~SQL) + INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) + VALUES(#{upload2.id}, #{chat_message.id}, NOW(), NOW()) + SQL expect { Chat::ChatMessageUpdater.update( guardian: guardian, @@ -365,7 +387,9 @@ describe Chat::ChatMessageUpdater do new_content: "I guess this is different", upload_ids: [], ) - }.to change { ChatUpload.where(chat_message: chat_message).count }.by(-2) + }.to change { chat_upload_count([upload1, upload2]) }.by(-2).and change { + UploadReference.where(target: chat_message).count + }.by(-2) end it "adds one upload if none exist" do @@ -377,7 +401,9 @@ describe Chat::ChatMessageUpdater do new_content: "I guess this is different", upload_ids: [upload1.id], ) - }.to change { ChatUpload.where(chat_message: chat_message).count }.by(1) + }.to not_change { chat_upload_count([upload1]) }.and change { + UploadReference.where(target: chat_message).count + }.by(1) end it "adds multiple uploads if none exist" do @@ -389,7 +415,9 @@ describe Chat::ChatMessageUpdater do new_content: "I guess this is different", upload_ids: [upload1.id, upload2.id], ) - }.to change { ChatUpload.where(chat_message: chat_message).count }.by(2) + }.to not_change { chat_upload_count([upload1, upload2]) }.and change { + UploadReference.where(target: chat_message).count + }.by(2) end it "doesn't remove existing uploads when upload ids that do not exist are passed in" do @@ -402,7 +430,9 @@ describe Chat::ChatMessageUpdater do new_content: "I guess this is different", upload_ids: [0], ) - }.not_to change { ChatUpload.where(chat_message: chat_message).count } + }.to not_change { chat_upload_count }.and not_change { + UploadReference.where(target: chat_message).count + } end it "doesn't add uploads if `chat_allow_uploads` is false" do @@ -415,7 +445,9 @@ describe Chat::ChatMessageUpdater do new_content: "I guess this is different", upload_ids: [upload1.id, upload2.id], ) - }.not_to change { ChatUpload.where(chat_message: chat_message).count } + }.to not_change { chat_upload_count([upload1, upload2]) }.and not_change { + UploadReference.where(target: chat_message).count + } end it "doesn't remove existing uploads if `chat_allow_uploads` is false" do @@ -434,7 +466,9 @@ describe Chat::ChatMessageUpdater do new_content: "I guess this is different", upload_ids: [], ) - }.not_to change { ChatUpload.where(chat_message: chat_message).count } + }.to not_change { chat_upload_count }.and not_change { + UploadReference.where(target: chat_message).count + } end it "updates if upload is present even if length is less than `chat_minimum_message_length`" do @@ -553,4 +587,12 @@ describe Chat::ChatMessageUpdater do end end end + + # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 + def chat_upload_count(uploads = nil) + return DB.query_single("SELECT COUNT(*) FROM chat_uploads").first if !uploads + DB.query_single( + "SELECT COUNT(*) FROM chat_uploads WHERE upload_id IN (#{uploads.map(&:id).join(",")})", + ).first + end end diff --git a/plugins/chat/spec/fabricators/chat_fabricator.rb b/plugins/chat/spec/fabricators/chat_fabricator.rb index e123c06bc5..a8d7f3e544 100644 --- a/plugins/chat/spec/fabricators/chat_fabricator.rb +++ b/plugins/chat/spec/fabricators/chat_fabricator.rb @@ -126,3 +126,16 @@ Fabricator(:user_chat_channel_membership_for_dm, from: :user_chat_channel_member desktop_notification_level 2 mobile_notification_level 2 end + +Fabricator(:chat_draft) do + user + chat_channel + + transient :value, "chat draft message" + transient :uploads, [] + transient :reply_to_msg + + data do |attrs| + { value: attrs[:value], replyToMsg: attrs[:reply_to_msg], uploads: attrs[:uploads] }.to_json + end +end diff --git a/plugins/chat/spec/jobs/chat_channel_delete_spec.rb b/plugins/chat/spec/jobs/chat_channel_delete_spec.rb index 033274f04c..3ab19ae6f3 100644 --- a/plugins/chat/spec/jobs/chat_channel_delete_spec.rb +++ b/plugins/chat/spec/jobs/chat_channel_delete_spec.rb @@ -17,10 +17,15 @@ describe Jobs::ChatChannelDelete do 10.times { ChatMessageReaction.create(chat_message: messages.sample, user: users.sample) } 10.times do - ChatUpload.create( - upload: Fabricate(:upload, user: users.sample), - chat_message: messages.sample, - ) + upload = Fabricate(:upload, user: users.sample) + message = messages.sample + + # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 + DB.exec(<<~SQL) + INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) + VALUES(#{upload.id}, #{message.id}, NOW(), NOW()) + SQL + UploadReference.create(target: message, upload: upload) end ChatMention.create( @@ -52,27 +57,45 @@ describe Jobs::ChatChannelDelete do chat_channel.trash! end + def counts + { + incoming_webhooks: IncomingChatWebhook.where(chat_channel_id: chat_channel.id).count, + webhook_events: + ChatWebhookEvent.where(incoming_chat_webhook_id: @incoming_chat_webhook_id).count, + drafts: ChatDraft.where(chat_channel: chat_channel).count, + channel_memberships: UserChatChannelMembership.where(chat_channel: chat_channel).count, + revisions: ChatMessageRevision.where(chat_message_id: @message_ids).count, + mentions: ChatMention.where(chat_message_id: @message_ids).count, + chat_uploads: + DB.query_single( + "SELECT COUNT(*) FROM chat_uploads WHERE chat_message_id IN (#{@message_ids.join(",")})", + ).first, + upload_references: + UploadReference.where(target_id: @message_ids, target_type: "ChatMessage").count, + messages: ChatMessage.where(id: @message_ids).count, + reactions: ChatMessageReaction.where(chat_message_id: @message_ids).count, + } + end + it "deletes all of the messages and related records completely" do - expect { described_class.new.execute(chat_channel_id: chat_channel.id) }.to change { - IncomingChatWebhook.where(chat_channel_id: chat_channel.id).count - }.by(-1).and change { - ChatWebhookEvent.where(incoming_chat_webhook_id: @incoming_chat_webhook_id).count - }.by(-1).and change { ChatDraft.where(chat_channel: chat_channel).count }.by( - -1, - ).and change { - UserChatChannelMembership.where(chat_channel: chat_channel).count - }.by(-3).and change { - ChatMessageRevision.where(chat_message_id: @message_ids).count - }.by(-1).and change { - ChatMention.where(chat_message_id: @message_ids).count - }.by(-1).and change { - ChatUpload.where(chat_message_id: @message_ids).count - }.by(-10).and change { - ChatMessage.where(id: @message_ids).count - }.by(-20).and change { - ChatMessageReaction.where( - chat_message_id: @message_ids, - ).count - }.by(-10) + initial_counts = counts + described_class.new.execute(chat_channel_id: chat_channel.id) + new_counts = counts + + expect(new_counts[:incoming_webhooks]).to eq(initial_counts[:incoming_webhooks] - 1) + expect(new_counts[:webhook_events]).to eq(initial_counts[:webhook_events] - 1) + expect(new_counts[:drafts]).to eq(initial_counts[:drafts] - 1) + expect(new_counts[:channel_memberships]).to eq(initial_counts[:channel_memberships] - 3) + expect(new_counts[:revisions]).to eq(initial_counts[:revisions] - 1) + expect(new_counts[:mentions]).to eq(initial_counts[:mentions] - 1) + expect(new_counts[:chat_uploads]).to eq(initial_counts[:chat_uploads] - 10) + expect(new_counts[:upload_references]).to eq(initial_counts[:upload_references] - 10) + expect(new_counts[:messages]).to eq(initial_counts[:messages] - 20) + expect(new_counts[:reactions]).to eq(initial_counts[:reactions] - 10) + end + + it "does not error if there are no messages in the channel" do + other_channel = Fabricate(:chat_channel) + expect { described_class.new.execute(chat_channel_id: other_channel.id) }.not_to raise_error end end diff --git a/plugins/chat/spec/lib/chat_channel_archive_service_spec.rb b/plugins/chat/spec/lib/chat_channel_archive_service_spec.rb index 5c4c97c392..144b103225 100644 --- a/plugins/chat/spec/lib/chat_channel_archive_service_spec.rb +++ b/plugins/chat/spec/lib/chat_channel_archive_service_spec.rb @@ -12,11 +12,13 @@ describe Chat::ChatChannelArchiveService do let(:topic_params) { { topic_title: "This will be a new topic", category_id: category.id } } subject { Chat::ChatChannelArchiveService } - describe "#begin_archive_process" do + before { SiteSetting.chat_enabled = true } + + describe "#create_archive_process" do before { 3.times { Fabricate(:chat_message, chat_channel: channel) } } it "marks the channel as read_only" do - subject.begin_archive_process( + subject.create_archive_process( chat_channel: channel, acting_user: user, topic_params: topic_params, @@ -25,7 +27,7 @@ describe Chat::ChatChannelArchiveService do end it "creates the chat channel archive record to save progress and topic params" do - subject.begin_archive_process( + subject.create_archive_process( chat_channel: channel, acting_user: user, topic_params: topic_params, @@ -40,7 +42,7 @@ describe Chat::ChatChannelArchiveService do it "enqueues the archive job" do channel_archive = - subject.begin_archive_process( + subject.create_archive_process( chat_channel: channel, acting_user: user, topic_params: topic_params, @@ -56,13 +58,13 @@ describe Chat::ChatChannelArchiveService do end it "does nothing if there is already an archive record for the channel" do - subject.begin_archive_process( + subject.create_archive_process( chat_channel: channel, acting_user: user, topic_params: topic_params, ) expect { - subject.begin_archive_process( + subject.create_archive_process( chat_channel: channel, acting_user: user, topic_params: topic_params, @@ -74,7 +76,7 @@ describe Chat::ChatChannelArchiveService do new_message = Fabricate(:chat_message, chat_channel: channel) new_message.trash! channel_archive = - subject.begin_archive_process( + subject.create_archive_process( chat_channel: channel, acting_user: user, topic_params: topic_params, @@ -90,7 +92,7 @@ describe Chat::ChatChannelArchiveService do def start_archive @channel_archive = - subject.begin_archive_process( + subject.create_archive_process( chat_channel: channel, acting_user: user, topic_params: topic_params, @@ -150,7 +152,7 @@ describe Chat::ChatChannelArchiveService do it "successfully links uploads from messages to the post" do create_messages(3) && start_archive - ChatUpload.create(chat_message: ChatMessage.last, upload: Fabricate(:upload)) + UploadReference.create(target: ChatMessage.last, upload: Fabricate(:upload)) subject.new(@channel_archive).execute expect(@channel_archive.reload.complete?).to eq(true) expect(@channel_archive.destination_topic.posts.last.upload_references.count).to eq(1) @@ -167,6 +169,37 @@ describe Chat::ChatChannelArchiveService do ) end + it "does not continue archiving if the destination topic fails to be created" do + SiteSetting.max_emojis_in_title = 1 + + create_messages(3) && start_archive + @channel_archive.update!(destination_topic_title: "Wow this is the new title :tada: :joy:") + subject.new(@channel_archive).execute + expect(@channel_archive.reload.complete?).to eq(false) + expect(@channel_archive.reload.failed?).to eq(true) + expect(@channel_archive.archive_error).to eq("Title can't have more than 1 emoji") + + pm_topic = Topic.private_messages.last + expect(pm_topic.title).to eq( + I18n.t("system_messages.chat_channel_archive_failed.subject_template"), + ) + expect(pm_topic.first_post.raw).to include("Title can't have more than 1 emoji") + end + + context "when enable_experimental_hashtag_autocomplete" do + before { SiteSetting.enable_experimental_hashtag_autocomplete = true } + + it "uses the channel slug to autolink a hashtag for the channel in the PM" do + create_messages(3) && start_archive + subject.new(@channel_archive).execute + expect(@channel_archive.reload.complete?).to eq(true) + pm_topic = Topic.private_messages.last + expect(pm_topic.first_post.cooked).to include( + "#{channel.title(user)}", + ) + end + end + describe "channel members" do before do create_messages(3) diff --git a/plugins/chat/spec/lib/chat_transcript_service_spec.rb b/plugins/chat/spec/lib/chat_transcript_service_spec.rb index 0f14d92f01..c9bf1b832e 100644 --- a/plugins/chat/spec/lib/chat_transcript_service_spec.rb +++ b/plugins/chat/spec/lib/chat_transcript_service_spec.rb @@ -130,10 +130,10 @@ describe ChatTranscriptService do original_filename: "test_img.jpg", extension: "jpg", ) - cu1 = ChatUpload.create(chat_message: message, created_at: 10.seconds.ago, upload: video) - cu2 = ChatUpload.create(chat_message: message, created_at: 9.seconds.ago, upload: audio) - cu3 = ChatUpload.create(chat_message: message, created_at: 8.seconds.ago, upload: attachment) - cu4 = ChatUpload.create(chat_message: message, created_at: 7.seconds.ago, upload: image) + UploadReference.create(target: message, created_at: 10.seconds.ago, upload: video) + UploadReference.create(target: message, created_at: 9.seconds.ago, upload: audio) + UploadReference.create(target: message, created_at: 8.seconds.ago, upload: attachment) + UploadReference.create(target: message, created_at: 7.seconds.ago, upload: image) video_markdown = UploadMarkdown.new(video).to_markdown audio_markdown = UploadMarkdown.new(audio).to_markdown attachment_markdown = UploadMarkdown.new(attachment).to_markdown @@ -166,7 +166,7 @@ describe ChatTranscriptService do original_filename: "test_img.jpg", extension: "jpg", ) - cu = ChatUpload.create(chat_message: message, created_at: 7.seconds.ago, upload: image) + UploadReference.create(target: message, created_at: 7.seconds.ago, upload: image) image_markdown = UploadMarkdown.new(image).to_markdown expect(service(message.id).generate_markdown).to eq(<<~MARKDOWN) diff --git a/plugins/chat/spec/lib/message_mover_spec.rb b/plugins/chat/spec/lib/message_mover_spec.rb index 43182c64c1..6fd4b6079b 100644 --- a/plugins/chat/spec/lib/message_mover_spec.rb +++ b/plugins/chat/spec/lib/message_mover_spec.rb @@ -112,7 +112,7 @@ describe Chat::MessageMover do it "updates references for reactions, uploads, revisions, mentions, etc." do reaction = Fabricate(:chat_message_reaction, chat_message: message1) - upload = Fabricate(:chat_upload, chat_message: message1) + upload = Fabricate(:upload_reference, target: message1) mention = Fabricate(:chat_mention, chat_message: message2, user: acting_user) revision = Fabricate(:chat_message_revision, chat_message: message3) webhook_event = Fabricate(:chat_webhook_event, chat_message: message3) @@ -121,7 +121,7 @@ describe Chat::MessageMover do moved_messages = ChatMessage.where(chat_channel: destination_channel).order("created_at ASC, id ASC").last(3) expect(reaction.reload.chat_message_id).to eq(moved_messages.first.id) - expect(upload.reload.chat_message_id).to eq(moved_messages.first.id) + expect(upload.reload.target_id).to eq(moved_messages.first.id) expect(mention.reload.chat_message_id).to eq(moved_messages.second.id) expect(revision.reload.chat_message_id).to eq(moved_messages.third.id) expect(webhook_event.reload.chat_message_id).to eq(moved_messages.third.id) diff --git a/plugins/chat/spec/models/chat_draft_spec.rb b/plugins/chat/spec/models/chat_draft_spec.rb new file mode 100644 index 0000000000..27794bafae --- /dev/null +++ b/plugins/chat/spec/models/chat_draft_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe ChatDraft do + before { SiteSetting.max_chat_draft_length = 100 } + + it "errors when data.value is greater than `max_chat_draft_length`" do + draft = + described_class.create( + user_id: Fabricate(:user).id, + chat_channel_id: Fabricate(:chat_channel).id, + data: { value: "A" * (SiteSetting.max_chat_draft_length + 1) }.to_json, + ) + + expect(draft.errors.full_messages).to eq( + [I18n.t("chat.errors.draft_too_long", { maximum: SiteSetting.max_chat_draft_length })], + ) + end +end diff --git a/plugins/chat/spec/models/chat_message_spec.rb b/plugins/chat/spec/models/chat_message_spec.rb index 1ce6a39ed3..c73b1a2431 100644 --- a/plugins/chat/spec/models/chat_message_spec.rb +++ b/plugins/chat/spec/models/chat_message_spec.rb @@ -234,8 +234,10 @@ describe ChatMessage do expect(cooked).to eq("

    @mention

    ") end - # TODO (martin) Remove this when enable_experimental_hashtag_autocomplete is default it "supports category-hashtag plugin" do + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + SiteSetting.enable_experimental_hashtag_autocomplete = false + category = Fabricate(:category) cooked = ChatMessage.cook("##{category.slug}") @@ -307,7 +309,7 @@ describe ChatMessage do gif = Fabricate(:upload, original_filename: "cat.gif", width: 400, height: 300, extension: "gif") message = Fabricate(:chat_message, message: "") - ChatUpload.create(chat_message: message, upload: gif) + UploadReference.create(target: message, upload: gif) expect(message.excerpt).to eq "cat.gif" end @@ -389,8 +391,8 @@ describe ChatMessage do ) image2 = Fabricate(:upload, original_filename: "meme.jpg", width: 10, height: 10, extension: "jpg") - ChatUpload.create(chat_message: message, upload: image) - ChatUpload.create(chat_message: message, upload: image2) + UploadReference.create!(target: message, upload: image) + UploadReference.create!(target: message, upload: image2) expect(message.to_markdown).to eq(<<~MSG.chomp) hey friend, what's up?! @@ -477,13 +479,20 @@ describe ChatMessage do expect { webhook_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end - it "destroys chat_uploads" do + it "destroys upload_references and chat_uploads" do message_1 = Fabricate(:chat_message) - chat_upload_1 = Fabricate(:chat_upload, chat_message: message_1) + upload_reference_1 = Fabricate(:upload_reference, target: message_1) + upload_1 = Fabricate(:upload) + # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 + DB.exec(<<~SQL) + INSERT INTO chat_uploads(upload_id, chat_message_id, created_at, updated_at) + VALUES(#{upload_1.id}, #{message_1.id}, NOW(), NOW()) + SQL message_1.destroy! - expect { chat_upload_1.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(DB.query("SELECT * FROM chat_uploads WHERE upload_id = #{upload_1.id}")).to eq([]) + expect { upload_reference_1.reload }.to raise_error(ActiveRecord::RecordNotFound) end describe "bookmarks" do @@ -530,4 +539,39 @@ describe ChatMessage do end end end + + describe "#attach_uploads" do + fab!(:chat_message) { Fabricate(:chat_message) } + fab!(:upload_1) { Fabricate(:upload) } + fab!(:upload_2) { Fabricate(:upload) } + + it "creates an UploadReference record for the provided uploads" do + chat_message.attach_uploads([upload_1, upload_2]) + upload_references = UploadReference.where(upload_id: [upload_1, upload_2]) + expect(chat_upload_count([upload_1, upload_2])).to eq(0) + expect(upload_references.count).to eq(2) + expect(upload_references.map(&:target_id).uniq).to eq([chat_message.id]) + expect(upload_references.map(&:target_type).uniq).to eq(["ChatMessage"]) + end + + it "does nothing if the message record is new" do + expect { ChatMessage.new.attach_uploads([upload_1, upload_2]) }.to not_change { + chat_upload_count + }.and not_change { UploadReference.count } + end + + it "does nothing for an empty uploads array" do + expect { chat_message.attach_uploads([]) }.to not_change { + chat_upload_count + }.and not_change { UploadReference.count } + end + end + + # TODO (martin) Remove this when we remove ChatUpload completely, 2023-04-01 + def chat_upload_count(uploads = nil) + return DB.query_single("SELECT COUNT(*) FROM chat_uploads").first if !uploads + DB.query_single( + "SELECT COUNT(*) FROM chat_uploads WHERE upload_id IN (#{uploads.map(&:id).join(",")})", + ).first + end end diff --git a/plugins/chat/spec/plugin_spec.rb b/plugins/chat/spec/plugin_spec.rb index 68cdda55f9..431d1ccc61 100644 --- a/plugins/chat/spec/plugin_spec.rb +++ b/plugins/chat/spec/plugin_spec.rb @@ -26,7 +26,7 @@ describe Chat do ) end - it "marks uploads with ChatUpload in use" do + it "marks uploads with reference to ChatMessage via UploadReference in use" do unused_upload expect { Jobs::CleanUpUploads.new.execute({}) }.to change { Upload.count }.by(-1) @@ -61,7 +61,7 @@ describe Chat do ) end - it "marks uploads with ChatUpload in use" do + it "marks uploads with reference to ChatMessage via UploadReference in use" do draft_upload unused_upload diff --git a/plugins/chat/spec/requests/api/chat_channels_archives_controller_spec.rb b/plugins/chat/spec/requests/api/chat_channels_archives_controller_spec.rb index c209f5f498..2acd568e9e 100644 --- a/plugins/chat/spec/requests/api/chat_channels_archives_controller_spec.rb +++ b/plugins/chat/spec/requests/api/chat_channels_archives_controller_spec.rb @@ -98,6 +98,22 @@ RSpec.describe Chat::Api::ChatChannelsArchivesController do }.not_to change { ChatChannelArchive.count } end + context "when archiving to a new topic" do + it "returns validation errors if the topic is not valid" do + SiteSetting.max_emojis_in_title = 1 + new_topic_params_invalid = new_topic_params.dup + new_topic_params_invalid[:archive][ + :title + ] = "Some new topic with too many emoji :joy: :sob: :tada:" + sign_in(admin) + expect { + post "/chat/api/channels/#{channel.id}/archives", params: new_topic_params_invalid + }.not_to change { ChatChannelArchive.count } + expect(response.status).to eq(400) + expect(response.parsed_body["errors"]).to eq(["Title can't have more than 1 emoji"]) + end + end + describe "when retrying the archive process" do fab!(:channel) { Fabricate(:category_channel, chatable: category, status: :read_only) } fab!(:archive) do diff --git a/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb b/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb index fd1a3375ea..96d48c05b4 100644 --- a/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb +++ b/plugins/chat/spec/requests/api/chat_channels_controller_spec.rb @@ -236,6 +236,30 @@ RSpec.describe Chat::Api::ChatChannelsController do ), ).to eq(true) end + + it "generates a valid new slug to prevent collisions" do + SiteSetting.max_topic_title_length = 20 + channel_1 = Fabricate(:chat_channel, name: "a" * SiteSetting.max_topic_title_length) + freeze_time(DateTime.parse("2022-07-08 09:30:00")) + old_slug = channel_1.slug + + delete( + "/chat/api/channels/#{channel_1.id}", + params: { + channel: { + name_confirmation: channel_1.title(current_user), + }, + }, + ) + + expect(response.status).to eq(200) + expect(channel_1.reload.slug).to eq( + "20220708-0930-#{old_slug}-deleted".truncate( + SiteSetting.max_topic_title_length, + omission: "", + ), + ) + end end end end @@ -263,11 +287,22 @@ RSpec.describe Chat::Api::ChatChannelsController do new_channel = ChatChannel.last expect(new_channel.name).to eq(params[:channel][:name]) + expect(new_channel.slug).to eq("channel-name") expect(new_channel.description).to eq(params[:channel][:description]) expect(new_channel.chatable_type).to eq(category.class.name) expect(new_channel.chatable_id).to eq(category.id) end + it "creates a channel using the user-provided slug" do + new_params = params.dup + new_params[:channel][:slug] = "wow-so-cool" + post "/chat/api/channels", params: new_params + + new_channel = ChatChannel.last + + expect(new_channel.slug).to eq("wow-so-cool") + end + it "creates a channel sets auto_join_users to false by default" do post "/chat/api/channels", params: params diff --git a/plugins/chat/spec/requests/chat_controller_spec.rb b/plugins/chat/spec/requests/chat_controller_spec.rb index 8ff5912df5..2b9c3e5e3b 100644 --- a/plugins/chat/spec/requests/chat_controller_spec.rb +++ b/plugins/chat/spec/requests/chat_controller_spec.rb @@ -997,7 +997,8 @@ RSpec.describe Chat::ChatController do end it "doesn't invite users who cannot chat" do - SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:admin] + SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:admins] + expect { put "/chat/#{chat_channel.id}/invite.json", params: { user_ids: [user.id] } }.not_to change { @@ -1285,6 +1286,19 @@ RSpec.describe Chat::ChatController do post "/chat/drafts.json", params: { chat_channel_id: dm_channel.id, data: "{}" } }.to change { ChatDraft.count }.by(1) end + + it "cannot create a too long chat draft" do + SiteSetting.max_chat_draft_length = 100 + + post "/chat/drafts.json", + params: { + chat_channel_id: chat_channel.id, + data: { value: "a" * (SiteSetting.max_chat_draft_length + 1) }.to_json, + } + + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to eq([I18n.t("chat.errors.draft_too_long")]) + end end describe "#message_link" do diff --git a/plugins/chat/spec/serializer/core_ext/current_user_serializer_spec.rb b/plugins/chat/spec/serializer/core_ext/current_user_serializer_spec.rb new file mode 100644 index 0000000000..2e2fd20a0e --- /dev/null +++ b/plugins/chat/spec/serializer/core_ext/current_user_serializer_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.describe CurrentUserSerializer do + fab!(:current_user) { Fabricate(:user) } + + let(:serializer) do + described_class.new(current_user, scope: Guardian.new(current_user), root: false) + end + + before do + SiteSetting.chat_enabled = true + SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone] + current_user.user_option.update(chat_enabled: true) + end + + describe "#chat_drafts" do + context "when user can't chat" do + before { SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff] } + + it "is not present" do + expect(serializer.as_json[:chat_drafts]).to be_blank + end + end + + it "is ordered by most recent drafts" do + Fabricate(:chat_draft, user: current_user, value: "second draft") + Fabricate(:chat_draft, user: current_user, value: "first draft") + + values = + serializer.as_json[:chat_drafts].map { |draft| MultiJson.load(draft[:data])["value"] } + expect(values).to eq(["first draft", "second draft"]) + end + + it "limits the numbers of drafts" do + 21.times { Fabricate(:chat_draft, user: current_user) } + + expect(serializer.as_json[:chat_drafts].length).to eq(20) + end + end +end diff --git a/plugins/chat/spec/system/archive_channel_spec.rb b/plugins/chat/spec/system/archive_channel_spec.rb index 998045e1b5..bfbb33f3e9 100644 --- a/plugins/chat/spec/system/archive_channel_spec.rb +++ b/plugins/chat/spec/system/archive_channel_spec.rb @@ -65,6 +65,18 @@ RSpec.describe "Archive channel", type: :system, js: true do expect(page).to have_css(".chat-channel-archive-status") end + it "shows an error when the topic is invalid" do + chat.visit_channel_settings(channel_1) + click_button(I18n.t("js.chat.channel_settings.archive_channel")) + find("#split-topic-name").fill_in( + with: "An interesting topic for cats :cat: :cat2: :smile_cat:", + ) + click_button(I18n.t("js.chat.channel_archive.title")) + + expect(page).not_to have_content(I18n.t("js.chat.channel_archive.process_started")) + expect(page).to have_content("Title can't have more than 1 emoji") + end + context "when archived channels had unreads" do before { channel_1.add(current_user) } diff --git a/plugins/chat/spec/system/channel_about_page_spec.rb b/plugins/chat/spec/system/channel_about_page_spec.rb index a1cd22bd85..192ddd94b6 100644 --- a/plugins/chat/spec/system/channel_about_page_spec.rb +++ b/plugins/chat/spec/system/channel_about_page_spec.rb @@ -16,14 +16,14 @@ RSpec.describe "Channel - Info - About page", type: :system, js: true do chat_page.visit_channel_about(channel_1) expect(page.find(".category-name")).to have_content(channel_1.chatable.name) - expect(page.find(".channel-info-about-view__title")).to have_content(channel_1.title) + expect(page.find(".channel-info-about-view__name")).to have_content(channel_1.title) end it "escapes channel title" do channel_1.update!(name: "") chat_page.visit_channel_about(channel_1) - expect(page.find(".channel-info-about-view__title")["innerHTML"].strip).to eq( + expect(page.find(".channel-info-about-view__name")["innerHTML"].strip).to eq( "<script>alert('hello')</script>", ) expect(page.find(".chat-channel-title__name")["innerHTML"].strip).to eq( @@ -31,10 +31,10 @@ RSpec.describe "Channel - Info - About page", type: :system, js: true do ) end - it "can’t edit title" do + it "can’t edit name" do chat_page.visit_channel_about(channel_1) - expect(page).to have_no_selector(".edit-title-btn") + expect(page).to have_no_selector(".edit-name-btn") end it "can’t edit description" do @@ -76,20 +76,17 @@ RSpec.describe "Channel - Info - About page", type: :system, js: true do before { sign_in(current_user) } - it "can edit title" do + it "can edit name" do chat_page.visit_channel_about(channel_1) - find(".edit-title-btn").click + find(".edit-name-btn").click - expect(page).to have_selector( - ".chat-channel-edit-title-modal__title-input", - text: channel_1.title, - ) + expect(find(".chat-channel-edit-name-modal__name-input").value).to eq(channel_1.title) - title = "A new title" - find(".chat-channel-edit-title-modal__title-input").fill_in(with: title) + name = "A new name" + find(".chat-channel-edit-name-modal__name-input").fill_in(with: name) find(".create").click - expect(page).to have_content(title) + expect(page).to have_content(name) end it "can edit description" do diff --git a/plugins/chat/spec/system/create_channel_spec.rb b/plugins/chat/spec/system/create_channel_spec.rb index d3d4007d47..8cf0d19d99 100644 --- a/plugins/chat/spec/system/create_channel_spec.rb +++ b/plugins/chat/spec/system/create_channel_spec.rb @@ -1,25 +1,43 @@ # frozen_string_literal: true RSpec.describe "Create channel", type: :system, js: true do - fab!(:current_admin_user) { Fabricate(:admin) } fab!(:category_1) { Fabricate(:category) } let(:chat_page) { PageObjects::Pages::Chat.new } + let(:channel_modal) { PageObjects::Modals::ChatChannelCreate.new } - before do - chat_system_bootstrap - sign_in(current_admin_user) + before { chat_system_bootstrap } + + context "when user cannot create channel" do + fab!(:current_user) { Fabricate(:user) } + before { sign_in(current_user) } + + it "does not show the create channel button" do + chat_page.visit_browse + expect(chat_page).not_to have_new_channel_button + end end context "when can create channel" do + fab!(:current_admin_user) { Fabricate(:admin) } + before { sign_in(current_admin_user) } + context "when selecting a category" do it "shows access hint" do - visit("/chat") - find(".new-channel-btn").click - find(".category-chooser").click - find(".category-row[data-value=\"#{category_1.id}\"]").click + chat_page.visit_browse + chat_page.new_channel_button.click + channel_modal.select_category(category_1) - expect(find(".create-channel-hint")).to have_content(Group[:everyone].name) + expect(channel_modal).to have_create_hint(Group[:everyone].name) + end + + it "does not override channel name if that was already specified" do + chat_page.visit_browse + chat_page.new_channel_button.click + channel_modal.fill_name("My Cool Channel") + channel_modal.select_category(category_1) + + expect(channel_modal).to have_name_prefilled("My Cool Channel") end context "when category is private" do @@ -27,12 +45,11 @@ RSpec.describe "Create channel", type: :system, js: true do fab!(:private_category_1) { Fabricate(:private_category, group: group_1) } it "shows access hint when selecting the category" do - visit("/chat") - find(".new-channel-btn").click - find(".category-chooser").click - find(".category-row[data-value=\"#{private_category_1.id}\"]").click + chat_page.visit_browse + chat_page.new_channel_button.click + channel_modal.select_category(private_category_1) - expect(find(".create-channel-hint")).to have_content(group_1.name) + expect(channel_modal).to have_create_hint(group_1.name) end context "when category is a child" do @@ -42,12 +59,11 @@ RSpec.describe "Create channel", type: :system, js: true do end it "shows access hint when selecting the category" do - visit("/chat") - find(".new-channel-btn").click - find(".category-chooser").click - find(".category-row[data-value=\"#{child_category.id}\"]").click + chat_page.visit_browse + chat_page.new_channel_button.click + channel_modal.select_category(child_category) - expect(find(".create-channel-hint")).to have_content(group_2.name) + expect(channel_modal).to have_create_hint(group_2.name) end end end @@ -62,44 +78,91 @@ RSpec.describe "Create channel", type: :system, js: true do fab!(:private_category_1) { Fabricate(:private_category, group: group_1) } it "escapes the group name" do - visit("/chat") - find(".new-channel-btn").click - find(".category-chooser").click - find(".category-row[data-value=\"#{private_category_1.id}\"]").click + chat_page.visit_browse + chat_page.new_channel_button.click + channel_modal.select_category(private_category_1) - expect(find(".create-channel-hint")["innerHTML"].strip).to include( + expect(channel_modal.create_channel_hint["innerHTML"].strip).to include( "<script>e</script>", ) end end + it "autogenerates slug from name and changes slug placeholder" do + chat_page.visit_browse + chat_page.new_channel_button.click + name = "Cats & Dogs" + channel_modal.select_category(category_1) + channel_modal.fill_name(name) + channel_modal.fill_description("All kind of cute cats") + + wait_for_attribute(channel_modal.slug_input, :placeholder, "cats-dogs") + + channel_modal.click_primary_button + + expect(page).to have_content(name) + created_channel = ChatChannel.find_by(chatable_id: category_1.id) + expect(created_channel.slug).to eq("cats-dogs") + expect(page).to have_current_path(chat.channel_path(created_channel.id, created_channel.slug)) + end + + it "allows the user to set a slug independently of name" do + chat_page.visit_browse + chat_page.new_channel_button.click + name = "Cats & Dogs" + channel_modal.select_category(category_1) + channel_modal.fill_name(name) + channel_modal.fill_description("All kind of cute cats") + channel_modal.fill_slug("pets-everywhere") + channel_modal.click_primary_button + + expect(page).to have_content(name) + created_channel = ChatChannel.find_by(chatable_id: category_1.id) + expect(created_channel.slug).to eq("pets-everywhere") + expect(page).to have_current_path(chat.channel_path(created_channel.id, created_channel.slug)) + end + context "when saving" do context "when error" do it "displays the error" do existing_channel = Fabricate(:chat_channel) - visit("/chat") - find(".new-channel-btn").click - find(".category-chooser").click - find(".category-row[data-value=\"#{existing_channel.chatable_id}\"]").click - fill_in("channel-name", with: existing_channel.name) - find(".create-channel-modal .create").click + chat_page.visit_browse + chat_page.new_channel_button.click + channel_modal.select_category(existing_channel.chatable) + channel_modal.fill_name(existing_channel.name) + channel_modal.click_primary_button expect(page).to have_content(I18n.t("chat.errors.channel_exists_for_category")) end end + context "when slug is already being used" do + it "displays the error" do + Fabricate(:chat_channel, slug: "pets-everywhere") + chat_page.visit_browse + chat_page.new_channel_button.click + channel_modal.select_category(category_1) + channel_modal.fill_name("Testing") + channel_modal.fill_slug("pets-everywhere") + channel_modal.click_primary_button + + expect(page).to have_content( + "Slug " + I18n.t("chat.category_channel.errors.is_already_in_use"), + ) + end + end + context "when successful" do it "redirects to created channel" do - visit("/chat") - find(".new-channel-btn").click - name = "Cats" - find(".category-chooser").click - find(".category-row[data-value=\"#{category_1.id}\"]").click - fill_in("channel-name", with: name) - fill_in("channel-description", with: "All kind of cute cats") - find(".create-channel-modal .create").click + chat_page.visit_browse + chat_page.new_channel_button.click + channel_modal.select_category(category_1) + expect(channel_modal).to have_name_prefilled(category_1.name) - expect(page).to have_content(name) + channel_modal.fill_description("All kind of cute cats") + channel_modal.click_primary_button + + expect(page).to have_content(category_1.name) created_channel = ChatChannel.find_by(chatable_id: category_1.id) expect(page).to have_current_path( chat.channel_path(created_channel.id, created_channel.slug), diff --git a/plugins/chat/spec/system/navigating_to_message_spec.rb b/plugins/chat/spec/system/navigating_to_message_spec.rb index 7dc60d697e..64d6f75ccb 100644 --- a/plugins/chat/spec/system/navigating_to_message_spec.rb +++ b/plugins/chat/spec/system/navigating_to_message_spec.rb @@ -50,7 +50,7 @@ RSpec.describe "Navigating to message", type: :system, js: true do ) end - it "highglights the correct message" do + it "highlights the correct message" do chat_page.visit_channel(channel_1) click_link(link) @@ -83,7 +83,7 @@ RSpec.describe "Navigating to message", type: :system, js: true do channel_2.add(current_user) end - it "highglights the correct message" do + it "highlights the correct message" do chat_page.visit_channel(channel_2) click_link(link) @@ -94,7 +94,7 @@ RSpec.describe "Navigating to message", type: :system, js: true do end context "when navigating directly to a message link" do - it "highglights the correct message" do + it "highlights the correct message" do visit("/chat/channel/#{channel_1.id}/-?messageId=#{first_message.id}") expect(page).to have_css( @@ -136,7 +136,7 @@ RSpec.describe "Navigating to message", type: :system, js: true do ) end - it "highglights the correct message" do + it "highlights the correct message" do visit("/") chat_page.open_from_header chat_drawer_page.open_channel(channel_1) diff --git a/plugins/chat/spec/system/page_objects/chat/chat.rb b/plugins/chat/spec/system/page_objects/chat/chat.rb index 38e341e46d..0ceceb7373 100644 --- a/plugins/chat/spec/system/page_objects/chat/chat.rb +++ b/plugins/chat/spec/system/page_objects/chat/chat.rb @@ -51,6 +51,14 @@ module PageObjects container.has_content?(message.message) container.has_content?(message.user.username) end + + def new_channel_button + find(".new-channel-btn") + end + + def has_new_channel_button? + has_css?(".new-channel-btn") + end end end end diff --git a/plugins/chat/spec/system/page_objects/modals/chat_channel_create.rb b/plugins/chat/spec/system/page_objects/modals/chat_channel_create.rb new file mode 100644 index 0000000000..41788b203d --- /dev/null +++ b/plugins/chat/spec/system/page_objects/modals/chat_channel_create.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module PageObjects + module Modals + class ChatChannelCreate < PageObjects::Modals::Base + def select_category(category) + find(".category-chooser").click + find(".category-row[data-value=\"#{category.id}\"]").click + end + + def create_channel_hint + find(".create-channel-hint") + end + + def slug_input + find(".create-channel-slug-input") + end + + def has_create_hint?(content) + create_channel_hint.has_content?(content) + end + + def fill_name(name) + fill_in("channel-name", with: name) + end + + def fill_slug(slug) + fill_in("channel-slug", with: slug) + end + + def fill_description(description) + fill_in("channel-description", with: description) + end + + def has_name_prefilled?(name) + has_field?("channel-name", with: name) + end + end + end +end diff --git a/plugins/chat/spec/system/page_objects/sidebar/sidebar.rb b/plugins/chat/spec/system/page_objects/sidebar/sidebar.rb index e416e67c89..ededff5373 100644 --- a/plugins/chat/spec/system/page_objects/sidebar/sidebar.rb +++ b/plugins/chat/spec/system/page_objects/sidebar/sidebar.rb @@ -3,6 +3,14 @@ module PageObjects module Pages class Sidebar < PageObjects::Pages::Base + def channels_section + find(".sidebar-section-chat-channels") + end + + def dms_section + find(".sidebar-section-chat-dms") + end + def open_draft_channel find(".sidebar-section-chat-dms .sidebar-section-header-button", visible: false).click end diff --git a/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb b/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb index 7126ffa832..dddfa46baa 100644 --- a/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb +++ b/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe "Shorcuts | chat composer", type: :system, js: true do +RSpec.describe "Shortcuts | chat composer", type: :system, js: true do fab!(:channel_1) { Fabricate(:chat_channel) } fab!(:current_user) { Fabricate(:user) } @@ -15,7 +15,7 @@ RSpec.describe "Shorcuts | chat composer", type: :system, js: true do end context "when using meta + l" do - xit "handles insert link shorcut" do + xit "handles insert link shortcut" do end end diff --git a/plugins/chat/spec/system/shortcuts/sidebar_spec.rb b/plugins/chat/spec/system/shortcuts/sidebar_spec.rb index ccf6c298e5..65427075c0 100644 --- a/plugins/chat/spec/system/shortcuts/sidebar_spec.rb +++ b/plugins/chat/spec/system/shortcuts/sidebar_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe "Shorcuts | sidebar", type: :system, js: true do +RSpec.describe "Shortcuts | sidebar", type: :system, js: true do fab!(:current_user) { Fabricate(:admin) } let(:chat) { PageObjects::Pages::Chat.new } diff --git a/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb b/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb index 33f7a9b78d..bdbde12575 100644 --- a/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb +++ b/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.describe "Sidebar navigation menu", type: :system, js: true do + let(:sidebar_page) { PageObjects::Pages::Sidebar.new } + fab!(:current_user) { Fabricate(:user) } before do @@ -18,8 +20,8 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do it "displays correct channels section title" do visit("/") - expect(page).to have_css( - ".sidebar-section-chat-channels .sidebar-section-header-text", + expect(sidebar_page.channels_section).to have_css( + ".sidebar-section-header-text", text: I18n.t("js.chat.chat_channels"), ) end @@ -27,8 +29,8 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do it "displays the correct hash icon prefix" do visit("/") - expect(page).to have_css( - ".sidebar-section-chat-channels .sidebar-section-link-#{channel_1.slug} .sidebar-section-link-prefix svg.prefix-icon.d-icon-hashtag", + expect(sidebar_page.channels_section).to have_css( + ".sidebar-section-link-#{channel_1.slug} .sidebar-section-link-prefix svg.prefix-icon.d-icon-hashtag", ) end @@ -53,8 +55,8 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do it "has a lock badge" do visit("/") - expect(page).to have_css( - ".sidebar-section-chat-channels .sidebar-section-link-#{private_channel_1.slug} .sidebar-section-link-prefix svg.prefix-badge.d-icon-lock", + expect(sidebar_page.channels_section).to have_css( + ".sidebar-section-link-#{private_channel_1.slug} .sidebar-section-link-prefix svg.prefix-badge.d-icon-lock", ) end end @@ -67,8 +69,8 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do it "unescapes the emoji" do visit("/") - expect(page).to have_css( - ".sidebar-section-chat-channels .sidebar-section-link-#{channel_1.slug} .emoji", + expect(sidebar_page.channels_section).to have_css( + ".sidebar-section-link-#{channel_1.slug} .emoji", ) end end @@ -88,8 +90,8 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do it "has a muted class" do visit("/") - expect(page).to have_css( - ".sidebar-section-chat-channels .sidebar-section-link-#{channel_2.slug}.sidebar-section-link--muted", + expect(sidebar_page.channels_section).to have_css( + ".sidebar-section-link-#{channel_2.slug}.sidebar-section-link--muted", ) end end @@ -101,9 +103,7 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do visit("/") expect( - page.find(".sidebar-section-chat-channels .sidebar-section-link-#{channel_1.slug}")[ - "title" - ], + sidebar_page.channels_section.find(".sidebar-section-link-#{channel_1.slug}")["title"], ).to eq("<script>alert('hello')</script>") end end @@ -118,8 +118,8 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do visit("/") expect( - page.find( - ".sidebar-section-chat-dms a.sidebar-section-link:nth-child(1) .sidebar-section-link-prefix img", + sidebar_page.dms_section.find( + "a.sidebar-section-link:nth-child(1) .sidebar-section-link-prefix img", )[ "src" ], @@ -130,7 +130,7 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do visit("/") expect( - page.find(".sidebar-section-chat-dms a.sidebar-section-link:nth-child(1)"), + sidebar_page.dms_section.find("a.sidebar-section-link:nth-child(1)"), ).to have_content(other_user.username) end @@ -143,27 +143,27 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do it "displays the status" do visit("/") - expect( - page.find(".sidebar-section-chat-dms a.sidebar-section-link:nth-child(1)"), - ).to have_css(".user-status") + expect(sidebar_page.dms_section.find("a.sidebar-section-link:nth-child(1)")).to have_css( + ".user-status", + ) end end end context "when channel has more than 2 participants" do - fab!(:user_1) { Fabricate(:user) } - fab!(:user_2) { Fabricate(:user) } + fab!(:user_1) { Fabricate(:user, username: "zoesmith") } + fab!(:user_2) { Fabricate(:user, username: "alansmith") } fab!(:dm_channel_1) do Fabricate(:direct_message_channel, users: [current_user, user_1, user_2]) end - it "displays all participants names" do + it "displays all participants names in alphabetical order" do visit("/") expect( - page.find( - ".sidebar-section-chat-dms a.sidebar-section-link:nth-child(1) .sidebar-section-link-content-text", + sidebar_page.dms_section.find( + "a.sidebar-section-link:nth-child(1) .sidebar-section-link-content-text", ), - ).to have_content("#{user_1.username}, #{user_2.username}") + ).to have_content("alansmith, zoesmith") end end @@ -179,7 +179,7 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do it "escapes the title attribute using it" do visit("/") - expect(page.find(".sidebar-section-chat-dms .channel-#{dm_channel_1.id}")["title"]).to eq( + expect(sidebar_page.dms_section.find(".channel-#{dm_channel_1.id}")["title"]).to eq( "Chat with @<script>alert('hello')</script>", ) end diff --git a/plugins/chat/spec/system/uploads_spec.rb b/plugins/chat/spec/system/uploads_spec.rb index 6e49290023..a012d29b86 100644 --- a/plugins/chat/spec/system/uploads_spec.rb +++ b/plugins/chat/spec/system/uploads_spec.rb @@ -79,15 +79,21 @@ describe "Uploading files in chat messages", type: :system, js: true do context "when editing a message with uploads" do fab!(:message_2) { Fabricate(:chat_message, user: current_user, chat_channel: channel_1) } - fab!(:chat_upload) { Fabricate(:chat_upload, chat_message: message_2, user: current_user) } + fab!(:upload_reference) do + Fabricate( + :upload_reference, + target: message_2, + upload: Fabricate(:upload, user: current_user), + ) + end before do channel_1.add(current_user) sign_in(current_user) file = file_from_fixtures("logo-dev.png", "images") - url = Discourse.store.store_upload(file, chat_upload.upload) - chat_upload.upload.update!(url: url, sha1: Upload.generate_digest(file)) + url = Discourse.store.store_upload(file, upload_reference.upload) + upload_reference.upload.update!(url: url, sha1: Upload.generate_digest(file)) end it "allows deleting uploads" do diff --git a/plugins/chat/test/javascripts/unit/lib/chat-emoji-reaction-store-test.js b/plugins/chat/test/javascripts/unit/lib/chat-emoji-reaction-store-test.js index b53a2084dc..2f34312a6d 100644 --- a/plugins/chat/test/javascripts/unit/lib/chat-emoji-reaction-store-test.js +++ b/plugins/chat/test/javascripts/unit/lib/chat-emoji-reaction-store-test.js @@ -16,13 +16,10 @@ module("Discourse Chat | Unit | chat-emoji-reaction-store", function (hooks) { this.chatEmojiReactionStore.reset(); }); - // TODO (martin) Remove site setting workarounds after core PR#1290 test("defaults", function (assert) { assert.deepEqual( this.chatEmojiReactionStore.favorites, - (this.siteSettings.default_emoji_reactions || "") - .split("|") - .filter((val) => val) + this.siteSettings.default_emoji_reactions.split("|").filter((val) => val) ); }); diff --git a/plugins/discourse-local-dates/config/locales/server.ar.yml b/plugins/discourse-local-dates/config/locales/server.ar.yml index ad4c5807b4..ac9487fbe7 100644 --- a/plugins/discourse-local-dates/config/locales/server.ar.yml +++ b/plugins/discourse-local-dates/config/locales/server.ar.yml @@ -6,7 +6,7 @@ ar: site_settings: - discourse_local_dates_enabled: "فعِّل ميزة discourse-local-dates. ستُضيف هذه الميزة دعم التواريخ التي تستوعب المناطق الزمنية المحلية في المنشورات باستخدام عنصر [date]." + discourse_local_dates_enabled: "قم بتمكين ميزة discourse-local-dates. ستُضيف هذه الميزة دعم التواريخ التي تستوعب المناطق الزمنية المحلية في المنشورات باستخدام عنصر [date]." discourse_local_dates_default_formats: "لمعرفة تنسيقات التاريخ والوقت الأكثر استخدامًا، راجع: تنسيق سلاسل momentjs النصية" discourse_local_dates_default_timezones: "القائمة الافتراضية للمناطق الزمنية، يجب أن تكون منطقة زمنية صالحة" discourse_local_dates_email_format: "التنسيق المستخدم لعرض التواريخ في الرسائل الإلكترونية." diff --git a/plugins/discourse-narrative-bot/config/locales/server.ar.yml b/plugins/discourse-narrative-bot/config/locales/server.ar.yml index 64e8afd582..9ca36aac21 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ar.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ar.yml @@ -228,7 +228,7 @@ ar: هل يمكنك إعادة المحاولة؟ استخدم زر الخط العريض B أو الخط المائل I في المحرِّر إذا واجهتك مشكلة. quoting: - instructions: "هل يمكنك محاولة الاقتباس مني عند الرد؛ حتى أعرف الجزء الذي ترد عليه بالضبط؟\n\n> إذا كانت هذه قهوة، فأحضر لي بعض الشاي من فضلك؛ ولكن إذا كان هذا هو الشاي، فأحضر لي بعض القهوة من فضلك.\n>\n> إحدى ميزات التحدُّث إلى نفسك هي أنك تعرف على الأقل أن شخصًا ما يستمع.\n> \n> بعض الأشخاص يجيدون استخدام للكلمات، والبعض الآخر... لا يجيد ذلك.\n\nحدِّد نص &uarr؛ الاقتباس الذي تفضِّله، ثم اضغط على الزر **اقتباس** الذي ينبثق فوق اختيارك - أو الزر **رد** في أسفل هذا المنشور.\n\nاكتب كلمة أو اثنين أسفل الاقتباس حول سبب اختيارك له؛ لأنني فضولي :thinking:" + instructions: "هل يمكنك محاولة الاقتباس مني عند الرد؛ حتى أعرف الجزء الذي ترد عليه بالتعيين؟\n\n> إذا كانت هذه قهوة، فأحضر لي بعض الشاي من فضلك؛ ولكن إذا كان هذا هو الشاي، فأحضر لي بعض القهوة من فضلك.\n>\n> إحدى ميزات التحدُّث إلى نفسك هي أنك تعرف على الأقل أن شخصًا ما يستمع.\n> \n> بعض الأشخاص يجيدون استخدام للكلمات، والبعض الآخر... لا يجيد ذلك.\n\nحدِّد نص &uarr؛ الاقتباس الذي تفضِّله، ثم اضغط على الزر **اقتباس** الذي ينبثق فوق اختيارك - أو الزر **رد** في أسفل هذا المنشور.\n\nاكتب كلمة أو اثنين أسفل الاقتباس حول سبب اختيارك له؛ لأنني فضولي :thinking:" reply: |- أحسنت، لقد اخترت اقتباسي المفضَّل! :left_speech_bubble: not_found: |- @@ -362,19 +362,29 @@ ar: هل تعلم أنه بإمكانك الإشارة إلي الفئات والوسوم في منشورك؟ مثلًا، هل عرضت الفئة %{category}؟ اكتب `#` وسط الجملة وحدِّد أي فئة أو وسم. + instructions_experimental: |- + هل كنت تعلم أن بإمكانك الإشارة إلي الفئات والوسوم في منشورك؟ على سبيل المثال، هل رأيت الفئة %{category}؟ + + اكتب `#` وسط الجملة وحدِّد الفئة أو الوسم. not_found: |- حسنًا، لا أرى فئة هناك في أي مكان. لاحظ أن الرمز `#` لا يمكن أن يكون الحرف الأول. هل يمكنك نسخ هذا في ردك التالي؟ ```text يمكنني إنشاء رابط فئة عبر # ``` + not_found_experimental: |- + حسنًا، لا أرى أي فئة هنا في أي مكان. هل يمكنك نسخ هذا في ردك التالي؟ + + ```text + I can create a category link via # + ``` reply: |- ممتاز! تذكَّر أن هذا ممكن مع كلتا الفئتين _ و_ الوسوم، إذا تم تفعيل الوسوم. change_topic_notification_level: instructions: |- لكل موضوع مستوى إشعارات معيَّن. يبدأ من "عادية"، مما يعني أنه سيتم إرسال الإشعارات إليك فقط عندما يتحدث شخص ما إليك مباشرةً. - يتم ضبط مستوى الإشعارات للرسائل الخاصة بشكلٍ افتراضي على أعلى مستوى من "المراقبة"، مما يعني أنه سيتم إرسال إشعار إليك بكل ردٍ جديد. ولكن يمكنك تجاوز مستوى الإشعارات _لأي_ موضوع إلى "مراقبة" أو "تتبُّع" أو "كتم". + يتم تعيين مستوى الإشعارات للرسائل الخاصة بشكلٍ افتراضي على أعلى مستوى من "المراقبة"، مما يعني أنه سيتم إرسال إشعار إليك بكل ردٍ جديد. ولكن يمكنك تجاوز مستوى الإشعارات _لأي_ موضوع إلى "مراقبة" أو "تتبُّع" أو "كتم". دعنا نجرِّب تغيير مستوى الإشعارات لهذا الموضوع. ستجد في الجزء السفلي من الموضوع زرًا يوضِّح أنك **تراقب** هذا الموضوع. هل يمكنك تغيير مستوى الإشعارات إلى **تتبُّع**؟ not_found: |- @@ -382,7 +392,7 @@ ar: reply: |- أحسنت! آمل أنك لم تكتم هذا الموضوع لأنني أصبح ثرثارًا في بعض الأحيان :grin:. - لاحظ أنه عند الرد على موضوع، أو قراءة موضوع لأكثر من بضع دقائق، ييتم ضبط مستوى الإشعارات تلقائيًا على "تتبُّع". يمكنك تغيير ذلك في [تفضيلات المستخدم](%{base_uri}/my/preferences). + لاحظ أنه عند الرد على موضوع، أو قراءة موضوع لأكثر من بضع دقائق، ييتم تعيين مستوى الإشعارات تلقائيًا على "تتبُّع". يمكنك تغيير ذلك في [تفضيلات المستخدم](%{base_uri}/my/preferences). poll: instructions: |- هل تعلم أنه يمكنك إضافة استطلاع إلى أي منشور؟ جرِّب استخدام الترس في المحرِّر **لإنشاء استطلاع**. diff --git a/plugins/discourse-narrative-bot/config/locales/server.en.yml b/plugins/discourse-narrative-bot/config/locales/server.en.yml index cf69370c2a..d1114d78f4 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.en.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.en.yml @@ -401,9 +401,19 @@ en: Did you know that you can refer to categories and tags in your post? For example, have you seen the %{category} category? Type `#` in the middle of a sentence and select any category or tag. + instructions_experimental: |- + Did you know that you can refer to categories and tags in your post? For example, have you seen the %{category} category? + + Type `#` anywhere in a sentence and select any category or tag. not_found: |- Hmm, I don’t see a category in there anywhere. Note that `#` can't be the first character. Can you copy this in your next reply? + ```text + I can create a category link via # + ``` + not_found_experimental: |- + Hmm, I don’t see a category in there anywhere. Can you copy this in your next reply? + ```text I can create a category link via # ``` diff --git a/plugins/discourse-narrative-bot/config/locales/server.fr.yml b/plugins/discourse-narrative-bot/config/locales/server.fr.yml index dacc356877..bfbd0e33bf 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.fr.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.fr.yml @@ -378,12 +378,22 @@ fr: Saviez-vous que vous pouviez faire référence aux catégories et étiquettes dans vos messages ? Par exemple, avez-vous vu la catégorie %{category} ? Saisissez « # » au milieu d'une phrase et sélectionnez n'importe quelle catégorie ou étiquette. + instructions_experimental: |- + Saviez-vous que vous pouviez faire référence aux catégories et étiquettes dans vos messages ? Par exemple, avez-vous vu la catégorie %{category} ? + + Saisissez « # » dans une phrase et sélectionnez la catégorie ou l'étiquette de votre choix. not_found: |- Hum, je ne vois de catégorie nulle part. Notez que « # » ne peut pas être le premier caractère. Pouvez-vous copier ceci dans votre prochaine réponse ? « texte Je peux créer un lien de catégorie via # » + not_found_experimental: |- + Hum, je ne vois aucune catégorie ici. Pouvez-vous copier ceci dans votre prochaine réponse ? + + ```text + Je peux créer un lien de catégorie via # + ``` reply: |- Excellent ! Retenez que ceci fonctionne pour les catégories _et_ les étiquettes, si les étiquettes sont activées. change_topic_notification_level: diff --git a/plugins/discourse-narrative-bot/config/locales/server.pt_BR.yml b/plugins/discourse-narrative-bot/config/locales/server.pt_BR.yml index b69b52503a..364b2ddfad 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.pt_BR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.pt_BR.yml @@ -376,9 +376,19 @@ pt_BR: Você sabia que pode se referir a categorias e etiquetas em sua postagem? Por exemplo, viu a categoria %{category}? Digite "#" no meio de uma frase e selecione qualquer categoria ou etiqueta. + instructions_experimental: |- + Você sabia que pode se referir a categorias e etiqueta na sua postagem? Por exemplo, você viu a categoria %{category}? + + Digite "#" dentro de uma frase e selecione qualquer categoria ou etiqueta. not_found: |- Hmm, eu não vejo uma categoria em nenhum lugar. Observe que "#' não pode ser o primeiro caractere. Você pode copiar isso na sua próxima resposta? + ```text + Eu posso criar um link de categoria via # + ``` + not_found_experimental: |- + Hmm, não vejo uma categoria em lugar algum. Você poderia copiar isto na sua próxima resposta? + ```text Eu posso criar um link de categoria via # ``` diff --git a/plugins/discourse-narrative-bot/config/locales/server.ru.yml b/plugins/discourse-narrative-bot/config/locales/server.ru.yml index a79987bcf9..080cde180b 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.ru.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.ru.yml @@ -18,7 +18,7 @@ ru: name: Квалифицированный участник description: "Основное обучение завершено" long_description: | - Эта награда выдается при успешном завершении интерактивного обучения нового пользователя. Вы успешно прошли основное обучение, и теперь вы квалифицированный участник форума! + Эта награда выдаётся при успешном завершении интерактивного обучения нового пользователя. Вы успешно прошли основное обучение, и теперь вы квалифицированный участник форума! licensed: name: Лицензированный участник description: "Дополнительное обучение завершено" @@ -169,7 +169,7 @@ ru: - По соображениям безопасности мы временно ограничиваем возможности новых пользователей. Прочитать об уровнях доверия [можно в нашем блоге](https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/) (а также в разделе [награды](%{base_uri}/badges)) на этом сайте. - - Мы всегда следуем [основным принципам сообщества](%{base_uri}/guidelines). + - Мы всегда следуем [рекомендациям для сообщества](%{base_uri}/guidelines). onebox: instructions: |- Не могли бы вы отправить одну из этих ссылок мне? Отправьте **только ссылку**, в этом случае при просмотре сообщения ссылка автоматически преобразуется в умную вставку, которая отобразит краткую информацию о просматриваемой странице Википедии. diff --git a/plugins/discourse-narrative-bot/config/locales/server.sv.yml b/plugins/discourse-narrative-bot/config/locales/server.sv.yml index 50c1fe8695..2b8ac8446e 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.sv.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.sv.yml @@ -378,12 +378,22 @@ sv: Visste du att du kan hänvisa till kategorier och taggar i dina inlägg? Har du till exempel sett kategorin %{category}? Skriv `#` i mitten på en mening och välj en kategori eller tagg. + instructions_experimental: |- + Visste du att du kan hänvisa till kategorier och taggar i dina inlägg? Har du till exempel sett kategorin %{category}? + + Skriv `#` var som helst i en mening och välj en kategori eller tagg. not_found: |- Hmm, jag ser ingen kategori någonstans. Notera att `#` inte kan vara det första tecknet. Kan du kopiera detta i ditt nästa svar? ```text Jag kan skapa en kategorilänk genom # ... + not_found_experimental: |- + Hmm, jag ser ingen kategori någonstans där. Kan du kopiera denna i ditt nästa svar? + + ```text + Jag kan skapa en kategorilänk genom # + ``` reply: |- Utmärkt! Kom ihåg att detta fungerar både för kategorier _och_ taggar, om taggar har aktiverats. change_topic_notification_level: diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb index 1fea4f13d5..8b93776603 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb @@ -46,10 +46,20 @@ module DiscourseNarrativeBot slug = "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{slug}" end - I18n.t( - "#{I18N_KEY}.category_hashtag.instructions", - i18n_post_args(category: "##{slug}"), - ) + # TODO (martin) When enable_experimental_hashtag_autocomplete is the only option + # update the instructions and remove instructions_experimental, as well as the + # not_found translation + if SiteSetting.enable_experimental_hashtag_autocomplete + I18n.t( + "#{I18N_KEY}.category_hashtag.instructions_experimental", + i18n_post_args(category: "##{slug}"), + ) + else + I18n.t( + "#{I18N_KEY}.category_hashtag.instructions", + i18n_post_args(category: "##{slug}"), + ) + end end, recover: { action: :reply_to_recover, @@ -288,7 +298,9 @@ module DiscourseNarrativeBot topic_id = @post.topic_id return unless valid_topic?(topic_id) - if Nokogiri::HTML5.fragment(@post.cooked).css(".hashtag").size > 0 + hashtag_css_class = + SiteSetting.enable_experimental_hashtag_autocomplete ? ".hashtag-cooked" : ".hashtag" + if Nokogiri::HTML5.fragment(@post.cooked).css(hashtag_css_class).size > 0 raw = <<~MD #{I18n.t("#{I18N_KEY}.category_hashtag.reply", i18n_post_args)} @@ -300,7 +312,14 @@ module DiscourseNarrativeBot else fake_delay unless @data[:attempted] - reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args)) + if SiteSetting.enable_experimental_hashtag_autocomplete + reply_to( + @post, + I18n.t("#{I18N_KEY}.category_hashtag.not_found_experimental", i18n_post_args), + ) + else + reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args)) + end end enqueue_timeout_job(@user) false diff --git a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb index 82e5024e99..529ad5c67a 100644 --- a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb +++ b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb @@ -377,6 +377,9 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do context "when reply contains the skip trigger" do it "should create the right reply" do + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + SiteSetting.enable_experimental_hashtag_autocomplete = false + parent_category = Fabricate(:category, name: "a") _category = Fabricate(:category, parent_category: parent_category, name: "b") @@ -414,6 +417,9 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do context "when user recovers a post in the right topic" do it "should create the right reply" do + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + SiteSetting.enable_experimental_hashtag_autocomplete = false + parent_category = Fabricate(:category, name: "a") _category = Fabricate(:category, parent_category: parent_category, name: "b") post @@ -442,6 +448,9 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do topic_id: topic.id, track: described_class.to_s, ) + + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + SiteSetting.enable_experimental_hashtag_autocomplete = false end context "when post is not in the right topic" do @@ -510,6 +519,28 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do :tutorial_change_topic_notification_level, ) end + + context "when enable_experimental_hashtag_autocomplete is true" do + before { SiteSetting.enable_experimental_hashtag_autocomplete = true } + + it "should create the right reply" do + category = Fabricate(:category) + + post.update!(raw: "Check out this ##{category.slug}") + narrative.input(:reply, user, post: post) + + expected_raw = <<~RAW + #{I18n.t("discourse_narrative_bot.advanced_user_narrative.category_hashtag.reply", base_uri: "")} + + #{I18n.t("discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.instructions", base_uri: "")} + RAW + + expect(Post.last.raw).to eq(expected_raw.chomp) + expect(narrative.get_data(user)[:state].to_sym).to eq( + :tutorial_change_topic_notification_level, + ) + end + end end context "with topic notification level tutorial" do diff --git a/plugins/lazy-yt/plugin.rb b/plugins/lazy-yt/plugin.rb index 30beccd7fa..5fb481c041 100644 --- a/plugins/lazy-yt/plugin.rb +++ b/plugins/lazy-yt/plugin.rb @@ -21,7 +21,7 @@ class Onebox::Engine::YoutubeOnebox alias_method :yt_onebox_to_html, :to_html def to_html - if video_id && !params["list"] + if SiteSetting.lazy_yt_enabled && video_id && !params["list"] size_restricted = [params["width"], params["height"]].any? video_width = (params["width"] && params["width"].to_i <= 695) ? params["width"] : 690 # embed width video_height = (params["height"] && params["height"].to_i <= 500) ? params["height"] : 388 # embed height diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js index a22b322130..109fc1a081 100644 --- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js +++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js @@ -134,6 +134,11 @@ function initializePolls(api) { id: "discourse-poll", }); api.cleanupStream(cleanUpPolls); + + const siteSettings = api.container.lookup("site-settings:main"); + if (siteSettings.poll_enabled) { + api.addSearchSuggestion("in:polls"); + } } export default { diff --git a/plugins/poll/config/locales/server.ar.yml b/plugins/poll/config/locales/server.ar.yml index 107abe436e..3ccb0441e6 100644 --- a/plugins/poll/config/locales/server.ar.yml +++ b/plugins/poll/config/locales/server.ar.yml @@ -11,7 +11,7 @@ ar: poll_edit_window_mins: "عدد الدقائق بعد إنشاء المنشور والتي يمكن خلالها تعديل الاستطلاعات" poll_minimum_trust_level_to_create: "الحد الأدنى لمستوى الثقة المطلوب لإنشاء استطلاعات الرأي" poll_groupable_user_fields: "مجموعة أسماء حقول المستخدم التي يمكن استخدامها لتجميع نتائج الاستطلاع وتصفيتها" - poll_export_data_explorer_query_id: "معرِّف استعلام مستكشف البيانات الذي سيتم استخدامه لتصدير نتائج الاستطلاع (اضبط القيمة على 0 للإيقاف)." + poll_export_data_explorer_query_id: "معرِّف استعلام مستكشف البيانات الذي سيتم استخدامه لتصدير نتائج الاستطلاع (اتعيين القيمة على 0 للإيقاف)." poll: poll: "استطلاع" invalid_argument: "القيمة غير صالحة '%{value}' للوسيطة '%{argument}'." diff --git a/plugins/poll/lib/poll.rb b/plugins/poll/lib/poll.rb index fa34b0bf22..e29564a7eb 100644 --- a/plugins/poll/lib/poll.rb +++ b/plugins/poll/lib/poll.rb @@ -306,6 +306,10 @@ class DiscoursePoll::Poll end def self.extract(raw, topic_id, user_id = nil) + # Poll Post handlers get called very early in the post + # creation process. `raw` could be nil here. + return [] if raw.blank? + # TODO: we should fix the callback mess so that the cooked version is available # in the validators instead of cooking twice raw = raw.sub(%r{\[quote.+/quote\]}m, "") diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb index b681593027..84b337a823 100644 --- a/plugins/poll/plugin.rb +++ b/plugins/poll/plugin.rb @@ -231,4 +231,12 @@ after_initialize do SiteSetting.poll_enabled && scope.user&.id.present? && preloaded_polls.present? && preloaded_polls.any? { |p| p.has_voted?(scope.user) } end + + register_search_advanced_filter(/in:polls/) do |posts, match| + if SiteSetting.poll_enabled + posts.joins(:polls) + else + posts + end + end end diff --git a/plugins/poll/spec/lib/search_spec.rb b/plugins/poll/spec/lib/search_spec.rb new file mode 100644 index 0000000000..e102d9ff05 --- /dev/null +++ b/plugins/poll/spec/lib/search_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +RSpec.describe Search do + fab!(:topic) { Fabricate(:topic) } + fab!(:topic2) { Fabricate(:topic) } + fab!(:regular_post) { Fabricate(:post, topic: topic, raw: <<~RAW) } + Somewhere over the rainbow but no poll. + RAW + + fab!(:post_with_poll) { Fabricate(:post, topic: topic2, raw: <<~RAW) } + Somewhere over the rainbow with a poll. + [poll] + * Like + * Dislike + [/poll] + RAW + + before do + SearchIndexer.enable + Jobs.run_immediately! + + SearchIndexer.index(topic2, force: true) + SearchIndexer.index(topic, force: true) + end + + after { SearchIndexer.disable } + + context "when using in:polls" do + it "displays only posts containing polls" do + results = Search.execute("rainbow in:polls", guardian: Guardian.new) + expect(results.posts).to contain_exactly(post_with_poll) + end + end + + context "when polls are disabled" do + it "ignores in:polls filter" do + SiteSetting.poll_enabled = false + results = Search.execute("rainbow in:polls", guardian: Guardian.new) + expect(results.posts).to contain_exactly(regular_post, post_with_poll) + end + end +end diff --git a/plugins/poll/spec/requests/posts_controller_spec.rb b/plugins/poll/spec/requests/posts_controller_spec.rb new file mode 100644 index 0000000000..8ec707779e --- /dev/null +++ b/plugins/poll/spec/requests/posts_controller_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe PostsController do + let(:admin) { Fabricate(:admin) } + + describe "#create" do + it "fails gracefully without a post body" do + key = Fabricate(:api_key).key + + expect do + post "/posts.json", + params: { + title: "this is test body", + }, + headers: { + HTTP_API_USERNAME: admin.username, + HTTP_API_KEY: key, + } + end.not_to change { Topic.count } + + expect(response.status).to eq(422) + end + end +end diff --git a/script/cache_critical_dns b/script/cache_critical_dns index 929770b4a4..76b679cdbb 100755 --- a/script/cache_critical_dns +++ b/script/cache_critical_dns @@ -15,6 +15,18 @@ require 'optparse' # service in the process environment. Any hosts that fail the healthcheck will # never be cached. # +# The list of environment variables that cache_critical_dns will read for +# critical service hostnames can be extended at process execution time by +# specifying environment variable names within the +# DISCOURSE_DNS_CACHE_ADDITIONAL_SERVICE_NAMES environment variable. This is a +# comma-delimited string of extra environment variables to be added to the list +# defined in the static CRITICAL_HOST_ENV_VARS hash. +# +# DISCOURSE_DNS_CACHE_ADDITIONAL_SERVICE_NAMES serves as a kind of lookup table +# for extra services for caching. Any environment variable names within this +# list are treated with the same rules as the DISCOURSE_DB_HOST (and co.) +# variables, as described below. +# # This is as far as you need to read if you are using CNAME or A records for # your services. # @@ -88,7 +100,11 @@ CRITICAL_HOST_ENV_VARS = %w{ DISCOURSE_REDIS_REPLICA_HOST DISCOURSE_MESSAGE_BUS_REDIS_HOST DISCOURSE_MESSAGE_BUS_REDIS_REPLICA_HOST -} +}.union( + ENV.fetch('DISCOURSE_DNS_CACHE_ADDITIONAL_SERVICE_NAMES', '') + .split(',') + .map(&:strip) +) DEFAULT_DB_NAME = "discourse" DEFAULT_REDIS_PORT = 6379 @@ -243,7 +259,10 @@ ensure client.close if client end -HEALTH_CHECKS = { +HEALTH_CHECKS = Hash.new( + # unknown keys (like services defined at runtime) are assumed to be healthy + lambda { |addr| true } +).merge!({ "DISCOURSE_DB_HOST": lambda { |addr| postgres_healthcheck( host: addr, @@ -274,7 +293,7 @@ HEALTH_CHECKS = { host: addr, port: env_as_int("DISCOURSE_MESSAGE_BUS_REDIS_REPLICA_PORT", DEFAULT_REDIS_PORT), password: ENV["DISCOURSE_MESSAGE_BUS_REDIS_PASSWORD"])}, -} +}) def log(msg) STDERR.puts "#{Time.now.utc.iso8601}: #{msg}" diff --git a/spec/fabricators/upload_fabricator.rb b/spec/fabricators/upload_fabricator.rb index aec9a20721..3101a110b2 100644 --- a/spec/fabricators/upload_fabricator.rb +++ b/spec/fabricators/upload_fabricator.rb @@ -97,3 +97,8 @@ Fabricator(:secure_upload_s3, from: :upload_s3) do sha1 { SecureRandom.hex(20) } original_sha1 { sequence(:sha1) { |n| Digest::SHA1.hexdigest(n.to_s) } } end + +Fabricator(:upload_reference) do + target + upload +end diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index 1a0fcca19a..ffbc5a9f4d 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -100,6 +100,8 @@ Fabricator(:trust_level_0, from: :user) { trust_level TrustLevel[0] } Fabricator(:trust_level_1, from: :user) { trust_level TrustLevel[1] } +Fabricator(:trust_level_3, from: :user) { trust_level TrustLevel[3] } + Fabricator(:trust_level_4, from: :user) do name "Leader McElderson" username { sequence(:username) { |i| "tl4#{i}" } } diff --git a/spec/fixtures/onebox/discourse_topic.response b/spec/fixtures/onebox/discourse_topic.response index 8c078f3164..2d79976ba8 100644 --- a/spec/fixtures/onebox/discourse_topic.response +++ b/spec/fixtures/onebox/discourse_topic.response @@ -194,6 +194,10 @@ And that too in just over an year, way to go! [boom]"> + + + + diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 0b70af8174..c25cd5efbb 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -640,6 +640,38 @@ RSpec.describe ApplicationHelper do expect(helper.crawlable_meta_data).not_to include("twitter:image") end end + + context "with breadcrumbs" do + subject(:metadata) { helper.crawlable_meta_data(breadcrumbs: breadcrumbs) } + + let(:breadcrumbs) do + [{ name: "section1", color: "ff0000" }, { name: "section2", color: "0000ff" }] + end + let(:tags) { <<~HTML.strip } + + + + + HTML + + it "generates section and color tags" do + expect(metadata).to include tags + end + end + + context "with tags" do + subject(:metadata) { helper.crawlable_meta_data(tags: tags) } + + let(:tags) { %w[tag1 tag2] } + let(:output_tags) { <<~HTML.strip } + + + HTML + + it "generates tag tags" do + expect(metadata).to include output_tags + end + end end describe "discourse_color_scheme_stylesheets" do diff --git a/spec/integration/secure_uploads_spec.rb b/spec/integration/secure_uploads_spec.rb new file mode 100644 index 0000000000..b9c3c4d067 --- /dev/null +++ b/spec/integration/secure_uploads_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +describe "Secure uploads" do + fab!(:user) { Fabricate(:user) } + fab!(:group) { Fabricate(:group) } + fab!(:secure_category) { Fabricate(:private_category, group: group) } + + before do + Jobs.run_immediately! + + # this is done so the after_save callbacks for site settings to make + # UploadReference records works + @original_provider = SiteSetting.provider + SiteSetting.provider = SiteSettings::DbProvider.new(SiteSetting) + setup_s3 + stub_s3_store + SiteSetting.secure_uploads = true + group.add(user) + user.reload + end + + after { SiteSetting.provider = @original_provider } + + def create_upload + filename = "logo.png" + file = file_from_fixtures(filename) + UploadCreator.new(file, filename).create_for(user.id) + end + + def stub_presign_upload_get(upload) + # this is necessary because by default any upload inside a secure post is considered "secure" + # for the purposes of fetching hotlinked images until proven otherwise, and this is easier + # than trying to stub the presigned URL for s3 in a different way + stub_request(:get, "https:#{upload.url}").to_return( + status: 200, + body: file_from_fixtures("logo.png"), + ) + Upload.stubs(:signed_url_from_secure_uploads_url).returns("https:#{upload.url}") + end + + it "does not convert an upload to secure when it was first used in a site setting then in a post" do + upload = create_upload + SiteSetting.favicon = upload + expect(upload.reload.upload_references.count).to eq(1) + create_post( + title: "Secure upload post", + raw: "This is a new post ", + category: secure_category, + user: user, + ) + upload.reload + expect(upload.upload_references.count).to eq(2) + expect(upload.secure).to eq(false) + end + + it "does not convert an upload to insecure when it was first used in a secure post then a site setting" do + upload = create_upload + create_post( + title: "Secure upload post", + raw: "This is a new post ", + category: secure_category, + user: user, + ) + expect(upload.reload.upload_references.count).to eq(1) + SiteSetting.favicon = upload + upload.reload + expect(upload.upload_references.count).to eq(2) + expect(upload.secure).to eq(true) + end + + it "does not convert an upload to secure when it was first used in a public post then in a secure post" do + upload = create_upload + + post = + create_post( + title: "Public upload post", + raw: "This is a new post ", + user: user, + ) + upload.reload + expect(upload.upload_references.count).to eq(1) + expect(upload.secure).to eq(false) + expect(upload.access_control_post).to eq(post) + + stub_presign_upload_get(upload) + create_post( + title: "Secure upload post", + raw: "This is a new post ", + category: secure_category, + user: user, + ) + upload.reload + expect(upload.upload_references.count).to eq(2) + expect(upload.secure).to eq(false) + expect(upload.access_control_post).to eq(post) + end + + it "does not convert an upload to insecure when it was first used in a secure post then in a public post" do + upload = create_upload + + stub_presign_upload_get(upload) + post = + create_post( + title: "Secure upload post", + raw: "This is a new post ", + category: secure_category, + user: user, + ) + upload.reload + expect(upload.upload_references.count).to eq(1) + expect(upload.secure).to eq(true) + expect(upload.access_control_post).to eq(post) + + create_post( + title: "Public upload post", + raw: "This is a new post ", + user: user, + ) + upload.reload + expect(upload.upload_references.count).to eq(2) + expect(upload.secure).to eq(true) + expect(upload.access_control_post).to eq(post) + end +end diff --git a/spec/integration/tag_counts_spec.rb b/spec/integration/tag_counts_spec.rb new file mode 100644 index 0000000000..82ec44d7b3 --- /dev/null +++ b/spec/integration/tag_counts_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +RSpec.describe "Updating tag counts" do + fab!(:tag1) { Fabricate(:tag) } + fab!(:tag2) { Fabricate(:tag) } + fab!(:group) { Fabricate(:group) } + fab!(:public_category) { Fabricate(:category) } + fab!(:public_category2) { Fabricate(:category) } + fab!(:private_category) { Fabricate(:private_category, group: group) } + fab!(:private_category2) { Fabricate(:private_category, group: group) } + + fab!(:topic_in_public_category) do + Fabricate(:topic, category: public_category, tags: [tag1, tag2]).tap do |topic| + Fabricate(:post, topic: topic) + end + end + + fab!(:topic_in_private_category) do + Fabricate(:topic, category: private_category, tags: [tag1, tag2]).tap do |topic| + Fabricate(:post, topic: topic) + end + end + + fab!(:private_message) do + topic = Fabricate(:private_message_post).topic + topic.update!(tags: [tag1, tag2]) + topic + end + + before do + expect(tag1.public_topic_count).to eq(1) + expect(tag1.staff_topic_count).to eq(2) + expect(tag1.pm_topic_count).to eq(1) + expect(tag2.reload.public_topic_count).to eq(1) + expect(tag2.staff_topic_count).to eq(2) + expect(tag2.pm_topic_count).to eq(1) + end + + it "should decrease Tag#public_topic_count for all tags when topic's category is changed from a public category to a read restricted category" do + expect { topic_in_public_category.change_category_to_id(private_category.id) }.to change { + tag1.reload.public_topic_count + }.by(-1).and change { tag2.reload.public_topic_count }.by(-1) + end + + it "should increase Tag#public_topic_count for all tags when topic's category is changed from a read restricted category to a public category" do + expect { topic_in_private_category.change_category_to_id(public_category.id) }.to change { + tag1.reload.public_topic_count + }.by(1).and change { tag2.reload.public_topic_count }.by(1) + end + + it "should not change Tag#public_topic_count for all tags when topic's category is changed from a public category to another public category" do + expect do + topic_in_public_category.change_category_to_id(public_category2.id) + end.to not_change { tag1.reload.public_topic_count }.and not_change { + tag2.reload.public_topic_count + } + end + + it "should not change Tag#public_topic_count for all tags when topic's category is changed from a read restricted category to another read restricted category" do + expect do + topic_in_private_category.change_category_to_id(private_category2.id) + end.to not_change { tag1.reload.public_topic_count }.and not_change { + tag2.reload.public_topic_count + } + end + + it "increases Tag#public_topic_count for all tags when topic is converted from private message to a regular topic in a public category" do + expect do + private_message.convert_to_public_topic( + Discourse.system_user, + category_id: public_category.id, + ) + end.to change { tag1.reload.public_topic_count }.by(1).and change { + tag2.reload.public_topic_count + }.by(1) + end + + it "should not change Tag#public_topic_count for all tags when topic is converted from private message to a regular topic in a read restricted category" do + expect do + private_message.convert_to_public_topic( + Discourse.system_user, + category_id: private_category.id, + ) + end.to not_change { tag1.reload.public_topic_count }.and not_change { + tag2.reload.public_topic_count + } + end + + it "should decrease Tag#public_topic_count for all tags when regular topic in public category is converted to a private message" do + expect do + topic_in_public_category.convert_to_private_message(Discourse.system_user) + end.to change { tag1.reload.public_topic_count }.by(-1).and change { + tag2.reload.public_topic_count + }.by(-1) + end + + it "should not change Tag#public_topic_count for all tags when regular topic in read restricted category is converted to a private message" do + expect do + topic_in_private_category.convert_to_private_message(Discourse.system_user) + end.to not_change { tag1.reload.public_topic_count }.and not_change { + tag2.reload.public_topic_count + } + end +end diff --git a/spec/integrity/common_mark_spec.rb b/spec/integrity/common_mark_spec.rb index c89f9ff0e8..85c8c82122 100644 --- a/spec/integrity/common_mark_spec.rb +++ b/spec/integrity/common_mark_spec.rb @@ -5,6 +5,9 @@ RSpec.describe "CommonMark" do SiteSetting.enable_markdown_typographer = false SiteSetting.highlighted_languages = "ruby|aa" + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + SiteSetting.enable_experimental_hashtag_autocomplete = false + html, state, md = nil failed = 0 diff --git a/spec/jobs/grant_anniversary_badges_spec.rb b/spec/jobs/grant_anniversary_badges_spec.rb index 5cee313087..57b54708c9 100644 --- a/spec/jobs/grant_anniversary_badges_spec.rb +++ b/spec/jobs/grant_anniversary_badges_spec.rb @@ -12,6 +12,15 @@ RSpec.describe Jobs::GrantAnniversaryBadges do expect(badge).to be_blank end + it "doesn't award to a bot" do + user = Fabricate(:bot, created_at: 400.days.ago) + Fabricate(:post, user: user, created_at: 1.week.ago) + granter.execute({}) + + badge = user.user_badges.where(badge_id: Badge::Anniversary) + expect(badge).to be_blank + end + it "doesn't award to an inactive user" do user = Fabricate(:user, created_at: 400.days.ago, active: false) Fabricate(:post, user: user, created_at: 1.week.ago) @@ -30,6 +39,33 @@ RSpec.describe Jobs::GrantAnniversaryBadges do expect(badge).to be_blank end + it "doesn't award to a suspended user" do + user = Fabricate(:user, created_at: 400.days.ago, suspended_till: 1.year.from_now) + Fabricate(:post, user: user, created_at: 1.week.ago) + granter.execute({}) + + badge = user.user_badges.where(badge_id: Badge::Anniversary) + expect(badge).to be_blank + end + + it "doesn't award to a staged user" do + user = Fabricate(:user, created_at: 400.days.ago, staged: true) + Fabricate(:post, user: user, created_at: 1.week.ago) + granter.execute({}) + + badge = user.user_badges.where(badge_id: Badge::Anniversary) + expect(badge).to be_blank + end + + it "doesn't award to an anonymous user" do + user = Fabricate(:anonymous, created_at: 400.days.ago) + Fabricate(:post, user: user, created_at: 1.week.ago) + granter.execute({}) + + badge = user.user_badges.where(badge_id: Badge::Anniversary) + expect(badge).to be_blank + end + it "doesn't award when a post is deleted" do user = Fabricate(:user, created_at: 400.days.ago) Fabricate(:post, user: user, created_at: 1.week.ago, deleted_at: 1.day.ago) diff --git a/spec/lib/discourse_js_processor_spec.rb b/spec/lib/discourse_js_processor_spec.rb index 3c2eeac803..90748d3aae 100644 --- a/spec/lib/discourse_js_processor_spec.rb +++ b/spec/lib/discourse_js_processor_spec.rb @@ -37,6 +37,56 @@ RSpec.describe DiscourseJsProcessor do end end + it "passes through modern JS syntaxes which are supported in our target browsers" do + script = <<~JS.chomp + optional?.chaining; + const template = func`test`; + let numericSeparator = 100_000_000; + logicalAssignment ||= 2; + nullishCoalescing ?? 'works'; + try { + "optional catch binding"; + } catch { + "works"; + } + async function* asyncGeneratorFunction() { + yield await Promise.resolve('a'); + } + let a = { + x, + y, + ...spreadRest + }; + JS + + result = DiscourseJsProcessor.transpile(script, "blah", "blah/mymodule") + expect(result).to eq <<~JS.strip + define("blah/mymodule", [], function () { + "use strict"; + + #{script.indent(2)} + }); + JS + end + + it "supports decorators and class properties without error" do + script = <<~JS.chomp + class MyClass { + classProperty = 1; + #privateProperty = 1; + #privateMethod() { + console.log("hello world"); + } + @decorated + myMethod(){ + } + } + JS + + result = DiscourseJsProcessor.transpile(script, "blah", "blah/mymodule") + expect(result).to include("_applyDecoratedDescriptor") + end + it "correctly transpiles widget hbs" do result = DiscourseJsProcessor.transpile(<<~JS, "blah", "blah/mymodule") import hbs from "discourse/widgets/hbs-compiler"; diff --git a/spec/lib/discourse_spec.rb b/spec/lib/discourse_spec.rb index 1a4227fe11..c0321ec6d7 100644 --- a/spec/lib/discourse_spec.rb +++ b/spec/lib/discourse_spec.rb @@ -300,6 +300,12 @@ RSpec.describe Discourse do Discourse.disable_readonly_mode(user_readonly_mode_key) expect(Discourse.readonly_mode?).to eq(false) end + + it "returns true when forced via global setting" do + expect(Discourse.readonly_mode?).to eq(false) + global_setting :pg_force_readonly_mode, true + expect(Discourse.readonly_mode?).to eq(true) + end end describe ".received_postgres_readonly!" do diff --git a/spec/lib/discourse_tagging_spec.rb b/spec/lib/discourse_tagging_spec.rb index 1188243c57..02782eefd5 100644 --- a/spec/lib/discourse_tagging_spec.rb +++ b/spec/lib/discourse_tagging_spec.rb @@ -641,9 +641,11 @@ RSpec.describe DiscourseTagging do it "user does not get an error when editing their topic with a hidden tag" do PostRevisor.new(post).revise!(admin, raw: post.raw, tags: [hidden_tag.name]) + expect( PostRevisor.new(post).revise!(topic.user, raw: post.raw + " edit", tags: []), ).to be_truthy + expect(topic.reload.tags).to eq([hidden_tag]) end end @@ -967,8 +969,16 @@ RSpec.describe DiscourseTagging do topic = Fabricate(:topic, tags: [tag2]) expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag2.name])).to eq(true) expect_same_tag_names(topic.reload.tags, [tag1]) - expect(tag1.reload.topic_count).to eq(1) - expect(tag2.reload.topic_count).to eq(0) + + tag1.reload + + expect(tag1.public_topic_count).to eq(1) + expect(tag1.staff_topic_count).to eq(1) + + tag2.reload + + expect(tag2.public_topic_count).to eq(0) + expect(tag2.staff_topic_count).to eq(0) end end end diff --git a/spec/lib/email/receiver_spec.rb b/spec/lib/email/receiver_spec.rb index 99035646be..baf9e0dae0 100644 --- a/spec/lib/email/receiver_spec.rb +++ b/spec/lib/email/receiver_spec.rb @@ -1550,10 +1550,12 @@ RSpec.describe Email::Receiver do handler_calls = 0 handler = proc do |topic| - expect(topic.incoming_email_addresses).to contain_exactly( - "discourse@bar.com", - "category@foo.com", - ) + expect( + [ + topic.incoming_email.first.from_address, + topic.incoming_email.first.to_addresses_split, + ].flatten, + ).to contain_exactly("discourse@bar.com", "category@foo.com") handler_calls += 1 end diff --git a/spec/lib/guardian/topic_guardian_spec.rb b/spec/lib/guardian/topic_guardian_spec.rb index 94ff370848..dbdc67c089 100644 --- a/spec/lib/guardian/topic_guardian_spec.rb +++ b/spec/lib/guardian/topic_guardian_spec.rb @@ -3,7 +3,8 @@ RSpec.describe TopicGuardian do fab!(:user) { Fabricate(:user) } fab!(:admin) { Fabricate(:admin) } - fab!(:tl3_user) { Fabricate(:leader) } + fab!(:tl3_user) { Fabricate(:trust_level_3) } + fab!(:tl4_user) { Fabricate(:trust_level_4) } fab!(:moderator) { Fabricate(:moderator) } fab!(:category) { Fabricate(:category) } fab!(:group) { Fabricate(:group) } @@ -74,6 +75,27 @@ RSpec.describe TopicGuardian do end end + describe "#can_see_deleted_topics?" do + it "returns true for staff" do + expect(Guardian.new(admin).can_see_deleted_topics?(topic.category)).to eq(true) + end + + it "returns true for group moderator" do + SiteSetting.enable_category_group_moderation = true + expect(Guardian.new(user).can_see_deleted_topics?(topic.category)).to eq(false) + category.update!(reviewable_by_group_id: group.id) + group.add(user) + topic.update!(category: category) + expect(Guardian.new(user).can_see_deleted_topics?(topic.category)).to eq(true) + end + + it "returns true when tl4 can delete posts and topics" do + expect(Guardian.new(tl4_user).can_see_deleted_topics?(topic.category)).to eq(false) + SiteSetting.tl4_delete_posts_and_topics = true + expect(Guardian.new(tl4_user).can_see_deleted_topics?(topic.category)).to eq(true) + end + end + describe "#can_edit_topic?" do context "when the topic is a shared draft" do let(:tl2_user) { Fabricate(:user, trust_level: TrustLevel[2]) } @@ -121,7 +143,6 @@ RSpec.describe TopicGuardian do describe "#can_review_topic?" do it "returns false for TL4 users" do - tl4_user = Fabricate(:user, trust_level: TrustLevel[4]) topic = Fabricate(:topic) expect(Guardian.new(tl4_user).can_review_topic?(topic)).to eq(false) @@ -134,8 +155,6 @@ RSpec.describe TopicGuardian do end it "returns true for TL4 users" do - tl4_user = Fabricate(:user, trust_level: TrustLevel[4]) - expect(Guardian.new(tl4_user).can_create_unlisted_topic?(topic)).to eq(true) end @@ -144,6 +163,20 @@ RSpec.describe TopicGuardian do end end + describe "#can_see_unlisted_topics?" do + it "is allowed for staff users" do + expect(Guardian.new(moderator).can_see_unlisted_topics?).to eq(true) + end + + it "is allowed for TL4 users" do + expect(Guardian.new(tl4_user).can_see_unlisted_topics?).to eq(true) + end + + it "is not allowed for lower level users" do + expect(Guardian.new(tl3_user).can_see_unlisted_topics?).to eq(false) + end + end + # The test cases here are intentionally kept brief because majority of the cases are already handled by # `TopicGuardianCanSeeConsistencyCheck` which we run to ensure that the implementation between `TopicGuardian#can_see_topic_ids` # and `TopicGuardian#can_see_topic?` is consistent. diff --git a/spec/lib/guardian_spec.rb b/spec/lib/guardian_spec.rb index 4e5bf8d88f..8bdfe06e42 100644 --- a/spec/lib/guardian_spec.rb +++ b/spec/lib/guardian_spec.rb @@ -1347,6 +1347,13 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_recover_topic?(topic)).to be_falsey end + it "returns true when tl4 can delete posts and topics" do + PostDestroyer.new(moderator, topic.first_post).destroy + expect(Guardian.new(trust_level_4).can_recover_topic?(topic)).to be_falsey + SiteSetting.tl4_delete_posts_and_topics = true + expect(Guardian.new(trust_level_4).can_recover_topic?(topic.reload)).to be_truthy + end + context "as a moderator" do describe "when post has been deleted" do it "should return the right value" do @@ -2195,6 +2202,12 @@ RSpec.describe Guardian do expect(Guardian.new(topic.user).can_delete?(topic)).to be_falsey end + it "returns true when tl4 can delete posts and topics" do + expect(Guardian.new(trust_level_4).can_delete?(topic)).to be_falsey + SiteSetting.tl4_delete_posts_and_topics = true + expect(Guardian.new(trust_level_4).can_delete?(topic)).to be_truthy + end + it "returns false if topic was created > 24h ago" do topic.update!(posts_count: 1, created_at: 48.hours.ago) expect(Guardian.new(topic.user).can_delete?(topic)).to be_falsey @@ -2241,6 +2254,12 @@ RSpec.describe Guardian do expect(Guardian.new(trust_level_4).can_delete?(post)).to be_falsey end + it "returns true when tl4 can delete posts and topics" do + expect(Guardian.new(trust_level_4).can_delete?(post)).to be_falsey + SiteSetting.tl4_delete_posts_and_topics = true + expect(Guardian.new(trust_level_4).can_delete?(post)).to be_truthy + end + it "returns false when self deletions are disabled" do SiteSetting.max_post_deletions_per_day = 0 expect(Guardian.new(user).can_delete?(post)).to be_falsey @@ -2384,6 +2403,22 @@ RSpec.describe Guardian do end end + describe "#can_see_deleted_posts?" do + it "returns true if the user is an admin" do + expect(Guardian.new(admin).can_see_deleted_posts?(post.topic.category)).to be_truthy + end + + it "returns true if the user is a moderator of category" do + expect(Guardian.new(moderator).can_see_deleted_posts?(post.topic.category)).to be_truthy + end + + it "returns true when tl4 can delete posts and topics" do + expect(Guardian.new(trust_level_4).can_see_deleted_posts?(post)).to be_falsey + SiteSetting.tl4_delete_posts_and_topics = true + expect(Guardian.new(trust_level_4).can_see_deleted_posts?(post)).to be_truthy + end + end + describe "#can_approve?" do it "wont allow a non-logged in user to approve" do expect(Guardian.new.can_approve?(user)).to be_falsey diff --git a/spec/lib/message_id_service_spec.rb b/spec/lib/message_id_service_spec.rb index dc8fa436fc..271bed7706 100644 --- a/spec/lib/message_id_service_spec.rb +++ b/spec/lib/message_id_service_spec.rb @@ -7,69 +7,6 @@ RSpec.describe Email::MessageIdService do subject { described_class } - describe "#generate_for_post" do - it "generates for the post using the message_id on the post's incoming_email" do - Fabricate(:incoming_email, message_id: "test@test.localhost", post: post) - post.reload - expect(subject.generate_for_post(post, use_incoming_email_if_present: true)).to eq( - "", - ) - end - - it "generates for the post without an incoming_email record" do - expect(subject.generate_for_post(post)).to match(subject.message_id_post_id_regexp) - expect(subject.generate_for_post(post, use_incoming_email_if_present: true)).to match( - subject.message_id_post_id_regexp, - ) - end - end - - describe "#generate_for_topic" do - it "generates for the topic using the message_id on the first post's incoming_email" do - Fabricate( - :incoming_email, - message_id: "test213428@somemailservice.com", - post: post, - created_via: IncomingEmail.created_via_types[:handle_mail], - ) - post.reload - expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to eq( - "", - ) - end - - it "does not use the first post's incoming email if it was created via group_smtp, only handle_mail" do - incoming = - Fabricate( - :incoming_email, - message_id: "test213428@somemailservice.com", - post: post, - created_via: IncomingEmail.created_via_types[:group_smtp], - ) - post.reload - expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to match( - subject.message_id_topic_id_regexp, - ) - incoming.update(created_via: IncomingEmail.created_via_types[:handle_mail]) - expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to eq( - "", - ) - end - - it "generates for the topic without an incoming_email record" do - expect(subject.generate_for_topic(topic)).to match(subject.message_id_topic_id_regexp) - expect(subject.generate_for_topic(topic, use_incoming_email_if_present: true)).to match( - subject.message_id_topic_id_regexp, - ) - end - - it "generates canonical for the topic" do - canonical_topic_id = subject.generate_for_topic(topic, canonical: true) - expect(canonical_topic_id).to match(subject.message_id_topic_id_regexp) - expect(canonical_topic_id).to eq("") - end - end - describe "#generate_or_use_existing" do it "does not override a post's existing outbound_message_id" do post.update!(outbound_message_id: "blah@host.test") diff --git a/spec/lib/mobile_detection_spec.rb b/spec/lib/mobile_detection_spec.rb index 853fe24b1b..584e20ebda 100644 --- a/spec/lib/mobile_detection_spec.rb +++ b/spec/lib/mobile_detection_spec.rb @@ -10,6 +10,8 @@ RSpec.describe MobileDetection do Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Mobile Safari/537.36 (comp Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.3626.121 Mobile Safari/537.36 (comp + Mozilla/5.0 (iPhone; CPU iPhone OS 14_8_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1 + Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/99.0.4844.59 Mobile/15E148 Safari/604.1 STR end @@ -17,7 +19,6 @@ RSpec.describe MobileDetection do (<<~STR).split("\n") Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1 Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Mobile Safari/537.36 (compatible; - Mozilla/5.0 (iPhone; CPU iPhone OS 14_8_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1 Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Mobile Safari/537.36 (compatible Mozilla/5.0 (Android 12; Mobile; rv:98.0) Gecko/98.0 Firefox/98.0 Mozilla/5.0 (iPad; CPU OS 15_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/99.0.4844.59 Mobile/15E148 Safari/604.1 @@ -26,7 +27,6 @@ RSpec.describe MobileDetection do Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 DiscourseHub Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.88 Mobile Safari/537.36 Mozilla/5.0 (Android 12; Mobile; rv:99.0) Gecko/99.0 Firefox/99.0 -Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/99.0.4844.59 Mobile/15E148 Safari/604.1 Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.88 Mobile Safari/537.36 Mozilla/5.0 (Linux; Android 12; Pixel 4a) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.88 Mobile Safari/537.36 Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Mobile Safari/537.36 @@ -36,11 +36,17 @@ Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHT it "detects modern browsers correctly" do modern_user_agents.each do |agent| - expect(MobileDetection.modern_mobile_device?(agent)).to eq(true) + expect(MobileDetection.modern_mobile_device?(agent)).to( + eq(true), + "Failed User Agent: '#{agent}'", + ) end old_user_agents.each do |agent| - expect(MobileDetection.modern_mobile_device?(agent)).to eq(false) + expect(MobileDetection.modern_mobile_device?(agent)).to( + eq(false), + "Failed User Agent: '#{agent}'", + ) end end end diff --git a/spec/lib/onebox/engine/discourse_topic_onebox_spec.rb b/spec/lib/onebox/engine/discourse_topic_onebox_spec.rb new file mode 100644 index 0000000000..7b8450c7e7 --- /dev/null +++ b/spec/lib/onebox/engine/discourse_topic_onebox_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.describe Onebox::Engine::DiscourseTopicOnebox do + subject(:onebox) { described_class.new(url) } + + describe "#data" do + subject(:data) { onebox.data } + + let(:url) do + "https://meta.discourse.org/t/congratulations-most-stars-in-2013-github-octoverse/12483" + end + let(:expected_data) do + { + article_published_time: "6 Feb 14", + article_published_time_title: "04:55AM - 06 February 2014", + article_tags: %w[how-to sso], + card: "summary", + categories: [{ name: "praise", color: "9EB83B" }], + data1: "1 mins 🕑", + data2: "9 ❤", + description: + "Congratulations Discourse for qualifying Repositories with the most stars on GitHub Octoverse. And that too in just over an year, way to go! 💥", + domain: "Discourse Meta", + favicon: + "https://d11a6trkgmumsb.cloudfront.net/optimized/3X/b/3/b33be9538df3547fcf9d1a51a4637d77392ac6f9_2_32x32.png", + ignore_canonical: "true", + image: + "https://d11a6trkgmumsb.cloudfront.net/optimized/2X/d/d063b3b0807377d98695ee08042a9ba0a8c593bd_2_690x362.png", + label1: "Reading time", + label2: "Likes", + link: + "https://meta.discourse.org/t/congratulations-most-stars-in-2013-github-octoverse/12483", + published_time: "2014-02-06T04:55:19+00:00", + render_category_block?: true, + render_tags?: true, + site_name: "Discourse Meta", + title: "Congratulations, most stars in 2013 GitHub Octoverse!", + url: + "https://meta.discourse.org/t/congratulations-most-stars-in-2013-github-octoverse/12483", + } + end + + before do + stub_request(:get, url).to_return(status: 200, body: onebox_response("discourse_topic")) + end + + it "returns the expected data" do + expect(data).to include expected_data + end + end +end diff --git a/spec/lib/onebox/open_graph_spec.rb b/spec/lib/onebox/open_graph_spec.rb index efbbb90bb4..71de9468e8 100644 --- a/spec/lib/onebox/open_graph_spec.rb +++ b/spec/lib/onebox/open_graph_spec.rb @@ -24,4 +24,32 @@ RSpec.describe Onebox::OpenGraph do og = described_class.new(doc) expect(og.image).to eq("http://test.com/test'ing.mp3") end + + describe "Collections" do + subject(:graph) { described_class.new(doc) } + + let(:doc) { Nokogiri.HTML(<<-HTML) } + + test + + + + + + + + HTML + + it "handles multiple article:tag tags" do + expect(graph.article_tags).to eq %w[tag1 tag2] + end + + it "handles multiple article:section tags" do + expect(graph.article_sections).to eq %w[category1 category2] + end + + it "handles multiple article:section:color tags" do + expect(graph.article_section_colors).to eq %w[ff0000 0000ff] + end + end end diff --git a/spec/lib/oneboxer_spec.rb b/spec/lib/oneboxer_spec.rb index ae969d015e..7e23517425 100644 --- a/spec/lib/oneboxer_spec.rb +++ b/spec/lib/oneboxer_spec.rb @@ -198,6 +198,66 @@ RSpec.describe Oneboxer do "

    http://test.localhost/new?%27class=black

    ", ) end + + it "escapes URLs of local audio uploads" do + result = + described_class.onebox_raw( + "#{Discourse.base_url}/uploads/default/original/1X/a1c31803be81b85ecafc4f77b1008eee9b3b82f4.wav#'<>", + ) + expect(result[:onebox]).to eq(<<~HTML) + + HTML + expect(result[:preview]).to eq(<<~HTML) + + HTML + end + + it "escapes URLs of local video uploads" do + result = + described_class.onebox_raw( + "#{Discourse.base_url}/uploads/default/original/1X/a1c31803be81b85ecafc4f77b1008eee9b3b82f4.mp4#'<>", + ) + expect(result[:onebox]).to eq(<<~HTML) + + HTML + expect(result[:preview]).to eq(<<~HTML) + + HTML + end + + it "escapes URLs of generic local links" do + result = described_class.onebox_raw("#{Discourse.base_url}/g/somegroup#'onerror='") + expect(result[:onebox]).to eq( + "http://test.localhost/g/somegroup#'onerror='", + ) + expect(result[:preview]).to eq( + "http://test.localhost/g/somegroup#'onerror='", + ) + end end describe ".external_onebox" do diff --git a/spec/lib/post_destroyer_spec.rb b/spec/lib/post_destroyer_spec.rb index 20a18a3e82..25fa3496dd 100644 --- a/spec/lib/post_destroyer_spec.rb +++ b/spec/lib/post_destroyer_spec.rb @@ -1201,4 +1201,26 @@ RSpec.describe PostDestroyer do ) end end + + describe "mailing_list_mode emails on recovery" do + fab!(:topic) { Fabricate(:topic) } + fab!(:post_1) { Fabricate(:post, topic: topic) } + fab!(:post_2) { Fabricate(:post, topic: topic) } + + it "enqueues the notify_mailing_list_subscribers_job for the post" do + PostDestroyer.new(admin, post_2).destroy + post_2.reload + expect_enqueued_with(job: :notify_mailing_list_subscribers, args: { post_id: post_2.id }) do + PostDestroyer.new(admin, post_2).recover + end + end + + it "enqueues the notify_mailing_list_subscribers_job for the op" do + PostDestroyer.new(admin, post_1).destroy + post_1.reload + expect_enqueued_with(job: :notify_mailing_list_subscribers, args: { post_id: post_1.id }) do + PostDestroyer.new(admin, post_1).recover + end + end + end end diff --git a/spec/lib/pretty_text_spec.rb b/spec/lib/pretty_text_spec.rb index 25a8d80749..7b3ef98320 100644 --- a/spec/lib/pretty_text_spec.rb +++ b/spec/lib/pretty_text_spec.rb @@ -1619,6 +1619,9 @@ RSpec.describe PrettyText do end it "produces hashtag links" do + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + SiteSetting.enable_experimental_hashtag_autocomplete = false + category = Fabricate(:category, name: "testing") category2 = Fabricate(:category, name: "known") Fabricate(:topic, tags: [Fabricate(:tag, name: "known")]) @@ -1908,6 +1911,9 @@ HTML end it "does not replace hashtags and mentions" do + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + SiteSetting.enable_experimental_hashtag_autocomplete = false + Fabricate(:user, username: "test") category = Fabricate(:category, slug: "test") Fabricate( @@ -1927,6 +1933,9 @@ HTML end it "does not replace hashtags and mentions when watched words are regular expressions" do + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + SiteSetting.enable_experimental_hashtag_autocomplete = false + SiteSetting.watched_words_regular_expressions = true Fabricate(:user, username: "test") diff --git a/spec/lib/scheduler/defer_spec.rb b/spec/lib/scheduler/defer_spec.rb index 67882c2a7e..f886335fd2 100644 --- a/spec/lib/scheduler/defer_spec.rb +++ b/spec/lib/scheduler/defer_spec.rb @@ -12,11 +12,41 @@ RSpec.describe Scheduler::Defer do end before do + Discourse.catch_job_exceptions! @defer = DeferInstance.new @defer.async = true end - after { @defer.stop! } + after do + @defer.stop! + Discourse.reset_catch_job_exceptions! + end + + it "supports basic instrumentation" do + @defer.later("first") {} + @defer.later("first") {} + @defer.later("second") {} + @defer.later("bad") { raise "boom" } + + wait_for(200) { @defer.length == 0 } + + stats = Hash[@defer.stats] + + expect(stats["first"][:queued]).to eq(2) + expect(stats["first"][:finished]).to eq(2) + expect(stats["first"][:errors]).to eq(0) + expect(stats["first"][:duration]).to be > 0 + + expect(stats["second"][:queued]).to eq(1) + expect(stats["second"][:finished]).to eq(1) + expect(stats["second"][:errors]).to eq(0) + expect(stats["second"][:duration]).to be > 0 + + expect(stats["bad"][:queued]).to eq(1) + expect(stats["bad"][:finished]).to eq(1) + expect(stats["bad"][:duration]).to be > 0 + expect(stats["bad"][:errors]).to eq(1) + end it "supports timeout reporting" do @defer.timeout = 0.05 diff --git a/spec/lib/search_spec.rb b/spec/lib/search_spec.rb index 01640a5f99..4388a13ebd 100644 --- a/spec/lib/search_spec.rb +++ b/spec/lib/search_spec.rb @@ -897,7 +897,7 @@ RSpec.describe Search do result = Search.execute("search term") expect(result.posts.first.topic_title_headline).to eq(<<~HTML.chomp) - Very very very very very very very long topic title with our search term in the middle of the title + Very very very very very very very long topic title with our search term in the middle of the title HTML end @@ -1316,7 +1316,28 @@ RSpec.describe Search do context "with non staff logged in" do it "shows doesn’t show group" do - expect(search.groups.map(&:name)).to be_empty + end + end + end + + context "with registered plugin callbacks" do + let!(:group) { Fabricate(:group, name: "plugin-special") } + + context "when :search_groups_set_query_callback is registered" do + it "changes the search results" do + # initial result (without applying the plugin callback ) + expect(search.groups.map(&:name).include?("plugin-special")).to eq(true) + + DiscoursePluginRegistry.register_search_groups_set_query_callback( + Proc.new { |query, term, guardian| query.where.not(name: "plugin-special") }, + Plugin::Instance.new, + ) + + # after using the callback we expect the search result to be changed because the + # query was altered + expect(search.groups.map(&:name).include?("plugin-special")).to eq(false) + + DiscoursePluginRegistry.reset_register!(:search_groups_set_query_callbacks) end end end @@ -1780,6 +1801,31 @@ RSpec.describe Search do Search.execute("group:#{group.id}", guardian: Guardian.new(user)).posts, ).to contain_exactly(post) end + + context "with registered plugin callbacks" do + context "when :search_groups_set_query_callback is registered" do + it "changes the search results" do + group.update!( + visibility_level: Group.visibility_levels[:public], + members_visibility_level: Group.visibility_levels[:public], + ) + + # initial result (without applying the plugin callback ) + expect(Search.execute("group:like_a_boss").posts).to contain_exactly(post) + + DiscoursePluginRegistry.register_search_groups_set_query_callback( + Proc.new { |query, term, guardian| query.where.not(name: "Like_a_Boss") }, + Plugin::Instance.new, + ) + + # after using the callback we expect the search result to be changed because the + # query was altered + expect(Search.execute("group:like_a_boss").posts).to be_blank + + DiscoursePluginRegistry.reset_register!(:search_groups_set_query_callbacks) + end + end + end end it "supports badge" do @@ -2356,7 +2402,7 @@ RSpec.describe Search do expect(results.posts.length).to eq(1) # TODO: this is a test we need to fix! - #expect(results.blurb(results.posts.first)).to include('Rágis') + # expect(results.blurb(results.posts.first)).to include('Rágis') results = Search.execute("สวัสดี", type_filter: "topic") expect(results.posts.length).to eq(1) diff --git a/spec/lib/topic_query_spec.rb b/spec/lib/topic_query_spec.rb index ffd62eaa1d..261a4cbb7c 100644 --- a/spec/lib/topic_query_spec.rb +++ b/spec/lib/topic_query_spec.rb @@ -16,6 +16,7 @@ RSpec.describe TopicQuery do fab!(:creator) { Fabricate(:user) } let(:topic_query) { TopicQuery.new(user) } + fab!(:tl4_user) { Fabricate(:trust_level_4) } fab!(:moderator) { Fabricate(:moderator) } fab!(:admin) { Fabricate(:admin) } @@ -408,6 +409,9 @@ RSpec.describe TopicQuery do fab!(:tagged_topic3) { Fabricate(:topic, tags: [tag, other_tag]) } fab!(:tagged_topic4) { Fabricate(:topic, tags: [uppercase_tag]) } fab!(:no_tags_topic) { Fabricate(:topic) } + fab!(:tag_group) do + Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [other_tag.name]) + end let(:synonym) { Fabricate(:tag, target_tag: tag, name: "synonym") } it "excludes a tag if desired" do @@ -415,6 +419,11 @@ RSpec.describe TopicQuery do expect(topics.any? { |t| t.tags.include?(tag) }).to eq(false) end + it "does not exclude a tagged topic without permission" do + topics = TopicQuery.new(user, exclude_tag: other_tag.name).list_latest.topics + expect(topics.map(&:id)).to include(tagged_topic2.id) + end + it "returns topics with the tag when filtered to it" do expect(TopicQuery.new(moderator, tags: tag.name).list_latest.topics).to contain_exactly( tagged_topic1, @@ -795,8 +804,11 @@ RSpec.describe TopicQuery do # includes the invisible topic if you're a moderator expect(TopicQuery.new(moderator).list_latest.topics.include?(invisible_topic)).to eq(true) - # includes the invisible topic if you're an admin" do + # includes the invisible topic if you're an admin expect(TopicQuery.new(admin).list_latest.topics.include?(invisible_topic)).to eq(true) + + # includes the invisible topic if you're a TL4 user + expect(TopicQuery.new(tl4_user).list_latest.topics.include?(invisible_topic)).to eq(true) end context "with sort_order" do diff --git a/spec/lib/topic_view_spec.rb b/spec/lib/topic_view_spec.rb index 560993003a..5032846488 100644 --- a/spec/lib/topic_view_spec.rb +++ b/spec/lib/topic_view_spec.rb @@ -750,8 +750,8 @@ RSpec.describe TopicView do end describe "page_title" do - fab!(:tag1) { Fabricate(:tag) } - fab!(:tag2) { Fabricate(:tag, topic_count: 2) } + fab!(:tag1) { Fabricate(:tag, staff_topic_count: 0, public_topic_count: 0) } + fab!(:tag2) { Fabricate(:tag, staff_topic_count: 2, public_topic_count: 2) } fab!(:op_post) { Fabricate(:post, topic: topic) } fab!(:post1) { Fabricate(:post, topic: topic) } fab!(:whisper) { Fabricate(:post, topic: topic, post_type: Post.types[:whisper]) } @@ -1072,4 +1072,17 @@ RSpec.describe TopicView do end end end + + describe "#tags" do + subject(:topic_view_tags) { topic_view.tags } + + let(:topic_view) { described_class.new(topic, user) } + let(:topic) { Fabricate.build(:topic, tags: tags) } + let(:tags) { Fabricate.build_times(2, :tag) } + let(:user) { Fabricate(:user) } + + it "returns the tags names" do + expect(topic_view_tags).to match tags.map(&:name) + end + end end diff --git a/spec/lib/upload_security_spec.rb b/spec/lib/upload_security_spec.rb index b5a307997f..8834ff4fc4 100644 --- a/spec/lib/upload_security_spec.rb +++ b/spec/lib/upload_security_spec.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true RSpec.describe UploadSecurity do - let(:private_category) { Fabricate(:private_category, group: Fabricate(:group)) } - let(:post_in_secure_context) do + fab!(:private_category) { Fabricate(:private_category, group: Fabricate(:group)) } + fab!(:post_in_secure_context) do Fabricate(:post, topic: Fabricate(:topic, category: private_category)) end fab!(:upload) { Fabricate(:upload) } let(:type) { nil } - let(:opts) { { type: type, creating: true } } + let(:opts) { { type: type, creating: creating } } + subject { described_class.new(upload, opts) } context "when secure uploads is enabled" do @@ -16,172 +17,303 @@ RSpec.describe UploadSecurity do SiteSetting.secure_uploads = true end - context "when login_required (everything should be secure except public context items)" do - before { SiteSetting.login_required = true } - it "returns true" do - expect(subject.should_be_secure?).to eq(true) - end + context "when creating the upload" do + let(:creating) { true } - context "when uploading in public context" do - describe "for a public type badge_image" do - let(:type) { "badge_image" } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for a public type group_flair" do - let(:type) { "group_flair" } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for a public type avatar" do - let(:type) { "avatar" } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for a public type custom_emoji" do - let(:type) { "custom_emoji" } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for a public type profile_background" do - let(:type) { "profile_background" } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for a public type avatar" do - let(:type) { "avatar" } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for a public type category_logo" do - let(:type) { "category_logo" } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for a public type category_background" do - let(:type) { "category_background" } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for a custom public type" do - let(:type) { "my_custom_type" } + context "when login_required (everything should be secure except public context items)" do + before { SiteSetting.login_required = true } - it "returns true if the custom type has not been added" do - expect(subject.should_be_secure?).to eq(true) - end - - it "returns false if the custom type has been added" do - UploadSecurity.register_custom_public_type(type) - expect(subject.should_be_secure?).to eq(false) - UploadSecurity.reset_custom_public_types - end - end - describe "for_theme" do - before { upload.stubs(:for_theme).returns(true) } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for_site_setting" do - before { upload.stubs(:for_site_setting).returns(true) } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - describe "for_gravatar" do - before { upload.stubs(:for_gravatar).returns(true) } - it "returns false" do - expect(subject.should_be_secure?).to eq(false) - end - end - - describe "when the upload is used for a custom emoji" do - it "returns false" do - CustomEmoji.create(name: "meme", upload: upload) - expect(subject.should_be_secure?).to eq(false) - end - end - - describe "when it is based on a regular emoji" do - it "returns false" do - falafel = - Emoji.all.find do |e| - e.url == "/images/emoji/twitter/falafel.png?v=#{Emoji::EMOJI_VERSION}" - end - upload.update!(origin: "http://localhost:3000#{falafel.url}") - expect(subject.should_be_secure?).to eq(false) - end - end - end - end - - context "when the access control post has_secure_uploads?" do - before { upload.update(access_control_post_id: post_in_secure_context.id) } - it "returns true" do - expect(subject.should_be_secure?).to eq(true) - end - - context "when the post is deleted" do - before { post_in_secure_context.trash! } - it "still determines whether the post has secure uploads; returns true" do - expect(subject.should_be_secure?).to eq(true) - end - end - end - - context "when uploading in the composer" do - let(:type) { "composer" } - it "returns true" do - expect(subject.should_be_secure?).to eq(true) - end - end - context "when uploading for a group message" do - before { upload.stubs(:for_group_message).returns(true) } - it "returns true" do - expect(subject.should_be_secure?).to eq(true) - end - end - context "when uploading for a PM" do - before { upload.stubs(:for_private_message).returns(true) } - it "returns true" do - expect(subject.should_be_secure?).to eq(true) - end - end - context "when upload is already secure" do - before { upload.update(secure: true) } - it "returns true" do - expect(subject.should_be_secure?).to eq(true) - end - end - - context "for attachments" do - before { upload.update(original_filename: "test.pdf") } - - context "when the access control post has_secure_uploads?" do - before { upload.update(access_control_post: post_in_secure_context) } it "returns true" do expect(subject.should_be_secure?).to eq(true) end + + context "when uploading in public context" do + describe "for a public type badge_image" do + let(:type) { "badge_image" } + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for a public type group_flair" do + let(:type) { "group_flair" } + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for a public type avatar" do + let(:type) { "avatar" } + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for a public type custom_emoji" do + let(:type) { "custom_emoji" } + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for a public type profile_background" do + let(:type) { "profile_background" } + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for a public type avatar" do + let(:type) { "avatar" } + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for a public type category_logo" do + let(:type) { "category_logo" } + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for a public type category_background" do + let(:type) { "category_background" } + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for a custom public type" do + let(:type) { "my_custom_type" } + + it "returns true if the custom type has not been added" do + expect(subject.should_be_secure?).to eq(true) + end + + it "returns false if the custom type has been added" do + UploadSecurity.register_custom_public_type(type) + expect(subject.should_be_secure?).to eq(false) + UploadSecurity.reset_custom_public_types + end + end + + describe "for_theme" do + before { upload.stubs(:for_theme).returns(true) } + + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for_site_setting" do + before { upload.stubs(:for_site_setting).returns(true) } + + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "for_gravatar" do + before { upload.stubs(:for_gravatar).returns(true) } + + it "returns false" do + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when it is based on a regular emoji" do + it "returns false" do + falafel = + Emoji.all.find do |e| + e.url == "/images/emoji/twitter/falafel.png?v=#{Emoji::EMOJI_VERSION}" + end + upload.update!(origin: "http://localhost:3000#{falafel.url}") + expect(subject.should_be_secure?).to eq(false) + end + end + end + end + + context "when the access control post has_secure_uploads?" do + before { upload.update(access_control_post_id: post_in_secure_context.id) } + + it "returns true" do + expect(subject.should_be_secure?).to eq(true) + end + + context "when the post is deleted" do + before { post_in_secure_context.trash! } + + it "still determines whether the post has secure uploads; returns true" do + expect(subject.should_be_secure?).to eq(true) + end + end + end + + context "when uploading in the composer" do + let(:type) { "composer" } + it "returns true" do + expect(subject.should_be_secure?).to eq(true) + end + end + + context "when uploading for a group message" do + before { upload.stubs(:for_group_message).returns(true) } + it "returns true" do + expect(subject.should_be_secure?).to eq(true) + end + end + + context "when uploading for a PM" do + before { upload.stubs(:for_private_message).returns(true) } + it "returns true" do + expect(subject.should_be_secure?).to eq(true) + end + end + + context "when upload is already secure" do + before { upload.update(secure: true) } + it "returns true" do + expect(subject.should_be_secure?).to eq(true) + end + end + + context "for attachments" do + before { upload.update(original_filename: "test.pdf") } + + context "when the access control post has_secure_uploads?" do + before { upload.update(access_control_post: post_in_secure_context) } + it "returns true" do + expect(subject.should_be_secure?).to eq(true) + end + end + end + end + + context "when checking an existing upload" do + let(:creating) { false } + + before do + # this is done so the after_save callbacks for site settings to make + # UploadReference records works + @original_provider = SiteSetting.provider + SiteSetting.provider = SiteSettings::DbProvider.new(SiteSetting) + setup_s3 + SiteSetting.secure_uploads = true + end + + after { SiteSetting.provider = @original_provider } + + def create_secure_post_reference + UploadReference.ensure_exist!(upload_ids: [upload.id], target: post_in_secure_context) + upload.update!(access_control_post: post_in_secure_context) + end + + describe "when the upload is first used for a post in a secure context" do + it "returns true" do + create_secure_post_reference + expect(subject.should_be_secure?).to eq(true) + end + end + + describe "when the upload is first used for a post in a secure context that is later deleted" do + it "returns false" do + create_secure_post_reference + post_in_secure_context.trash! + CustomEmoji.create(name: "meme", upload: upload) + + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a site setting" do + it "returns false" do + SiteSetting.favicon = upload + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a theme field" do + it "returns false" do + Fabricate(:theme_field, type_id: ThemeField.types[:theme_upload_var], upload: upload) + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a group flair image" do + it "returns false" do + Fabricate(:group, flair_upload: upload) + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a custom emoji" do + it "returns false" do + CustomEmoji.create(name: "meme", upload: upload) + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a badge" do + it "returns false" do + Fabricate(:badge, image_upload: upload) + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a category image (logo, dark logo, background)" do + it "returns false" do + Fabricate(:category, uploaded_logo: upload) + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a user profile (profile background, card background)" do + it "returns false" do + user = Fabricate(:user) + user.user_profile.update!(card_background_upload: upload) + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a user uploaded avatar" do + it "returns false" do + Fabricate(:user, uploaded_avatar: upload) + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end + end + + describe "when the upload is first used for a UserAvatar" do + it "returns false" do + Fabricate(:user_avatar, custom_upload: upload) + create_secure_post_reference + expect(subject.should_be_secure?).to eq(false) + end end end end context "when secure uploads is disabled" do + let(:creating) { true } + before { SiteSetting.secure_uploads = false } + it "returns false" do expect(subject.should_be_secure?).to eq(false) end context "for attachments" do before { upload.update(original_filename: "test.pdf") } + it "returns false" do expect(subject.should_be_secure?).to eq(false) end diff --git a/spec/lib/validators/enable_new_notifications_menu_validator_spec.rb b/spec/lib/validators/enable_new_notifications_menu_validator_spec.rb new file mode 100644 index 0000000000..309db022d8 --- /dev/null +++ b/spec/lib/validators/enable_new_notifications_menu_validator_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe EnableNewNotificationsMenuValidator do + it "does not allow `enable_new_notifications_menu` site settings to be enabled when `navigation_menu` site settings is not set to `legacy`" do + SiteSetting.navigation_menu = "sidebar" + + expect { SiteSetting.enable_new_notifications_menu = true }.to raise_error( + Discourse::InvalidParameters, + /#{I18n.t("site_settings.errors.enable_new_notifications_menu_not_legacy_navigation_menu")}/, + ) + end + + it "allows `enable_new_notifications_menu` site settings to be enabled when `navigation_menu` site settings is set to `legacy`" do + SiteSetting.navigation_menu = "legacy" + + expect { SiteSetting.enable_new_notifications_menu = true }.to_not raise_error + end +end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index ecabfc50c6..da44f31eb4 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -577,6 +577,12 @@ RSpec.describe Category do expect(SiteSetting.shared_drafts_category).to be_blank end + it "deletes related embeddable host" do + embeddable_host = Fabricate(:embeddable_host, category: @category) + @category.destroy! + expect { embeddable_host.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + it "triggers a extensibility event" do event = DiscourseEvent.track(:category_destroyed) { @category.destroy } diff --git a/spec/models/embeddable_host_spec.rb b/spec/models/embeddable_host_spec.rb index 68871b9402..a72c577be8 100644 --- a/spec/models/embeddable_host_spec.rb +++ b/spec/models/embeddable_host_spec.rb @@ -88,8 +88,8 @@ RSpec.describe EmbeddableHost do expect(EmbeddableHost.url_allowed?("http://discourse.org")).to eq(true) end - it "always allow forum own URL" do - expect(EmbeddableHost.url_allowed?(Discourse.base_url)).to eq(true) + it "does not allow forum own URL" do + expect(EmbeddableHost.url_allowed?(Discourse.base_url)).to eq(false) end end diff --git a/spec/models/group_request_spec.rb b/spec/models/group_request_spec.rb new file mode 100644 index 0000000000..3dd9a660a3 --- /dev/null +++ b/spec/models/group_request_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.describe GroupRequest do + it { is_expected.to belong_to :user } + it { is_expected.to belong_to :group } + + it do + is_expected.to validate_length_of(:reason).is_at_most(described_class::REASON_CHARACTER_LIMIT) + end +end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index deb5e637f9..7fdb40fbaa 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -3,9 +3,11 @@ RSpec.describe Post do fab!(:coding_horror) { Fabricate(:coding_horror) } + let(:upload_path) { Discourse.store.upload_path } + before { Oneboxer.stubs :onebox } - let(:upload_path) { Discourse.store.upload_path } + it { is_expected.to have_many(:reviewables).dependent(:destroy) } describe "#hidden_reasons" do context "when verifying enum sequence" do diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 7aff07ab79..402c6c7334 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -202,29 +202,101 @@ RSpec.describe Tag do end end - describe "topic counts" do + describe ".ensure_consistency!" do it "should exclude private message topics" do topic Fabricate(:private_message_topic, tags: [tag]) Tag.ensure_consistency! tag.reload - expect(tag.topic_count).to eq(1) + expect(tag.staff_topic_count).to eq(1) + expect(tag.public_topic_count).to eq(1) + end + + it "should update Tag#topic_count and Tag#public_topic_count correctly" do + tag = Fabricate(:tag, name: "tag1") + tag2 = Fabricate(:tag, name: "tag2") + tag3 = Fabricate(:tag, name: "tag3") + group = Fabricate(:group) + category = Fabricate(:category) + private_category = Fabricate(:private_category, group: group) + private_category2 = Fabricate(:private_category, group: group) + + _topic_with_tag = Fabricate(:topic, category: category, tags: [tag]) + + _topic_with_tag_in_private_category = + Fabricate(:topic, category: private_category, tags: [tag]) + + _topic_with_tag2_in_private_category2 = + Fabricate(:topic, category: private_category2, tags: [tag2]) + + tag.update!(staff_topic_count: 123, public_topic_count: 456) + tag2.update!(staff_topic_count: 123, public_topic_count: 456) + tag3.update!(staff_topic_count: 123, public_topic_count: 456) + + Tag.ensure_consistency! + + tag.reload + tag2.reload + tag3.reload + + expect(tag.staff_topic_count).to eq(2) + expect(tag.public_topic_count).to eq(1) + expect(tag2.staff_topic_count).to eq(1) + expect(tag2.public_topic_count).to eq(0) + expect(tag3.staff_topic_count).to eq(0) + expect(tag3.public_topic_count).to eq(0) end end describe "unused tags scope" do let!(:tags) do [ - Fabricate(:tag, name: "used_publically", topic_count: 2, pm_topic_count: 0), - Fabricate(:tag, name: "used_privately", topic_count: 0, pm_topic_count: 3), - Fabricate(:tag, name: "used_everywhere", topic_count: 0, pm_topic_count: 3), - Fabricate(:tag, name: "unused1", topic_count: 0, pm_topic_count: 0), - Fabricate(:tag, name: "unused2", topic_count: 0, pm_topic_count: 0), + Fabricate( + :tag, + name: "used_publically", + staff_topic_count: 2, + public_topic_count: 2, + pm_topic_count: 0, + ), + Fabricate( + :tag, + name: "used_privately", + staff_topic_count: 0, + public_topic_count: 0, + pm_topic_count: 3, + ), + Fabricate( + :tag, + name: "used_everywhere", + staff_topic_count: 0, + public_topic_count: 0, + pm_topic_count: 3, + ), + Fabricate( + :tag, + name: "unused1", + staff_topic_count: 0, + public_topic_count: 0, + pm_topic_count: 0, + ), + Fabricate( + :tag, + name: "unused2", + staff_topic_count: 0, + public_topic_count: 0, + pm_topic_count: 0, + ), ] end let(:tag_in_group) do - Fabricate(:tag, name: "unused_in_group", topic_count: 0, pm_topic_count: 0) + Fabricate( + :tag, + name: "unused_in_group", + public_topic_count: 0, + staff_topic_count: 0, + pm_topic_count: 0, + ) end let!(:tag_group) { Fabricate(:tag_group, tag_names: [tag_in_group.name]) } @@ -292,4 +364,22 @@ RSpec.describe Tag do expect(category.reload.tags).to include(tag2) end end + + describe ".topic_count_column" do + fab!(:admin) { Fabricate(:admin) } + + it "returns 'staff_topic_count' when user is staff" do + expect(Tag.topic_count_column(Guardian.new(admin))).to eq("staff_topic_count") + end + + it "returns 'public_topic_count' when user is not staff" do + expect(Tag.topic_count_column(Guardian.new(user))).to eq("public_topic_count") + end + + it "returns 'staff_topic_count' when user is not staff but `include_secure_categories_in_tag_counts` site setting is enabled" do + SiteSetting.include_secure_categories_in_tag_counts = true + + expect(Tag.topic_count_column(Guardian.new(user))).to eq("staff_topic_count") + end + end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 4db4f3a554..4dd28ebff3 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -3271,85 +3271,6 @@ RSpec.describe Topic do end end - describe "#incoming_email_addresses" do - fab!(:group) do - Fabricate( - :group, - smtp_server: "imap.gmail.com", - smtp_port: 587, - email_username: "discourse@example.com", - email_password: "discourse@example.com", - ) - end - - fab!(:topic) do - Fabricate( - :private_message_topic, - topic_allowed_groups: [Fabricate.build(:topic_allowed_group, group: group)], - ) - end - - let!(:incoming1) do - Fabricate( - :incoming_email, - to_addresses: "discourse@example.com", - from_address: "johnsmith@user.com", - topic: topic, - post: topic.posts.first, - created_at: 20.minutes.ago, - ) - end - let!(:incoming2) do - Fabricate( - :incoming_email, - from_address: "discourse@example.com", - to_addresses: "johnsmith@user.com", - topic: topic, - post: Fabricate(:post, topic: topic), - created_at: 10.minutes.ago, - ) - end - let!(:incoming3) do - Fabricate( - :incoming_email, - to_addresses: "discourse@example.com", - from_address: "johnsmith@user.com", - topic: topic, - post: topic.posts.first, - cc_addresses: "otherguy@user.com", - created_at: 2.minutes.ago, - ) - end - let!(:incoming4) do - Fabricate( - :incoming_email, - to_addresses: "unrelated@test.com", - from_address: "discourse@example.com", - topic: topic, - post: topic.posts.first, - created_at: 1.minutes.ago, - ) - end - - it "returns an array of all the incoming email addresses" do - expect(topic.incoming_email_addresses).to match_array( - %w[discourse@example.com johnsmith@user.com otherguy@user.com unrelated@test.com], - ) - end - - it "returns an array of all the incoming email addresses where incoming was received before X" do - expect(topic.incoming_email_addresses(received_before: 5.minutes.ago)).to match_array( - %w[discourse@example.com johnsmith@user.com], - ) - end - - context "when the group is present" do - it "excludes incoming emails that are not to or CCd to the group" do - expect(topic.incoming_email_addresses(group: group)).not_to include("unrelated@test.com") - end - end - end - describe "#cannot_permanently_delete_reason" do fab!(:post) { Fabricate(:post) } let!(:topic) { post.topic } diff --git a/spec/models/topic_tag_spec.rb b/spec/models/topic_tag_spec.rb index 33779f1e34..b4a8e231ce 100644 --- a/spec/models/topic_tag_spec.rb +++ b/spec/models/topic_tag_spec.rb @@ -1,33 +1,56 @@ # frozen_string_literal: true RSpec.describe TopicTag do + fab!(:group) { Fabricate(:group) } + fab!(:private_category) { Fabricate(:private_category, group: group) } fab!(:topic) { Fabricate(:topic) } + fab!(:topic_in_private_category) { Fabricate(:topic, category: private_category) } fab!(:tag) { Fabricate(:tag) } let(:topic_tag) { Fabricate(:topic_tag, topic: topic, tag: tag) } describe "#after_create" do - it "tag topic_count should be increased" do - expect { topic_tag }.to change(tag, :topic_count).by(1) + it "should increase Tag#staff_topic_count and Tag#public_topic_count for a regular topic in a public category" do + expect { topic_tag }.to change { tag.reload.staff_topic_count }.by(1).and change { + tag.reload.public_topic_count + }.by(1) end - it "tag topic_count should not be increased" do + it "should only increase Tag#staff_topic_count for a regular topic in a read restricted category" do + expect { Fabricate(:topic_tag, topic: topic_in_private_category, tag: tag) }.to change { + tag.reload.staff_topic_count + }.by(1) + + expect(tag.reload.public_topic_count).to eq(0) + end + + it "should increase Tag#pm_topic_count for a private message topic" do topic.archetype = Archetype.private_message - expect { topic_tag }.not_to change(tag, :topic_count) + expect { topic_tag }.to change { tag.reload.pm_topic_count }.by(1) end end describe "#after_destroy" do - it "tag topic_count should be decreased" do + it "should decrease Tag#staff_topic_count and Tag#public_topic_count for a regular topic in a public category" do topic_tag - expect { topic_tag.destroy }.to change(tag, :topic_count).by(-1) + + expect { topic_tag.destroy! }.to change { tag.reload.staff_topic_count }.by(-1).and change { + tag.reload.public_topic_count + }.by(-1) end - it "tag topic_count should not be decreased" do + it "should only decrease Topic#topic_count for a regular topic in a read restricted category" do + topic_tag = Fabricate(:topic_tag, topic: topic_in_private_category, tag: tag) + + expect { topic_tag.destroy! }.to change { tag.reload.staff_topic_count }.by(-1) + expect(tag.reload.public_topic_count).to eq(0) + end + + it "should decrease Tag#pm_topic_count for a private message topic" do topic.archetype = Archetype.private_message topic_tag - expect { topic_tag.destroy }.not_to change(tag, :topic_count) + expect { topic_tag.destroy! }.to change { tag.reload.pm_topic_count }.by(-1) end end end diff --git a/spec/models/topic_tracking_state_spec.rb b/spec/models/topic_tracking_state_spec.rb index b63421d4d4..154ff98c93 100644 --- a/spec/models/topic_tracking_state_spec.rb +++ b/spec/models/topic_tracking_state_spec.rb @@ -3,33 +3,91 @@ RSpec.describe TopicTrackingState do fab!(:user) { Fabricate(:user) } fab!(:whisperers_group) { Fabricate(:group) } - - let(:post) { create_post } - - let(:topic) { post.topic } fab!(:private_message_post) { Fabricate(:private_message_post) } let(:private_message_topic) { private_message_post.topic } + let(:post) { create_post } + let(:topic) { post.topic } - fab!(:read_restricted_category) { Fabricate(:category, read_restricted: true) } - fab!(:read_restricted_topic) { Fabricate(:topic, category: read_restricted_category) } + shared_examples "does not publish message for private topics" do |method| + it "should not publish any message for a private topic" do + messages = + MessageBus.track_publish { described_class.public_send(method, private_message_topic) } - describe ".publish_new" do - it "should publish message only to admin group when category is read restricted but no groups have been granted access" do + expect(messages).to eq([]) + end + end + + shared_examples "publishes message to right groups and users" do |message_bus_channel, method| + fab!(:public_category) { Fabricate(:category, read_restricted: false) } + fab!(:topic_in_public_category) { Fabricate(:topic, category: public_category) } + fab!(:group) { Fabricate(:group) } + fab!(:read_restricted_category_with_groups) { Fabricate(:private_category, group: group) } + + fab!(:topic_in_read_restricted_category_with_groups) do + Fabricate(:topic, category: read_restricted_category_with_groups) + end + + fab!(:read_restricted_category_with_no_groups) { Fabricate(:category, read_restricted: true) } + + fab!(:topic_in_read_restricted_category_with_no_groups) do + Fabricate(:topic, category: read_restricted_category_with_no_groups) + end + + it "should publish message to everyone for a topic in a category that is not read restricted" do message = MessageBus - .track_publish("/new") { described_class.publish_new(read_restricted_topic) } + .track_publish(message_bus_channel) do + described_class.public_send(method, topic_in_public_category) + end .first data = message.data - expect(data["topic_id"]).to eq(read_restricted_topic.id) - expect(data["message_type"]).to eq(described_class::NEW_TOPIC_MESSAGE_TYPE) - expect(message.group_ids).to contain_exactly(Group::AUTO_GROUPS[:admin]) + expect(data["topic_id"]).to eq(topic_in_public_category.id) + expect(message.group_ids).to eq(nil) + expect(message.user_ids).to eq(nil) + end + + it "should publish message only to admin group and groups that have permission to read a category when topic is in category that is restricted to certain groups" do + message = + MessageBus + .track_publish(message_bus_channel) do + described_class.public_send(method, topic_in_read_restricted_category_with_groups) + end + .first + + data = message.data + + expect(data["topic_id"]).to eq(topic_in_read_restricted_category_with_groups.id) + expect(message.group_ids).to contain_exactly(Group::AUTO_GROUPS[:admins], group.id) + expect(message.user_ids).to eq(nil) + end + + it "should publish message only to admin group when topic is in category that is read restricted but no groups have been granted access" do + message = + MessageBus + .track_publish(message_bus_channel) do + described_class.public_send(method, topic_in_read_restricted_category_with_no_groups) + end + .first + + data = message.data + + expect(data["topic_id"]).to eq(topic_in_read_restricted_category_with_no_groups.id) + expect(message.group_ids).to contain_exactly(Group::AUTO_GROUPS[:admins]) expect(message.user_ids).to eq(nil) end end + describe ".publish_new" do + include_examples("publishes message to right groups and users", "/new", :publish_new) + include_examples("does not publish message for private topics", :publish_new) + end + describe ".publish_latest" do + include_examples("publishes message to right groups and users", "/latest", :publish_latest) + include_examples("does not publish message for private topics", :publish_latest) + it "can correctly publish latest" do message = MessageBus.track_publish("/latest") { described_class.publish_latest(topic) }.first @@ -38,6 +96,8 @@ RSpec.describe TopicTrackingState do expect(data["topic_id"]).to eq(topic.id) expect(data["message_type"]).to eq(described_class::LATEST_MESSAGE_TYPE) expect(data["payload"]["archetype"]).to eq(Archetype.default) + expect(message.group_ids).to eq(nil) + expect(message.user_ids).to eq(nil) end it "publishes whisper post to staff users and members of whisperers group" do @@ -54,29 +114,6 @@ RSpec.describe TopicTrackingState do expect(message.group_ids).to contain_exactly(whisperers_group.id, Group::AUTO_GROUPS[:staff]) end - - it "should publish message only to admin group when category is read restricted but no groups have been granted access" do - message = - MessageBus - .track_publish("/latest") { described_class.publish_latest(read_restricted_topic) } - .first - - data = message.data - - expect(data["topic_id"]).to eq(read_restricted_topic.id) - expect(data["message_type"]).to eq(described_class::LATEST_MESSAGE_TYPE) - expect(message.group_ids).to contain_exactly(Group::AUTO_GROUPS[:admin]) - expect(message.user_ids).to eq(nil) - end - - describe "private message" do - it "should not publish any message" do - messages = - MessageBus.track_publish { described_class.publish_latest(private_message_topic) } - - expect(messages).to eq([]) - end - end end describe ".publish_read" do @@ -241,6 +278,8 @@ RSpec.describe TopicTrackingState do let(:user) { Fabricate(:user, last_seen_at: Date.today) } let(:post) { create_post(user: user) } + include_examples("does not publish message for private topics", :publish_muted) + it "can correctly publish muted" do TopicUser.find_by(topic: topic, user: post.user).update(notification_level: 0) messages = MessageBus.track_publish("/latest") { TopicTrackingState.publish_muted(topic) } @@ -273,6 +312,8 @@ RSpec.describe TopicTrackingState do let(:third_user) { Fabricate(:user, last_seen_at: Date.today) } let(:post) { create_post(user: user) } + include_examples("does not publish message for private topics", :publish_unmuted) + it "can correctly publish unmuted" do Fabricate(:topic_tag, topic: topic) SiteSetting.mute_all_categories_by_default = true @@ -734,50 +775,17 @@ RSpec.describe TopicTrackingState do end describe ".publish_recover" do - it "should publish message only to admin group when category is read restricted but no groups have been granted access" do - message = - MessageBus - .track_publish("/recover") { described_class.publish_recover(read_restricted_topic) } - .first - - data = message.data - - expect(data["topic_id"]).to eq(read_restricted_topic.id) - expect(data["message_type"]).to eq(described_class::RECOVER_MESSAGE_TYPE) - expect(message.group_ids).to contain_exactly(Group::AUTO_GROUPS[:admin]) - expect(message.user_ids).to eq(nil) - end + include_examples("publishes message to right groups and users", "/recover", :publish_recover) + include_examples("does not publish message for private topics", :publish_recover) end describe ".publish_delete" do - it "should publish message only to admin group when category is read restricted but no groups have been granted access" do - message = - MessageBus - .track_publish("/delete") { described_class.publish_delete(read_restricted_topic) } - .first - - data = message.data - - expect(data["topic_id"]).to eq(read_restricted_topic.id) - expect(data["message_type"]).to eq(described_class::DELETE_MESSAGE_TYPE) - expect(message.group_ids).to contain_exactly(Group::AUTO_GROUPS[:admin]) - expect(message.user_ids).to eq(nil) - end + include_examples("publishes message to right groups and users", "/delete", :publish_delete) + include_examples("does not publish message for private topics", :publish_delete) end describe ".publish_destroy" do - it "should publish message only to admin group when category is read restricted but no groups have been granted access" do - message = - MessageBus - .track_publish("/destroy") { described_class.publish_destroy(read_restricted_topic) } - .first - - data = message.data - - expect(data["topic_id"]).to eq(read_restricted_topic.id) - expect(data["message_type"]).to eq(described_class::DESTROY_MESSAGE_TYPE) - expect(message.group_ids).to contain_exactly(Group::AUTO_GROUPS[:admin]) - expect(message.user_ids).to eq(nil) - end + include_examples("publishes message to right groups and users", "/destroy", :publish_destroy) + include_examples("does not publish message for private topics", :publish_destroy) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3b40512912..f700de0bd1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2573,6 +2573,11 @@ RSpec.describe User do expect(User.find(user.id).email).to eq(secondary_email_record.email) expect(user.secondary_emails.count).to eq(0) end + + it "returns error if email is nil" do + user.email = nil + expect { user.save! }.to raise_error(ActiveRecord::RecordInvalid) + end end describe "set_random_avatar" do @@ -3399,4 +3404,32 @@ RSpec.describe User do expect(user.new_personal_messages_notifications_count).to eq(1) end end + + describe "#redesigned_user_menu_enabled?" do + it "returns true when `navigation_menu` site settings is `legacy` and `enable_new_notifications_menu` site settings is enabled" do + SiteSetting.navigation_menu = "legacy" + SiteSetting.enable_new_notifications_menu = true + + expect(user.redesigned_user_menu_enabled?).to eq(true) + end + + it "returns false when `navigation_menu` site settings is `legacy` and `enable_new_notifications_menu` site settings is not enabled" do + SiteSetting.navigation_menu = "legacy" + SiteSetting.enable_new_notifications_menu = false + + expect(user.redesigned_user_menu_enabled?).to eq(false) + end + + it "returns true when `navigation_menu` site settings is `sidebar`" do + SiteSetting.navigation_menu = "sidebar" + + expect(user.redesigned_user_menu_enabled?).to eq(true) + end + + it "returns true when `navigation_menu` site settings is `header_dropdown`" do + SiteSetting.navigation_menu = "header dropdown" + + expect(user.redesigned_user_menu_enabled?).to eq(true) + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 1644f6af06..22009af6fa 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -190,6 +190,18 @@ RSpec.configure do |config| # instead of true. config.use_transactional_fixtures = true + # Sometimes you may have a large string or object that you are comparing + # with some expectation, and you want to see the full diff between actual + # and expected without rspec truncating 90% of the diff. Setting the + # max_formatted_output_length to nil disables this truncation completely. + # + # c.f. https://www.rubydoc.info/gems/rspec-expectations/RSpec/Expectations/Configuration#max_formatted_output_length=-instance_method + if ENV["RSPEC_DISABLE_DIFF_TRUNCATION"] + config.expect_with :rspec do |expectation| + expectation.max_formatted_output_length = nil + end + end + # If true, the base class of anonymous controllers will be inferred # automatically. This will be the default behavior in future versions of # rspec-rails. @@ -240,6 +252,17 @@ RSpec.configure do |config| capybara_config.server_port = 31_337 + ENV["TEST_ENV_NUMBER"].to_i end + module IgnoreUnicornCapturedErrors + def raise_server_error! + super + rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN => e + # Ignore these exceptions - caused by client. Handled by unicorn in dev/prod + # https://github.com/defunkt/unicorn/blob/d947cb91cf/lib/unicorn/http_server.rb#L570-L573 + end + end + + Capybara::Session.class_eval { prepend IgnoreUnicornCapturedErrors } + # The valid values for SELENIUM_BROWSER_LOG_LEVEL are: # # OFF diff --git a/spec/requests/admin/api_controller_spec.rb b/spec/requests/admin/api_controller_spec.rb index f6bb816393..4f92635a52 100644 --- a/spec/requests/admin/api_controller_spec.rb +++ b/spec/requests/admin/api_controller_spec.rb @@ -428,6 +428,8 @@ RSpec.describe Admin::ApiController do "global", "badges", "categories", + "search", + "invites", "wordpress", ) end diff --git a/spec/requests/admin/groups_controller_spec.rb b/spec/requests/admin/groups_controller_spec.rb index c09142449e..5f101bf10b 100644 --- a/spec/requests/admin/groups_controller_spec.rb +++ b/spec/requests/admin/groups_controller_spec.rb @@ -146,157 +146,6 @@ RSpec.describe Admin::GroupsController do end end - describe "#add_owners" do - context "when logged in as an admin" do - before { sign_in(admin) } - - it "should work" do - put "/admin/groups/#{group.id}/owners.json", - params: { - group: { - usernames: [user.username, admin.username].join(","), - }, - } - - expect(response.status).to eq(200) - - response_body = response.parsed_body - - expect(response_body["usernames"]).to contain_exactly(user.username, admin.username) - - expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly(user, admin) - end - - it "returns not-found error when there is no group" do - group.destroy! - - put "/admin/groups/#{group.id}/owners.json", params: { group: { usernames: user.username } } - - expect(response.status).to eq(404) - end - - it "does not allow adding owners to an automatic group" do - group.update!(automatic: true) - - expect do - put "/admin/groups/#{group.id}/owners.json", - params: { - group: { - usernames: user.username, - }, - } - end.to_not change { group.group_users.count } - - expect(response.status).to eq(422) - expect(response.parsed_body["errors"]).to eq(["You cannot modify an automatic group"]) - end - - it "does not notify users when the param is not present" do - put "/admin/groups/#{group.id}/owners.json", params: { group: { usernames: user.username } } - expect(response.status).to eq(200) - - topic = - Topic.find_by( - title: - I18n.t( - "system_messages.user_added_to_group_as_owner.subject_template", - group_name: group.name, - ), - archetype: "private_message", - ) - expect(topic.nil?).to eq(true) - end - - it "notifies users when the param is present" do - put "/admin/groups/#{group.id}/owners.json", - params: { - group: { - usernames: user.username, - notify_users: true, - }, - } - expect(response.status).to eq(200) - - topic = - Topic.find_by( - title: - I18n.t( - "system_messages.user_added_to_group_as_owner.subject_template", - group_name: group.name, - ), - archetype: "private_message", - ) - expect(topic.nil?).to eq(false) - expect(topic.topic_users.map(&:user_id)).to include(-1, user.id) - end - end - - context "when logged in as a moderator" do - before { sign_in(moderator) } - - context "with moderators_manage_categories_and_groups enabled" do - before { SiteSetting.moderators_manage_categories_and_groups = true } - - it "adds owners" do - put "/admin/groups/#{group.id}/owners.json", - params: { - group: { - usernames: [user.username, admin.username, moderator.username].join(","), - }, - } - - response_body = response.parsed_body - - expect(response.status).to eq(200) - expect(response_body["usernames"]).to contain_exactly( - user.username, - admin.username, - moderator.username, - ) - expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly( - user, - admin, - moderator, - ) - end - end - - context "with moderators_manage_categories_and_groups disabled" do - before { SiteSetting.moderators_manage_categories_and_groups = false } - - it "prevents adding of owners with a 403 response" do - put "/admin/groups/#{group.id}/owners.json", - params: { - group: { - usernames: [user.username, admin.username, moderator.username].join(","), - }, - } - - expect(response.status).to eq(403) - expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) - expect(group.group_users.where(owner: true).map(&:user)).to be_empty - end - end - end - - context "when logged in as a non-staff user" do - before { sign_in(user) } - - it "prevents adding of owners with a 404 response" do - put "/admin/groups/#{group.id}/owners.json", - params: { - group: { - usernames: [user.username, admin.username].join(","), - }, - } - - expect(response.status).to eq(404) - expect(response.parsed_body["errors"]).to include(I18n.t("not_found")) - expect(group.group_users.where(owner: true).map(&:user)).to be_empty - end - end - end - describe "#remove_owner" do let(:user2) { Fabricate(:user) } let(:user3) { Fabricate(:user) } diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index 602f84710e..87866be101 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -1777,6 +1777,26 @@ RSpec.describe Admin::UsersController do expect(user.username).to eq("Hokli") end + it "can sync up with the sso without email" do + sso.name = "Bob The Bob" + sso.username = "bob" + sso.email = "bob@bob.com" + sso.external_id = "1" + + user = + DiscourseConnect.parse( + sso.payload, + secure_session: read_secure_session, + ).lookup_or_create_user + + sso.name = "Bill" + sso.username = "Hokli$$!!" + sso.email = nil + + post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) + expect(response.status).to eq(200) + end + it "should create new users" do sso.name = "Dr. Claw" sso.username = "dr_claw" diff --git a/spec/requests/api/schemas/json/group_create_response.json b/spec/requests/api/schemas/json/group_create_response.json index 2d77910bbe..6c50cb851b 100644 --- a/spec/requests/api/schemas/json/group_create_response.json +++ b/spec/requests/api/schemas/json/group_create_response.json @@ -119,6 +119,9 @@ "can_admin_group": { "type": "boolean" }, + "can_edit_group": { + "type": "boolean" + }, "publish_read_state": { "type": "boolean" } diff --git a/spec/requests/api/schemas/json/group_response.json b/spec/requests/api/schemas/json/group_response.json index a663ba0e1b..9163e33f9d 100644 --- a/spec/requests/api/schemas/json/group_response.json +++ b/spec/requests/api/schemas/json/group_response.json @@ -122,6 +122,9 @@ "can_admin_group": { "type": "boolean" }, + "can_edit_group": { + "type": "boolean" + }, "publish_read_state": { "type": "boolean" }, diff --git a/spec/requests/api/schemas/json/groups_list_response.json b/spec/requests/api/schemas/json/groups_list_response.json index cea82af0f2..25fc1ac007 100644 --- a/spec/requests/api/schemas/json/groups_list_response.json +++ b/spec/requests/api/schemas/json/groups_list_response.json @@ -131,6 +131,9 @@ "can_admin_group": { "type": "boolean" }, + "can_edit_group": { + "type": "boolean" + }, "publish_read_state": { "type": "boolean" } diff --git a/spec/requests/api/schemas/json/site_response.json b/spec/requests/api/schemas/json/site_response.json index 09d239f432..78f03880ef 100644 --- a/spec/requests/api/schemas/json/site_response.json +++ b/spec/requests/api/schemas/json/site_response.json @@ -127,6 +127,9 @@ }, "following_replied": { "type": "integer" + }, + "circles_activity": { + "type": "integer" } }, "required": [ diff --git a/spec/requests/drafts_controller_spec.rb b/spec/requests/drafts_controller_spec.rb index 9cb50febee..4af1fd795d 100644 --- a/spec/requests/drafts_controller_spec.rb +++ b/spec/requests/drafts_controller_spec.rb @@ -192,6 +192,40 @@ RSpec.describe DraftsController do expect(response.status).to eq(409) end + + context "when data is too big" do + let(:user) { Fabricate(:user) } + let(:data) { "a" * (SiteSetting.max_draft_length + 1) } + + before do + SiteSetting.max_draft_length = 500 + sign_in(user) + end + + it "returns an error" do + post "/drafts.json", + params: { + draft_key: "xyz", + data: { reply: data }.to_json, + sequence: 0, + } + expect(response).to have_http_status :bad_request + end + end + + context "when data is not too big" do + context "when data is not proper JSON" do + let(:user) { Fabricate(:user) } + let(:data) { "not-proper-json" } + + before { sign_in(user) } + + it "returns an error" do + post "/drafts.json", params: { draft_key: "xyz", data: data, sequence: 0 } + expect(response).to have_http_status :bad_request + end + end + end end describe "#destroy" do diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 0e108963ae..87c7ad0628 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -1638,6 +1638,164 @@ RSpec.describe GroupsController do end end + describe "#add_owners" do + context "when logged in as an admin" do + before { sign_in(admin) } + + it "should work" do + put "/groups/#{group.id}/owners.json", + params: { + usernames: [user.username, admin.username].join(","), + } + + expect(response.status).to eq(200) + + response_body = response.parsed_body + + expect(response_body["usernames"]).to contain_exactly(user.username, admin.username) + + expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly(user, admin) + end + + it "returns not-found error when there is no group" do + group.destroy! + + put "/groups/#{group.id}/owners.json", params: { usernames: user.username } + + expect(response.status).to eq(404) + end + + it "does not allow adding owners to an automatic group" do + group.update!(automatic: true) + + expect do + put "/groups/#{group.id}/owners.json", params: { usernames: user.username } + end.to_not change { group.group_users.count } + + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to eq( + [I18n.t("groups.errors.can_not_modify_automatic")], + ) + end + + it "does not notify users when the param is not present" do + put "/groups/#{group.id}/owners.json", params: { usernames: user.username } + expect(response.status).to eq(200) + + topic = + Topic.find_by( + title: + I18n.t( + "system_messages.user_added_to_group_as_owner.subject_template", + group_name: group.name, + ), + archetype: "private_message", + ) + expect(topic.nil?).to eq(true) + end + + it "notifies users when the param is present" do + put "/groups/#{group.id}/owners.json", + params: { + usernames: user.username, + notify_users: true, + } + expect(response.status).to eq(200) + + topic = + Topic.find_by( + title: + I18n.t( + "system_messages.user_added_to_group_as_owner.subject_template", + group_name: group.name, + ), + archetype: "private_message", + ) + expect(topic.nil?).to eq(false) + expect(topic.topic_users.map(&:user_id)).to include(-1, user.id) + end + end + + context "when logged in as a moderator" do + before { sign_in(moderator) } + + context "with moderators_manage_categories_and_groups enabled" do + before { SiteSetting.moderators_manage_categories_and_groups = true } + + it "adds owners" do + put "/groups/#{group.id}/owners.json", + params: { + usernames: [user.username, admin.username, moderator.username].join(","), + } + + response_body = response.parsed_body + + expect(response.status).to eq(200) + expect(response_body["usernames"]).to contain_exactly( + user.username, + admin.username, + moderator.username, + ) + expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly( + user, + admin, + moderator, + ) + end + end + + context "with moderators_manage_categories_and_groups disabled" do + before { SiteSetting.moderators_manage_categories_and_groups = false } + + it "prevents adding of owners with a 403 response" do + put "/groups/#{group.id}/owners.json", + params: { + usernames: [user.username, admin.username, moderator.username].join(","), + } + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + expect(group.group_users.where(owner: true).map(&:user)).to be_empty + end + end + end + + context "when logged in as a non-owner" do + before { sign_in(user) } + + it "prevents adding of owners with a 403 response" do + put "/groups/#{group.id}/owners.json", + params: { + usernames: [user.username, admin.username].join(","), + } + + expect(response.status).to eq(403) + expect(response.parsed_body["errors"]).to include(I18n.t("invalid_access")) + expect(group.group_users.where(owner: true).map(&:user)).to be_empty + end + end + + context "when logged in as an owner" do + before { sign_in(user) } + + it "allows adding new owners" do + group.add_owner(user) + + put "/groups/#{group.id}/owners.json", + params: { + usernames: [user.username, admin.username].join(","), + } + + expect(response.status).to eq(200) + expect(response.parsed_body["usernames"]).to contain_exactly( + user.username, + admin.username, + ) + expect(group.group_users.where(owner: true).map(&:user)).to contain_exactly(user, admin) + end + end + end + describe "#join" do let(:public_group) { Fabricate(:public_group) } @@ -2040,6 +2198,20 @@ RSpec.describe GroupsController do expect(response.status).to eq(409) end + it "limits the character count of the reason" do + sign_in(user) + + post "/groups/#{group.name}/request_membership.json", + params: { + reason: "x" * (GroupRequest::REASON_CHARACTER_LIMIT + 1), + } + + expect(response.status).to eq(422) + expect(response.parsed_body["errors"]).to contain_exactly( + "Reason is too long (maximum is 280 characters)", + ) + end + it "should create the right PM" do owner1 = Fabricate(:user, last_seen_at: Time.zone.now) owner2 = Fabricate(:user, last_seen_at: Time.zone.now - 1.day) diff --git a/spec/requests/hashtags_controller_spec.rb b/spec/requests/hashtags_controller_spec.rb index 9cffbf5b96..565e2268d1 100644 --- a/spec/requests/hashtags_controller_spec.rb +++ b/spec/requests/hashtags_controller_spec.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true RSpec.describe HashtagsController do - fab!(:category) { Fabricate(:category) } - fab!(:tag) { Fabricate(:tag) } + fab!(:category) { Fabricate(:category, name: "Random", slug: "random") } + fab!(:tag) { Fabricate(:tag, name: "bug") } fab!(:group) { Fabricate(:group) } - fab!(:private_category) { Fabricate(:private_category, group: group) } + fab!(:private_category) do + Fabricate(:private_category, group: group, name: "Staff", slug: "staff") + end fab!(:hidden_tag) { Fabricate(:tag, name: "hidden") } let(:tag_group) do @@ -18,126 +20,391 @@ RSpec.describe HashtagsController do end describe "#lookup" do - context "when logged in" do - context "as regular user" do - before { sign_in(Fabricate(:user)) } + context "when enable_experimental_hashtag_autocomplete disabled" do + # TODO (martin) Remove when enable_experimental_hashtag_autocomplete is default for all sites + before { SiteSetting.enable_experimental_hashtag_autocomplete = false } + context "when logged in" do + context "as regular user" do + before { sign_in(Fabricate(:user)) } - it "returns only valid categories and tags" do - get "/hashtags.json", - params: { - slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], - } + it "returns only valid categories and tags" do + get "/hashtags.json", + params: { + slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], + } - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - category.slug => category.url, - }, - "tags" => { - tag.name => tag.full_url, - }, - ) + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + "categories" => { + category.slug => category.url, + }, + "tags" => { + tag.name => tag.full_url, + }, + ) + end + + it "handles tags with the TAG_HASHTAG_POSTFIX" do + get "/hashtags.json", + params: { + slugs: ["#{tag.name}#{PrettyText::Helpers::TAG_HASHTAG_POSTFIX}"], + } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + "categories" => { + }, + "tags" => { + tag.name => tag.full_url, + }, + ) + end + + it "does not return restricted categories or hidden tags" do + get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq("categories" => {}, "tags" => {}) + end end - it "handles tags with the TAG_HASHTAG_POSTFIX" do - get "/hashtags.json", - params: { - slugs: ["#{tag.name}#{PrettyText::Helpers::TAG_HASHTAG_POSTFIX}"], - } + context "as admin" do + fab!(:admin) { Fabricate(:admin) } - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - }, - "tags" => { - tag.name => tag.full_url, - }, - ) + before { sign_in(admin) } + + it "returns restricted categories and hidden tags" do + group.add(admin) + + get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + "categories" => { + private_category.slug => private_category.url, + }, + "tags" => { + hidden_tag.name => hidden_tag.full_url, + }, + ) + end end - it "does not return restricted categories or hidden tags" do - get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } + context "with sub-sub-categories" do + before do + SiteSetting.max_category_nesting = 3 + sign_in(Fabricate(:user)) + end - expect(response.status).to eq(200) - expect(response.parsed_body).to eq("categories" => {}, "tags" => {}) + it "works" do + foo = Fabricate(:category_with_definition, slug: "foo") + foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) + foobarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) + + qux = Fabricate(:category_with_definition, slug: "qux") + quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) + quxbarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id) + + invalid_slugs = [":"] + child_slugs = %w[bar baz] + deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz] + get "/hashtags.json", + params: { + slugs: + invalid_slugs + child_slugs + deeply_nested_slugs + + %w[foo foo:bar bar:baz qux qux:bar], + } + + expect(response.status).to eq(200) + expect(response.parsed_body["categories"]).to eq( + "foo" => foo.url, + "foo:bar" => foobar.url, + "bar:baz" => foobarbaz.id < quxbarbaz.id ? foobarbaz.url : quxbarbaz.url, + "qux" => qux.url, + "qux:bar" => quxbar.url, + ) + end end end - context "as admin" do - fab!(:admin) { Fabricate(:admin) } - - before { sign_in(admin) } - - it "returns restricted categories and hidden tags" do - group.add(admin) - - get "/hashtags.json", params: { slugs: [private_category.slug, hidden_tag.name] } - - expect(response.status).to eq(200) - expect(response.parsed_body).to eq( - "categories" => { - private_category.slug => private_category.url, - }, - "tags" => { - hidden_tag.name => hidden_tag.full_url, - }, - ) + context "when not logged in" do + it "returns invalid access" do + get "/hashtags.json", params: { slugs: [] } + expect(response.status).to eq(403) end end + end - context "with sub-sub-categories" do - before do - SiteSetting.max_category_nesting = 3 - sign_in(Fabricate(:user)) - end + context "when enable_experimental_hashtag_autocomplete enabled" do + before { SiteSetting.enable_experimental_hashtag_autocomplete = true } - it "works" do - foo = Fabricate(:category_with_definition, slug: "foo") - foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) - foobarbaz = - Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) + context "when logged in" do + context "as regular user" do + before { sign_in(Fabricate(:user)) } - qux = Fabricate(:category_with_definition, slug: "qux") - quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) - quxbarbaz = - Fabricate(:category_with_definition, slug: "baz", parent_category_id: quxbar.id) + it "returns only valid categories and tags" do + get "/hashtags.json", + params: { + slugs: [category.slug, private_category.slug, "none", tag.name, hidden_tag.name], + order: %w[category tag], + } - get "/hashtags.json", - params: { - slugs: [ - ":", # should not work - "foo", - "bar", # should not work - "baz", # should not work - "foo:bar", - "bar:baz", - "foo:bar:baz", # should not work - "qux", - "qux:bar", - "qux:bar:baz", # should not work + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [ + { + "relative_url" => category.url, + "text" => category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => category.slug, + "slug" => category.slug, + }, ], - } + "tag" => [ + { + "relative_url" => tag.url, + "text" => tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => tag.name, + "slug" => tag.name, + "secondary_text" => "x0", + }, + ], + }, + ) + end - expect(response.status).to eq(200) - expect(response.parsed_body["categories"]).to eq( - "foo" => foo.url, - "foo:bar" => foobar.url, - "bar:baz" => foobarbaz.id < quxbarbaz.id ? foobarbaz.url : quxbarbaz.url, - "qux" => qux.url, - "qux:bar" => quxbar.url, - ) + it "handles tags with the ::tag type suffix" do + get "/hashtags.json", params: { slugs: ["#{tag.name}::tag"], order: %w[category tag] } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [], + "tag" => [ + { + "relative_url" => tag.url, + "text" => tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => "#{tag.name}::tag", + "slug" => tag.name, + "secondary_text" => "x0", + }, + ], + }, + ) + end + + it "does not return restricted categories or hidden tags" do + get "/hashtags.json", + params: { + slugs: [private_category.slug, hidden_tag.name], + order: %w[category tag], + } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq({ "category" => [], "tag" => [] }) + end end + + context "as admin" do + fab!(:admin) { Fabricate(:admin) } + + before { sign_in(admin) } + + it "returns restricted categories and hidden tags" do + group.add(admin) + + get "/hashtags.json", + params: { + slugs: [private_category.slug, hidden_tag.name], + order: %w[category tag], + } + + expect(response.status).to eq(200) + expect(response.parsed_body).to eq( + { + "category" => [ + { + "relative_url" => private_category.url, + "text" => private_category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => private_category.slug, + "slug" => private_category.slug, + }, + ], + "tag" => [ + { + "relative_url" => hidden_tag.url, + "text" => hidden_tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => hidden_tag.name, + "slug" => hidden_tag.name, + "secondary_text" => "x0", + }, + ], + }, + ) + end + end + + context "with sub-sub-categories" do + before do + SiteSetting.max_category_nesting = 3 + sign_in(Fabricate(:user)) + end + + it "works" do + foo = Fabricate(:category_with_definition, slug: "foo") + foobar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: foo.id) + foobarbaz = + Fabricate(:category_with_definition, slug: "baz", parent_category_id: foobar.id) + + qux = Fabricate(:category_with_definition, slug: "qux") + quxbar = Fabricate(:category_with_definition, slug: "bar", parent_category_id: qux.id) + + invalid_slugs = [":"] + child_slugs = %w[bar baz] + deeply_nested_slugs = %w[foo:bar:baz qux:bar:baz] + get "/hashtags.json", + params: { + slugs: + invalid_slugs + child_slugs + deeply_nested_slugs + + %w[foo foo:bar bar:baz qux qux:bar], + order: %w[category tag], + } + + expect(response.status).to eq(200) + found_categories = response.parsed_body["category"] + expect(found_categories.map { |c| c["ref"] }).to match_array( + %w[foo foo:bar bar:baz qux qux:bar], + ) + expect(found_categories.find { |c| c["ref"] == "foo" }["relative_url"]).to eq(foo.url) + expect(found_categories.find { |c| c["ref"] == "foo:bar" }["relative_url"]).to eq( + foobar.url, + ) + expect(found_categories.find { |c| c["ref"] == "bar:baz" }["relative_url"]).to eq( + foobarbaz.url, + ) + expect(found_categories.find { |c| c["ref"] == "qux" }["relative_url"]).to eq(qux.url) + expect(found_categories.find { |c| c["ref"] == "qux:bar" }["relative_url"]).to eq( + quxbar.url, + ) + end + end + end + + context "when not logged in" do + it "returns invalid access" do + get "/hashtags.json", params: { slugs: [], order: %w[category tag] } + expect(response.status).to eq(403) + end + end + end + end + + describe "#search" do + fab!(:tag_2) { Fabricate(:tag, name: "random") } + + context "when logged in" do + before { sign_in(Fabricate(:user)) } + + it "returns the found category and then tag" do + get "/hashtags/search.json", params: { term: "rand", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq( + [ + { + "relative_url" => category.url, + "text" => category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => category.slug, + "slug" => category.slug, + }, + { + "relative_url" => tag_2.url, + "text" => tag_2.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => "#{tag_2.name}::tag", + "slug" => tag_2.name, + "secondary_text" => "x0", + }, + ], + ) + end + + it "does not return hidden and restricted categories/tags" do + get "/hashtags/search.json", params: { term: "staff", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq([]) + + get "/hashtags/search.json", params: { term: "hidden", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq([]) + end + end + + context "when logged in as admin" do + before { sign_in(Fabricate(:admin)) } + + it "returns hidden and restricted categories/tags" do + get "/hashtags/search.json", params: { term: "staff", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq( + [ + { + "relative_url" => private_category.url, + "text" => private_category.name, + "description" => nil, + "icon" => "folder", + "type" => "category", + "ref" => private_category.slug, + "slug" => private_category.slug, + }, + ], + ) + + get "/hashtags/search.json", params: { term: "hidden", order: %w[category tag] } + expect(response.status).to eq(200) + expect(response.parsed_body["results"]).to eq( + [ + { + "relative_url" => hidden_tag.url, + "text" => hidden_tag.name, + "description" => nil, + "icon" => "tag", + "type" => "tag", + "ref" => "#{hidden_tag.name}", + "slug" => hidden_tag.name, + "secondary_text" => "x0", + }, + ], + ) end end context "when not logged in" do it "returns invalid access" do - get "/hashtags.json", params: { slugs: [] } + get "/hashtags/search.json", params: { term: "rand", order: %w[category tag] } expect(response.status).to eq(403) end end end - - # TODO (martin) write a spec here for the new - # #lookup behaviour and the new #search behaviour end diff --git a/spec/requests/list_controller_spec.rb b/spec/requests/list_controller_spec.rb index 9307d13353..547e057313 100644 --- a/spec/requests/list_controller_spec.rb +++ b/spec/requests/list_controller_spec.rb @@ -30,6 +30,9 @@ RSpec.describe ListController do get "/latest?page=1111111111111111111111111111111111111111" expect(response.status).to eq(400) + + get "/latest?tags[1]=hello" + expect(response.status).to eq(400) end it "returns 200 for legit requests" do @@ -59,6 +62,9 @@ RSpec.describe ListController do get "/latest.json?topic_ids=14583%2C14584" expect(response.status).to eq(200) + + get "/latest?tags[]=hello" + expect(response.status).to eq(200) end (Discourse.anonymous_filters - [:categories]).each do |filter| diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb index a8e12fcf0b..ad5310dd38 100644 --- a/spec/requests/posts_controller_spec.rb +++ b/spec/requests/posts_controller_spec.rb @@ -1578,10 +1578,30 @@ RSpec.describe PostsController do end end - context "with mentions" do + context "when `enable_user_status` site setting is enabled" do fab!(:user_to_mention) { Fabricate(:user) } + before { SiteSetting.enable_user_status = true } + + it "does not return mentioned users when `enable_user_status` site setting is disabled" do + SiteSetting.enable_user_status = false + + post "/posts.json", + params: { + raw: "I am mentioning @#{user_to_mention.username}", + topic_id: topic.id, + } + + expect(response.status).to eq(200) + + json = response.parsed_body + + expect(json["mentioned_users"]).to eq(nil) + end + it "returns mentioned users" do + user_to_mention.set_status!("off to dentist", "tooth") + post "/posts.json", params: { raw: "I am mentioning @#{user_to_mention.username}", @@ -1596,6 +1616,11 @@ RSpec.describe PostsController do expect(mentioned_user["id"]).to be(user_to_mention.id) expect(mentioned_user["name"]).to eq(user_to_mention.name) expect(mentioned_user["username"]).to eq(user_to_mention.username) + + status = mentioned_user["status"] + expect(status).to be_present + expect(status["emoji"]).to eq(user_to_mention.user_status.emoji) + expect(status["description"]).to eq(user_to_mention.user_status.description) end it "returns an empty list of mentioned users if nobody was mentioned" do @@ -1611,43 +1636,6 @@ RSpec.describe PostsController do expect(response.status).to eq(200) expect(response.parsed_body["mentioned_users"].length).to be(0) end - - it "doesn't return user status on mentions by default" do - user_to_mention.set_status!("off to dentist", "tooth") - - post "/posts.json", - params: { - raw: "I am mentioning @#{user_to_mention.username}", - topic_id: topic.id, - } - - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["mentioned_users"].length).to be(1) - - status = json["mentioned_users"][0]["status"] - expect(status).to be_nil - end - - it "returns user status on mentions if status is enabled in site settings" do - SiteSetting.enable_user_status = true - user_to_mention.set_status!("off to dentist", "tooth") - - post "/posts.json", - params: { - raw: "I am mentioning @#{user_to_mention.username}", - topic_id: topic.id, - } - - expect(response.status).to eq(200) - json = response.parsed_body - expect(json["mentioned_users"].length).to be(1) - - status = json["mentioned_users"][0]["status"] - expect(status).to be_present - expect(status["emoji"]).to eq(user_to_mention.user_status.emoji) - expect(status["description"]).to eq(user_to_mention.user_status.description) - end end end @@ -2049,6 +2037,76 @@ RSpec.describe PostsController do end end + describe "#permanently_delete_revisions" do + before { SiteSetting.can_permanently_delete = true } + + fab!(:post) do + Fabricate( + :post, + user: Fabricate(:user), + raw: "Lorem ipsum dolor sit amet, cu nam libris tractatos, ancillae senserit ius ex", + ) + end + + fab!(:post_with_no_revisions) do + Fabricate( + :post, + user: Fabricate(:user), + raw: "Lorem ipsum dolor sit amet, cu nam libris tractatos, ancillae senserit ius ex", + ) + end + + fab!(:post_revision) { Fabricate(:post_revision, post: post) } + fab!(:post_revision_2) { Fabricate(:post_revision, post: post) } + + let(:post_id) { post.id } + + describe "when logged in as a regular user" do + it "does not delete revisions" do + sign_in(user) + delete "/posts/#{post_id}/revisions/permanently_delete.json" + expect(response).to_not be_successful + end + end + + describe "when logged in as staff" do + before { sign_in(admin) } + + it "fails when post record is not found" do + delete "/posts/#{post_id + 1}/revisions/permanently_delete.json" + expect(response).to_not be_successful + end + + it "fails when no post revisions are found" do + delete "/posts/#{post_with_no_revisions.id}/revisions/permanently_delete.json" + expect(response).to_not be_successful + end + + it "fails when 'can_permanently_delete' setting is false" do + SiteSetting.can_permanently_delete = false + delete "/posts/#{post_id}/revisions/permanently_delete.json" + expect(response).to_not be_successful + end + + it "permanently deletes revisions from post and adds a staff log" do + delete "/posts/#{post_id}/revisions/permanently_delete.json" + expect(response.status).to eq(200) + + # It creates a staff log + logs = + UserHistory.find_by( + action: UserHistory.actions[:permanently_delete_post_revisions], + acting_user_id: admin.id, + post_id: post_id, + ) + expect(logs).to be_present + + # ensure post revisions are deleted + expect(PostRevision.where(post: post)).to eq([]) + end + end + end + describe "#revert" do include_examples "action requires login", :put, "/posts/123/revisions/2/revert.json" diff --git a/spec/requests/slugs_controller_spec.rb b/spec/requests/slugs_controller_spec.rb new file mode 100644 index 0000000000..ec56d73a47 --- /dev/null +++ b/spec/requests/slugs_controller_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.describe SlugsController do + fab!(:current_user) { Fabricate(:user, trust_level: TrustLevel[4]) } + + describe "#generate" do + let(:name) { "Arts & Media" } + + context "when user not logged in" do + it "returns a 403 error" do + get "/slugs/generate.json?name=#{name}" + expect(response.status).to eq(403) + end + end + + context "when user is logged in" do + before { sign_in(current_user) } + + it "generates a slug from the name" do + get "/slugs/generate.json", params: { name: name } + expect(response.status).to eq(200) + expect(response.parsed_body["slug"]).to eq(Slug.for(name, "")) + end + + it "rate limits" do + RateLimiter.enable + + stub_const(SlugsController, "MAX_SLUG_GENERATIONS_PER_MINUTE", 1) do + get "/slugs/generate.json?name=#{name}" + get "/slugs/generate.json?name=#{name}" + end + + expect(response.status).to eq(429) + end + + it "requires name" do + get "/slugs/generate.json" + expect(response.status).to eq(400) + end + + context "when user is not TL4 or higher" do + before { current_user.change_trust_level!(1) } + + it "returns a 403 error" do + get "/slugs/generate.json?name=#{name}" + expect(response.status).to eq(403) + end + end + + context "when user is admin" do + fab!(:current_user) { Fabricate(:admin) } + + it "generates a slug from the name" do + get "/slugs/generate.json", params: { name: name } + expect(response.status).to eq(200) + expect(response.parsed_body["slug"]).to eq(Slug.for(name, "")) + end + end + end + end +end diff --git a/spec/requests/static_controller_spec.rb b/spec/requests/static_controller_spec.rb index 0fc1f68f4a..884f7ae55f 100644 --- a/spec/requests/static_controller_spec.rb +++ b/spec/requests/static_controller_spec.rb @@ -122,6 +122,22 @@ RSpec.describe StaticController do File.delete(file_path) end end + + it "can serve sourcemaps on adjacent paths" do + assets_path = Rails.root.join("public/assets") + + FileUtils.mkdir_p(assets_path) + + file_path = assets_path.join("test.map") + File.write(file_path, "fake source map") + GlobalSetting.stubs(:cdn_url).returns("https://www.example.com/") + + get "/brotli_asset/test.map" + + expect(response.status).to eq(200) + ensure + File.delete(file_path) + end end describe "#cdn_asset" do diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb index ba04422120..ac85694fc7 100644 --- a/spec/requests/tags_controller_spec.rb +++ b/spec/requests/tags_controller_spec.rb @@ -12,18 +12,43 @@ RSpec.describe TagsController do describe "#index" do fab!(:test_tag) { Fabricate(:tag, name: "test") } - fab!(:topic_tag) { Fabricate(:tag, name: "topic-test", topic_count: 1) } + fab!(:topic_tag) do + Fabricate(:tag, name: "topic-test", public_topic_count: 1, staff_topic_count: 1) + end fab!(:synonym) { Fabricate(:tag, name: "synonym", target_tag: topic_tag) } - shared_examples "successfully retrieve tags with topic_count > 0" do - it "should return the right response" do + shared_examples "retrieves the right tags" do + it "retrieves all tags as a staff user" do + sign_in(admin) + + get "/tags.json" + + expect(response.status).to eq(200) + + tags = response.parsed_body["tags"] + + expect(tags[0]["name"]).to eq(test_tag.name) + expect(tags[0]["count"]).to eq(0) + expect(tags[0]["pm_count"]).to eq(0) + + expect(tags[1]["name"]).to eq(topic_tag.name) + expect(tags[1]["count"]).to eq(1) + expect(tags[1]["pm_count"]).to eq(0) + end + + it "only retrieve tags that have been used in public topics for non-staff user" do + sign_in(user) + get "/tags.json" expect(response.status).to eq(200) tags = response.parsed_body["tags"] expect(tags.length).to eq(1) - expect(tags[0]["text"]).to eq("topic-test") + + expect(tags[0]["name"]).to eq(topic_tag.name) + expect(tags[0]["count"]).to eq(1) + expect(tags[0]["pm_count"]).to eq(0) end end @@ -76,13 +101,14 @@ RSpec.describe TagsController do context "with tags_listed_by_group enabled" do before { SiteSetting.tags_listed_by_group = true } - include_examples "successfully retrieve tags with topic_count > 0" + include_examples "retrieves the right tags" it "works for tags in groups" do tag_group = Fabricate(:tag_group, tags: [test_tag, topic_tag, synonym]) - get "/tags.json" - expect(response.status).to eq(200) + get "/tags.json" + + expect(response.status).to eq(200) tags = response.parsed_body["tags"] expect(tags.length).to eq(0) group = response.parsed_body.dig("extras", "tag_groups")&.first @@ -90,24 +116,52 @@ RSpec.describe TagsController do expect(group["tags"].length).to eq(2) expect(group["tags"].map { |t| t["id"] }).to contain_exactly(test_tag.name, topic_tag.name) end + + it "does not result in N+1 queries with multiple tag_groups" do + tag_group1 = Fabricate(:tag_group, tags: [test_tag, topic_tag, synonym]) + + # warm up + get "/tags.json" + expect(response.status).to eq(200) + + initial_sql_queries_count = + track_sql_queries do + get "/tags.json" + + expect(response.status).to eq(200) + + tag_groups = response.parsed_body.dig("extras", "tag_groups") + + expect(tag_groups.length).to eq(1) + expect(tag_groups.map { |tag_group| tag_group["name"] }).to contain_exactly( + tag_group1.name, + ) + end.length + + tag_group2 = Fabricate(:tag_group, tags: [topic_tag]) + + new_sql_queries_count = + track_sql_queries do + get "/tags.json" + + expect(response.status).to eq(200) + + tag_groups = response.parsed_body.dig("extras", "tag_groups") + + expect(tag_groups.length).to eq(2) + expect(tag_groups.map { |tag_group| tag_group["name"] }).to contain_exactly( + tag_group1.name, + tag_group2.name, + ) + end.length + + expect(new_sql_queries_count).to be <= initial_sql_queries_count + end end context "with tags_listed_by_group disabled" do before { SiteSetting.tags_listed_by_group = false } - include_examples "successfully retrieve tags with topic_count > 0" - end - - context "when user can admin tags" do - it "successfully retrieve all tags" do - sign_in(admin) - - get "/tags.json" - - expect(response.status).to eq(200) - - tags = response.parsed_body["tags"] - expect(tags.length).to eq(2) - end + include_examples "retrieves the right tags" end context "with hidden tags" do @@ -622,6 +676,18 @@ RSpec.describe TagsController do expect(response.status).to eq(200) end + it "returns a 404 when tag is restricted" do + tag_group = Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["test"]) + + get "/tag/test/l/latest.json" + expect(response.status).to eq(404) + + sign_in(admin) + + get "/tag/test/l/latest.json" + expect(response.status).to eq(200) + end + context "with muted tags" do before do TagUser.create!( @@ -701,6 +767,18 @@ RSpec.describe TagsController do get "/tag/#{tag.name}/l/top.json?period=decadely" expect(response.status).to eq(400) end + + it "returns a 404 if tag is restricted" do + tag_group = Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["test"]) + + get "/tag/test/l/top.json" + expect(response.status).to eq(404) + + sign_in(admin) + + get "/tag/test/l/top.json" + expect(response.status).to eq(200) + end end describe "#search" do @@ -721,10 +799,10 @@ RSpec.describe TagsController do expect(response.parsed_body["results"].map { |j| j["id"] }.sort).to eq(%w[stuff stumped]) end - it "returns tags ordered by topic_count, and prioritises exact matches" do - Fabricate(:tag, name: "tag1", topic_count: 10) - Fabricate(:tag, name: "tag2", topic_count: 100) - Fabricate(:tag, name: "tag", topic_count: 1) + it "returns tags ordered by public_topic_count, and prioritises exact matches" do + Fabricate(:tag, name: "tag1", public_topic_count: 10, staff_topic_count: 10) + Fabricate(:tag, name: "tag2", public_topic_count: 100, staff_topic_count: 100) + Fabricate(:tag, name: "tag", public_topic_count: 1, staff_topic_count: 1) get "/tags/filter/search.json", params: { q: "tag", limit: 2 } expect(response.status).to eq(200) @@ -934,11 +1012,41 @@ RSpec.describe TagsController do context "with some tags" do let!(:tags) do [ - Fabricate(:tag, name: "used_publically", topic_count: 2, pm_topic_count: 0), - Fabricate(:tag, name: "used_privately", topic_count: 0, pm_topic_count: 3), - Fabricate(:tag, name: "used_everywhere", topic_count: 0, pm_topic_count: 3), - Fabricate(:tag, name: "unused1", topic_count: 0, pm_topic_count: 0), - Fabricate(:tag, name: "unused2", topic_count: 0, pm_topic_count: 0), + Fabricate( + :tag, + name: "used_publically", + public_topic_count: 2, + staff_topic_count: 2, + pm_topic_count: 0, + ), + Fabricate( + :tag, + name: "used_privately", + public_topic_count: 0, + staff_topic_count: 0, + pm_topic_count: 3, + ), + Fabricate( + :tag, + name: "used_everywhere", + public_topic_count: 0, + staff_topic_count: 0, + pm_topic_count: 3, + ), + Fabricate( + :tag, + name: "unused1", + public_topic_count: 0, + staff_topic_count: 0, + pm_topic_count: 0, + ), + Fabricate( + :tag, + name: "unused2", + public_topic_count: 0, + staff_topic_count: 0, + pm_topic_count: 0, + ), ] end diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index ea369bef34..5f9c62c03b 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -1037,6 +1037,99 @@ RSpec.describe TopicsController do expect(topic.posts.last.action_code).to eq("visible.enabled") end end + + context "with API key" do + let(:api_key) { Fabricate(:api_key, user: moderator, created_by: moderator) } + + context "when key scope has restricted params" do + before do + ApiKeyScope.create( + resource: "topics", + action: "update", + api_key_id: api_key.id, + allowed_parameters: { + "category_id" => ["#{topic.category_id}"], + }, + ) + end + + it "fails to update topic status in an unpermitted category" do + put "/t/#{topic.id}/status.json", + params: { + status: "closed", + enabled: "true", + category_id: tracked_category.id, + }, + headers: { + "HTTP_API_KEY" => api_key.key, + "HTTP_API_USERNAME" => api_key.user.username, + } + + expect(response.status).to eq(403) + expect(response.body).to include(I18n.t("invalid_access")) + expect(topic.reload.closed).to eq(false) + end + + it "fails without a category_id" do + put "/t/#{topic.id}/status.json", + params: { + status: "closed", + enabled: "true", + }, + headers: { + "HTTP_API_KEY" => api_key.key, + "HTTP_API_USERNAME" => api_key.user.username, + } + + expect(response.status).to eq(403) + expect(response.body).to include(I18n.t("invalid_access")) + expect(topic.reload.closed).to eq(false) + end + + it "updates topic status in a permitted category" do + put "/t/#{topic.id}/status.json", + params: { + status: "closed", + enabled: "true", + category_id: topic.category_id, + }, + headers: { + "HTTP_API_KEY" => api_key.key, + "HTTP_API_USERNAME" => api_key.user.username, + } + + expect(response.status).to eq(200) + expect(topic.reload.closed).to eq(true) + end + end + + context "when key scope has no param restrictions" do + before do + ApiKeyScope.create( + resource: "topics", + action: "update", + api_key_id: api_key.id, + allowed_parameters: { + }, + ) + end + + it "updates topic status" do + put "/t/#{topic.id}/status.json", + params: { + status: "closed", + enabled: "true", + }, + headers: { + "HTTP_API_KEY" => api_key.key, + "HTTP_API_USERNAME" => api_key.user.username, + } + + expect(response.status).to eq(200) + expect(topic.reload.closed).to eq(true) + end + end + end end describe "#destroy_timings" do @@ -2711,7 +2804,7 @@ RSpec.describe TopicsController do expect(response.status).to eq(200) end - context "with mentions" do + context "when `enable_user_status` site setting is enabled" do fab!(:post) { Fabricate(:post, user: post_author1) } fab!(:topic) { post.topic } fab!(:post2) do @@ -2723,7 +2816,23 @@ RSpec.describe TopicsController do ) end - it "returns mentions" do + before { SiteSetting.enable_user_status = true } + + it "does not return mentions when `enable_user_status` site setting is disabled" do + SiteSetting.enable_user_status = false + + get "/t/#{topic.slug}/#{topic.id}.json" + + expect(response.status).to eq(200) + + json = response.parsed_body + + expect(json["post_stream"]["posts"][1]["mentioned_users"]).to eq(nil) + end + + it "returns mentions with status" do + post_author1.set_status!("off to dentist", "tooth") + get "/t/#{topic.slug}/#{topic.id}.json" expect(response.status).to eq(200) @@ -2735,39 +2844,14 @@ RSpec.describe TopicsController do expect(mentioned_user["id"]).to be(post_author1.id) expect(mentioned_user["name"]).to eq(post_author1.name) expect(mentioned_user["username"]).to eq(post_author1.username) - end - it "doesn't return status on mentions by default" do - post_author1.set_status!("off to dentist", "tooth") - - get "/t/#{topic.slug}/#{topic.id}.json" - - expect(response.status).to eq(200) - - json = response.parsed_body - expect(json["post_stream"]["posts"][1]["mentioned_users"].length).to be(1) - status = json["post_stream"]["posts"][1]["mentioned_users"][0]["status"] - expect(status).to be_nil - end - - it "returns mentions with status if user status is enabled" do - SiteSetting.enable_user_status = true - post_author1.set_status!("off to dentist", "tooth") - - get "/t/#{topic.slug}/#{topic.id}.json" - - expect(response.status).to eq(200) - - json = response.parsed_body - expect(json["post_stream"]["posts"][1]["mentioned_users"].length).to be(1) - - status = json["post_stream"]["posts"][1]["mentioned_users"][0]["status"] + status = mentioned_user["status"] expect(status).to be_present expect(status["emoji"]).to eq(post_author1.user_status.emoji) expect(status["description"]).to eq(post_author1.user_status.description) end - it "returns an empty list of mentioned users if there is no mentions in a post" do + it "returns an empty list of mentioned users if there are no mentions in a post" do Fabricate(:post, user: post_author2, topic: topic, raw: "Post without mentions.") get "/t/#{topic.slug}/#{topic.id}.json" diff --git a/spec/requests/user_avatars_controller_spec.rb b/spec/requests/user_avatars_controller_spec.rb index 45e7851e40..db4146208a 100644 --- a/spec/requests/user_avatars_controller_spec.rb +++ b/spec/requests/user_avatars_controller_spec.rb @@ -139,7 +139,7 @@ RSpec.describe UserAvatarsController do expect(response.status).to eq(302) expect(response.location).to eq("https://s3-cdn.example.com/optimized/path") - expect(response.headers["Cache-Control"]).to eq("max-age=3600, public, immutable") + expect(response.headers["Cache-Control"]).to eq("max-age=86400, public, immutable") end it "serves new version for old urls" do diff --git a/spec/requests/webhooks_controller_spec.rb b/spec/requests/webhooks_controller_spec.rb index 8d6050096c..452774d773 100644 --- a/spec/requests/webhooks_controller_spec.rb +++ b/spec/requests/webhooks_controller_spec.rb @@ -105,6 +105,53 @@ RSpec.describe WebhooksController do expect(email_log.bounce_error_code).to eq("5.0.0") expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score) end + + it "verifies signatures" do + SiteSetting.sendgrid_verification_key = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g==" + + post "/webhooks/sendgrid.json", + headers: { + "X-Twilio-Email-Event-Webhook-Signature" => + "MEUCIGHQVtGj+Y3LkG9fLcxf3qfI10QysgDWmMOVmxG0u6ZUAiEAyBiXDWzM+uOe5W0JuG+luQAbPIqHh89M15TluLtEZtM=", + "X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502", + }, + params: + "[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"\",\"timestamp\":1600112492}]\r\n" + + expect(response.status).to eq(200) + end + + it "returns error if signature verification fails" do + SiteSetting.sendgrid_verification_key = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g==" + + post "/webhooks/sendgrid.json", + headers: { + "X-Twilio-Email-Event-Webhook-Signature" => + "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=", + "X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502", + }, + params: + "[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"\",\"timestamp\":1600112492}]\r\n" + + expect(response.status).to eq(406) + end + + it "returns error if signature is invalid" do + SiteSetting.sendgrid_verification_key = "foo" + + post "/webhooks/sendgrid.json", + headers: { + "X-Twilio-Email-Event-Webhook-Signature" => + "MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=", + "X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502", + }, + params: + "[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"\",\"timestamp\":1600112492}]\r\n" + + expect(response.status).to eq(406) + end end describe "#mailjet" do @@ -127,9 +174,47 @@ RSpec.describe WebhooksController do expect(email_log.bounce_error_code).to eq(nil) # mailjet doesn't give us this expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score) end + + it "verifies signatures" do + SiteSetting.mailjet_webhook_token = "foo" + user = Fabricate(:user, email: email) + email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) + + post "/webhooks/mailjet.json?t=foo", + params: { + "event" => "bounce", + "email" => email, + "hard_bounce" => true, + "CustomID" => message_id, + } + + expect(response.status).to eq(200) + expect(email_log.reload.bounced).to eq(true) + end + + it "returns error if signature verification fails" do + SiteSetting.mailjet_webhook_token = "foo" + user = Fabricate(:user, email: email) + email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) + + post "/webhooks/mailjet.json?t=bar", + params: { + "event" => "bounce", + "email" => email, + "hard_bounce" => true, + "CustomID" => message_id, + } + + expect(response.status).to eq(406) + expect(email_log.reload.bounced).to eq(false) + end end describe "#mandrill" do + let(:payload) do + "mandrill_events=%5B%7B%22event%22%3A%22hard_bounce%22%2C%22msg%22%3A%7B%22email%22%3A%22em%40il.com%22%2C%22diag%22%3A%225.1.1%22%2C%22bounce_description%22%3A%22smtp%3B+550-5.1.1+The+email+account+that+you+tried+to+reach+does+not+exist.%22%2C%22metadata%22%3A%7B%22message_id%22%3A%2212345%40il.com%22%7D%7D%7D%5D" + end + it "works" do user = Fabricate(:user, email: email) email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) @@ -159,6 +244,38 @@ RSpec.describe WebhooksController do expect(email_log.bounce_error_code).to eq("5.1.1") expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score) end + + it "verifies signatures" do + SiteSetting.mandrill_authentication_key = "wr_JeJNO9OI65RFDrvk3Zw" + + post "/webhooks/mandrill.json", + headers: { + "X-Mandrill-Signature" => "Q5pCb903EjEqRZ99gZrlYKOfvIU=", + }, + params: payload + + expect(response.status).to eq(200) + end + + it "returns error if signature verification fails" do + SiteSetting.mandrill_authentication_key = "wr_JeJNO9OI65RFDrvk3Zw" + + post "/webhooks/mandrill.json", headers: { "X-Mandrill-Signature" => "foo" }, params: payload + + expect(response.status).to eq(406) + end + + it "returns error if signature is invalid" do + SiteSetting.mandrill_authentication_key = "foo" + + post "/webhooks/mandrill.json", + headers: { + "X-Mandrill-Signature" => "Q5pCb903EjEqRZ99gZrlYKOfvIU=", + }, + params: payload + + expect(response.status).to eq(406) + end end describe "#mandrill_head" do @@ -187,6 +304,7 @@ RSpec.describe WebhooksController do expect(email_log.bounce_error_code).to eq(nil) # postmark doesn't give us this expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score) end + it "soft bounces" do user = Fabricate(:user, email: email) email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) @@ -204,6 +322,38 @@ RSpec.describe WebhooksController do expect(email_log.bounce_error_code).to eq(nil) # postmark doesn't give us this expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score) end + + it "verifies signatures" do + SiteSetting.postmark_webhook_token = "foo" + user = Fabricate(:user, email: email) + email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) + + post "/webhooks/postmark.json?t=foo", + params: { + "Type" => "HardBounce", + "MessageID" => message_id, + "Email" => email, + } + + expect(response.status).to eq(200) + expect(email_log.reload.bounced).to eq(true) + end + + it "returns error if signature verification fails" do + SiteSetting.postmark_webhook_token = "foo" + user = Fabricate(:user, email: email) + email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) + + post "/webhooks/postmark.json?t=bar", + params: { + "Type" => "HardBounce", + "MessageID" => message_id, + "Email" => email, + } + + expect(response.status).to eq(406) + expect(email_log.reload.bounced).to eq(false) + end end describe "#sparkpost" do @@ -235,5 +385,136 @@ RSpec.describe WebhooksController do expect(email_log.bounced).to eq(true) expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score) end + + it "verifies signatures" do + SiteSetting.sparkpost_webhook_token = "foo" + user = Fabricate(:user, email: email) + email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) + + post "/webhooks/sparkpost.json?t=foo", + params: { + "_json" => [ + { + "msys" => { + "message_event" => { + "bounce_class" => 10, + "error_code" => "554", + "rcpt_to" => email, + "rcpt_meta" => { + "message_id" => message_id, + }, + }, + }, + }, + ], + } + + expect(response.status).to eq(200) + expect(email_log.reload.bounced).to eq(true) + end + + it "returns error if signature verification fails" do + SiteSetting.sparkpost_webhook_token = "foo" + user = Fabricate(:user, email: email) + email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) + + post "/webhooks/sparkpost.json?t=bar", + params: { + "_json" => [ + { + "msys" => { + "message_event" => { + "bounce_class" => 10, + "error_code" => "554", + "rcpt_to" => email, + "rcpt_meta" => { + "message_id" => message_id, + }, + }, + }, + }, + ], + } + + expect(response.status).to eq(406) + expect(email_log.reload.bounced).to eq(false) + end + end + + describe "#aws" do + let(:payload) do + { + "Type" => "Notification", + "Message" => { + "notificationType" => "Bounce", + :"bounce" => { + "bounceType" => "Permanent", + "reportingMTA" => "dns; email.example.com", + :"bouncedRecipients" => [ + { + "emailAddress" => email, + "status" => "5.1.1", + "action" => "failed", + "diagnosticCode" => "smtp; 550 5.1.1 <#{email}>... User", + }, + ], + "bounceSubType" => "General", + "timestamp" => "2016-01-27T14:59:38.237Z", + "feedbackId" => "00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa068a-000000", + "remoteMtaIp" => "127.0.2.0", + }, + :"mail" => { + "timestamp" => "2016-01-27T14:59:38.237Z", + "source" => "john@example.com", + "sourceArn" => "arn:aws:ses:us-east-1:888888888888:identity/example.com", + "sourceIp" => "127.0.3.0", + "sendingAccountId" => "123456789012", + "callerIdentity" => "IAM_user_or_role_name", + "messageId" => message_id, + "destination" => [email, "jane@example.com", "mary@example.com", "richard@example.com"], + "headersTruncated" => false, + "headers" => [ + { "name" => "From", "value" => "\"John Doe\" " }, + { + "name" => "To", + "value" => + "\"Test\" <#{email}>, \"Jane Doe\" , \"Mary Doe\" , \"Richard Doe\" ", + }, + { "name" => "Message-ID", "value" => message_id }, + { "name" => "Subject", "value" => "Hello" }, + { "name" => "Content-Type", "value" => "text/plain; charset=\"UTF-8\"" }, + { "name" => "Content-Transfer-Encoding", "value" => "base64" }, + { "name" => "Date", "value" => "Wed, 27 Jan 2016 14:05:45 +0000" }, + ], + "commonHeaders" => { + "from" => ["John Doe "], + "date" => "Wed, 27 Jan 2016 14:05:45 +0000", + "to" => [ + "\"Test\" <#{email}>, Jane Doe , Mary Doe , Richard Doe ", + ], + "messageId" => message_id, + "subject" => "Hello", + }, + }, + }.to_json, + }.to_json + end + + before { Jobs.run_immediately! } + + it "works" do + user = Fabricate(:user, email: email) + email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) + + require "aws-sdk-sns" + Aws::SNS::MessageVerifier.any_instance.stubs(:authentic?).with(payload).returns(true) + + post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload } + expect(response.status).to eq(200) + + email_log.reload + expect(email_log.bounced).to eq(true) + expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score) + end end end diff --git a/spec/serializers/tag_serializer_spec.rb b/spec/serializers/tag_serializer_spec.rb new file mode 100644 index 0000000000..c9eab114fc --- /dev/null +++ b/spec/serializers/tag_serializer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.describe TagSerializer do + fab!(:user) { Fabricate(:user) } + fab!(:admin) { Fabricate(:admin) } + fab!(:tag) { Fabricate(:tag) } + fab!(:group) { Fabricate(:group) } + fab!(:private_category) { Fabricate(:private_category, group: group) } + fab!(:topic_in_public_category) { Fabricate(:topic, tags: [tag]) } + fab!(:topic_in_private_category) { Fabricate(:topic, category: private_category, tags: [tag]) } + + describe "#topic_count" do + it "should return the value of `Tag#public_topic_count` for a non-staff user" do + serialized = described_class.new(tag, scope: Guardian.new(user), root: false).as_json + + expect(serialized[:topic_count]).to eq(1) + end + + it "should return the vavlue of `Tag#topic_count` for a staff user" do + serialized = described_class.new(tag, scope: Guardian.new(admin), root: false).as_json + + expect(serialized[:topic_count]).to eq(2) + end + end +end diff --git a/spec/serializers/topic_view_serializer_spec.rb b/spec/serializers/topic_view_serializer_spec.rb index 1d064811c8..015ef3d1f3 100644 --- a/spec/serializers/topic_view_serializer_spec.rb +++ b/spec/serializers/topic_view_serializer_spec.rb @@ -279,9 +279,33 @@ RSpec.describe TopicViewSerializer do end describe "tags order" do - fab!(:tag1) { Fabricate(:tag, name: "ctag", description: "c description", topic_count: 5) } - fab!(:tag2) { Fabricate(:tag, name: "btag", description: "b description", topic_count: 9) } - fab!(:tag3) { Fabricate(:tag, name: "atag", description: "a description", topic_count: 3) } + fab!(:tag1) do + Fabricate( + :tag, + name: "ctag", + description: "c description", + staff_topic_count: 5, + public_topic_count: 5, + ) + end + fab!(:tag2) do + Fabricate( + :tag, + name: "btag", + description: "b description", + staff_topic_count: 9, + public_topic_count: 9, + ) + end + fab!(:tag3) do + Fabricate( + :tag, + name: "atag", + description: "a description", + staff_topic_count: 3, + public_topic_count: 3, + ) + end before do topic.tags << tag1 diff --git a/spec/services/hashtag_autocomplete_service_spec.rb b/spec/services/hashtag_autocomplete_service_spec.rb index f37eb5810c..986124548d 100644 --- a/spec/services/hashtag_autocomplete_service_spec.rb +++ b/spec/services/hashtag_autocomplete_service_spec.rb @@ -3,7 +3,9 @@ RSpec.describe HashtagAutocompleteService do fab!(:user) { Fabricate(:user) } fab!(:category1) { Fabricate(:category, name: "The Book Club", slug: "the-book-club") } - fab!(:tag1) { Fabricate(:tag, name: "great-books", topic_count: 22) } + fab!(:tag1) do + Fabricate(:tag, name: "great-books", staff_topic_count: 22, public_topic_count: 22) + end fab!(:topic1) { Fabricate(:topic) } let(:guardian) { Guardian.new(user) } @@ -68,7 +70,7 @@ RSpec.describe HashtagAutocompleteService do end it "includes the tag count" do - tag1.update!(topic_count: 78) + tag1.update!(staff_topic_count: 78, public_topic_count: 78) expect(subject.search("book", %w[tag category]).map(&:text)).to eq( ["great-books", "The Book Club"], ) @@ -149,8 +151,8 @@ RSpec.describe HashtagAutocompleteService do category6 = Fabricate(:category, name: "Book Reviews", slug: "book-reviews") Fabricate(:category, name: "Good Books", slug: "book", parent_category: category6) - Fabricate(:tag, name: "bookmania", topic_count: 15) - Fabricate(:tag, name: "awful-books", topic_count: 56) + Fabricate(:tag, name: "bookmania", staff_topic_count: 15, public_topic_count: 15) + Fabricate(:tag, name: "awful-books", staff_topic_count: 56, public_topic_count: 56) expect(subject.search("book", %w[category tag]).map(&:ref)).to eq( [ @@ -220,9 +222,13 @@ RSpec.describe HashtagAutocompleteService do end fab!(:category4) { Fabricate(:category, name: "Bookworld", slug: "book", topic_count: 56) } fab!(:category5) { Fabricate(:category, name: "Media", slug: "media", topic_count: 446) } - fab!(:tag2) { Fabricate(:tag, name: "mid-books", topic_count: 33) } - fab!(:tag3) { Fabricate(:tag, name: "terrible-books", topic_count: 2) } - fab!(:tag4) { Fabricate(:tag, name: "book", topic_count: 1) } + fab!(:tag2) do + Fabricate(:tag, name: "mid-books", staff_topic_count: 33, public_topic_count: 33) + end + fab!(:tag3) do + Fabricate(:tag, name: "terrible-books", staff_topic_count: 2, public_topic_count: 2) + end + fab!(:tag4) { Fabricate(:tag, name: "book", staff_topic_count: 1, public_topic_count: 1) } it "returns the 'most polular' categories and tags (based on topic_count) that the user can access" do category1.update!(read_restricted: true) diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb index 43d63b46d6..793239bd8a 100644 --- a/spec/services/post_alerter_spec.rb +++ b/spec/services/post_alerter_spec.rb @@ -1053,10 +1053,12 @@ RSpec.describe PostAlerter do it "triggers :before_create_notification" do type = Notification.types[:private_message] events = - DiscourseEvent.track_events { PostAlerter.new.create_notification(user, type, post, {}) } + DiscourseEvent.track_events do + PostAlerter.new.create_notification(user, type, post, { revision_number: 1 }) + end expect(events).to include( event_name: :before_create_notification, - params: [user, type, post, {}], + params: [user, type, post, { revision_number: 1 }], ) end end @@ -1207,6 +1209,22 @@ RSpec.describe PostAlerter do expect(JSON.parse(body)).to eq(payload) end + it "does not have invalid HTML in the excerpt when enable_experimental_hashtag_autocomplete is enabled" do + SiteSetting.enable_experimental_hashtag_autocomplete = true + Fabricate(:category, slug: "random") + Jobs.run_immediately! + body = nil + + stub_request(:post, "https://site2.com/push").to_return do |request| + body = request.body + { status: 200, body: "OK" } + end + create_post_with_alerts(user: user, raw: "this, @eviltrout, is a test with #random") + expect(JSON.parse(body)["notifications"][0]["excerpt"]).to eq( + "this, @eviltrout, is a test with #random", + ) + end + context "with push subscriptions" do before do Fabricate(:push_subscription, user: evil_trout) diff --git a/spec/services/tag_hashtag_data_source_spec.rb b/spec/services/tag_hashtag_data_source_spec.rb index 3598c664f9..f1475973ac 100644 --- a/spec/services/tag_hashtag_data_source_spec.rb +++ b/spec/services/tag_hashtag_data_source_spec.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true RSpec.describe TagHashtagDataSource do - fab!(:tag1) { Fabricate(:tag, name: "fact", topic_count: 0) } - fab!(:tag2) { Fabricate(:tag, name: "factor", topic_count: 5) } - fab!(:tag3) { Fabricate(:tag, name: "factory", topic_count: 4) } - fab!(:tag4) { Fabricate(:tag, name: "factorio", topic_count: 3) } - fab!(:tag5) { Fabricate(:tag, name: "factz", topic_count: 1) } + fab!(:tag1) { Fabricate(:tag, name: "fact", public_topic_count: 0) } + fab!(:tag2) { Fabricate(:tag, name: "factor", public_topic_count: 5) } + fab!(:tag3) { Fabricate(:tag, name: "factory", public_topic_count: 4) } + fab!(:tag4) { Fabricate(:tag, name: "factorio", public_topic_count: 3) } + fab!(:tag5) { Fabricate(:tag, name: "factz", public_topic_count: 1) } fab!(:user) { Fabricate(:user) } let(:guardian) { Guardian.new(user) } describe "#search" do - it "orders tag results by exact search match, then topic count, then name" do + it "orders tag results by exact search match, then public topic count, then name" do expect(described_class.search(guardian, "fact", 5).map(&:slug)).to eq( %w[fact factor factory factorio factz], ) @@ -31,7 +31,7 @@ RSpec.describe TagHashtagDataSource do ) end - it "includes the topic count for the text of the tag in secondary text" do + it "includes the public topic count for the text of the tag in secondary text" do expect(described_class.search(guardian, "fact", 5).map(&:secondary_text)).to eq( %w[x0 x5 x4 x3 x1], ) @@ -52,7 +52,7 @@ RSpec.describe TagHashtagDataSource do end describe "#search_without_term" do - it "returns distinct tags sorted by topic_count" do + it "returns distinct tags sorted by public topic count" do expect(described_class.search_without_term(guardian, 5).map(&:slug)).to eq( %w[factor factory factorio factz fact], ) diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb index 7813ec137e..028483bf88 100644 --- a/spec/support/system_helpers.rb +++ b/spec/support/system_helpers.rb @@ -39,6 +39,18 @@ module SystemHelpers retry end + def wait_for_attribute( + element, + attribute, + value, + timeout: Capybara.default_max_wait_time, + frequency: 0.01 + ) + try_until_success(timeout: timeout, frequency: frequency) do + expect(element[attribute.to_sym]).to eq(value) + end + end + def resize_window(width: nil, height: nil) original_size = page.driver.browser.manage.window.size page.driver.browser.manage.window.resize_to( diff --git a/spec/support/user_sidebar_serializer_attributes.rb b/spec/support/user_sidebar_serializer_attributes.rb index f7cea951b3..9bf6eaa5d5 100644 --- a/spec/support/user_sidebar_serializer_attributes.rb +++ b/spec/support/user_sidebar_serializer_attributes.rb @@ -65,7 +65,9 @@ RSpec.shared_examples "User Sidebar Serializer Attributes" do |serializer_klass| describe "#sidebar_tags" do fab!(:tag) { Fabricate(:tag, name: "foo") } - fab!(:pm_tag) { Fabricate(:tag, name: "bar", pm_topic_count: 5, topic_count: 0) } + fab!(:pm_tag) do + Fabricate(:tag, name: "bar", pm_topic_count: 5, staff_topic_count: 0, public_topic_count: 0) + end fab!(:hidden_tag) { Fabricate(:tag, name: "secret") } fab!(:staff_tag_group) do Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["secret"]) diff --git a/spec/system/hashtag_autocomplete_spec.rb b/spec/system/hashtag_autocomplete_spec.rb index 131c19d32f..254f6a686c 100644 --- a/spec/system/hashtag_autocomplete_spec.rb +++ b/spec/system/hashtag_autocomplete_spec.rb @@ -10,8 +10,8 @@ describe "Using #hashtag autocompletion to search for and lookup categories and fab!(:category2) do Fabricate(:category, name: "Other Category", slug: "other-cat", topic_count: 23) end - fab!(:tag) { Fabricate(:tag, name: "cooltag", topic_count: 324) } - fab!(:tag2) { Fabricate(:tag, name: "othertag", topic_count: 66) } + fab!(:tag) { Fabricate(:tag, name: "cooltag", staff_topic_count: 324, public_topic_count: 324) } + fab!(:tag2) { Fabricate(:tag, name: "othertag", staff_topic_count: 66, public_topic_count: 66) } fab!(:topic) { Fabricate(:topic, category: category, tags: [tag]) } fab!(:post) { Fabricate(:post, topic: topic) } let(:uncategorized_category) { Category.find(SiteSetting.uncategorized_category_id) } diff --git a/spec/system/page_objects/components/composer.rb b/spec/system/page_objects/components/composer.rb index 28dd6b88a2..1ac5ebfb6a 100644 --- a/spec/system/page_objects/components/composer.rb +++ b/spec/system/page_objects/components/composer.rb @@ -3,12 +3,6 @@ module PageObjects module Components class Composer < PageObjects::Components::Base - def open_new_topic - visit("/latest") - find("button#create-topic").click - self - end - def open_composer_actions find(".composer-action-title .btn").click self @@ -20,10 +14,23 @@ module PageObjects end def fill_content(content) - find("#reply-control .d-editor-input").fill_in(with: content) + composer_input.fill_in(with: content) self end + def type_content(content) + composer_input.send_keys(content) + self + end + + def clear_content + fill_content("") + end + + def has_content?(content) + composer_input.value == content + end + def select_action(action) find(action(action)).click self @@ -40,6 +47,10 @@ module PageObjects def button_label find("#reply-control .btn-primary .d-button-label") end + + def composer_input + find("#reply-control .d-editor .d-editor-input") + end end end end diff --git a/spec/system/page_objects/components/sidebar.rb b/spec/system/page_objects/components/sidebar.rb new file mode 100644 index 0000000000..06fde5dc8c --- /dev/null +++ b/spec/system/page_objects/components/sidebar.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module PageObjects + module Components + class Sidebar < PageObjects::Components::Base + def visible? + page.has_css?("#d-sidebar") + end + + def has_category_section_link?(category) + page.has_link?(category.name, class: "sidebar-section-link") + end + end + end +end diff --git a/spec/system/page_objects/modals/base.rb b/spec/system/page_objects/modals/base.rb index 7bad184882..5d8e68996f 100644 --- a/spec/system/page_objects/modals/base.rb +++ b/spec/system/page_objects/modals/base.rb @@ -17,6 +17,10 @@ module PageObjects def click_outside find(".modal-outer-container").click(x: 0, y: 0) end + + def click_primary_button + find(".modal-footer .btn-primary").click + end end end end diff --git a/spec/system/page_objects/pages/base.rb b/spec/system/page_objects/pages/base.rb index 60c2ffb24f..48b005fc1c 100644 --- a/spec/system/page_objects/pages/base.rb +++ b/spec/system/page_objects/pages/base.rb @@ -4,10 +4,6 @@ module PageObjects module Pages class Base include Capybara::DSL - - def setup_component_classes!(component_classes) - @component_classes = component_classes - end end end end diff --git a/spec/system/page_objects/pages/topic.rb b/spec/system/page_objects/pages/topic.rb index efe278d05c..f863315468 100644 --- a/spec/system/page_objects/pages/topic.rb +++ b/spec/system/page_objects/pages/topic.rb @@ -4,13 +4,7 @@ module PageObjects module Pages class Topic < PageObjects::Pages::Base def initialize - setup_component_classes!( - post_show_more_actions: ".show-more-actions", - post_action_button_bookmark: ".bookmark.with-reminder", - reply_button: ".topic-footer-main-buttons > .create", - composer: "#reply-control", - composer_textarea: "#reply-control .d-editor .d-editor-input", - ) + @composer_component = PageObjects::Components::Composer.new end def visit_topic(topic) @@ -18,6 +12,17 @@ module PageObjects self end + def open_new_topic + page.visit "/" + find("button#create-topic").click + self + end + + def open_new_message + page.visit "/new-message" + self + end + def visit_topic_and_open_composer(topic) visit_topic(topic) click_reply_button @@ -85,24 +90,20 @@ module PageObjects has_css?("#reply-control.open") end - def find_composer - find("#reply-control .d-editor .d-editor-input") - end - def type_in_composer(input) - find_composer.send_keys(input) + @composer_component.type_content(input) end def fill_in_composer(input) - find_composer.fill_in(with: input) + @composer_component.fill_content(input) end def clear_composer - fill_in_composer("") + @composer_component.clear_content end def has_composer_content?(content) - find_composer.value == content + @composer_component.has_content?(content) end def send_reply @@ -110,7 +111,7 @@ module PageObjects end def fill_in_composer_title(title) - find("#reply-title").fill_in(with: title) + @composer_component.fill_title(title) end private diff --git a/spec/system/page_objects/pages/user_preferences.rb b/spec/system/page_objects/pages/user_preferences.rb index 42bdc01628..dda52924af 100644 --- a/spec/system/page_objects/pages/user_preferences.rb +++ b/spec/system/page_objects/pages/user_preferences.rb @@ -16,7 +16,7 @@ module PageObjects find(".horizontal-overflow-nav__scroll-left").click end - INTERFACE_LINK_CSS_SELECTOR = ".nav-tracking" + INTERFACE_LINK_CSS_SELECTOR = ".user-nav__preferences-tracking" def has_interface_link_visible? horizontal_secondary_link_visible?(INTERFACE_LINK_CSS_SELECTOR, visible: true) @@ -26,7 +26,7 @@ module PageObjects horizontal_secondary_link_visible?(INTERFACE_LINK_CSS_SELECTOR, visible: false) end - ACCOUNT_LINK_CSS_SELECTOR = ".nav-account" + ACCOUNT_LINK_CSS_SELECTOR = ".user-nav__preferences-account" def has_account_link_visible? horizontal_secondary_link_visible?(ACCOUNT_LINK_CSS_SELECTOR, visible: true) diff --git a/spec/system/viewing_sidebar_spec.rb b/spec/system/viewing_sidebar_spec.rb new file mode 100644 index 0000000000..4b9da86e9c --- /dev/null +++ b/spec/system/viewing_sidebar_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +describe "Viewing sidebar", type: :system, js: true do + fab!(:admin) { Fabricate(:admin) } + fab!(:user) { Fabricate(:user) } + fab!(:category_sidebar_section_link) { Fabricate(:category_sidebar_section_link, user: user) } + + describe "when using the legacy navigation menu" do + before { SiteSetting.navigation_menu = "legacy" } + + it "should display the sidebar when `enable_sidebar` query param is '1'" do + sign_in(user) + + visit("/latest?enable_sidebar=1") + + sidebar = PageObjects::Components::Sidebar.new + + expect(sidebar).to be_visible + expect(sidebar).to have_category_section_link(category_sidebar_section_link.linkable) + end + end +end diff --git a/spec/tasks/hashtags_spec.rb b/spec/tasks/hashtags_spec.rb new file mode 100644 index 0000000000..0186174da3 --- /dev/null +++ b/spec/tasks/hashtags_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.describe "tasks/hashtags" do + before do + Rake::Task.clear + Discourse::Application.load_tasks + end + + describe "hashtag:mark_old_format_for_rebake" do + fab!(:category) { Fabricate(:category, slug: "support") } + + before { SiteSetting.enable_experimental_hashtag_autocomplete = false } + + it "sets the baked_version to 0 for matching posts" do + post_1 = Fabricate(:post, raw: "This is a cool #support hashtag") + post_2 = + Fabricate( + :post, + raw: + "Some other thing which will not match some weird custom thing", + ) + + SiteSetting.enable_experimental_hashtag_autocomplete = true + post_3 = Fabricate(:post, raw: "This is a cool #support hashtag") + SiteSetting.enable_experimental_hashtag_autocomplete = false + + Rake::Task["hashtags:mark_old_format_for_rebake"].invoke + + [post_1, post_2, post_3].each(&:reload) + + expect(post_1.baked_version).to eq(0) + expect(post_2.baked_version).to eq(Post::BAKED_VERSION) + expect(post_3.baked_version).to eq(Post::BAKED_VERSION) + end + end +end diff --git a/yarn.lock b/yarn.lock index 40febf39c5..402409e17c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -209,6 +209,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.6.tgz#845338edecad65ebffef058d3be851f1d28a63bc" integrity sha512-uQVSa9jJUe/G/304lXspfWVpKpK4euFLgGiMQFOCpM/bgcAdeoHwi/OQz23O9GK2osz26ZiXRRV9aV+Yl1O8tw== +"@babel/parser@^7.9.4": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b" + integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg== + "@babel/plugin-proposal-decorators@^7.18.2": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.6.tgz#68e9fd0f022b944f84a8824bb28bfaee724d2595" @@ -411,6 +416,13 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jsdoc/salty@^0.2.1": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.2.tgz#567017ddda2048c5ff921aeffd38564a0578fdca" + integrity sha512-A1FrVnc7L9qI2gUGsfN0trTiJNK72Y0CL/VAyrmYEmeKI3pnHDawP64CEev31XLyAAOx2xmDo3tbadPxC0CSbw== + dependencies: + lodash "^4.17.21" + "@json-editor/json-editor@^2.6.1": version "2.6.1" resolved "https://registry.yarnpkg.com/@json-editor/json-editor/-/json-editor-2.6.1.tgz#169e8b88305d71ccac391c3ae22d4145bc63c9f7" @@ -493,6 +505,24 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/linkify-it@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" + integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== + +"@types/markdown-it@^12.2.3": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" + +"@types/mdurl@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" + integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== + "@types/node@*": version "14.11.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" @@ -537,6 +567,13 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-escape-sequences@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz#2483c8773f50dd9174dd9557e92b1718f1816097" + integrity sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw== + dependencies: + array-back "^3.0.1" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -567,6 +604,40 @@ aria-query@^5.0.0: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== +array-back@^1.0.2, array-back@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b" + integrity sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw== + dependencies: + typical "^2.6.0" + +array-back@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022" + integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw== + dependencies: + typical "^2.6.1" + +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + +array-back@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-5.0.0.tgz#e196609edcec48376236d163958df76e659a0d36" + integrity sha512-kgVWwJReZWmVuWOQKEOohXKJX+nD02JAZ54D1RRWlv8L0NebauKAaFxACKzB74RTclt1+WNz5KHaLRDAPZbDEw== + +array-back@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.2.tgz#f567d99e9af88a6d3d2f9dfcc21db6f9ba9fd157" + integrity sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" @@ -611,6 +682,11 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -649,6 +725,15 @@ buffer@^5.2.1, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +cache-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cache-point/-/cache-point-2.0.0.tgz#91e03c38da9cfba9d95ac6a34d24cfe6eff8920f" + integrity sha512-4gkeHlFpSKgm3vm2gJN5sPqfmijYRFYCQ6tv5cLw0xVmT6r1z1vd4FNnpuOREco3cBs1G709sZ72LdgddKvL5w== + dependencies: + array-back "^4.0.1" + fs-then-native "^2.0.0" + mkdirp2 "^1.0.4" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -659,6 +744,13 @@ caniuse-lite@^1.0.30001359: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15" integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg== +catharsis@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" + integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== + dependencies: + lodash "^4.17.15" + chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -740,6 +832,14 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +collect-all@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/collect-all/-/collect-all-1.0.4.tgz#50cd7119ac24b8e12a661f0f8c3aa0ea7222ddfc" + integrity sha512-RKZhRwJtJEP5FWul+gkSMEnaK6H3AGPTTWOiRimCcs+rc/OmQE3Yhy1Q7A7KsdkG3ZXVdZq68Y6ONSdvkeEcKA== + dependencies: + stream-connect "^1.0.2" + stream-via "^1.0.4" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -769,6 +869,37 @@ colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== +command-line-args@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-tool@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/command-line-tool/-/command-line-tool-0.8.0.tgz#b00290ef1dfc11cc731dd1f43a92cfa5f21e715b" + integrity sha512-Xw18HVx/QzQV3Sc5k1vy3kgtOeGmsKIqwtFFoyjI4bbcpSgnw2CWVULvtakyw4s6fhyAdI6soQQhXc2OzJy62g== + dependencies: + ansi-escape-sequences "^4.0.0" + array-back "^2.0.0" + command-line-args "^5.0.0" + command-line-usage "^4.1.0" + typical "^2.6.1" + +command-line-usage@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-4.1.0.tgz#a6b3b2e2703b4dcf8bd46ae19e118a9a52972882" + integrity sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g== + dependencies: + ansi-escape-sequences "^4.0.0" + array-back "^2.0.0" + table-layout "^0.4.2" + typical "^2.6.1" + commander@2.11.x: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -784,11 +915,23 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +common-sequence@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/common-sequence/-/common-sequence-2.0.2.tgz#accc76bdc5876a1fcd92b73484d4285fff99d838" + integrity sha512-jAg09gkdkrDO9EWTdXfv80WWH3yeZl5oT69fGfedBNS9pXUKYInVJ1bJ+/ht2+Moeei48TmSbQDYMc8EOx9G0g== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +config-master@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/config-master/-/config-master-3.1.0.tgz#667663590505a283bf26a484d68489d74c5485da" + integrity sha512-n7LBL1zBzYdTpF1mx5DNcZnZn05CWIdsdvtPL4MosvqbBUK3Rq6VWEtGUuF3Y0s9/CIhMejezqlSkP6TnCJ/9g== + dependencies: + walk-back "^2.0.1" + convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -849,6 +992,11 @@ debug@^2.6.8: dependencies: ms "2.0.0" +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -878,6 +1026,24 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dmd@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/dmd/-/dmd-6.2.0.tgz#d267a9fb1ce62b74edca8bf5bcbd3b8e08574fe7" + integrity sha512-uXWxLF1H7TkUAuoHK59/h/ts5cKavm2LnhrIgJWisip4BVzPoXavlwyoprFFn2CzcahKYgvkfaebS6oxzgflkg== + dependencies: + array-back "^6.2.2" + cache-point "^2.0.0" + common-sequence "^2.0.2" + file-set "^4.0.2" + handlebars "^4.7.7" + marked "^4.2.3" + object-get "^2.1.1" + reduce-flatten "^3.0.1" + reduce-unique "^2.0.1" + reduce-without "^1.0.1" + test-value "^3.0.0" + walk-back "^5.1.0" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -969,6 +1135,11 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -979,6 +1150,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -1240,6 +1416,14 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-set@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/file-set/-/file-set-4.0.2.tgz#8d67c92a864202c2085ac9f03f1c9909c7e27030" + integrity sha512-fuxEgzk4L8waGXaAkd8cMr73Pm0FxOVkn8hztzUW7BAHhOGH90viQNXbiOsnecCWmfInqU6YmAMwxRMdKETceQ== + dependencies: + array-back "^5.0.0" + glob "^7.1.6" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -1247,6 +1431,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + find-up@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -1299,6 +1490,11 @@ fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-then-native@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fs-then-native/-/fs-then-native-2.0.0.tgz#19a124d94d90c22c8e045f2e8dd6ebea36d48c67" + integrity sha512-X712jAOaWXkemQCAmWeg5rOT2i+KOpWz1Z/txk/cW0qlOu2oQ9H61vc5w3X/iyuUEfq/OyaFJ78/cZAQD1/bgA== + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1371,7 +1567,7 @@ glob-stream@^7.0.0: to-absolute-glob "^2.0.2" unique-stream "^2.3.1" -glob@^7.1.3, glob@^7.2.0: +glob@^7.1.3, glob@^7.1.6, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1418,11 +1614,23 @@ globby@^13.1.2: merge2 "^1.4.1" slash "^4.0.0" -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: +graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1589,6 +1797,74 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +js2xmlparser@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" + integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== + dependencies: + xmlcreate "^2.0.4" + +jsdoc-api@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-api/-/jsdoc-api-8.0.0.tgz#4b2c25ff60f91b80da51b6cd33943acc7b2cab74" + integrity sha512-Rnhor0suB1Ds1abjmFkFfKeD+kSMRN9oHMTMZoJVUrmtCGDwXty+sWMA9sa4xbe4UyxuPjhC7tavZ40mDKK6QQ== + dependencies: + array-back "^6.2.2" + cache-point "^2.0.0" + collect-all "^1.0.4" + file-set "^4.0.2" + fs-then-native "^2.0.0" + jsdoc "^4.0.0" + object-to-spawn-args "^2.0.1" + temp-path "^1.0.0" + walk-back "^5.1.0" + +jsdoc-parse@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/jsdoc-parse/-/jsdoc-parse-6.2.0.tgz#2b71d3925acfc4badc72526f2470766e0561f6b5" + integrity sha512-Afu1fQBEb7QHt6QWX/6eUWvYHJofB90Fjx7FuJYF7mnG9z5BkAIpms1wsnvYLytfmqpEENHs/fax9p8gvMj7dw== + dependencies: + array-back "^6.2.2" + lodash.omit "^4.5.0" + lodash.pick "^4.4.0" + reduce-extract "^1.0.0" + sort-array "^4.1.5" + test-value "^3.0.0" + +jsdoc-to-markdown@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-to-markdown/-/jsdoc-to-markdown-8.0.0.tgz#27f32ed200d3b84dbf22a49beed485790f93b3ce" + integrity sha512-2FQvYkg491+FP6s15eFlgSSWs69CvQrpbABGYBtvAvGWy/lWo8IKKToarT283w59rQFrpcjHl3YdhHCa3l7gXg== + dependencies: + array-back "^6.2.2" + command-line-tool "^0.8.0" + config-master "^3.1.0" + dmd "^6.2.0" + jsdoc-api "^8.0.0" + jsdoc-parse "^6.2.0" + walk-back "^5.1.0" + +jsdoc@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-4.0.0.tgz#9569f79ea5b14ba4bc726da1a48fe6a241ad7893" + integrity sha512-tzTgkklbWKrlaQL2+e3NNgLcZu3NaK2vsHRx7tyHQ+H5jcB9Gx0txSd2eJWlMC/xU1+7LQu4s58Ry0RkuaEQVg== + dependencies: + "@babel/parser" "^7.9.4" + "@jsdoc/salty" "^0.2.1" + "@types/markdown-it" "^12.2.3" + bluebird "^3.7.2" + catharsis "^0.9.0" + escape-string-regexp "^2.0.0" + js2xmlparser "^4.0.2" + klaw "^3.0.0" + markdown-it "^12.3.2" + markdown-it-anchor "^8.4.1" + marked "^4.0.10" + mkdirp "^1.0.4" + requizzle "^0.2.3" + strip-json-comments "^3.1.0" + underscore "~1.13.2" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -1618,6 +1894,13 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +klaw@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" + integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== + dependencies: + graceful-fs "^4.1.9" + language-subtag-registry@~0.3.2: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" @@ -1688,6 +1971,13 @@ lighthouse-logger@^1.0.0: debug "^2.6.8" marky "^1.2.0" +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -1709,6 +1999,11 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.kebabcase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -1719,7 +2014,22 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.14: +lodash.omit@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" + integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg== + +lodash.padend@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" + integrity sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw== + +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== + +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1744,6 +2054,27 @@ magnific-popup@1.1.0: resolved "https://registry.yarnpkg.com/magnific-popup/-/magnific-popup-1.1.0.tgz#3e7362c5bd18f6785fe99e59d013e20af33d3049" integrity sha1-PnNixb0Y9nhf6Z5Z0BPiCvM9MEk= +markdown-it-anchor@^8.4.1: + version "8.6.6" + resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.6.tgz#4a12e358c9c2167ee28cb7a5f10e29d6f1ffd7ca" + integrity sha512-jRW30YGywD2ESXDc+l17AiritL0uVaSnWsb26f+68qaW9zgbIIr1f4v2Nsvc0+s0Z2N3uX6t/yAw7BwCQ1wMsA== + +markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +marked@^4.0.10, marked@^4.2.3: + version "4.2.5" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.5.tgz#979813dfc1252cc123a79b71b095759a32f42a5d" + integrity sha512-jPueVhumq7idETHkb203WDD4fMA3yV9emQ5vLwop58lu8bTclMghBWcYAavlDqIEMaisADinV1TooIFCfqOsYQ== + marky@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.1.tgz#a3fcf82ffd357756b8b8affec9fdbf3a30dc1b02" @@ -1754,6 +2085,11 @@ mdn-data@2.0.27: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.27.tgz#1710baa7b0db8176d3b3d565ccb7915fc69525ab" integrity sha512-kwqO0I0jtWr25KcfLm9pia8vLZ8qoAKhWZuZMbneJq3jjBD3gl5nZs8l8Tu3ZBlBAHVQtDur9rdDGyvtfVraHQ== +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1779,11 +2115,26 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimist@^1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + mkdirp-classic@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mkdirp2@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/mkdirp2/-/mkdirp2-1.0.5.tgz#68bbe61defefafce4b48948608ec0bac942512c2" + integrity sha512-xOE9xbICroUDmG1ye2h4bZ8WBie9EGmACaco8K8cx6RlkJJrxGIqjGqztAI+NMhexXBcdGbSEzI6N3EJPevxZw== + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + moment-timezone@0.5.39: version "0.5.39" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.39.tgz#342625a3b98810f04c8f4ea917e448d3525e600b" @@ -1811,6 +2162,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -1831,6 +2187,16 @@ node-releases@^2.0.5: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== +object-get@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/object-get/-/object-get-2.1.1.tgz#1dad63baf6d94df184d1c58756cc9be55b174dac" + integrity sha512-7n4IpLMzGGcLEMiQKsNR7vCe+N5E9LORFrtNUVy4sO3dj9a3HedZCxEL2T7QuLhcHN1NBuBsMOKaOsAYI9IIvg== + +object-to-spawn-args@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz#cf8b8e3c9b3589137a469cac90391f44870144a5" + integrity sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2086,6 +2452,35 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +reduce-extract@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/reduce-extract/-/reduce-extract-1.0.0.tgz#67f2385beda65061b5f5f4312662e8b080ca1525" + integrity sha512-QF8vjWx3wnRSL5uFMyCjDeDc5EBMiryoT9tz94VvgjKfzecHAVnqmXAwQDcr7X4JmLc2cjkjFGCVzhMqDjgR9g== + dependencies: + test-value "^1.0.1" + +reduce-flatten@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327" + integrity sha512-j5WfFJfc9CoXv/WbwVLHq74i/hdTUpy+iNC534LxczMRP67vJeK3V9JOdnL0N1cIRbn9mYhE2yVjvvKXDxvNXQ== + +reduce-flatten@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-3.0.1.tgz#3db6b48ced1f4dbe4f4f5e31e422aa9ff0cd21ba" + integrity sha512-bYo+97BmUUOzg09XwfkwALt4PQH1M5L0wzKerBt6WLm3Fhdd43mMS89HiT1B9pJIqko/6lWx3OnV4J9f2Kqp5Q== + +reduce-unique@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/reduce-unique/-/reduce-unique-2.0.1.tgz#fb34b90e89297c1e08d75dcf17e9a6443ea71081" + integrity sha512-x4jH/8L1eyZGR785WY+ePtyMNhycl1N2XOLxhCbzZFaqF4AXjLzqSxa2UHgJ2ZVR/HHyPOvl1L7xRnW8ye5MdA== + +reduce-without@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/reduce-without/-/reduce-without-1.0.1.tgz#68ad0ead11855c9a37d4e8256c15bbf87972fc8c" + integrity sha512-zQv5y/cf85sxvdrKPlfcRzlDn/OqKFThNimYmsS3flmkioKvkUGn2Qg9cJVoQiEvdxFGLE0MQER/9fZ9sUqdxg== + dependencies: + test-value "^2.0.0" + regexpp@^3.0.0, regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -2111,6 +2506,13 @@ requireindex@~1.1.0: resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162" integrity sha1-5UBLgVV+91225JxacgBIk/4D4WI= +requizzle@^0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" + integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== + dependencies: + lodash "^4.17.21" + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -2217,22 +2619,47 @@ snake-case@^3.0.3: dot-case "^3.0.4" tslib "^2.0.3" +sort-array@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/sort-array/-/sort-array-4.1.5.tgz#64b92aaba222aec606786f4df28ae4e3e3e68313" + integrity sha512-Ya4peoS1fgFN42RN1REk2FgdNOeLIEMKFGJvs7VTP3OklF8+kl2SkpVliZ4tk/PurWsrWRsdNdU+tgyOBkB9sA== + dependencies: + array-back "^5.0.0" + typical "^6.0.1" + source-map-js@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + squoosh@discourse/squoosh#dc9649d: version "2.0.0" resolved "https://codeload.github.com/discourse/squoosh/tar.gz/dc9649d0a4d396d1251c22291b17d99f1716da44" dependencies: wasm-feature-detect "^1.2.11" +stream-connect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-connect/-/stream-connect-1.0.2.tgz#18bc81f2edb35b8b5d9a8009200a985314428a97" + integrity sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ== + dependencies: + array-back "^1.0.2" + stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +stream-via@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/stream-via/-/stream-via-1.0.4.tgz#8dccbb0ac909328eb8bc8e2a4bd3934afdaf606c" + integrity sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ== + string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -2287,6 +2714,17 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +table-layout@^0.4.2: + version "0.4.5" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.5.tgz#d906de6a25fa09c0c90d1d08ecd833ecedcb7378" + integrity sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw== + dependencies: + array-back "^2.0.0" + deep-extend "~0.6.0" + lodash.padend "^4.6.1" + typical "^2.6.1" + wordwrapjs "^3.0.0" + tar-fs@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -2308,6 +2746,35 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +temp-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-path/-/temp-path-1.0.0.tgz#24b1543973ab442896d9ad367dd9cbdbfafe918b" + integrity sha512-TvmyH7kC6ZVTYkqCODjJIbgvu0FKiwQpZ4D1aknE7xpcDf/qEOB8KZEK5ef2pfbVoiBhNWs3yx4y+ESMtNYmlg== + +test-value@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/test-value/-/test-value-1.1.0.tgz#a09136f72ec043d27c893707c2b159bfad7de93f" + integrity sha512-wrsbRo7qP+2Je8x8DsK8ovCGyxe3sYfQwOraIY/09A2gFXU9DYKiTF14W4ki/01AEh56kMzAmlj9CaHGDDUBJA== + dependencies: + array-back "^1.0.2" + typical "^2.4.2" + +test-value@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291" + integrity sha512-+1epbAxtKeXttkGFMTX9H42oqzOTufR1ceCF+GYA5aOmvaPq9wd4PUS8329fn2RRLGNeUkgRLnVpycjx8DsO2w== + dependencies: + array-back "^1.0.3" + typical "^2.6.0" + +test-value@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/test-value/-/test-value-3.0.0.tgz#9168c062fab11a86b8d444dd968bb4b73851ce92" + integrity sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ== + dependencies: + array-back "^2.0.0" + typical "^2.6.1" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -2388,6 +2855,31 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +typical@^2.4.2, typical@^2.6.0, typical@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" + integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg== + +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/typical/-/typical-6.0.1.tgz#89bd1a6aa5e5e96fa907fb6b7579223bff558a06" + integrity sha512-+g3NEp7fJLe9DPa1TArHm9QAA7YciZmWnfAqEaFrBihQ7epOv9i99rjtgb6Iz0wh3WuQDjsCTDfgRoGnmHN81A== + +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +uglify-js@^3.1.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + unbzip2-stream@1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -2401,6 +2893,11 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +underscore@~1.13.2: + version "1.13.6" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== + unique-stream@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" @@ -2444,6 +2941,16 @@ v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +walk-back@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-2.0.1.tgz#554e2a9d874fac47a8cb006bf44c2f0c4998a0a4" + integrity sha512-Nb6GvBR8UWX1D+Le+xUq0+Q1kFmRBIWVrfLnQAOmcpEzA9oAxwJ9gIr36t9TWYfzvWRvuMtjHiVsJYEkXWaTAQ== + +walk-back@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-5.1.0.tgz#486d6f29e67f56ab89b952d987028bbb1a4e956c" + integrity sha512-Uhxps5yZcVNbLEAnb+xaEEMdgTXl9qAQDzKYejG2AZ7qPwRQ81lozY9ECDbjLPNWm7YsO1IK5rsP1KoQzXAcGA== + wasm-feature-detect@^1.2.11: version "1.3.0" resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.3.0.tgz#fb3fc5dd4a1ba950a429be843daad67fe048bc42" @@ -2481,6 +2988,19 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +wordwrapjs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz#c94c372894cadc6feb1a66bff64e1d9af92c5d1e" + integrity sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw== + dependencies: + reduce-flatten "^1.0.1" + typical "^2.6.1" + workbox-cacheable-response@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-4.3.1.tgz#f53e079179c095a3f19e5313b284975c91428c91" @@ -2548,6 +3068,11 @@ ws@^7.2.0: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== +xmlcreate@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" + integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== + xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"