diff --git a/.eslintignore b/.eslintignore index 3956d17380..a07eade1d2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -16,3 +16,4 @@ app/assets/javascripts/discourse/tests/test-boot-rails.js app/assets/javascripts/discourse/tests/fixtures node_modules/ dist/ +tmp/ diff --git a/.github/workflows/ember_with_plugins.yml b/.github/workflows/ember_with_plugins.yml index ecb934bb56..52b4277415 100644 --- a/.github/workflows/ember_with_plugins.yml +++ b/.github/workflows/ember_with_plugins.yml @@ -26,7 +26,7 @@ jobs: run: echo "::set-output name=dir::$(yarn cache dir)" - name: Yarn cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: yarn-cache with: path: ${{ steps.yarn-cache-dir.outputs.dir }} diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index ef6906dd78..e1187dcffa 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -28,7 +28,7 @@ jobs: git config --global user.name "Discourse CI" - name: Bundler cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} @@ -49,7 +49,7 @@ jobs: run: echo "::set-output name=dir::$(yarn cache dir)" - name: Yarn cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: yarn-cache with: path: ${{ steps.yarn-cache-dir.outputs.dir }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9af681a4bd..fc76e8c78b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,7 +63,7 @@ jobs: sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';" - name: Bundler cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} @@ -84,7 +84,7 @@ jobs: run: echo "::set-output name=dir::$(yarn cache dir)" - name: Yarn cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: yarn-cache with: path: ${{ steps.yarn-cache-dir.outputs.dir }} @@ -104,7 +104,7 @@ jobs: run: bin/rake plugin:pull_compatible_all - name: Fetch app state cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: app-cache with: path: tmp/app-cache @@ -216,7 +216,7 @@ jobs: run: echo "::set-output name=dir::$(yarn cache dir)" - name: Yarn cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: yarn-cache with: path: ${{ steps.yarn-cache-dir.outputs.dir }} @@ -235,16 +235,19 @@ jobs: sudo -E -u discourse -H yarn ember build --environment=test -o /tmp/emberbuild - name: Core QUnit 1 + if: ${{ always() }} working-directory: ./app/assets/javascripts/discourse run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=1 --launch "${{ matrix.browser }}" --random timeout-minutes: 20 - name: Core QUnit 2 + if: ${{ always() }} working-directory: ./app/assets/javascripts/discourse run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=2 --launch "${{ matrix.browser }}" --random timeout-minutes: 20 - name: Core QUnit 3 + if: ${{ always() }} working-directory: ./app/assets/javascripts/discourse run: sudo -E -u discourse -H yarn ember exam --path /tmp/emberbuild --split=3 --partition=3 --launch "${{ matrix.browser }}" --random timeout-minutes: 20 diff --git a/Gemfile b/Gemfile index c66a3f2554..2766c3fead 100644 --- a/Gemfile +++ b/Gemfile @@ -31,9 +31,7 @@ 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' +gem 'sprockets' # this will eventually be added to rails, # allows us to precompile all our templates in the unicorn master diff --git a/Gemfile.lock b/Gemfile.lock index 35eec5d987..9de187f37d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,7 @@ GEM chunky_png (1.4.0) coderay (1.1.3) colored2 (3.1.2) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) connection_pool (2.2.5) cose (1.2.0) cbor (~> 0.5.9) @@ -129,10 +129,10 @@ GEM sprockets (>= 3.3, < 4.1) ember-source (2.18.2) erubi (1.10.0) - excon (0.92.1) + excon (0.92.2) execjs (2.8.1) exifr (1.3.9) - fabrication (2.27.0) + fabrication (2.28.0) faker (2.20.0) i18n (>= 1.8.11, < 2) fakeweb (1.3.0) @@ -194,7 +194,7 @@ GEM json (2.6.1) json-schema (2.8.1) addressable (>= 2.4) - json_schemer (0.2.19) + json_schemer (0.2.20) ecma-re-validator (~> 0.3) hana (~> 1.3) regexp_parser (~> 2.0) @@ -211,7 +211,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) literate_randomizer (0.4.0) - lograge (0.11.2) + lograge (0.12.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -220,7 +220,7 @@ GEM logstash-logger (0.26.1) logstash-event (~> 1.2) logster (2.11.0) - loofah (2.15.0) + loofah (2.16.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) lru_redux (1.1.0) @@ -241,7 +241,7 @@ GEM ffi (~> 1.9) minitest (5.15.0) mocha (1.13.0) - msgpack (1.4.5) + msgpack (1.5.1) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) @@ -293,12 +293,12 @@ GEM openssl-signature_algorithm (1.1.1) openssl (~> 2.0) optimist (3.0.1) - parallel (1.22.0) - parallel_tests (3.7.3) + parallel (1.22.1) + parallel_tests (3.8.1) parallel - parser (3.1.1.0) + parser (3.1.2.0) ast (~> 2.4.1) - pg (1.3.4) + pg (1.3.5) progress (3.6.0) pry (0.13.1) coderay (~> 1.1) @@ -308,8 +308,8 @@ GEM pry (~> 0.13.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (4.0.6) - puma (5.6.2) + public_suffix (4.0.7) + puma (5.6.4) nio4r (~> 2.0) r2 (0.2.7) racc (1.6.0) @@ -352,7 +352,7 @@ GEM redis (4.5.1) redis-namespace (1.8.2) redis (>= 3.0.4) - regexp_parser (2.2.1) + regexp_parser (2.3.0) request_store (1.5.1) rack (>= 1.4) rexml (3.2.5) @@ -374,7 +374,7 @@ GEM rspec-html-matchers (0.9.4) nokogiri (~> 1) rspec (>= 3.0.0.a, < 4) - rspec-mocks (3.11.0) + rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) rspec-rails (5.1.1) @@ -393,7 +393,7 @@ GEM json-schema (~> 2.2) railties (>= 3.1, < 7.1) rtlit (0.0.5) - rubocop (1.26.0) + rubocop (1.27.0) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) @@ -402,7 +402,7 @@ GEM rubocop-ast (>= 1.16.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.16.0) + rubocop-ast (1.17.0) parser (>= 3.1.1.0) rubocop-discourse (2.5.0) rubocop (>= 1.1.0) @@ -443,7 +443,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sprockets (3.7.2) + sprockets (4.0.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.4.2) @@ -452,7 +452,7 @@ GEM sprockets (>= 3.0.0) sshkey (2.0.0) stackprof (0.2.19) - test-prof (1.0.7) + test-prof (1.0.8) thor (1.2.1) tilt (2.0.10) tzinfo (2.0.4) @@ -466,7 +466,7 @@ GEM unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) - uniform_notifier (1.15.0) + uniform_notifier (1.16.0) uri_template (0.7.0) webmock (3.14.0) addressable (>= 2.8.0) @@ -602,7 +602,7 @@ DEPENDENCIES shoulda-matchers sidekiq simplecov - sprockets (= 3.7.2) + sprockets sprockets-rails sshkey stackprof diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000000..ac907b3677 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1 @@ +//= link_tree ../images diff --git a/app/assets/javascripts/admin/addon/components/site-settings/color.js b/app/assets/javascripts/admin/addon/components/site-settings/color.js index 8d098cb7d8..0b1db9a5c1 100644 --- a/app/assets/javascripts/admin/addon/components/site-settings/color.js +++ b/app/assets/javascripts/admin/addon/components/site-settings/color.js @@ -5,7 +5,7 @@ function RGBToHex(rgb) { // Choose correct separator let sep = rgb.indexOf(",") > -1 ? "," : " "; // Turn "rgb(r,g,b)" into [r,g,b] - rgb = rgb.substr(4).split(")")[0].split(sep); + rgb = rgb.slice(4).split(")")[0].split(sep); let r = (+rgb[0]).toString(16), g = (+rgb[1]).toString(16), diff --git a/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js b/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js index dc5ba1193d..3dbcc3742c 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-site-settings.js @@ -30,7 +30,7 @@ export default Controller.extend({ } if (word.startsWith("plugin:")) { - pluginFilter = word.substr("plugin:".length).trim(); + pluginFilter = word.slice("plugin:".length).trim(); return false; } diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js index c11df41a5f..807bb39932 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js @@ -222,11 +222,6 @@ export default Controller.extend(CanCheckEmails, { .then((result) => { if (result.email_confirmation_required) { bootbox.alert(I18n.t("admin.user.grant_admin_confirm")); - } else { - const controller = showModal("grant-admin-second-factor", { - model: this.model, - }); - controller.setResult(result); } }) .catch((error) => { diff --git a/app/assets/javascripts/admin/addon/models/color-scheme-color.js b/app/assets/javascripts/admin/addon/models/color-scheme-color.js index 49cb552a92..24beb9130c 100644 --- a/app/assets/javascripts/admin/addon/models/color-scheme-color.js +++ b/app/assets/javascripts/admin/addon/models/color-scheme-color.js @@ -76,17 +76,17 @@ const ColorSchemeColor = EmberObject.extend({ if (hex.length === 6 || hex.length === 3) { if (hex.length === 3) { hex = - hex.substr(0, 1) + - hex.substr(0, 1) + - hex.substr(1, 1) + - hex.substr(1, 1) + - hex.substr(2, 1) + - hex.substr(2, 1); + hex.slice(0, 1) + + hex.slice(0, 1) + + hex.slice(1, 2) + + hex.slice(1, 2) + + hex.slice(2, 3) + + hex.slice(2, 3); } return Math.round( - (parseInt(hex.substr(0, 2), 16) * 299 + - parseInt(hex.substr(2, 2), 16) * 587 + - parseInt(hex.substr(4, 2), 16) * 114) / + (parseInt(hex.slice(0, 2), 16) * 299 + + parseInt(hex.slice(2, 4), 16) * 587 + + parseInt(hex.slice(4, 6), 16) * 114) / 1000 ); } diff --git a/app/assets/javascripts/admin/addon/models/version-check.js b/app/assets/javascripts/admin/addon/models/version-check.js index 4f939064af..edf6ad5e0e 100644 --- a/app/assets/javascripts/admin/addon/models/version-check.js +++ b/app/assets/javascripts/admin/addon/models/version-check.js @@ -28,7 +28,7 @@ const VersionCheck = EmberObject.extend({ @discourseComputed("installed_sha") shortSha(installedSHA) { if (installedSHA) { - return installedSHA.substr(0, 10); + return installedSHA.slice(0, 10); } }, }); diff --git a/app/assets/javascripts/admin/addon/templates/badges-show.hbs b/app/assets/javascripts/admin/addon/templates/badges-show.hbs index a1260d0481..524a53c812 100644 --- a/app/assets/javascripts/admin/addon/templates/badges-show.hbs +++ b/app/assets/javascripts/admin/addon/templates/badges-show.hbs @@ -68,7 +68,9 @@ value=buffered.badge_type_id content=badgeTypes onChange=(action (mut buffered.badge_type_id)) - isDisabled=readOnly + options=(hash + disabled=readOnly + ) }} @@ -155,7 +157,9 @@ value=buffered.trigger content=badgeTriggers onChange=(action (mut buffered.trigger)) - disabled=readOnly + options=(hash + disabled=readOnly + ) }} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs b/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs index bac1facc69..c306bcc68f 100644 --- a/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/report-filters/group.hbs @@ -1,11 +1,11 @@ {{combo-box - filterable=true valueProperty="value" content=groupOptions value=groupId - none="admin.dashboard.reports.groups" onChange=(action "onChange") options=(hash allowAny=filter.allow_any + filterable=true + none="admin.dashboard.reports.groups" ) }} diff --git a/app/assets/javascripts/admin/addon/templates/components/report-filters/list.hbs b/app/assets/javascripts/admin/addon/templates/components/report-filters/list.hbs index 88d71d0747..e01c74268b 100644 --- a/app/assets/javascripts/admin/addon/templates/components/report-filters/list.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/report-filters/list.hbs @@ -1,8 +1,10 @@ {{combo-box content=filter.choices - filterable=true - allowAny=filter.allow_any value=filter.default - none="admin.dashboard.report_filter_any" onChange=(action "onChange") + options=(hash + allowAny=filter.allow_any + filterable=true + none="admin.dashboard.report_filter_any" + ) }} diff --git a/app/assets/javascripts/admin/addon/templates/components/site-settings/category.hbs b/app/assets/javascripts/admin/addon/templates/components/site-settings/category.hbs index e2545c6863..27a80e7aaa 100644 --- a/app/assets/javascripts/admin/addon/templates/components/site-settings/category.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/site-settings/category.hbs @@ -1,8 +1,8 @@ {{category-chooser value=value - allowUncategorized=true onChange=(action (mut value)) options=(hash + allowUncategorized=true none=(eq setting.default "") ) }} diff --git a/app/assets/javascripts/admin/addon/templates/components/value-list.hbs b/app/assets/javascripts/admin/addon/templates/components/value-list.hbs index 518922c965..c9852ed170 100644 --- a/app/assets/javascripts/admin/addon/templates/components/value-list.hbs +++ b/app/assets/javascripts/admin/addon/templates/components/value-list.hbs @@ -34,13 +34,13 @@ {{/if}} {{combo-box - options=(hash - allowAny=true - ) - none=noneKey valueProperty=null nameProperty=null value=newValue content=filteredChoices onChange=(action "selectChoice") + options=(hash + allowAny=true + none=noneKey + ) }} diff --git a/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs index ee702a56d5..811510fa3e 100644 --- a/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/addon/templates/customize-themes-show.hbs @@ -159,10 +159,12 @@
{{color-palettes content=colorSchemes - filterable=true - forceEscape=true value=colorSchemeId - icon="paint-brush"}} + icon="paint-brush" + options=(hash + filterable=true + ) + }}
{{i18n "admin.customize.theme.color_scheme_select"}}
diff --git a/app/assets/javascripts/admin/addon/templates/email-bounced.hbs b/app/assets/javascripts/admin/addon/templates/email-bounced.hbs index e1c995437c..5677cc4664 100644 --- a/app/assets/javascripts/admin/addon/templates/email-bounced.hbs +++ b/app/assets/javascripts/admin/addon/templates/email-bounced.hbs @@ -5,7 +5,7 @@ {{i18n "admin.email.time"}} {{i18n "admin.email.user"}} {{i18n "admin.email.to_address"}} - {{i18n "admin.email.email_type"}} + {{i18n "admin.email.email_type"}} @@ -13,7 +13,7 @@ {{i18n "admin.email.logs.filters.title"}} {{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}} {{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}} - {{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}} + {{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}} {{#each model as |l|}} @@ -28,15 +28,26 @@ {{/if}} {{l.to_address}} - {{#if l.has_bounce_key}} - {{l.email_type}} - {{else}} - {{l.email_type}} - {{/if}} + + {{#if l.has_bounce_key}} + + {{l.email_type}} + + {{else}} + {{l.email_type}} + {{/if}} + + + {{#if l.has_bounce_key}} + + {{d-icon "info-circle"}} + + {{/if}} + {{else}} {{#unless loading}} - {{i18n "admin.email.logs.none"}} + {{i18n "admin.email.logs.none"}} {{/unless}} {{/each}} diff --git a/app/assets/javascripts/admin/addon/templates/email-rejected.hbs b/app/assets/javascripts/admin/addon/templates/email-rejected.hbs index 2338af8d15..c214a57e37 100644 --- a/app/assets/javascripts/admin/addon/templates/email-rejected.hbs +++ b/app/assets/javascripts/admin/addon/templates/email-rejected.hbs @@ -6,7 +6,7 @@ {{i18n "admin.email.incoming_emails.from_address"}} {{i18n "admin.email.incoming_emails.to_addresses"}} {{i18n "admin.email.incoming_emails.subject"}} - {{i18n "admin.email.incoming_emails.error"}} + {{i18n "admin.email.incoming_emails.error"}} @@ -16,7 +16,7 @@ {{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}} {{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}} {{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}} - {{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}} + {{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}} {{#each model as |email|}} @@ -50,9 +50,14 @@ {{email.error}} + + + {{d-icon "info-circle"}} + + {{else}} - {{i18n "admin.email.incoming_emails.none"}} + {{i18n "admin.email.incoming_emails.none"}} {{/each}} diff --git a/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs index 4213160362..afb605991b 100644 --- a/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/addon/templates/logs/staff-action-logs.hbs @@ -34,8 +34,10 @@ {{combo-box content=userHistoryActions value=filterActionId - none="admin.logs.staff_actions.all" onChange=(action "filterActionIdChanged") + options=(hash + none="admin.logs.staff_actions.all" + ) }} {{/if}} diff --git a/app/assets/javascripts/admin/addon/templates/user-badges.hbs b/app/assets/javascripts/admin/addon/templates/user-badges.hbs index 1b401b7e42..44549f6613 100644 --- a/app/assets/javascripts/admin/addon/templates/user-badges.hbs +++ b/app/assets/javascripts/admin/addon/templates/user-badges.hbs @@ -17,10 +17,12 @@
{{combo-box - filterable=true value=selectedBadgeId content=grantableBadges onChange=(action (mut selectedBadgeId)) + options=(hash + filterable=true + ) }}
diff --git a/app/assets/javascripts/admin/addon/templates/user-index.hbs b/app/assets/javascripts/admin/addon/templates/user-index.hbs index ad93faf07d..643a5bb031 100644 --- a/app/assets/javascripts/admin/addon/templates/user-index.hbs +++ b/app/assets/javascripts/admin/addon/templates/user-index.hbs @@ -551,8 +551,10 @@ {{combo-box content=model.customGroups value=model.primary_group_id - none="admin.groups.no_primary" onChange=(action (mut model.primary_group_id)) + options=(hash + none="admin.groups.no_primary" + ) }}
{{#if primaryGroupDirty}} @@ -732,7 +734,7 @@ {{#if model.active}} {{#if model.can_impersonate}} {{d-button - class="btn-danger" + class="btn-danger btn-impersonate" action=(action "impersonate") icon="crosshairs" label="admin.impersonate.title" @@ -743,21 +745,21 @@ {{#if model.can_be_anonymized}} {{d-button label="admin.user.anonymize" icon="exclamation-triangle" - class="btn-danger" + class="btn-danger btn-anonymize" action=(action "anonymize")}} {{/if}} {{#if model.canBeDeleted}} {{d-button label="admin.user.delete" icon="trash-alt" - class="btn-danger" + class="btn-danger btn-user-delete" action=(action "destroy")}} {{/if}} {{#if model.can_be_merged}} {{d-button label="admin.user.merge.button" icon="arrows-alt-h" - class="btn-danger" + class="btn-danger btn-user-merge" action=(action "promptTargetUser")}} {{/if}} diff --git a/app/assets/javascripts/browser-detect.js b/app/assets/javascripts/browser-detect.js index d8d0eda481..3b93c14499 100644 --- a/app/assets/javascripts/browser-detect.js +++ b/app/assets/javascripts/browser-detect.js @@ -1,9 +1,4 @@ -if ( - !window.WeakMap || - !window.Promise || - typeof globalThis === "undefined" || - !String.prototype.replaceAll -) { +if (!window.WeakMap || !window.Promise || typeof globalThis === "undefined") { window.unsupportedBrowser = true; } else { // Some implementations of `WeakMap.prototype.has` do not accept false diff --git a/app/assets/javascripts/browser-update.js b/app/assets/javascripts/browser-update.js index 07b5c9b5f4..31a5b410f6 100644 --- a/app/assets/javascripts/browser-update.js +++ b/app/assets/javascripts/browser-update.js @@ -29,11 +29,16 @@ // find the element with the "data-path" attribute set for (var i = 0; i < noscriptElements.length; ++i) { if (noscriptElements[i].getAttribute("data-path")) { - // noscriptElements[i].innerHTML contains encoded HTML - if (noscriptElements[i].childNodes.length > 0) { - mainElement.innerHTML = noscriptElements[i].childNodes[0].nodeValue; - break; + // noscriptElements[i].innerHTML contains encoded HTML, so we need to access + // the childNodes instead. Browsers seem to split very long content into multiple + // text childNodes. + var result = ""; + for (var j = 0; j < noscriptElements[i].childNodes.length; j++) { + result += noscriptElements[i].childNodes[j].nodeValue; } + + mainElement.outerHTML = result; + break; } } diff --git a/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js b/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js index d60b94eaf7..b66172c73b 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js +++ b/app/assets/javascripts/discourse-common/addon/lib/attribute-hook.js @@ -33,7 +33,7 @@ AttributeHook.prototype.unhook = function (node, prop, next) { } let colonPosition = prop.indexOf(":"); - let localName = colonPosition > -1 ? prop.substr(colonPosition + 1) : prop; + let localName = colonPosition > -1 ? prop.slice(colonPosition + 1) : prop; node.removeAttributeNS(this.namespace, localName); }; diff --git a/app/assets/javascripts/discourse-common/addon/lib/get-url.js b/app/assets/javascripts/discourse-common/addon/lib/get-url.js index 4a5a0f1715..fab9081c6f 100644 --- a/app/assets/javascripts/discourse-common/addon/lib/get-url.js +++ b/app/assets/javascripts/discourse-common/addon/lib/get-url.js @@ -41,7 +41,7 @@ export function getURLWithCDN(url) { } export function getAbsoluteURL(path) { - return baseUrl + path; + return baseUrl + withoutPrefix(path); } export function isAbsoluteURL(url) { diff --git a/app/assets/javascripts/discourse-common/package.json b/app/assets/javascripts/discourse-common/package.json index 68f1a4c629..ebf77fa1e3 100644 --- a/app/assets/javascripts/discourse-common/package.json +++ b/app/assets/javascripts/discourse-common/package.json @@ -15,12 +15,12 @@ "start": "ember serve" }, "dependencies": { - "@uppy/aws-s3": "^2.0.4", - "@uppy/aws-s3-multipart": "^2.1.0", - "@uppy/core": "^2.1.0", - "@uppy/drop-target": "^1.1.0", - "@uppy/utils": "^4.0.3", - "@uppy/xhr-upload": "^2.0.4", + "@uppy/aws-s3": "^2.0.8", + "@uppy/aws-s3-multipart": "^2.2.1", + "@uppy/core": "^2.1.6", + "@uppy/drop-target": "^1.1.2", + "@uppy/utils": "^4.0.5", + "@uppy/xhr-upload": "^2.0.7", "ember-auto-import": "^2.2.4", "ember-cli-babel": "^7.13.0", "ember-cli-htmlbars": "^4.2.0", diff --git a/app/assets/javascripts/discourse/app/components/bookmark-list.js b/app/assets/javascripts/discourse/app/components/bookmark-list.js index 066a9019cd..9559c87a7e 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark-list.js +++ b/app/assets/javascripts/discourse/app/components/bookmark-list.js @@ -93,19 +93,23 @@ export default Component.extend(Scrolling, { @action editBookmark(bookmark) { - openBookmarkModal(bookmark, { - onAfterSave: (savedData) => { - this.appEvents.trigger( - "bookmarks:changed", - savedData, - bookmark.attachedTo() - ); - this.reload(); + openBookmarkModal( + bookmark, + { + onAfterSave: (savedData) => { + this.appEvents.trigger( + "bookmarks:changed", + savedData, + bookmark.attachedTo() + ); + this.reload(); + }, + onAfterDelete: () => { + this.reload(); + }, }, - onAfterDelete: () => { - this.reload(); - }, - }); + { use_polymorphic_bookmarks: this.siteSettings.use_polymorphic_bookmarks } + ); }, @action diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js index 8aa1dafb80..df1409a793 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark.js +++ b/app/assets/javascripts/discourse/app/components/bookmark.js @@ -5,7 +5,10 @@ import I18n from "I18n"; import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; import ItsATrap from "@discourse/itsatrap"; import { Promise } from "rsvp"; -import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut"; +import { + TIME_SHORTCUT_TYPES, + defaultTimeShortcuts, +} from "discourse/lib/time-shortcut"; import { action } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import bootbox from "bootbox"; @@ -149,12 +152,19 @@ export default Component.extend({ const data = { reminder_at: reminderAtISO, name: this.model.name, - post_id: this.model.postId, id: this.model.id, auto_delete_preference: this.autoDeletePreference, - for_topic: this.model.forTopic, }; + if (this.siteSettings.use_polymorphic_bookmarks) { + data.bookmarkable_id = this.model.bookmarkableId; + data.bookmarkable_type = this.model.bookmarkableType; + } else { + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. + data.post_id = this.model.postId; + data.for_topic = this.model.forTopic; + } + if (this.editingExistingBookmark) { return ajax(`/bookmarks/${this.model.id}`, { type: "PUT", @@ -173,15 +183,25 @@ export default Component.extend({ if (!this.afterSave) { return; } - this.afterSave({ + + const data = { reminder_at: reminderAtISO, - for_topic: this.model.forTopic, auto_delete_preference: this.autoDeletePreference, - post_id: this.model.postId, id: this.model.id || response.id, name: this.model.name, - topic_id: this.model.topicId, - }); + }; + + if (this.siteSettings.use_polymorphic_bookmarks) { + data.bookmarkable_id = this.model.bookmarkableId; + data.bookmarkable_type = this.model.bookmarkableType; + } else { + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. + data.post_id = this.model.postId; + data.for_topic = this.model.forTopic; + data.topic_id = this.model.topicId; + } + + this.afterSave(data); }, _deleteBookmark() { @@ -274,12 +294,12 @@ export default Component.extend({ }); }, - @discourseComputed() - customTimeShortcutOptions() { - let customOptions = []; + @discourseComputed("userTimezone") + timeOptions(userTimezone) { + const options = defaultTimeShortcuts(userTimezone); if (this.showPostLocalDate) { - customOptions.push({ + options.push({ icon: "globe-americas", id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE, label: "time_shortcut.post_local_date", @@ -289,7 +309,7 @@ export default Component.extend({ }); } - return customOptions; + return options; }, @discourseComputed("existingBookmarkHasReminder") diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index eed18fe3b1..f836b7ff78 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -1,8 +1,4 @@ -import { - authorizedExtensions, - authorizesAllExtensions, - authorizesOneOrMoreImageExtensions, -} from "discourse/lib/uploads"; +import { authorizesOneOrMoreImageExtensions } from "discourse/lib/uploads"; import { alias } from "@ember/object/computed"; import { BasePlugin } from "@uppy/core"; import { resolveAllShortUrls } from "pretty-text/upload-short-url"; @@ -103,7 +99,10 @@ export default Component.extend(ComposerUploadUppy, { editorClass: ".d-editor", 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", composerModel: alias("composer"), @@ -198,24 +197,6 @@ export default Component.extend(ComposerUploadUppy, { }); }, - @discourseComputed() - acceptsAllFormats() { - return ( - this.capabilities.isIOS || - authorizesAllExtensions(this.currentUser.staff, this.siteSettings) - ); - }, - - @discourseComputed() - acceptedFormats() { - const extensions = authorizedExtensions( - this.currentUser.staff, - this.siteSettings - ); - - return extensions.map((ext) => `.${ext}`).join(); - }, - @bind _afterMentionComplete(value) { this.composer.set("reply", value); diff --git a/app/assets/javascripts/discourse/app/components/composer-messages.js b/app/assets/javascripts/discourse/app/components/composer-messages.js index ad323bcb55..2f00c76a49 100644 --- a/app/assets/javascripts/discourse/app/components/composer-messages.js +++ b/app/assets/javascripts/discourse/app/components/composer-messages.js @@ -158,7 +158,7 @@ export default Component.extend({ } // TODO pass the 200 in from somewhere - const raw = (composer.get("reply") || "").substr(0, 200); + const raw = (composer.get("reply") || "").slice(0, 200); const title = composer.get("title") || ""; // Ensure we have at least a title diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js index 8e86af03e4..afae9e0428 100644 --- a/app/assets/javascripts/discourse/app/components/d-editor.js +++ b/app/assets/javascripts/discourse/app/components/d-editor.js @@ -576,11 +576,15 @@ export default Component.extend(TextareaTextManipulation, { return resolve(options); }) - .then((list) => - list.map((code) => { + .then((list) => { + if (list === SKIP) { + return []; + } + + return list.map((code) => { return { code, src: emojiUrlFor(code) }; - }) - ) + }); + }) .then((list) => { if (list.length) { list.push({ label: I18n.t("composer.more_emoji"), term }); diff --git a/app/assets/javascripts/discourse/app/components/edit-category-tags.js b/app/assets/javascripts/discourse/app/components/edit-category-tags.js index f9dc2d914b..b12a980ed7 100644 --- a/app/assets/javascripts/discourse/app/components/edit-category-tags.js +++ b/app/assets/javascripts/discourse/app/components/edit-category-tags.js @@ -1,8 +1,29 @@ import { and, empty } from "@ember/object/computed"; import { buildCategoryPanel } from "discourse/components/edit-category-panel"; +import { action, set } from "@ember/object"; export default buildCategoryPanel("tags", { allowedTagsEmpty: empty("category.allowed_tags"), allowedTagGroupsEmpty: empty("category.allowed_tag_groups"), disableAllowGlobalTags: and("allowedTagsEmpty", "allowedTagGroupsEmpty"), + + @action + onTagGroupChange(rtg, valueArray) { + // A little strange, but we're using a multi-select component + // to select a single tag group. This action takes the array + // and extracts the first value in it. + set(rtg, "name", valueArray[0]); + }, + + @action + addRequiredTagGroup() { + this.category.required_tag_groups.pushObject({ + min_count: 1, + }); + }, + + @action + deleteRequiredTagGroup(rtg) { + this.category.required_tag_groups.removeObject(rtg); + }, }); diff --git a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js index f3d984bcd5..4489a1353b 100644 --- a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js +++ b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js @@ -14,9 +14,11 @@ import I18n from "I18n"; import { action } from "@ember/object"; import Component from "@ember/component"; import { isEmpty } from "@ember/utils"; -import { MOMENT_MONDAY, now, startOfDay } from "discourse/lib/time-utils"; import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; -import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut"; +import { + TIME_SHORTCUT_TYPES, + timeShortcuts, +} from "discourse/lib/time-shortcut"; import ItsATrap from "@discourse/itsatrap"; export default Component.extend({ @@ -81,23 +83,19 @@ export default Component.extend({ }, @discourseComputed() - customTimeShortcutOptions() { + timeOptions() { const timezone = this.currentUser.resolvedTimezone(this.currentUser); + const shortcuts = timeShortcuts(timezone); + return [ - { - icon: "far-clock", - id: "two_weeks", - label: "time_shortcut.two_weeks", - time: startOfDay(now(timezone).add(2, "weeks").day(MOMENT_MONDAY)), - timeFormatKey: "dates.long_no_year", - }, - { - icon: "far-calendar-plus", - id: "six_months", - label: "time_shortcut.six_months", - time: startOfDay(now(timezone).add(6, "months").startOf("month")), - timeFormatKey: "dates.long_no_year", - }, + shortcuts.laterToday(), + shortcuts.tomorrow(), + shortcuts.laterThisWeek(), + shortcuts.thisWeekend(), + shortcuts.monday(), + shortcuts.twoWeeks(), + shortcuts.nextMonth(), + shortcuts.sixMonths(), ]; }, diff --git a/app/assets/javascripts/discourse/app/components/emoji-picker.js b/app/assets/javascripts/discourse/app/components/emoji-picker.js index 9d577d8b83..d54f52160e 100644 --- a/app/assets/javascripts/discourse/app/components/emoji-picker.js +++ b/app/assets/javascripts/discourse/app/components/emoji-picker.js @@ -153,9 +153,10 @@ export default Component.extend({ }, @action - onClose() { + onClose(event) { + event?.stopPropagation(); document.removeEventListener("click", this.handleOutsideClick); - this.onEmojiPickerClose && this.onEmojiPickerClose(); + this.onEmojiPickerClose && this.onEmojiPickerClose(event); }, diversityScales: computed("selectedDiversity", function () { @@ -221,7 +222,7 @@ export default Component.extend({ }); if (this.site.isMobileDevice) { - this.onClose(); + this.onClose(event); } }, @@ -236,7 +237,7 @@ export default Component.extend({ @action keydown(event) { if (event.code === "Escape") { - this.onClose(); + this.onClose(event); return false; } }, @@ -334,7 +335,7 @@ export default Component.extend({ handleOutsideClick(event) { const emojiPicker = document.querySelector(".emoji-picker"); if (emojiPicker && !emojiPicker.contains(event.target)) { - this.onClose(); + this.onClose(event); } }, }); diff --git a/app/assets/javascripts/discourse/app/components/group-manage-save-button.js b/app/assets/javascripts/discourse/app/components/group-manage-save-button.js index 3c7e17d634..3999c2b756 100644 --- a/app/assets/javascripts/discourse/app/components/group-manage-save-button.js +++ b/app/assets/javascripts/discourse/app/components/group-manage-save-button.js @@ -1,5 +1,4 @@ import Component from "@ember/component"; -import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; @@ -37,26 +36,7 @@ export default Component.extend({ return group .save(opts) - .then((data) => { - if (data.user_count) { - const controller = showModal("group-default-notifications", { - model: { - count: data.user_count, - }, - }); - - controller.set("onClose", () => { - this.updateExistingUsers = controller.updateExistingUsers; - this.send("save"); - }); - - return; - } - - if (data.route_to) { - DiscourseURL.routeTo(data.route_to); - } - + .then(() => { this.setProperties({ saved: true, updateExistingUsers: null, @@ -66,7 +46,21 @@ export default Component.extend({ this.afterSave(); } }) - .catch(popupAjaxError) + .catch((error) => { + const json = error.jqXHR.responseJSON; + if (error.jqXHR.status === 422 && json.user_count) { + const controller = showModal("group-default-notifications", { + model: { count: json.user_count }, + }); + + controller.set("onClose", () => { + this.updateExistingUsers = controller.updateExistingUsers; + this.send("save"); + }); + } else { + popupAjaxError(error); + } + }) .finally(() => this.set("saving", false)); }, }, diff --git a/app/assets/javascripts/discourse/app/components/json-editor.js b/app/assets/javascripts/discourse/app/components/json-editor.js index c0f418d8bb..2ac1dcc734 100644 --- a/app/assets/javascripts/discourse/app/components/json-editor.js +++ b/app/assets/javascripts/discourse/app/components/json-editor.js @@ -29,8 +29,9 @@ export default Component.extend({ schema: this.model.jsonSchema, disable_array_delete_all_rows: true, disable_array_delete_last_row: true, - disable_array_reorder: true, - disable_array_copy: true, + disable_array_reorder: false, + disable_array_copy: false, + enable_array_copy: true, disable_edit_json: true, disable_properties: true, disable_collapse: true, @@ -70,8 +71,11 @@ export default Component.extend({ class DiscourseJsonSchemaEditorIconlib { constructor() { this.mapping = { - delete: "times", + delete: "trash-alt", add: "plus", + moveup: "arrow-up", + movedown: "arrow-down", + copy: "copy", }; } diff --git a/app/assets/javascripts/discourse/app/components/pick-files-button.js b/app/assets/javascripts/discourse/app/components/pick-files-button.js index 9ab6465bcd..3f2482ad50 100644 --- a/app/assets/javascripts/discourse/app/components/pick-files-button.js +++ b/app/assets/javascripts/discourse/app/components/pick-files-button.js @@ -1,25 +1,47 @@ import Component from "@ember/component"; -import { action } from "@ember/object"; -import { empty } from "@ember/object/computed"; -import computed, { bind } from "discourse-common/utils/decorators"; -import I18n from "I18n"; import bootbox from "bootbox"; +import { isBlank } from "@ember/utils"; +import { + authorizedExtensions, + authorizesAllExtensions, +} from "discourse/lib/uploads"; +import { action } from "@ember/object"; +import discourseComputed, { bind } from "discourse-common/utils/decorators"; +import I18n from "I18n"; +// This picker is intended to be used with UppyUploadMixin or with +// ComposerUploadUppy, which is why there are no change events registered +// for the input. They are handled by the uppy mixins directly. +// +// However, if you provide an onFilesPicked action to this component, the change +// binding will still be added, and the file type will be validated here. This +// is sometimes useful if you need to do something outside the uppy upload with +// the file, such as directly using JSON or CSV data from a file in JS. export default Component.extend({ + fileInputId: null, + fileInputClass: null, + fileInputDisabled: false, classNames: ["pick-files-button"], - acceptedFileTypes: null, - acceptAnyFile: empty("acceptedFileTypes"), + acceptedFormatsOverride: null, + allowMultiple: false, + showButton: false, didInsertElement() { this._super(...arguments); - const fileInput = this.element.querySelector("input"); - this.set("fileInput", fileInput); - fileInput.addEventListener("change", this.onChange, false); + + if (this.onFilesPicked) { + const fileInput = this.element.querySelector("input"); + this.set("fileInput", fileInput); + fileInput.addEventListener("change", this.onChange, false); + } }, willDestroyElement() { this._super(...arguments); - this.fileInput.removeEventListener("change", this.onChange); + + if (this.onFilesPicked) { + this.fileInput.removeEventListener("change", this.onChange); + } }, @bind @@ -28,33 +50,27 @@ export default Component.extend({ this._filesPicked(files); }, - @computed - acceptedFileTypesString() { - if (!this.acceptedFileTypes) { - return null; - } - - return this.acceptedFileTypes.join(","); + @discourseComputed() + acceptsAllFormats() { + return ( + this.capabilities.isIOS || + authorizesAllExtensions(this.currentUser.staff, this.siteSettings) + ); }, - @computed - acceptedExtensions() { - if (!this.acceptedFileTypes) { - return null; + @discourseComputed() + acceptedFormats() { + // the acceptedFormatsOverride can be a list of extensions or mime types + if (!isBlank(this.acceptedFormatsOverride)) { + return this.acceptedFormatsOverride; } - return this.acceptedFileTypes - .filter((type) => type.startsWith(".")) - .map((type) => type.substring(1)); - }, + const extensions = authorizedExtensions( + this.currentUser.staff, + this.siteSettings + ); - @computed - acceptedMimeTypes() { - if (!this.acceptedFileTypes) { - return null; - } - - return this.acceptedFileTypes.filter((type) => !type.startsWith(".")); + return extensions.map((ext) => `.${ext}`).join(); }, @action @@ -79,25 +95,18 @@ export default Component.extend({ _haveAcceptedTypes(files) { for (const file of files) { - if ( - !(this._hasAcceptedExtension(file) || this._hasAcceptedMimeType(file)) - ) { + if (!this._hasAcceptedExtensionOrType(file)) { return false; } } return true; }, - _hasAcceptedExtension(file) { + _hasAcceptedExtensionOrType(file) { const extension = this._fileExtension(file.name); return ( - !this.acceptedExtensions || this.acceptedExtensions.includes(extension) - ); - }, - - _hasAcceptedMimeType(file) { - return ( - !this.acceptedMimeTypes || this.acceptedMimeTypes.includes(file.type) + this.acceptedFormats.includes(`.${extension}`) || + this.acceptedFormats.includes(file.type) ); }, diff --git a/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js b/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js index 261d01bc08..c6c088decb 100644 --- a/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js +++ b/app/assets/javascripts/discourse/app/components/scrolling-post-stream.js @@ -58,24 +58,6 @@ export default MountWidget.extend({ ); }, - beforePatch() { - this.prevHeight = document.body.clientHeight; - this.prevScrollTop = document.body.scrollTop; - }, - - afterPatch() { - const height = document.body.clientHeight; - - // This hack is for when swapping out many cloaked views at once - // when using keyboard navigation. It could suddenly move the scroll - if ( - this.prevHeight === height && - document.body.scrollTop !== this.prevScrollTop - ) { - document.body.scroll({ left: 0, top: this.prevScrollTop }); - } - }, - scrolled() { if (this.isDestroyed || this.isDestroying) { return; @@ -103,7 +85,7 @@ export default MountWidget.extend({ const slack = Math.round(windowHeight * 5); const onscreen = []; const nearby = []; - const windowTop = document.documentElement.scrollTop; + const windowTop = document.scrollingElement.scrollTop; const postsWrapperTop = domUtils.offset( document.querySelector(".posts-wrapper") ).top; @@ -202,9 +184,7 @@ export default MountWidget.extend({ const elem = postsNodes.item(onscreen[0]); const elemId = elem.id; const elemPos = domUtils.position(elem); - const distToElement = elemPos - ? document.body.scrollTop - elemPos.top - : 0; + const distToElement = elemPos?.top || 0; const topRefresh = () => { refresh(() => { @@ -213,7 +193,7 @@ export default MountWidget.extend({ // Quickly going back might mean the element is destroyed const position = domUtils.position(refreshedElem); if (position && position.top) { - let whereY = position.top + distToElement; + let whereY = position.top - distToElement; document.documentElement.scroll({ top: whereY, left: 0 }); // This seems weird, but somewhat infrequently a rerender diff --git a/app/assets/javascripts/discourse/app/components/search-result-entry.js b/app/assets/javascripts/discourse/app/components/search-result-entry.js index 73e340671f..9312174ed1 100644 --- a/app/assets/javascripts/discourse/app/components/search-result-entry.js +++ b/app/assets/javascripts/discourse/app/components/search-result-entry.js @@ -1,4 +1,6 @@ import Component from "@ember/component"; +import { action } from "@ember/object"; +import { logSearchLinkClick } from "discourse/lib/search"; export default Component.extend({ tagName: "div", @@ -6,4 +8,15 @@ export default Component.extend({ classNameBindings: ["bulkSelectEnabled"], attributeBindings: ["role"], role: "listitem", + + @action + logClick(topicId) { + if (this.searchLogId && topicId) { + logSearchLinkClick({ + searchLogId: this.searchLogId, + searchResultId: topicId, + searchResultType: "topic", + }); + } + }, }); diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js index d541ad74da..82a4d9f843 100644 --- a/app/assets/javascripts/discourse/app/components/site-header.js +++ b/app/assets/javascripts/discourse/app/components/site-header.js @@ -444,6 +444,5 @@ export default SiteHeaderComponent.extend({ export function headerTop() { const header = document.querySelector("header.d-header"); - const headerOffsetTop = header.offsetTop ? header.offsetTop : 0; - return headerOffsetTop - document.body.scrollTop; + return header.offsetTop ? header.offsetTop : 0; } diff --git a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js index 5839191f48..46890b6afe 100644 --- a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js +++ b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js @@ -9,7 +9,7 @@ import { } from "discourse/lib/time-utils"; import { TIME_SHORTCUT_TYPES, - defaultShortcutOptions, + defaultTimeShortcuts, specialShortcutOptions, } from "discourse/lib/time-shortcut"; import discourseComputed, { @@ -169,30 +169,23 @@ export default Component.extend({ }, @discourseComputed( + "timeShortcuts", "hiddenOptions", - "customOptions", "customLabels", "userTimezone" ) - options(hiddenOptions, customOptions, customLabels, userTimezone) { + options(timeShortcuts, hiddenOptions, customLabels, userTimezone) { this._loadLastUsedCustomDatetime(); - let options = defaultShortcutOptions(userTimezone); + let options; + if (timeShortcuts && timeShortcuts.length) { + options = timeShortcuts; + } else { + options = defaultTimeShortcuts(userTimezone); + } this._hideDynamicOptions(options); - options = options.concat(customOptions); - - options.sort((a, b) => { - if (a.time < b.time) { - return -1; - } - if (a.time > b.time) { - return 1; - } - return 0; - }); let specialOptions = specialShortcutOptions(); - if (this.lastCustomDate && this.lastCustomTime) { let lastCustom = specialOptions.findBy( "id", @@ -202,7 +195,6 @@ export default Component.extend({ lastCustom.timeFormatKey = "dates.long_no_year"; lastCustom.hidden = false; } - options = options.concat(specialOptions); if (hiddenOptions.length > 0) { diff --git a/app/assets/javascripts/discourse/app/components/topic-entrance.js b/app/assets/javascripts/discourse/app/components/topic-entrance.js index 2b73f4de6e..6cfe3d4fb6 100644 --- a/app/assets/javascripts/discourse/app/components/topic-entrance.js +++ b/app/assets/javascripts/discourse/app/components/topic-entrance.js @@ -2,7 +2,7 @@ import CleansUp from "discourse/mixins/cleans-up"; import Component from "@ember/component"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; -import discourseComputed from "discourse-common/utils/decorators"; +import discourseComputed, { bind } from "discourse-common/utils/decorators"; import { scheduleOnce } from "@ember/runloop"; function entranceDate(dt, showTime) { @@ -31,9 +31,11 @@ function entranceDate(dt, showTime) { export default Component.extend(CleansUp, { elementId: "topic-entrance", classNameBindings: ["visible::hidden"], - _position: null, topic: null, visible: null, + _position: null, + _originalActiveElement: null, + _activeButton: null, @discourseComputed("topic.created_at") createdDate: (createdAt) => new Date(createdAt), @@ -74,12 +76,64 @@ export default Component.extend(CleansUp, { $self.css(pos); }, + @bind + _escListener(e) { + if (e.key === "Escape") { + this.cleanUp(); + } else if (e.key === "Tab") { + if (this._activeButton === "top") { + this._jumpBottomButton().focus(); + this._activeButton = "bottom"; + e.preventDefault(); + } else if (this._activeButton === "bottom") { + this._jumpTopButton().focus(); + this._activeButton = "top"; + e.preventDefault(); + } + } + }, + + _jumpTopButton() { + return this.element.querySelector(".jump-top"); + }, + + _jumpBottomButton() { + return this.element.querySelector(".jump-bottom"); + }, + + _setupEscListener() { + document.body.addEventListener("keydown", this._escListener); + }, + + _removeEscListener() { + document.body.removeEventListener("keydown", this._escListener); + }, + + _trapFocus() { + this._originalActiveElement = document.activeElement; + this._jumpTopButton().focus(); + this._activeButton = "top"; + }, + + _releaseFocus() { + if (this._originalActiveElement) { + this._originalActiveElement.focus(); + this._originalActiveElement = null; + } + }, + + _applyDomChanges() { + this._setCSS(); + this._setupEscListener(); + this._trapFocus(); + }, + _show(data) { this._position = data.position; this.setProperties({ topic: data.topic, visible: true }); - scheduleOnce("afterRender", this, this._setCSS); + scheduleOnce("afterRender", this, this._applyDomChanges); $("html") .off("mousedown.topic-entrance") @@ -98,6 +152,8 @@ export default Component.extend(CleansUp, { cleanUp() { this.setProperties({ topic: null, visible: false }); $("html").off("mousedown.topic-entrance"); + this._removeEscListener(); + this._releaseFocus(); }, willDestroyElement() { diff --git a/app/assets/javascripts/discourse/app/components/topic-list-item.js b/app/assets/javascripts/discourse/app/components/topic-list-item.js index 7e3a46dec8..4bdbd7de4b 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list-item.js +++ b/app/assets/javascripts/discourse/app/components/topic-list-item.js @@ -1,4 +1,7 @@ -import discourseComputed, { observes } from "discourse-common/utils/decorators"; +import discourseComputed, { + bind, + observes, +} from "discourse-common/utils/decorators"; import Component from "@ember/component"; import DiscourseURL from "discourse/lib/url"; import I18n from "I18n"; @@ -58,6 +61,13 @@ export default Component.extend({ if (this.selected && this.selected.includes(this.topic)) { this.element.querySelector("input.bulk-select").checked = true; } + if (this._shouldFocusLastVisited()) { + const title = this._titleElement(); + if (title) { + title.addEventListener("focus", this._onTitleFocus); + title.addEventListener("blur", this._onTitleBlur); + } + } }); } }, @@ -98,6 +108,13 @@ export default Component.extend({ if (this.includeUnreadIndicator) { this.messageBus.unsubscribe(this.unreadIndicatorChannel); } + if (this._shouldFocusLastVisited()) { + const title = this._titleElement(); + if (title) { + title.removeEventListener("focus", this._onTitleFocus); + title.removeEventListener("blur", this._onTitleBlur); + } + } }, @discourseComputed("topic.id") @@ -259,12 +276,17 @@ export default Component.extend({ return; } - const $topic = $(this.element); - $topic - .addClass("highlighted") - .attr("data-islastviewedtopic", opts.isLastViewedTopic); - - $topic.on("animationend", () => $topic.removeClass("highlighted")); + this.element.classList.add("highlighted"); + this.element.setAttribute( + "data-islastviewedtopic", + opts.isLastViewedTopic + ); + this.element.addEventListener("animationend", () => { + this.element.classList.remove("highlighted"); + }); + if (opts.isLastViewedTopic && this._shouldFocusLastVisited()) { + this._titleElement()?.focus(); + } }); }, @@ -279,4 +301,30 @@ export default Component.extend({ this.highlight(); } }), + + @bind + _onTitleFocus() { + if (this.element && !this.isDestroying && !this.isDestroyed) { + this._mainLinkElement().classList.add("focused"); + } + }, + + @bind + _onTitleBlur() { + if (this.element && !this.isDestroying && !this.isDestroyed) { + this._mainLinkElement().classList.remove("focused"); + } + }, + + _shouldFocusLastVisited() { + return !this.site.mobileView && this.focusLastVisitedTopic; + }, + + _mainLinkElement() { + return this.element.querySelector(".main-link"); + }, + + _titleElement() { + return this.element.querySelector(".main-link .title"); + }, }); diff --git a/app/assets/javascripts/discourse/app/components/topic-navigation.js b/app/assets/javascripts/discourse/app/components/topic-navigation.js index 9c2b2ef70a..a08444022c 100644 --- a/app/assets/javascripts/discourse/app/components/topic-navigation.js +++ b/app/assets/javascripts/discourse/app/components/topic-navigation.js @@ -45,6 +45,11 @@ export default Component.extend(PanEvents, { let info = this.info; + // Safari's window.innerWidth doesn't match CSS media queries + let windowWidth = this.capabilities.isSafari + ? document.documentElement.clientWidth + : window.innerWidth; + if (info.get("topicProgressExpanded")) { info.set("renderTimeline", true); } else { @@ -55,7 +60,7 @@ export default Component.extend(PanEvents, { if (composer) { renderTimeline = - window.innerWidth > MIN_WIDTH_TIMELINE && + windowWidth > MIN_WIDTH_TIMELINE && window.innerHeight - composer.offsetHeight - headerOffset() > MIN_HEIGHT_TIMELINE; } diff --git a/app/assets/javascripts/discourse/app/components/uppy-image-uploader.js b/app/assets/javascripts/discourse/app/components/uppy-image-uploader.js index 1b794cd5c4..02fd2b93fa 100644 --- a/app/assets/javascripts/discourse/app/components/uppy-image-uploader.js +++ b/app/assets/javascripts/discourse/app/components/uppy-image-uploader.js @@ -50,6 +50,12 @@ export default Component.extend(UppyUploadMixin, { return { imagesOnly: true }; }, + _uppyReady() { + this._onPreProcessComplete(() => { + this.set("processing", false); + }); + }, + uploadDone(upload) { this.setProperties({ imageFilesize: upload.human_filesize, diff --git a/app/assets/javascripts/discourse/app/components/user-info.js b/app/assets/javascripts/discourse/app/components/user-info.js index 024825ed78..dc9cf2e86d 100644 --- a/app/assets/javascripts/discourse/app/components/user-info.js +++ b/app/assets/javascripts/discourse/app/components/user-info.js @@ -13,6 +13,8 @@ export default Component.extend({ attributeBindings: ["data-username"], size: "small", "data-username": alias("user.username"), + includeLink: true, + includeAvatar: true, @discourseComputed("user.username") userPath(username) { diff --git a/app/assets/javascripts/discourse/app/controllers/bookmark.js b/app/assets/javascripts/discourse/app/controllers/bookmark.js index 6fce0c15ea..0d906bbf90 100644 --- a/app/assets/javascripts/discourse/app/controllers/bookmark.js +++ b/app/assets/javascripts/discourse/app/controllers/bookmark.js @@ -1,4 +1,5 @@ import Controller from "@ember/controller"; +import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { action } from "@ember/object"; import { Promise } from "rsvp"; @@ -10,28 +11,53 @@ export function openBookmarkModal( onCloseWithoutSaving: null, onAfterSave: null, onAfterDelete: null, + }, + options = { + use_polymorphic_bookmarks: false, } ) { return new Promise((resolve) => { const modalTitle = () => { - if (bookmark.for_topic) { - return bookmark.id - ? "post.bookmarks.edit_for_topic" - : "post.bookmarks.create_for_topic"; + if (options.use_polymorphic_bookmarks) { + return I18n.t( + bookmark.id ? "bookmarks.edit_for" : "bookmarks.create_for", + { + type: bookmark.bookmarkable_type, + } + ); + } else if (bookmark.for_topic) { + return I18n.t( + bookmark.id + ? "post.bookmarks.edit_for_topic" + : "post.bookmarks.create_for_topic" + ); + } else { + return I18n.t( + bookmark.id ? "post.bookmarks.edit" : "post.bookmarks.create" + ); } - return bookmark.id ? "post.bookmarks.edit" : "post.bookmarks.create"; }; + + const model = { + id: bookmark.id, + reminderAt: bookmark.reminder_at, + autoDeletePreference: bookmark.auto_delete_preference, + name: bookmark.name, + }; + + if (options.use_polymorphic_bookmarks) { + model.bookmarkableId = bookmark.bookmarkable_id; + model.bookmarkableType = bookmark.bookmarkable_type; + } else { + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. + model.postId = bookmark.post_id; + model.topicId = bookmark.topic_id; + model.forTopic = bookmark.for_topic; + } + let modalController = showModal("bookmark", { - model: { - postId: bookmark.post_id, - topicId: bookmark.topic_id, - id: bookmark.id, - reminderAt: bookmark.reminder_at, - autoDeletePreference: bookmark.auto_delete_preference, - name: bookmark.name, - forTopic: bookmark.for_topic, - }, - title: modalTitle(), + model, + titleTranslated: modalTitle(), modalClass: "bookmark-with-reminder", }); modalController.setProperties({ diff --git a/app/assets/javascripts/discourse/app/controllers/composer.js b/app/assets/javascripts/discourse/app/controllers/composer.js index 3af5345297..c57d70fa75 100644 --- a/app/assets/javascripts/discourse/app/controllers/composer.js +++ b/app/assets/javascripts/discourse/app/controllers/composer.js @@ -1331,6 +1331,7 @@ export default Controller.extend({ this.close(); }) .finally(() => { + this.appEvents.trigger("composer:cancelled"); resolve(); }); }, @@ -1338,6 +1339,7 @@ export default Controller.extend({ this._saveDraft(); this.model.clearState(); this.close(); + this.appEvents.trigger("composer:cancelled"); resolve(); }, // needed to resume saving drafts if composer stays open @@ -1351,6 +1353,7 @@ export default Controller.extend({ this.close(); }) .finally(() => { + this.appEvents.trigger("composer:cancelled"); resolve(); }); } @@ -1432,17 +1435,12 @@ export default Controller.extend({ tagValidation(category, tags, lastValidatedAt) { const tagsArray = tags || []; if (this.site.can_tag_topics && !this.currentUser.staff && category) { - if ( - category.minimum_required_tags > tagsArray.length || - (category.required_tag_groups && - category.min_tags_from_required_group > tagsArray.length) - ) { + // category.minimumRequiredTags incorporates both minimum_required_tags, and required_tag_groups + if (category.minimumRequiredTags > tagsArray.length) { return EmberObject.create({ failed: true, reason: I18n.t("composer.error.tags_missing", { - count: - category.minimum_required_tags || - category.min_tags_from_required_group, + count: category.minimumRequiredTags, }), lastShownAt: lastValidatedAt, }); diff --git a/app/assets/javascripts/discourse/app/controllers/full-page-search.js b/app/assets/javascripts/discourse/app/controllers/full-page-search.js index c364cb3f81..65922b664a 100644 --- a/app/assets/javascripts/discourse/app/controllers/full-page-search.js +++ b/app/assets/javascripts/discourse/app/controllers/full-page-search.js @@ -3,6 +3,7 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators"; import { getSearchKey, isValidSearchTerm, + logSearchLinkClick, searchContextDescription, translateResults, updateRecentSearches, @@ -470,15 +471,10 @@ export default Controller.extend({ logClick(topicId) { if (this.get("model.grouped_search_result.search_log_id") && topicId) { - ajax("/search/click", { - type: "POST", - data: { - search_log_id: this.get( - "model.grouped_search_result.search_log_id" - ), - search_result_id: topicId, - search_result_type: "topic", - }, + logSearchLinkClick({ + searchLogId: this.get("model.grouped_search_result.search_log_id"), + searchResultId: topicId, + searchResultType: "topic", }); } }, diff --git a/app/assets/javascripts/discourse/app/controllers/grant-admin-second-factor.js b/app/assets/javascripts/discourse/app/controllers/grant-admin-second-factor.js deleted file mode 100644 index 62c000b473..0000000000 --- a/app/assets/javascripts/discourse/app/controllers/grant-admin-second-factor.js +++ /dev/null @@ -1,84 +0,0 @@ -import Controller from "@ember/controller"; -import { action } from "@ember/object"; -import discourseComputed from "discourse-common/utils/decorators"; -import { getWebauthnCredential } from "discourse/lib/webauthn"; -import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { SECOND_FACTOR_METHODS } from "discourse/models/user"; -import I18n from "I18n"; -import bootbox from "bootbox"; - -export default Controller.extend(ModalFunctionality, { - showSecondFactor: false, - secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, - secondFactorToken: null, - securityKeyCredential: null, - - inProgress: false, - - onShow() { - this.setProperties({ - showSecondFactor: false, - secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, - secondFactorToken: null, - securityKeyCredential: null, - }); - }, - - @discourseComputed("inProgress", "securityKeyCredential", "secondFactorToken") - disabled(inProgress, securityKeyCredential, secondFactorToken) { - return inProgress || (!securityKeyCredential && !secondFactorToken); - }, - - setResult(result) { - this.setProperties({ - otherMethodAllowed: result.multiple_second_factor_methods, - secondFactorRequired: true, - showLoginButtons: false, - backupEnabled: result.backup_enabled, - showSecondFactor: result.totp_enabled, - showSecurityKey: result.security_key_enabled, - secondFactorMethod: result.security_key_enabled - ? SECOND_FACTOR_METHODS.SECURITY_KEY - : SECOND_FACTOR_METHODS.TOTP, - securityKeyChallenge: result.challenge, - securityKeyAllowedCredentialIds: result.allowed_credential_ids, - }); - }, - - @action - authenticateSecurityKey() { - getWebauthnCredential( - this.securityKeyChallenge, - this.securityKeyAllowedCredentialIds, - (credentialData) => { - this.set("securityKeyCredential", credentialData); - this.send("authenticate"); - }, - (errorMessage) => { - this.flash(errorMessage, "error"); - } - ); - }, - - @action - authenticate() { - this.set("inProgress", true); - this.model - .grantAdmin({ - second_factor_token: - this.securityKeyCredential || this.secondFactorToken, - second_factor_method: this.secondFactorMethod, - timezone: moment.tz.guess(), - }) - .then((result) => { - if (result.success) { - this.send("closeModal"); - bootbox.alert(I18n.t("admin.user.grant_admin_success")); - } else { - this.flash(result.error, "error"); - this.setResult(result); - } - }) - .finally(() => this.set("inProgress", false)); - }, -}); diff --git a/app/assets/javascripts/discourse/app/controllers/ignore-duration.js b/app/assets/javascripts/discourse/app/controllers/ignore-duration.js index 929581be2e..77e77f3e95 100644 --- a/app/assets/javascripts/discourse/app/controllers/ignore-duration.js +++ b/app/assets/javascripts/discourse/app/controllers/ignore-duration.js @@ -18,7 +18,7 @@ export default Controller.extend(ModalFunctionality, { this.set("loading", true); this.model .updateNotificationLevel({ - level: "ignored", + level: "ignore", expiringAt: this.ignoredUntil, }) .then(() => { diff --git a/app/assets/javascripts/discourse/app/controllers/invites-show.js b/app/assets/javascripts/discourse/app/controllers/invites-show.js index 4a6307d75c..7079a5ea2c 100644 --- a/app/assets/javascripts/discourse/app/controllers/invites-show.js +++ b/app/assets/javascripts/discourse/app/controllers/invites-show.js @@ -1,4 +1,4 @@ -import { alias, notEmpty, or, readOnly } from "@ember/object/computed"; +import { alias, not, or, readOnly } from "@ember/object/computed"; import Controller, { inject as controller } from "@ember/controller"; import DiscourseURL from "discourse/lib/url"; import EmberObject from "@ember/object"; @@ -33,7 +33,7 @@ export default Controller.extend( emailVerifiedByLink: alias("model.email_verified_by_link"), differentExternalEmail: alias("model.different_external_email"), accountUsername: alias("model.username"), - passwordRequired: notEmpty("accountPassword"), + passwordRequired: not("externalAuthsOnly"), successMessage: null, errorMessage: null, userFields: null, diff --git a/app/assets/javascripts/discourse/app/controllers/review-index.js b/app/assets/javascripts/discourse/app/controllers/review-index.js index 4c19441594..6c2c9a84b1 100644 --- a/app/assets/javascripts/discourse/app/controllers/review-index.js +++ b/app/assets/javascripts/discourse/app/controllers/review-index.js @@ -111,7 +111,7 @@ export default Controller.extend({ if (newList.length === 0) { this.refreshModel(); } else { - this.set("reviewables", newList); + this.reviewables.setObjects(newList); } }, diff --git a/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js b/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js index 996e5f06ff..4de8d672c3 100644 --- a/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js +++ b/app/assets/javascripts/discourse/app/controllers/second-factor-auth.js @@ -194,7 +194,11 @@ export default Controller.extend({ type: response.callback_method, data: { second_factor_nonce: this.nonce }, }) - .then(() => DiscourseURL.routeTo(response.redirect_path)) + .then((callbackResponse) => { + const redirectUrl = + callbackResponse.redirect_url || response.redirect_url; + DiscourseURL.routeTo(redirectUrl); + }) .catch((error) => this.displayError(extractError(error))); }) .catch((error) => { diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index 1398450d05..9e0b113774 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -754,6 +754,7 @@ export default Controller.extend(bufferedProperty("model"), { } }, + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. toggleBookmark(post) { if (!this.currentUser) { return bootbox.alert(I18n.t("bookmarks.not_bookmarked")); @@ -784,6 +785,39 @@ export default Controller.extend(bufferedProperty("model"), { } }, + toggleBookmarkPolymorphic(post) { + if (!this.currentUser) { + return bootbox.alert(I18n.t("bookmarks.not_bookmarked")); + } else if (post) { + const bookmarkForPost = this.model.bookmarks.find( + (bookmark) => + bookmark.bookmarkable_id === post.id && + bookmark.bookmarkable_type === "Post" + ); + return this._modifyPostBookmark( + bookmarkForPost || + Bookmark.create({ + bookmarkable_id: post.id, + bookmarkable_type: "Post", + auto_delete_preference: this.currentUser + .bookmark_auto_delete_preference, + }), + post + ); + } else { + return this._toggleTopicLevelBookmarkPolymorphic().then( + (changedIds) => { + if (!changedIds) { + return; + } + changedIds.forEach((id) => + this.appEvents.trigger("post-stream:refresh", { id }) + ); + } + ); + } + }, + jumpToIndex(index) { this._jumpToIndex(index); }, @@ -1238,46 +1272,56 @@ export default Controller.extend(bufferedProperty("model"), { }, _modifyTopicBookmark(bookmark) { - return openBookmarkModal(bookmark, { - onAfterSave: (savedData) => { - this._syncBookmarks(savedData); - this.model.set("bookmarking", false); - this.model.set("bookmarked", true); - this.model.incrementProperty("bookmarksWereChanged"); - this.appEvents.trigger( - "bookmarks:changed", - savedData, - bookmark.attachedTo() - ); + return openBookmarkModal( + bookmark, + { + onAfterSave: (savedData) => { + this._syncBookmarks(savedData); + this.model.set("bookmarking", false); + this.model.set("bookmarked", true); + this.model.incrementProperty("bookmarksWereChanged"); + this.appEvents.trigger( + "bookmarks:changed", + savedData, + bookmark.attachedTo() + ); - // TODO (martin) (2022-02-01) Remove these old bookmark events, replaced by bookmarks:changed. - this.appEvents.trigger("topic:bookmark-toggled"); + // TODO (martin) (2022-02-01) Remove these old bookmark events, replaced by bookmarks:changed. + this.appEvents.trigger("topic:bookmark-toggled"); + }, + onAfterDelete: (topicBookmarked, bookmarkId) => { + this.model.removeBookmark(bookmarkId); + }, }, - onAfterDelete: (topicBookmarked, bookmarkId) => { - this.model.removeBookmark(bookmarkId); - }, - }); + { use_polymorphic_bookmarks: this.siteSettings.use_polymorphic_bookmarks } + ); }, _modifyPostBookmark(bookmark, post) { - return openBookmarkModal(bookmark, { - onCloseWithoutSaving: () => { - post.appEvents.trigger("post-stream:refresh", { - id: bookmark.post_id, - }); + return openBookmarkModal( + bookmark, + { + onCloseWithoutSaving: () => { + post.appEvents.trigger("post-stream:refresh", { + id: this.siteSettings.use_polymorphic_bookmarks + ? bookmark.bookmarkable_id + : bookmark.post_id, + }); + }, + onAfterSave: (savedData) => { + this._syncBookmarks(savedData); + this.model.set("bookmarking", false); + post.createBookmark(savedData); + this.model.afterPostBookmarked(post, savedData); + return [post.id]; + }, + onAfterDelete: (topicBookmarked, bookmarkId) => { + this.model.removeBookmark(bookmarkId); + post.deleteBookmark(topicBookmarked); + }, }, - onAfterSave: (savedData) => { - this._syncBookmarks(savedData); - this.model.set("bookmarking", false); - post.createBookmark(savedData); - this.model.afterPostBookmarked(post, savedData); - return [post.id]; - }, - onAfterDelete: (topicBookmarked, bookmarkId) => { - this.model.removeBookmark(bookmarkId); - post.deleteBookmark(topicBookmarked); - }, - }); + { use_polymorphic_bookmarks: this.siteSettings.use_polymorphic_bookmarks } + ); }, _syncBookmarks(data) { @@ -1295,6 +1339,7 @@ export default Controller.extend(bufferedProperty("model"), { } }, + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. async _toggleTopicLevelBookmark() { if (this.model.bookmarking) { return Promise.resolve(); @@ -1329,6 +1374,41 @@ export default Controller.extend(bufferedProperty("model"), { } }, + async _toggleTopicLevelBookmarkPolymorphic() { + if (this.model.bookmarking) { + return Promise.resolve(); + } + + if (this.model.bookmarkCount > 1) { + return this._maybeClearAllBookmarks(); + } + + if (this.model.bookmarkCount === 1) { + const topicBookmark = this.model.bookmarks.findBy( + "bookmarkable_type", + "Topic" + ); + if (topicBookmark) { + return this._modifyTopicBookmark(topicBookmark); + } else { + const bookmark = this.model.bookmarks[0]; + const post = await this.model.postById(bookmark.bookmarkable_id); + return this._modifyPostBookmark(bookmark, post); + } + } + + if (this.model.bookmarkCount === 0) { + return this._modifyTopicBookmark( + Bookmark.create({ + bookmarkable_id: this.model.id, + bookmarkable_type: "Topic", + auto_delete_preference: this.currentUser + .bookmark_auto_delete_preference, + }) + ); + } + }, + _maybeClearAllBookmarks() { return new Promise((resolve) => { bootbox.confirm( diff --git a/app/assets/javascripts/discourse/app/controllers/user.js b/app/assets/javascripts/discourse/app/controllers/user.js index 49199aafd7..2efaf9724c 100644 --- a/app/assets/javascripts/discourse/app/controllers/user.js +++ b/app/assets/javascripts/discourse/app/controllers/user.js @@ -116,9 +116,9 @@ export default Controller.extend(CanCheckEmails, { ); }, - @discourseComputed("viewingSelf", "currentUser.staff") - showNotificationsTab(viewingSelf, staff) { - return viewingSelf || staff; + @discourseComputed("viewingSelf", "currentUser.admin") + showNotificationsTab(viewingSelf, isAdmin) { + return viewingSelf || isAdmin; }, @discourseComputed("model.name") @@ -168,14 +168,19 @@ export default Controller.extend(CanCheckEmails, { "currentUser.ignored_ids", "model.ignored", "model.muted", - function () { - if (this.get("model.ignored")) { - return "changeToIgnored"; - } else if (this.get("model.muted")) { - return "changeToMuted"; - } else { - return "changeToNormal"; - } + { + get() { + if (this.get("model.ignored")) { + return "changeToIgnored"; + } else if (this.get("model.muted")) { + return "changeToMuted"; + } else { + return "changeToNormal"; + } + }, + set(key, value) { + return value; + }, } ), @@ -249,8 +254,8 @@ export default Controller.extend(CanCheckEmails, { bootbox.dialog(message, buttons, { classes: "delete-user-modal" }); }, - updateNotificationLevel(level) { - return this.model.updateNotificationLevel({ level }); + updateNotificationLevel(params) { + return this.model.updateNotificationLevel(params); }, }, }); diff --git a/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js b/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js new file mode 100644 index 0000000000..448cf41a44 --- /dev/null +++ b/app/assets/javascripts/discourse/app/initializers/image-aspect-ratio.js @@ -0,0 +1,60 @@ +import { withPluginApi } from "discourse/lib/plugin-api"; + +// Browsers automatically calculate an aspect ratio based on the width/height attributes of an ` { + element.querySelectorAll("img").forEach((img) => { + const declaredHeight = parseFloat(img.getAttribute("height")); + const declaredWidth = parseFloat(img.getAttribute("width")); + + if ( + isNaN(declaredHeight) || + isNaN(declaredWidth) || + img.style.aspectRatio + ) { + return; + } + + if (supportsAspectRatio) { + img.style.setProperty( + "aspect-ratio", + `${declaredWidth} / ${declaredHeight}` + ); + } else { + // For older browsers (e.g. iOS < 15), we need to apply the aspect ratio manually. + // It's not perfect, because it won't recompute on browser resize. + // This property is consumed in `topic-post.scss` for responsive images only. + // It's a no-op for non-responsive images. + const calculatedHeight = + img.width / (declaredWidth / declaredHeight); + + img.style.setProperty( + "--calculated-height", + `${calculatedHeight}px` + ); + } + }); + }, + { id: "image-aspect-ratio" } + ); + }, + + initialize() { + withPluginApi("1.2.0", this.initWithApi); + }, +}; diff --git a/app/assets/javascripts/discourse/app/initializers/strip-mobile-app-url-params.js b/app/assets/javascripts/discourse/app/initializers/strip-mobile-app-url-params.js index 07d3f02db8..0ad6bc5c33 100644 --- a/app/assets/javascripts/discourse/app/initializers/strip-mobile-app-url-params.js +++ b/app/assets/javascripts/discourse/app/initializers/strip-mobile-app-url-params.js @@ -6,7 +6,7 @@ export default { if (queryStrings.indexOf("user_api_public_key") !== -1) { let params = queryStrings.startsWith("?") - ? queryStrings.substr(1).split("&") + ? queryStrings.slice(1).split("&") : []; params = params.filter((param) => { diff --git a/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js b/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js index b8592c13eb..b1f330c5c7 100644 --- a/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js +++ b/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js @@ -11,7 +11,8 @@ const DEFER_PRIORITY = 500; export default { name: "topic-footer-buttons", - initialize() { + initialize(container) { + const siteSettings = container.lookup("site-settings:main"); registerTopicFooterButton({ id: "share-and-invite", icon: "d-topic-share", @@ -98,9 +99,14 @@ export default { if (this.topic.bookmarkCount === 0) { return I18n.t("bookmarked.help.bookmark"); } else if (this.topic.bookmarkCount === 1) { - if ( - this.topic.bookmarks.filter((bookmark) => bookmark.for_topic).length - ) { + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. + const anyTopicBookmarks = this.topic.bookmarks.some((bookmark) => { + return siteSettings.use_polymorphic_bookmarks + ? bookmark.for_topic + : bookmark.bookmarkable_type === "Topic"; + }); + + if (anyTopicBookmarks) { return I18n.t("bookmarked.help.edit_bookmark_for_topic"); } else { return I18n.t("bookmarked.help.edit_bookmark"); @@ -113,7 +119,10 @@ export default { return I18n.t("bookmarked.help.unbookmark"); } }, - action: "toggleBookmark", + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. + action: siteSettings.use_polymorphic_bookmarks + ? "toggleBookmarkPolymorphic" + : "toggleBookmark", dropdown() { return this.site.mobileView; }, diff --git a/app/assets/javascripts/discourse/app/lib/ajax.js b/app/assets/javascripts/discourse/app/lib/ajax.js index d3591a5ef9..4ec61986c9 100644 --- a/app/assets/javascripts/discourse/app/lib/ajax.js +++ b/app/assets/javascripts/discourse/app/lib/ajax.js @@ -29,14 +29,9 @@ export function handleLogoff(xhr) { } } -function handleRedirect(data) { - if ( - data && - data.getResponseHeader && - data.getResponseHeader("Discourse-Xhr-Redirect") - ) { - window.location.replace(data.responseText); - window.location.reload(); +function handleRedirect(xhr) { + if (xhr && xhr.getResponseHeader("Discourse-Xhr-Redirect")) { + window.location = xhr.responseText; } } @@ -99,7 +94,7 @@ export function ajax() { } args.success = (data, textStatus, xhr) => { - handleRedirect(data); + handleRedirect(xhr); handleLogoff(xhr); run(() => { diff --git a/app/assets/javascripts/discourse/app/lib/formatter.js b/app/assets/javascripts/discourse/app/lib/formatter.js index 3a57516b18..b8d8e7509f 100644 --- a/app/assets/javascripts/discourse/app/lib/formatter.js +++ b/app/assets/javascripts/discourse/app/lib/formatter.js @@ -25,7 +25,7 @@ export function tinyDateYear(date) { // TODO: locale support ? export function toTitleCase(str) { return str.replace(/\w\S*/g, function (txt) { - return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase(); }); } diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js index ab11910b45..0e986f53ca 100644 --- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js @@ -772,7 +772,7 @@ export default { }, _onScrollEndsCallback() { - document.querySelector(".topic-post.selected a.tabLoc")?.focus(); + document.querySelector(".topic-post.selected span.tabLoc")?.focus(); }, categoriesTopicsList() { diff --git a/app/assets/javascripts/discourse/app/lib/link-hashtags.js b/app/assets/javascripts/discourse/app/lib/link-hashtags.js index da3391d5d4..9d4201b3c1 100644 --- a/app/assets/javascripts/discourse/app/lib/link-hashtags.js +++ b/app/assets/javascripts/discourse/app/lib/link-hashtags.js @@ -22,13 +22,13 @@ export function linkSeenHashtags(elem) { if (hashtags.length === 0) { return []; } - const slugs = [...hashtags.map((hashtag) => hashtag.innerText.substr(1))]; + const slugs = [...hashtags.map((hashtag) => hashtag.innerText.slice(1))]; hashtags.forEach((hashtag, index) => { let slug = slugs[index]; const hasTagSuffix = slug.endsWith(TAG_HASHTAG_POSTFIX); if (hasTagSuffix) { - slug = slug.substr(0, slug.length - TAG_HASHTAG_POSTFIX.length); + slug = slug.slice(0, slug.length - TAG_HASHTAG_POSTFIX.length); } const lowerSlug = slug.toLowerCase(); diff --git a/app/assets/javascripts/discourse/app/lib/link-mentions.js b/app/assets/javascripts/discourse/app/lib/link-mentions.js index e72575fadd..5d1cf91889 100644 --- a/app/assets/javascripts/discourse/app/lib/link-mentions.js +++ b/app/assets/javascripts/discourse/app/lib/link-mentions.js @@ -81,7 +81,7 @@ export function linkSeenMentions(elem, siteSettings) { ...elem.querySelectorAll("span.mention:not(.mention-tested)"), ]; if (mentions.length) { - const usernames = mentions.map((m) => m.innerText.substr(1)); + const usernames = mentions.map((m) => m.innerText.slice(1)); updateFound(mentions, usernames); return usernames .uniq() diff --git a/app/assets/javascripts/discourse/app/lib/local-dates.js b/app/assets/javascripts/discourse/app/lib/local-dates.js new file mode 100644 index 0000000000..65ec52d322 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/local-dates.js @@ -0,0 +1,11 @@ +export function applyLocalDates(dates, siteSettings) { + if (!siteSettings.discourse_local_dates_enabled) { + return; + } + + const _applyLocalDates = requirejs( + "discourse/plugins/discourse-local-dates/initializers/discourse-local-dates" + ).applyLocalDates; + + _applyLocalDates(dates, siteSettings); +} diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 8375b4f337..74aff2737d 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -84,6 +84,7 @@ import { registerTopicFooterDropdown } from "discourse/lib/register-topic-footer import { registerDesktopNotificationHandler } from "discourse/lib/desktop-notifications"; import { replaceFormatter } from "discourse/lib/utilities"; import { replaceTagRenderer } from "discourse/lib/render-tag"; +import { registerCustomLastUnreadUrlCallback } from "discourse/models/topic"; import { setNewCategoryDefaultColors } from "discourse/routes/new-category"; import { addSearchResultsCallback } from "discourse/lib/search"; import { @@ -98,7 +99,7 @@ import { consolePrefix } from "discourse/lib/source-identifier"; // based on Semantic Versioning 2.0.0. Please update the changelog at // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // using the format described at https://keepachangelog.com/en/1.0.0/. -const PLUGIN_API_VERSION = "1.1.0"; +const PLUGIN_API_VERSION = "1.2.0"; // This helper prevents us from applying the same `modifyClass` over and over in test mode. function canModify(klass, type, resolverName, changes) { @@ -1290,6 +1291,21 @@ class PluginApi { replaceTagRenderer(fn); } + /** + * Register a custom last unread url for a topic list item. + * If a non-null value is returned, it will be used right away. + * + * Example: + * + * function testLastUnreadUrl(context) { + * return context.urlForPostNumber(1); + * } + * api.registerCustomLastUnreadUrlCallback(testLastUnreadUrl); + **/ + registerCustomLastUnreadUrlCallback(fn) { + registerCustomLastUnreadUrlCallback(fn); + } + /** * Registers custom languages for use with HighlightJS. * diff --git a/app/assets/javascripts/discourse/app/lib/search.js b/app/assets/javascripts/discourse/app/lib/search.js index 6df0545ede..40352647ed 100644 --- a/app/assets/javascripts/discourse/app/lib/search.js +++ b/app/assets/javascripts/discourse/app/lib/search.js @@ -244,3 +244,14 @@ export function updateRecentSearches(currentUser, term) { recentSearches.unshiftObject(term); currentUser.set("recent_searches", recentSearches); } + +export function logSearchLinkClick(params) { + ajax("/search/click", { + type: "POST", + data: { + search_log_id: params.searchLogId, + search_result_id: params.searchResultId, + search_result_type: params.searchResultType, + }, + }); +} diff --git a/app/assets/javascripts/discourse/app/lib/static-route-builder.js b/app/assets/javascripts/discourse/app/lib/static-route-builder.js index 200714b4f3..bf5862a5db 100644 --- a/app/assets/javascripts/discourse/app/lib/static-route-builder.js +++ b/app/assets/javascripts/discourse/app/lib/static-route-builder.js @@ -25,7 +25,7 @@ export default function (page) { activate() { this._super(...arguments); - jumpToElement(document.location.hash.substr(1)); + jumpToElement(document.location.hash.slice(1)); }, model() { diff --git a/app/assets/javascripts/discourse/app/lib/text.js b/app/assets/javascripts/discourse/app/lib/text.js index 635a5c44b2..24d97d6a94 100644 --- a/app/assets/javascripts/discourse/app/lib/text.js +++ b/app/assets/javascripts/discourse/app/lib/text.js @@ -142,7 +142,7 @@ export function excerpt(cooked, length) { if (element.nodeType === Node.TEXT_NODE) { if (resultLength + element.textContent.length > length) { - const text = element.textContent.substr(0, length - resultLength); + const text = element.textContent.slice(0, length - resultLength); result += encode(text); result += "…"; resultLength += text.length; diff --git a/app/assets/javascripts/discourse/app/lib/time-shortcut.js b/app/assets/javascripts/discourse/app/lib/time-shortcut.js index 37d6585cdf..5d59365443 100644 --- a/app/assets/javascripts/discourse/app/lib/time-shortcut.js +++ b/app/assets/javascripts/discourse/app/lib/time-shortcut.js @@ -6,8 +6,10 @@ import { nextBusinessWeekStart, nextMonth, now, + sixMonths, thisWeekend, tomorrow, + twoWeeks, } from "discourse/lib/time-utils"; export const TIME_SHORTCUT_TYPES = { @@ -24,54 +26,15 @@ export const TIME_SHORTCUT_TYPES = { POST_LOCAL_DATE: "post_local_date", }; -export function defaultShortcutOptions(timezone) { +export function defaultTimeShortcuts(timezone) { + const shortcuts = timeShortcuts(timezone); return [ - { - icon: "angle-right", - id: TIME_SHORTCUT_TYPES.LATER_TODAY, - label: "time_shortcut.later_today", - time: laterToday(timezone), - timeFormatKey: "dates.time", - }, - { - icon: "far-sun", - id: TIME_SHORTCUT_TYPES.TOMORROW, - label: "time_shortcut.tomorrow", - time: tomorrow(timezone), - timeFormatKey: "dates.time_short_day", - }, - { - icon: "angle-double-right", - id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK, - label: "time_shortcut.later_this_week", - time: laterThisWeek(timezone), - timeFormatKey: "dates.time_short_day", - }, - { - icon: "bed", - id: TIME_SHORTCUT_TYPES.THIS_WEEKEND, - label: "time_shortcut.this_weekend", - time: thisWeekend(timezone), - timeFormatKey: "dates.time_short_day", - }, - { - icon: "briefcase", - id: TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK, - label: - now(timezone).day() === MOMENT_MONDAY || - now(timezone).day() === MOMENT_SUNDAY - ? "time_shortcut.start_of_next_business_week_alt" - : "time_shortcut.start_of_next_business_week", - time: nextBusinessWeekStart(timezone), - timeFormatKey: "dates.long_no_year", - }, - { - icon: "far-calendar-plus", - id: TIME_SHORTCUT_TYPES.NEXT_MONTH, - label: "time_shortcut.next_month", - time: nextMonth(timezone), - timeFormatKey: "dates.long_no_year", - }, + shortcuts.laterToday(), + shortcuts.tomorrow(), + shortcuts.laterThisWeek(), + shortcuts.thisWeekend(), + shortcuts.monday(), + shortcuts.nextMonth(), ]; } @@ -99,3 +62,84 @@ export function specialShortcutOptions() { }, ]; } + +export function timeShortcuts(timezone) { + return { + laterToday() { + return { + icon: "angle-right", + id: TIME_SHORTCUT_TYPES.LATER_TODAY, + label: "time_shortcut.later_today", + time: laterToday(timezone), + timeFormatKey: "dates.time", + }; + }, + tomorrow() { + return { + icon: "far-sun", + id: TIME_SHORTCUT_TYPES.TOMORROW, + label: "time_shortcut.tomorrow", + time: tomorrow(timezone), + timeFormatKey: "dates.time_short_day", + }; + }, + laterThisWeek() { + return { + icon: "angle-double-right", + id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK, + label: "time_shortcut.later_this_week", + time: laterThisWeek(timezone), + timeFormatKey: "dates.time_short_day", + }; + }, + thisWeekend() { + return { + icon: "bed", + id: TIME_SHORTCUT_TYPES.THIS_WEEKEND, + label: "time_shortcut.this_weekend", + time: thisWeekend(timezone), + timeFormatKey: "dates.time_short_day", + }; + }, + monday() { + return { + icon: "briefcase", + id: TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK, + label: + now(timezone).day() === MOMENT_MONDAY || + now(timezone).day() === MOMENT_SUNDAY + ? "time_shortcut.start_of_next_business_week_alt" + : "time_shortcut.start_of_next_business_week", + time: nextBusinessWeekStart(timezone), + timeFormatKey: "dates.long_no_year", + }; + }, + nextMonth() { + return { + icon: "far-calendar-plus", + id: TIME_SHORTCUT_TYPES.NEXT_MONTH, + label: "time_shortcut.next_month", + time: nextMonth(timezone), + timeFormatKey: "dates.long_no_year", + }; + }, + twoWeeks() { + return { + icon: "far-clock", + id: "two_weeks", + label: "time_shortcut.two_weeks", + time: twoWeeks(timezone), + timeFormatKey: "dates.long_no_year", + }; + }, + sixMonths() { + return { + icon: "far-calendar-plus", + id: "six_months", + label: "time_shortcut.six_months", + time: sixMonths(timezone), + timeFormatKey: "dates.long_no_year", + }; + }, + }; +} diff --git a/app/assets/javascripts/discourse/app/lib/time-utils.js b/app/assets/javascripts/discourse/app/lib/time-utils.js index 5266362e5b..2a8e1e4342 100644 --- a/app/assets/javascripts/discourse/app/lib/time-utils.js +++ b/app/assets/javascripts/discourse/app/lib/time-utils.js @@ -43,6 +43,14 @@ export function nextMonth(timezone) { return startOfDay(now(timezone).add(1, "month").startOf("month")); } +export function twoWeeks(timezone) { + return startOfDay(now(timezone).add(2, "weeks").day(MOMENT_MONDAY)); +} + +export function sixMonths(timezone) { + return startOfDay(now(timezone).add(6, "months").startOf("month")); +} + export function nextBusinessWeekStart(timezone) { return startOfDay(now(timezone).add(7, "days")).day(MOMENT_MONDAY); } diff --git a/app/assets/javascripts/discourse/app/lib/timeframes-builder.js b/app/assets/javascripts/discourse/app/lib/timeframes-builder.js index 361ad2e336..08db9414dc 100644 --- a/app/assets/javascripts/discourse/app/lib/timeframes-builder.js +++ b/app/assets/javascripts/discourse/app/lib/timeframes-builder.js @@ -33,7 +33,7 @@ const TIMEFRAMES = [ buildTimeframe({ id: "later_this_week", format: "ddd, h a", - enabled: (opts) => !opts.canScheduleToday && opts.day > 0 && opts.day < 4, + enabled: (opts) => opts.day > 0 && opts.day < 4, when: (time, timeOfDay) => time.add(2, "day").hour(timeOfDay).minute(0), }), buildTimeframe({ diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js index 37a1e4940f..ff5516da22 100644 --- a/app/assets/javascripts/discourse/app/lib/transform-post.js +++ b/app/assets/javascripts/discourse/app/lib/transform-post.js @@ -18,7 +18,7 @@ export function transformBasicPost(post) { deleted: post.get("deleted"), deleted_at: post.deleted_at, user_deleted: post.user_deleted, - isDeleted: post.deleted_at || post.user_deleted, // xxxxx + isDeleted: post.deleted_at || post.user_deleted, deletedByAvatarTemplate: null, deletedByUsername: null, primary_group_name: post.primary_group_name, @@ -84,6 +84,7 @@ export function transformBasicPost(post) { readCount: post.readers_count, canPublishPage: false, trustLevel: post.trust_level, + userSuspended: post.user_suspended, }; _additionalAttributes.forEach((a) => (postAtts[a] = post[a])); diff --git a/app/assets/javascripts/discourse/app/lib/uploads.js b/app/assets/javascripts/discourse/app/lib/uploads.js index 6b75f07e90..51a640a5d4 100644 --- a/app/assets/javascripts/discourse/app/lib/uploads.js +++ b/app/assets/javascripts/discourse/app/lib/uploads.js @@ -10,7 +10,7 @@ function isGUID(value) { } export function markdownNameFromFileName(fileName) { - let name = fileName.substr(0, fileName.lastIndexOf(".")); + let name = fileName.slice(0, fileName.lastIndexOf(".")); if (isAppleDevice() && isGUID(name)) { name = I18n.t("upload_selector.default_image_alt_text"); diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index 8a78eb0a4b..8cb4ba4552 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -313,7 +313,7 @@ export function isAppleDevice() { // IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone // This will apply hack on all iDevices let caps = helperContext().capabilities; - return caps.isIOS && !navigator.userAgent.match(/Trident/g); + return caps.isIOS && !window.navigator.userAgent.match(/Trident/g); } let iPadDetected = undefined; @@ -321,8 +321,8 @@ let iPadDetected = undefined; export function isiPad() { if (iPadDetected === undefined) { iPadDetected = - navigator.userAgent.match(/iPad/g) && - !navigator.userAgent.match(/Trident/g); + window.navigator.userAgent.match(/iPad/g) && + !window.navigator.userAgent.match(/Trident/g); } return iPadDetected; } @@ -482,7 +482,7 @@ export function inCodeBlock(text, pos) { // Character at position `pos` can be in a code block that is unfinished. // To check this case, we look for any open code blocks after the last closed // code block. - const lastOpenBlock = text.substr(end).search(OPEN_CODE_BLOCKS_REGEX); + const lastOpenBlock = text.slice(end).search(OPEN_CODE_BLOCKS_REGEX); return lastOpenBlock !== -1 && pos >= end + lastOpenBlock; } @@ -512,8 +512,8 @@ export function translateModKey(string) { export function clipboardCopy(text) { // Use the Async Clipboard API when available. // Requires a secure browsing context (i.e. HTTPS) - if (navigator.clipboard) { - return navigator.clipboard.writeText(text).catch(function (err) { + if (window.navigator.clipboard) { + return window.navigator.clipboard.writeText(text).catch(function (err) { throw err !== undefined ? err : new DOMException("The request is not allowed", "NotAllowedError"); @@ -532,12 +532,29 @@ export function clipboardCopy(text) { // // Note that the promise passed in should return a Blob with type of // text/plain. -export function clipboardCopyAsync(promise) { +export function clipboardCopyAsync(functionReturningPromise) { // Use the Async Clipboard API when available. // Requires a secure browsing context (i.e. HTTPS) - if (navigator.clipboard) { - return navigator.clipboard - .write([new window.ClipboardItem({ "text/plain": promise() })]) + if (window.navigator.clipboard) { + // Firefox does not support window.ClipboardItem yet (it is behind + // a flag (dom.events.asyncClipboard.clipboardItem) as at version 87.) + // so we need to fall back to the normal non-async clipboard copy, that + // works in every browser except Safari. + // + // TODO: (martin) Look at this on 2022-07-01 to see if support has + // changed. + if (!window.ClipboardItem) { + return functionReturningPromise().then((textBlob) => { + return textBlob.text().then((text) => { + return clipboardCopy(text); + }); + }); + } + + return window.navigator.clipboard + .write([ + new window.ClipboardItem({ "text/plain": functionReturningPromise() }), + ]) .catch(function (err) { throw err !== undefined ? err @@ -546,7 +563,7 @@ export function clipboardCopyAsync(promise) { } // ...Otherwise, use document.execCommand() fallback - return promise().then((textBlob) => { + return functionReturningPromise().then((textBlob) => { textBlob.text().then((text) => { return clipboardCopyFallback(text); }); diff --git a/app/assets/javascripts/discourse/app/mixins/card-contents-base.js b/app/assets/javascripts/discourse/app/mixins/card-contents-base.js index de7e63f3dc..cc798fd4b6 100644 --- a/app/assets/javascripts/discourse/app/mixins/card-contents-base.js +++ b/app/assets/javascripts/discourse/app/mixins/card-contents-base.js @@ -112,6 +112,7 @@ export default Mixin.create({ }); document.addEventListener("mousedown", this._clickOutsideHandler); + document.addEventListener("keyup", this._escListener); _cardClickListenerSelectors.forEach((selector) => { document @@ -320,6 +321,7 @@ export default Mixin.create({ this._super(...arguments); document.removeEventListener("mousedown", this._clickOutsideHandler); + document.removeEventListener("keyup", this._escListener); _cardClickListenerSelectors.forEach((selector) => { document @@ -340,14 +342,6 @@ export default Mixin.create({ this._hide(); }, - keyUp(e) { - if (e.key === "Escape") { - const target = this.cardTarget; - this._close(); - target.focus(); - } - }, - @bind _clickOutsideHandler(event) { if (this.visible) { @@ -365,4 +359,13 @@ export default Mixin.create({ return true; }, + + @bind + _escListener(event) { + if (this.visible && event.key === "Escape") { + this._close(); + this.cardTarget?.focus(); + return; + } + }, }); diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js index 0442cf9697..6dce54f500 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -37,6 +37,7 @@ import { run } from "@ember/runloop"; export default Mixin.create(ExtendableUploader, UppyS3Multipart, { uploadRootPath: "/uploads", uploadTargetBound: false, + useUploadPlaceholders: true, @bind _cancelSingleUpload(data) { @@ -67,9 +68,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.editorEl?.removeEventListener("paste", this.pasteEventListener); - this.appEvents.off(`${this.eventPrefix}:add-files`, this._addFiles); + this.appEvents.off(`${this.composerEventPrefix}:add-files`, this._addFiles); this.appEvents.off( - `${this.eventPrefix}:cancel-upload`, + `${this.composerEventPrefix}:cancel-upload`, this._cancelSingleUpload ); @@ -84,7 +85,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { }, _abortAndReset() { - this.appEvents.trigger(`${this.eventPrefix}:uploads-aborted`); + this.appEvents.trigger(`${this.composerEventPrefix}:uploads-aborted`); this._reset(); return false; }, @@ -97,9 +98,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.fileInputEl = document.getElementById(this.fileUploadElementId); const isPrivateMessage = this.get("composerModel.privateMessage"); - this.appEvents.on(`${this.eventPrefix}:add-files`, this._addFiles); + this.appEvents.on(`${this.composerEventPrefix}:add-files`, this._addFiles); this.appEvents.on( - `${this.eventPrefix}:cancel-upload`, + `${this.composerEventPrefix}:cancel-upload`, this._cancelSingleUpload ); @@ -136,7 +137,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { }); if (!isUploading) { - this.appEvents.trigger(`${this.eventPrefix}:uploads-aborted`); + this.appEvents.trigger(`${this.composerEventPrefix}:uploads-aborted`); } return isUploading; }, @@ -233,7 +234,10 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { if (reason === "cancel-all") { return; } - + this.appEvents.trigger( + `${this.composerEventPrefix}:upload-cancelled`, + file.id + ); file.meta.cancelled = true; this._removeInProgressUpload(file.id); this._resetUpload(file, { removePlaceholder: true }); @@ -289,12 +293,16 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this.placeholders[file.id] = { uploadPlaceholder: placeholder, }; + + if (this.useUploadPlaceholders) { + this.appEvents.trigger( + `${this.composerEventPrefix}:insert-text`, + placeholder + ); + } + this.appEvents.trigger( - `${this.eventPrefix}:insert-text`, - placeholder - ); - this.appEvents.trigger( - `${this.eventPrefix}:upload-started`, + `${this.composerEventPrefix}:upload-started`, file.name ); }); @@ -315,15 +323,17 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { cacheShortUploadUrl(upload.short_url, upload); - this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, - this.placeholders[file.id].uploadPlaceholder.trim(), - markdown - ); + if (this.useUploadPlaceholders) { + this.appEvents.trigger( + `${this.composerEventPrefix}:replace-text`, + this.placeholders[file.id].uploadPlaceholder.trim(), + markdown + ); + } this._resetUpload(file, { removePlaceholder: false }); this.appEvents.trigger( - `${this.eventPrefix}:upload-success`, + `${this.composerEventPrefix}:upload-success`, file.name, upload ); @@ -334,7 +344,9 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { this._uppyInstance.on("complete", () => { run(() => { - this.appEvents.trigger(`${this.eventPrefix}:all-uploads-complete`); + this.appEvents.trigger( + `${this.composerEventPrefix}:all-uploads-complete` + ); this._reset(); }); }); @@ -345,18 +357,20 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { if (this.userCancelled) { Object.values(this.placeholders).forEach((data) => { run(() => { - this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, - data.uploadPlaceholder, - "" - ); + if (this.useUploadPlaceholders) { + this.appEvents.trigger( + `${this.composerEventPrefix}:replace-text`, + data.uploadPlaceholder, + "" + ); + } }); }); this.set("userCancelled", false); this._reset(); - this.appEvents.trigger(`${this.eventPrefix}:uploads-cancelled`); + this.appEvents.trigger(`${this.composerEventPrefix}:uploads-cancelled`); } }); @@ -381,7 +395,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { if (!this.userCancelled) { displayErrorForUpload(response || error, this.siteSettings, file.name); - this.appEvents.trigger(`${this.eventPrefix}:upload-error`, file); + this.appEvents.trigger(`${this.composerEventPrefix}:upload-error`, file); } if (this.inProgressUploads.length === 0) { @@ -434,7 +448,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { )}]()\n`; this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, + `${this.composerEventPrefix}:replace-text`, placeholderData.uploadPlaceholder, placeholderData.processingPlaceholder ); @@ -445,7 +459,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { run(() => { let placeholderData = this.placeholders[file.id]; this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, + `${this.composerEventPrefix}:replace-text`, placeholderData.processingPlaceholder, placeholderData.uploadPlaceholder ); @@ -458,7 +472,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { isCancellable: true, }); this.appEvents.trigger( - `${this.eventPrefix}:uploads-preprocessing-complete` + `${this.composerEventPrefix}:uploads-preprocessing-complete` ); }); } @@ -536,7 +550,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { _resetUpload(file, opts) { if (opts.removePlaceholder) { this.appEvents.trigger( - `${this.eventPrefix}:replace-text`, + `${this.composerEventPrefix}:replace-text`, this.placeholders[file.id].uploadPlaceholder, "" ); diff --git a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js index 9bcf1b54af..ca2a312d3b 100644 --- a/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js +++ b/app/assets/javascripts/discourse/app/mixins/extendable-uploader.js @@ -92,7 +92,7 @@ export default Mixin.create(UploadDebugging, { }); }, - _onPreProcessComplete(callback, allCompleteCallback) { + _onPreProcessComplete(callback, allCompleteCallback = null) { this._uppyInstance.on("preprocess-complete", (file, skipped, pluginId) => { this._consoleDebug( `[${pluginId}] ${skipped ? "skipped" : "completed"} processing file ${ @@ -105,7 +105,9 @@ export default Mixin.create(UploadDebugging, { this._completePreProcessing(pluginId, (allComplete) => { if (allComplete) { this._consoleDebug("[uppy] All upload preprocessors complete!"); - allCompleteCallback(); + if (allCompleteCallback) { + allCompleteCallback(); + } } }); }); diff --git a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js index ab6aeab38a..b5a3e49512 100644 --- a/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js +++ b/app/assets/javascripts/discourse/app/mixins/textarea-text-manipulation.js @@ -34,6 +34,13 @@ export function getHead(head, prev) { export default Mixin.create({ init() { this._super(...arguments); + + // fallback in the off chance someone has implemented a custom composer + // which does not define this + if (!this.composerEventPrefix) { + this.composerEventPrefix = "composer"; + } + generateLinkifyFunction(this.markdownOptions || {}).then((linkify) => { // When pasting links, we should use the same rules to match links as we do when creating links for a cooked post. this._cachedLinkify = linkify; @@ -41,14 +48,7 @@ export default Mixin.create({ }, // ensures textarea scroll position is correct - // - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase focusTextArea() { - this._focusTextArea(); - }, - - _focusTextArea() { if (!this.element || this.isDestroying || this.isDestroyed) { return; } @@ -61,33 +61,15 @@ export default Mixin.create({ this._textarea.focus(); }, - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase insertBlock(text) { - this._insertBlock(text); - }, - - _insertBlock(text) { this._addBlock(this.getSelected(), text); }, - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase insertText(text, options) { - this._insertText(text, options); + this.addText(this.getSelected(), text, options); }, - _insertText(text, options) { - this._addText(this.getSelected(), text, options); - }, - - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase getSelected(trimLeading, opts) { - return this._getSelected(trimLeading, opts); - }, - - _getSelected(trimLeading, opts) { if (!this.ready || !this.element) { return; } @@ -114,7 +96,7 @@ export default Mixin.create({ if (opts && opts.lineVal) { const lineVal = value.split("\n")[ - value.substr(0, this._textarea.selectionStart).split("\n").length - 1 + value.slice(0, this._textarea.selectionStart).split("\n").length - 1 ]; return { start, end, value: selVal, pre, post, lineVal }; } else { @@ -122,13 +104,7 @@ export default Mixin.create({ } }, - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase selectText(from, length, opts = { scroll: true }) { - this._selectText(from, length, opts); - }, - - _selectText(from, length, opts = { scroll: true }) { next(() => { if (!this.element) { return; @@ -147,13 +123,7 @@ export default Mixin.create({ }); }, - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase replaceText(oldVal, newVal, opts = {}) { - this._replaceText(oldVal, newVal, opts); - }, - - _replaceText(oldVal, newVal, opts = {}) { const val = this.value; const needleStart = val.indexOf(oldVal); @@ -196,13 +166,7 @@ export default Mixin.create({ } }, - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase applySurround(sel, head, tail, exampleKey, opts) { - this._applySurround(sel, head, tail, exampleKey, opts); - }, - - _applySurround(sel, head, tail, exampleKey, opts) { const pre = sel.pre; const post = sel.post; @@ -337,16 +301,10 @@ export default Mixin.create({ this._$textarea.prop("selectionStart", (pre + text).length + 2); this._$textarea.prop("selectionEnd", (pre + text).length + 2); - schedule("afterRender", this, this._focusTextArea); + schedule("afterRender", this, this.focusTextArea); }, - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase addText(sel, text, options) { - this._addText(sel, text, options); - }, - - _addText(sel, text, options) { if (options && options.ensureSpace) { if ((sel.pre + "").length > 0) { if (!sel.pre.match(/\s$/)) { @@ -367,16 +325,10 @@ export default Mixin.create({ this._$textarea.prop("selectionStart", insert.length); this._$textarea.prop("selectionEnd", insert.length); next(() => this._$textarea.trigger("change")); - this._focusTextArea(); + this.focusTextArea(); }, - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase extractTable(text) { - return this._extractTable(text); - }, - - _extractTable(text) { if (text.endsWith("\n")) { text = text.substring(0, text.length - 1); } @@ -413,13 +365,7 @@ export default Mixin.create({ return null; }, - // TODO (martin) clean up this indirection, functions used outside this - // file should not be prefixed with lowercase isInside(text, regex) { - return this._isInside(text, regex); - }, - - _isInside(text, regex) { const matches = text.match(regex); return matches && matches.length % 2; }, @@ -445,7 +391,7 @@ export default Mixin.create({ const selected = this.getSelected(null, { lineVal: true }); const { pre, value: selectedValue, lineVal } = selected; const isInlinePasting = pre.match(/[^\n]$/); - const isCodeBlock = this._isInside(pre, /(^|\n)```/g); + const isCodeBlock = this.isInside(pre, /(^|\n)```/g); if ( plainText && @@ -454,9 +400,12 @@ export default Mixin.create({ !isCodeBlock ) { plainText = plainText.replace(/\r/g, ""); - const table = this._extractTable(plainText); + const table = this.extractTable(plainText); if (table) { - this.appEvents.trigger("composer:insert-text", table); + this.appEvents.trigger( + `${this.composerEventPrefix}:insert-text`, + table + ); handled = true; } } @@ -465,7 +414,7 @@ export default Mixin.create({ if (isInlinePasting) { canPasteHtml = !( lineVal.match(/^```/) || - this._isInside(pre, /`/g) || + this.isInside(pre, /`/g) || lineVal.match(/^ /) ); } else { @@ -492,7 +441,7 @@ export default Mixin.create({ ) { // When specified, linkify supports fuzzy links and emails. Prefer providing the protocol. // eg: pasting "example@discourse.org" may apply a link format of "mailto:example@discourse.org" - this._addText(selected, `[${selectedValue}](${match.url})`); + this.addText(selected, `[${selectedValue}](${match.url})`); handled = true; } } @@ -508,7 +457,10 @@ export default Mixin.create({ } if (isComposer) { - this.appEvents.trigger("composer:insert-text", markdown); + this.appEvents.trigger( + `${this.composerEventPrefix}:insert-text`, + markdown + ); handled = true; } } @@ -614,9 +566,9 @@ export default Mixin.create({ if (isEmpty(captures)) { if (selected.pre.match(/\S$/)) { - this._addText(selected, ` :${code}:`); + this.addText(selected, ` :${code}:`); } else { - this._addText(selected, `:${code}:`); + this.addText(selected, `:${code}:`); } } else { let numOfRemovedChars = selected.pre.length - captures[1].length; @@ -626,7 +578,7 @@ export default Mixin.create({ ); selected.start -= numOfRemovedChars; selected.end -= numOfRemovedChars; - this._addText(selected, `${code}:`); + this.addText(selected, `${code}:`); } }, }); diff --git a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js index aaf6b4ec99..0807f98f40 100644 --- a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js +++ b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js @@ -1,4 +1,6 @@ import Mixin from "@ember/object/mixin"; +import { run } from "@ember/runloop"; +import ExtendableUploader from "discourse/mixins/extendable-uploader"; import { or } from "@ember/object/computed"; import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; @@ -23,7 +25,7 @@ import bootbox from "bootbox"; export const HUGE_FILE_THRESHOLD_BYTES = 104_857_600; // 100MB -export default Mixin.create(UppyS3Multipart, { +export default Mixin.create(UppyS3Multipart, ExtendableUploader, { uploading: false, uploadProgress: 0, _uppyInstance: null, @@ -55,6 +57,10 @@ export default Mixin.create(UppyS3Multipart, { this.fileInputEventListener ); this.appEvents.off(`upload-mixin:${this.id}:add-files`, this._addFiles); + this.appEvents.off( + `upload-mixin:${this.id}:cancel-upload`, + this._cancelSingleUpload + ); this._uppyInstance?.close(); this._uppyInstance = null; }, @@ -66,6 +72,7 @@ export default Mixin.create(UppyS3Multipart, { }); this.set("allowMultipleFiles", this.fileInputEl.multiple); this.set("inProgressUploads", []); + this._triggerInProgressUploadsEvent(); this._bindFileInputChange(); @@ -105,6 +112,7 @@ export default Mixin.create(UppyS3Multipart, { uploadProgress: 0, uploading: isValid && this.autoStartUploads, filesAwaitingUpload: !this.autoStartUploads, + cancellable: isValid && this.autoStartUploads, }); return isValid; }, @@ -141,8 +149,8 @@ export default Mixin.create(UppyS3Multipart, { }, }); + // droptarget is a UI plugin, only preprocessors must call _useUploadPlugin this._uppyInstance.use(DropTarget, this._uploadDropTargetOptions()); - this._uppyInstance.use(UppyChecksum, { capabilities: this.capabilities }); this._uppyInstance.on("progress", (progress) => { if (this.isDestroying || this.isDestroyed) { @@ -153,48 +161,78 @@ export default Mixin.create(UppyS3Multipart, { }); this._uppyInstance.on("upload", (data) => { + this._addNeedProcessing(data.fileIDs.length); const files = data.fileIDs.map((fileId) => this._uppyInstance.getFile(fileId) ); + this.setProperties({ + processing: true, + cancellable: false, + }); files.forEach((file) => { - this.inProgressUploads.push( + // The inProgressUploads is meant to be used to display these uploads + // in a UI, and Ember will only update the array in the UI if pushObject + // is used to notify it. + this.inProgressUploads.pushObject( EmberObject.create({ fileName: file.name, id: file.id, progress: 0, + extension: file.extension, + processing: false, }) ); + this._triggerInProgressUploadsEvent(); + }); + }); + + this._uppyInstance.on("upload-progress", (file, progress) => { + run(() => { + if (this.isDestroying || this.isDestroyed) { + return; + } + + const upload = this.inProgressUploads.find((upl) => upl.id === file.id); + if (upload) { + const percentage = Math.round( + (progress.bytesUploaded / progress.bytesTotal) * 100 + ); + upload.set("progress", percentage); + } }); }); this._uppyInstance.on("upload-success", (file, response) => { - this._removeInProgressUpload(file.id); - if (this.usingS3Uploads) { this.setProperties({ uploading: false, processing: true }); this._completeExternalUpload(file) .then((completeResponse) => { + this._removeInProgressUpload(file.id); + this.appEvents.trigger( + `upload-mixin:${this.id}:upload-success`, + file.name, + completeResponse + ); this.uploadDone( deepMerge(completeResponse, { file_name: file.name }) ); - if (this.inProgressUploads.length === 0) { - this._reset(); - } + this._triggerInProgressUploadsEvent(); }) .catch((errResponse) => { displayErrorForUpload(errResponse, this.siteSettings, file.name); - if (this.inProgressUploads.length === 0) { - this._reset(); - } + this._triggerInProgressUploadsEvent(); }); } else { - this.uploadDone( - deepMerge(response?.body || {}, { file_name: file.name }) + this._removeInProgressUpload(file.id); + const upload = response?.body || {}; + this.appEvents.trigger( + `upload-mixin:${this.id}:upload-success`, + file.name, + upload ); - if (this.inProgressUploads.length === 0) { - this._reset(); - } + this.uploadDone(deepMerge(upload, { file_name: file.name })); + this._triggerInProgressUploadsEvent(); } }); @@ -204,6 +242,28 @@ export default Mixin.create(UppyS3Multipart, { this._reset(); }); + this._uppyInstance.on("file-removed", (file, reason) => { + run(() => { + // we handle the cancel-all event specifically, so no need + // to do anything here. this event is also fired when some files + // are handled by an upload handler + if (reason === "cancel-all") { + return; + } + this.appEvents.trigger( + `upload-mixin:${this.id}:upload-cancelled`, + file.id + ); + }); + }); + + this._uppyInstance.on("complete", () => { + run(() => { + this.appEvents.trigger(`upload-mixin:${this.id}:all-uploads-complete`); + this._reset(); + }); + }); + // TODO (martin) preventDirectS3Uploads is necessary because some of // the current upload mixin components, for example the emoji uploader, // send the upload to custom endpoints that do fancy things in the rails @@ -228,8 +288,33 @@ export default Mixin.create(UppyS3Multipart, { } } + this._uppyInstance.on("cancel-all", () => { + this.appEvents.trigger(`upload-mixin:${this.id}:uploads-cancelled`); + if (!this.isDestroyed && !this.isDestroying) { + this.set("inProgressUploads", []); + this._triggerInProgressUploadsEvent(); + } + }); + this.appEvents.on(`upload-mixin:${this.id}:add-files`, this._addFiles); + this.appEvents.on( + `upload-mixin:${this.id}:cancel-upload`, + this._cancelSingleUpload + ); this._uppyReady(); + + // It is important that the UppyChecksum preprocessor is the last one to + // be added; the preprocessors are run in order and since other preprocessors + // may modify the file (e.g. the UppyMediaOptimization one), we need to + // checksum once we are sure the file data has "settled". + this._useUploadPlugin(UppyChecksum, { capabilities: this.capabilities }); + }, + + _triggerInProgressUploadsEvent() { + this.appEvents.trigger( + `upload-mixin:${this.id}:in-progress-uploads`, + this.inProgressUploads + ); }, // This should be overridden in a child component if you need to @@ -325,6 +410,12 @@ export default Mixin.create(UppyS3Multipart, { ); }, + @bind + _cancelSingleUpload(data) { + this._uppyInstance.removeFile(data.fileId); + this._removeInProgressUpload(data.fileId); + }, + @bind _addFiles(files, opts = {}) { files = Array.isArray(files) ? files : [files]; @@ -362,6 +453,7 @@ export default Mixin.create(UppyS3Multipart, { this.setProperties({ uploading: false, processing: false, + cancellable: false, uploadProgress: 0, filesAwaitingUpload: false, }); @@ -369,10 +461,15 @@ export default Mixin.create(UppyS3Multipart, { }, _removeInProgressUpload(fileId) { + if (this.isDestroyed || this.isDestroying) { + return; + } + this.set( "inProgressUploads", this.inProgressUploads.filter((upl) => upl.id !== fileId) ); + this._triggerInProgressUploadsEvent(); }, // target must be provided as a DOM element, however the diff --git a/app/assets/javascripts/discourse/app/models/bookmark.js b/app/assets/javascripts/discourse/app/models/bookmark.js index 8852409a77..f28c92845e 100644 --- a/app/assets/javascripts/discourse/app/models/bookmark.js +++ b/app/assets/javascripts/discourse/app/models/bookmark.js @@ -38,6 +38,14 @@ const Bookmark = RestModel.extend({ }, attachedTo() { + if (this.siteSettings.use_polymorphic_bookmarks) { + return { + target: this.bookmarkable_type.toLowerCase(), + targetId: this.bookmarkable_id, + }; + } + + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. if (this.for_topic) { return { target: "topic", targetId: this.topic_id }; } diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js index 1ff851355f..e4435bd8b1 100644 --- a/app/assets/javascripts/discourse/app/models/category.js +++ b/app/assets/javascripts/discourse/app/models/category.js @@ -35,21 +35,13 @@ const Category = RestModel.extend({ } }, - @on("init") - setupRequiredTagGroups() { - if (this.required_tag_group_name) { - this.set("required_tag_groups", [this.required_tag_group_name]); - } - }, - - @discourseComputed( - "required_tag_groups", - "min_tags_from_required_group", - "minimum_required_tags" - ) + @discourseComputed("required_tag_groups", "minimum_required_tags") minimumRequiredTags() { - if (this.required_tag_groups) { - return this.min_tags_from_required_group; + if (this.required_tag_groups?.length > 0) { + return this.required_tag_groups.reduce( + (sum, rtg) => sum + rtg.min_count, + 0 + ); } else { return this.minimum_required_tags > 0 ? this.minimum_required_tags : null; } @@ -200,7 +192,8 @@ const Category = RestModel.extend({ const url = id ? `/categories/${id}` : "/categories"; return ajax(url, { - data: { + contentType: "application/json", + data: JSON.stringify({ name: this.name, slug: this.slug, color: this.color, @@ -225,20 +218,10 @@ const Category = RestModel.extend({ all_topics_wiki: this.all_topics_wiki, allow_unlimited_owner_edits_on_first_post: this .allow_unlimited_owner_edits_on_first_post, - allowed_tags: - this.allowed_tags && this.allowed_tags.length > 0 - ? this.allowed_tags - : null, - allowed_tag_groups: - this.allowed_tag_groups && this.allowed_tag_groups.length > 0 - ? this.allowed_tag_groups - : null, + allowed_tags: this.allowed_tags, + allowed_tag_groups: this.allowed_tag_groups, allow_global_tags: this.allow_global_tags, - required_tag_group_name: - this.required_tag_groups && this.required_tag_groups.length > 0 - ? this.required_tag_groups[0] - : null, - min_tags_from_required_group: this.min_tags_from_required_group, + required_tag_groups: this.required_tag_groups, sort_order: this.sort_order, sort_ascending: this.sort_ascending, topic_featured_link_allowed: this.topic_featured_link_allowed, @@ -255,7 +238,7 @@ const Category = RestModel.extend({ reviewable_by_group_name: this.reviewable_by_group_name, read_only_banner: this.read_only_banner, default_list_filter: this.default_list_filter, - }, + }), type: id ? "PUT" : "POST", }); }, diff --git a/app/assets/javascripts/discourse/app/models/invite.js b/app/assets/javascripts/discourse/app/models/invite.js index 122208bc1b..6608ecb079 100644 --- a/app/assets/javascripts/discourse/app/models/invite.js +++ b/app/assets/javascripts/discourse/app/models/invite.js @@ -36,7 +36,7 @@ const Invite = EmberObject.extend({ @discourseComputed("invite_key") shortKey(key) { - return key.substr(0, 4) + "..."; + return key.slice(0, 4) + "..."; }, @discourseComputed("groups") diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index 685750b52e..050d0247eb 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -441,7 +441,7 @@ const TopicTrackingState = EmberObject.extend({ }, _generateCallbackId() { - return Math.random().toString(12).substr(2, 9); + return Math.random().toString(12).slice(2, 11); }, onStateChange(cb) { diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js index ae74d2ea63..e6b960f4ea 100644 --- a/app/assets/javascripts/discourse/app/models/topic.js +++ b/app/assets/javascripts/discourse/app/models/topic.js @@ -41,6 +41,7 @@ export function loadTopicView(topic, args) { } export const ID_CONSTRAINT = /^\d+$/; +let _customLastUnreadUrlCallbacks = []; const Topic = RestModel.extend({ message: null, @@ -256,6 +257,19 @@ const Topic = RestModel.extend({ @discourseComputed("last_read_post_number", "highest_post_number", "url") lastUnreadUrl(lastReadPostNumber, highestPostNumber) { + let customUrl = null; + _customLastUnreadUrlCallbacks.some((cb) => { + const result = cb(this); + if (result) { + customUrl = result; + return true; + } + }); + + if (customUrl) { + return customUrl; + } + if (highestPostNumber <= lastReadPostNumber) { if (this.get("category.navigate_to_first_post_after_read")) { return this.urlForPostNumber(1); @@ -383,7 +397,15 @@ const Topic = RestModel.extend({ this.set( "bookmarks", this.bookmarks.filter((bookmark) => { - if (bookmark.id === id && bookmark.for_topic) { + // TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. + if ( + (!this.siteSettings.use_polymorphic_bookmarks && + bookmark.id === id && + bookmark.for_topic) || + (this.siteSettings.use_polymorphic_bookmarks && + bookmark.id === id && + bookmark.bookmarkable_type === "Topic") + ) { // TODO (martin) (2022-02-01) Remove these old bookmark events, replaced by bookmarks:changed. this.appEvents.trigger("topic:bookmark-toggled"); this.appEvents.trigger( @@ -403,7 +425,11 @@ const Topic = RestModel.extend({ clearBookmarks() { this.toggleProperty("bookmarked"); - const postIds = this.bookmarks.mapBy("post_id"); + const postIds = this.siteSettings.use_polymorphic_bookmarks + ? this.bookmarks + .filterBy("bookmarkable_type", "Post") + .mapBy("bookmarkable_id") + : this.bookmarks.mapBy("post_id"); postIds.forEach((postId) => { const loadedPost = this.postStream.findLoadedPost(postId); if (loadedPost) { @@ -548,7 +574,7 @@ const Topic = RestModel.extend({ @discourseComputed("excerpt") excerptTruncated(excerpt) { - return excerpt && excerpt.substr(excerpt.length - 8, 8) === "…"; + return excerpt && excerpt.slice(-8) === "…"; }, readLastPost: propertyEqual("last_read_post_number", "highest_post_number"), @@ -876,4 +902,13 @@ export function mergeTopic(topicId, data) { ); } +export function registerCustomLastUnreadUrlCallback(fn) { + _customLastUnreadUrlCallbacks.push(fn); +} + +// Should only be used in tests +export function clearCustomLastUnreadUrlCallbacks() { + _customLastUnreadUrlCallbacks.clear(); +} + export default Topic; diff --git a/app/assets/javascripts/discourse/app/routes/discourse.js b/app/assets/javascripts/discourse/app/routes/discourse.js index eb2b4f3c43..9890e5c2ca 100644 --- a/app/assets/javascripts/discourse/app/routes/discourse.js +++ b/app/assets/javascripts/discourse/app/routes/discourse.js @@ -74,6 +74,7 @@ const DiscourseRoute = Route.extend({ } }, + // deprecated, use isCurrentUser() instead isAnotherUsersPage(user) { if (!this.currentUser) { return true; @@ -82,6 +83,14 @@ const DiscourseRoute = Route.extend({ return user.username !== this.currentUser.username; }, + isCurrentUser(user) { + if (!this.currentUser) { + return false; // the current user is anonymous + } + + return user.id === this.currentUser.id; + }, + isPoppedState(transition) { return !transition._discourse_intercepted && !!transition.intent.url; }, diff --git a/app/assets/javascripts/discourse/app/routes/topic-from-params.js b/app/assets/javascripts/discourse/app/routes/topic-from-params.js index b1f30baa5c..5875089d6d 100644 --- a/app/assets/javascripts/discourse/app/routes/topic-from-params.js +++ b/app/assets/javascripts/discourse/app/routes/topic-from-params.js @@ -92,7 +92,7 @@ export default DiscourseRoute.extend({ const opts = {}; if (document.location.hash) { - opts.anchor = document.location.hash.substr(1); + opts.anchor = document.location.hash.slice(1); } else if (_discourse_anchor) { opts.anchor = _discourse_anchor; } 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 0eaf79c8b0..67b73d3634 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-drafts.js @@ -11,7 +11,7 @@ export default DiscourseRoute.extend({ return draftsStream.findItems(this.site).then(() => { return { stream: draftsStream, - isAnotherUsersPage: this.isAnotherUsersPage(user), + isAnotherUsersPage: !this.isCurrentUser(user), emptyState: this.emptyState(), }; }); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-stream.js b/app/assets/javascripts/discourse/app/routes/user-activity-stream.js index 69fabb3173..f0e1287d37 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-stream.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-stream.js @@ -16,7 +16,7 @@ export default DiscourseRoute.extend(ViewingActionType, { return { stream, - isAnotherUsersPage: this.isAnotherUsersPage(user), + isAnotherUsersPage: !this.isCurrentUser(user), emptyState: this.emptyState(), emptyStateOthers: this.emptyStateOthers, }; 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 9e48ca30d5..bb8ee51b78 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js @@ -23,8 +23,15 @@ export default UserTopicListRoute.extend({ }, emptyState() { + const user = this.modelFor("user"); + const title = this.isCurrentUser(user) + ? I18n.t("user_activity.no_topics_title") + : I18n.t("user_activity.no_topics_title_others", { + username: user.username, + }); + return { - title: I18n.t("user_activity.no_topics_title"), + title, body: "", }; }, diff --git a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs index e7b76e8bb9..44d14a340a 100644 --- a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs @@ -38,9 +38,9 @@ {{#if userHasTimezoneSet}} {{time-shortcut-picker + timeShortcuts=timeOptions prefilledDatetime=prefilledDatetime onTimeSelected=(action "onTimeSelected") - customOptions=customTimeShortcutOptions hiddenOptions=hiddenTimeShortcutOptions customLabels=customTimeShortcutLabels _itsatrap=_itsatrap diff --git a/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs index 77aac380bc..767b0a854a 100644 --- a/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/composer-editor.hbs @@ -23,9 +23,5 @@ {{/d-editor}} {{#if allowUpload}} - {{#if acceptsAllFormats}} - - {{else}} - - {{/if}} + {{pick-files-button fileInputId="file-uploader" allowMultiple=true}} {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs index 8e16ab00a9..aa408070cb 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-general.hbs @@ -15,10 +15,10 @@ value=category.parent_category_id categories=parentCategories allowSubCategories=true - allowUncategorized=false allowRestrictedCategories=true onChange=(action (mut category.parent_category_id)) options=(hash + allowUncategorized=false excludeCategoryId=category.id none=true ) diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-category-tags.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-category-tags.hbs index 13d1031a4b..b093ec89e6 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-category-tags.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-category-tags.hbs @@ -18,6 +18,7 @@ {{tag-group-chooser id="category-allowed-tag-groups" tagGroups=category.allowed_tag_groups + onChange=(action (mut category.allowed_tag_groups)) }} {{#link-to "tagGroups" class="manage-tag-groups"}}{{i18n "category.manage_tag_groups_link"}}{{/link-to}} @@ -34,23 +35,33 @@
- {{i18n "category.required_tag_group_description"}} + {{i18n "category.required_tag_group.description"}}
-
- - {{text-field value=category.min_tags_from_required_group id="category-min-tags-from-group" type="number" min="1"}} -
-
- - {{tag-group-chooser - id="category-required-tag-group" - tagGroups=category.required_tag_groups - options=(hash - maximum=1 - filterPlaceholder="category.tag_group_selector_placeholder" - ) - }} +
+ {{#each category.required_tag_groups as |rtg|}} +
+ {{text-field value=rtg.min_count type="number" min="1"}} + {{tag-group-chooser + tagGroups=(if rtg.name (array rtg.name) (array)) + onChange=(action "onTagGroupChange" rtg) + options=(hash + maximum=1 + filterPlaceholder="category.required_tag_group.placeholder" + ) + }} + {{d-button + label="category.required_tag_group.delete" + action=(action "deleteRequiredTagGroup" rtg) + icon="trash-alt" + class="delete-required-tag-group"}} +
+ {{/each}} + {{d-button + label="category.required_tag_group.add" + action=(action "addRequiredTagGroup") + icon="plus" + class="add-required-tag-group"}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs index 6655b533f7..c9d6d28ed7 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs @@ -12,17 +12,19 @@ {{category-chooser value=topicTimer.category_id - excludeCategoryId=excludeCategoryId onChange=(action (mut topicTimer.category_id)) + options=(hash + excludeCategoryId=excludeCategoryId + ) }} {{/if}} {{#if showFutureDateInput}} {{time-shortcut-picker + timeShortcuts=timeOptions prefilledDatetime=topicTimer.execute_at onTimeSelected=onTimeSelected - customOptions=customTimeShortcutOptions hiddenOptions=hiddenTimeShortcutOptions _itsatrap=_itsatrap }} diff --git a/app/assets/javascripts/discourse/app/templates/components/group-imap-email-settings.hbs b/app/assets/javascripts/discourse/app/templates/components/group-imap-email-settings.hbs index e4d19aaf30..9b66d8079f 100644 --- a/app/assets/javascripts/discourse/app/templates/components/group-imap-email-settings.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/group-imap-email-settings.hbs @@ -28,9 +28,11 @@ value=group.imap_mailbox_name valueProperty="value" content=mailboxes - none="groups.manage.email.mailboxes.disabled" tabindex="10" onChange=(action (mut group.imap_mailbox_name)) + options=(hash + none="groups.manage.email.mailboxes.disabled" + ) }} {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs b/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs index 409a3becbd..5b91833292 100644 --- a/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/pick-files-button.hbs @@ -1,6 +1,8 @@ -{{d-button action=(action "openSystemFilePicker") label=label icon=icon}} -{{#if acceptAnyFile}} - -{{else}} - +{{#if showButton}} + {{d-button action=(action "openSystemFilePicker") label=label icon=icon}} +{{/if}} +{{#if acceptsAllFormats}} + +{{else}} + {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/reviewable-bundled-action.hbs b/app/assets/javascripts/discourse/app/templates/components/reviewable-bundled-action.hbs index c40ac432c3..17eb883752 100644 --- a/app/assets/javascripts/discourse/app/templates/components/reviewable-bundled-action.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/reviewable-bundled-action.hbs @@ -2,13 +2,13 @@ {{dropdown-select-box class="reviewable-action-dropdown" nameProperty="label" - title=bundle.label content=bundle.actions onChange=(action "performById") options=(hash icon=bundle.icon disabled=reviewableUpdating placement=placement + translatedNone=bundle.label ) }} {{else}} diff --git a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs index 5cafc9ede9..d3d0aa4aa0 100644 --- a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs @@ -23,11 +23,11 @@ {{tag-chooser id="search-with-tags" tags=searchedTerms.tags - allowCreate=false everyTag=true unlimitedTagCount=true onChange=(action "onChangeSearchTermForTags") options=(hash + allowAny=false headerAriaLabel=(i18n "search.advanced.with_tags.aria_label") ) }} diff --git a/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs b/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs index ea93c26dbd..941ec29b38 100644 --- a/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs @@ -1,5 +1,11 @@
{{#each posts as |post|}} - {{search-result-entry post=post bulkSelectEnabled=bulkSelectEnabled selected=selected highlightQuery=highlightQuery}} + {{search-result-entry + post=post + bulkSelectEnabled=bulkSelectEnabled + selected=selected + highlightQuery=highlightQuery + searchLogId=searchLogId + }} {{/each}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs b/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs index 4437c84545..3fabd1678d 100644 --- a/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/tag-groups-form.hbs @@ -8,10 +8,10 @@ {{tag-chooser tags=buffered.tag_names everyTag=true - allowCreate=true unlimitedTagCount=true excludeSynonyms=true options=(hash + allowAny=true filterPlaceholder="tagging.groups.tags_placeholder" ) }} @@ -23,11 +23,11 @@ {{tag-chooser tags=buffered.parent_tag_name everyTag=true - maximum=1 - allowCreate=true excludeSynonyms=true options=(hash + allowAny=true filterPlaceholder="tagging.groups.parent_tag_placeholder" + maximum=1 ) }} diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs index c6e9c5cae2..b7947a4f14 100644 --- a/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/topic-entrance.hbs @@ -1,7 +1,7 @@ -{{#d-button action=(action "enterTop") class="btn-default full jump-top"}} +{{#d-button action=(action "enterTop") class="btn-default full jump-top" ariaLabel="topic_entrance.sr_jump_top_button"}} {{d-icon "step-backward"}} {{html-safe topDate}} {{/d-button}} -{{#d-button action=(action "enterBottom") class="btn-default full jump-bottom"}} +{{#d-button action=(action "enterBottom") class="btn-default full jump-bottom" ariaLabel="topic_entrance.sr_jump_bottom_button"}} {{html-safe bottomDate}} {{d-icon "step-forward"}} {{/d-button}} diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs index 3ff3f75731..3a554aea95 100644 --- a/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/topic-list.hbs @@ -42,7 +42,8 @@ lastVisitedTopic=lastVisitedTopic selected=selected lastChecked=lastChecked - tagsForUser=tagsForUser}} + tagsForUser=tagsForUser + focusLastVisitedTopic=focusLastVisitedTopic}} {{raw "list/visited-line" lastVisitedTopic=lastVisitedTopic topic=topic}} {{/each}} diff --git a/app/assets/javascripts/discourse/app/templates/components/uppy-image-uploader.hbs b/app/assets/javascripts/discourse/app/templates/components/uppy-image-uploader.hbs index 1038cfe06c..9244794936 100644 --- a/app/assets/javascripts/discourse/app/templates/components/uppy-image-uploader.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/uppy-image-uploader.hbs @@ -5,7 +5,7 @@
{{#if imageUrl}} diff --git a/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs index e4ed951fe8..b9b85d5df9 100644 --- a/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/user-card-contents.hbs @@ -1,4 +1,5 @@ {{#if this.visible}} + {{plugin-outlet name="before-user-card-content" args=(hash user=this.user)}}
{{#if this.loading}}
@@ -73,6 +74,10 @@ label="user.private_message"}} {{/if}} + {{plugin-outlet + name="user-card-below-message-button" connectorTagName="li" + args=(hash user=this.user close=(action "close")) + tagName=""}} {{#if this.showFilter}}
  • {{d-button diff --git a/app/assets/javascripts/discourse/app/templates/components/user-fields/dropdown.hbs b/app/assets/javascripts/discourse/app/templates/components/user-fields/dropdown.hbs index f34fe5267b..540b3f9756 100644 --- a/app/assets/javascripts/discourse/app/templates/components/user-fields/dropdown.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/user-fields/dropdown.hbs @@ -12,8 +12,10 @@ valueProperty=null nameProperty=null value=this.value - none=this.noneLabel onChange=(action (mut this.value)) + options=(hash + none=this.noneLabel + ) }}
    {{html-safe this.field.description}}
  • diff --git a/app/assets/javascripts/discourse/app/templates/components/user-fields/multiselect.hbs b/app/assets/javascripts/discourse/app/templates/components/user-fields/multiselect.hbs index f81fa36fd4..250206d819 100644 --- a/app/assets/javascripts/discourse/app/templates/components/user-fields/multiselect.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/user-fields/multiselect.hbs @@ -12,8 +12,10 @@ valueProperty=null nameProperty=null value=this.value - none=this.noneLabel onChange=(action (mut this.value)) + options=(hash + none=this.noneLabel + ) }}
    {{html-safe this.field.description}}
    diff --git a/app/assets/javascripts/discourse/app/templates/components/user-info.hbs b/app/assets/javascripts/discourse/app/templates/components/user-info.hbs index 86246ae820..eaf91b06e0 100644 --- a/app/assets/javascripts/discourse/app/templates/components/user-info.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/user-info.hbs @@ -1,14 +1,32 @@ -
    -
    - {{avatar @user imageSize="large"}} - {{user-avatar-flair user=@user}} +{{#if includeAvatar}} +
    +
    + {{avatar @user imageSize="large"}} + {{user-avatar-flair user=@user}} +
    -
    +{{/if}}
    - {{if nameFirst this.name (format-username @user.username)}} - {{if nameFirst (format-username @user.username) this.name}} + + {{#if includeLink}} + + {{if nameFirst this.name (format-username @user.username)}} + + {{else}} + {{if nameFirst this.name (format-username @user.username)}} + {{/if}} + + + {{#if includeLink}} + + {{if nameFirst (format-username @user.username) this.name}} + + {{else}} + {{if nameFirst (format-username @user.username) this.name}} + {{/if}} + {{plugin-outlet name="after-user-name" tagName="span" connectorTagName="span" args=(hash user=user)}}
    {{@user.title}}
    diff --git a/app/assets/javascripts/discourse/app/templates/composer.hbs b/app/assets/javascripts/discourse/app/templates/composer.hbs index b447c883c8..71d136501c 100644 --- a/app/assets/javascripts/discourse/app/templates/composer.hbs +++ b/app/assets/javascripts/discourse/app/templates/composer.hbs @@ -118,8 +118,8 @@ {{category-chooser value=model.categoryId onChange=(action (mut model.categoryId)) - isDisabled=disableCategoryChooser options=(hash + disabled=disableCategoryChooser scopedCategoryId=scopedCategoryId prioritizedCategoryId=prioritizedCategoryId ) @@ -131,9 +131,9 @@ {{#if canEditTags}} {{mini-tag-chooser value=model.tags - isDisabled=disableTagsChooser onChange=(action (mut model.tags)) options=(hash + disabled=disableTagsChooser categoryId=model.categoryId minimum=model.minimumRequiredTags ) diff --git a/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs index 3c2997c58c..946d599382 100644 --- a/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/app/templates/discovery/topics.hbs @@ -62,7 +62,9 @@ model=model showResetNew=showResetNew showDismissRead=showDismissRead resetNew=( topics=model.topics discoveryList=true scrollOnLoad=true - onScroll=discoveryTopicList.saveScrollPosition}} + onScroll=discoveryTopicList.saveScrollPosition + focusLastVisitedTopic=true + }} {{/if}} {{plugin-outlet name="after-topic-list" tagName="span" connectorTagName="div" args=(hash category=category)}} diff --git a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs index 4669c11f3e..1fb8cb4076 100644 --- a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs @@ -131,6 +131,7 @@ bulkSelectEnabled=bulkSelectEnabled selected=selected highlightQuery=highlightQuery + searchLogId=model.grouped_search_result.search_log_id }} {{#conditional-loading-spinner condition=loading}} diff --git a/app/assets/javascripts/discourse/app/templates/group/manage/tags.hbs b/app/assets/javascripts/discourse/app/templates/group/manage/tags.hbs index 3b1206b178..74a2ba061d 100644 --- a/app/assets/javascripts/discourse/app/templates/group/manage/tags.hbs +++ b/app/assets/javascripts/discourse/app/templates/group/manage/tags.hbs @@ -13,9 +13,11 @@ {{tag-chooser tags=model.watching_tags blacklist=selectedTags - allowCreate=false everyTag=true unlimitedTagCount=true + options=(hash + allowAny=false + ) }}
    @@ -29,9 +31,11 @@ {{tag-chooser tags=model.tracking_tags blacklist=selectedTags - allowCreate=false everyTag=true unlimitedTagCount=true + options=(hash + allowAny=false + ) }}
    @@ -45,9 +49,11 @@ {{tag-chooser tags=model.watching_first_post_tags blacklist=selectedTags - allowCreate=false everyTag=true unlimitedTagCount=true + options=(hash + allowAny=false + ) }}
    @@ -61,9 +67,11 @@ {{tag-chooser tags=model.regular_tags blacklist=selectedTags - allowCreate=false everyTag=true unlimitedTagCount=true + options=(hash + allowAny=false + ) }}
    @@ -77,9 +85,11 @@ {{tag-chooser tags=model.muted_tags blacklist=selectedTags - allowCreate=false everyTag=true unlimitedTagCount=true + options=(hash + allowAny=false + ) }}
    diff --git a/app/assets/javascripts/discourse/app/templates/invites/show.hbs b/app/assets/javascripts/discourse/app/templates/invites/show.hbs index cda2c35542..a5554b07dd 100644 --- a/app/assets/javascripts/discourse/app/templates/invites/show.hbs +++ b/app/assets/javascripts/discourse/app/templates/invites/show.hbs @@ -97,10 +97,11 @@ {{password-field value=accountPassword class=(value-entered accountPassword) type="password" id="new-account-password" capsLockOn=capsLockOn}} {{input-tip validation=passwordValidation}}
    - {{passwordInstructions}} {{i18n "invites.optional_description"}} + {{passwordInstructions}}
    {{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}
    diff --git a/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr b/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr index a697ff9e01..0087b01566 100644 --- a/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr +++ b/app/assets/javascripts/discourse/app/templates/list/posts-count-column.hbr @@ -1,5 +1,5 @@ <{{view.tagName}} class='num posts-map posts {{view.likesHeat}} topic-list-data' title='{{view.title}}'> - diff --git a/app/assets/javascripts/discourse/app/templates/modal/grant-admin-second-factor.hbs b/app/assets/javascripts/discourse/app/templates/modal/grant-admin-second-factor.hbs deleted file mode 100644 index 3c49f73034..0000000000 --- a/app/assets/javascripts/discourse/app/templates/modal/grant-admin-second-factor.hbs +++ /dev/null @@ -1,33 +0,0 @@ -{{#d-modal-body title="admin.user.grant_admin"}} - {{#second-factor-form - secondFactorMethod=secondFactorMethod - secondFactorToken=secondFactorToken - class=secondFactorClass - backupEnabled=backupEnabled - }} - {{#if showSecurityKey}} - {{#security-key-form - allowedCredentialIds=securityKeyAllowedCredentialIds - challenge=securityKeyChallenge - showSecurityKey=showSecurityKey - showSecondFactor=showSecondFactor - secondFactorMethod=secondFactorMethod - otherMethodAllowed=otherMethodAllowed - action=(action "authenticateSecurityKey")}} - {{/security-key-form}} - {{else}} - {{second-factor-input value=secondFactorToken inputId="second-factor-confirmation" secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} - {{/if}} - {{/second-factor-form}} - - {{#unless showSecurityKey}} - - {{/unless}} -{{/d-modal-body}} diff --git a/app/assets/javascripts/discourse/app/templates/modal/grant-badge.hbs b/app/assets/javascripts/discourse/app/templates/modal/grant-badge.hbs index 4a4d464690..edfb21507b 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/grant-badge.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/grant-badge.hbs @@ -5,11 +5,13 @@ {{else}}

    {{combo-box - filterable=true value=selectedBadgeId content=grantableBadges - none="badges.none" onChange=(action (mut selectedBadgeId)) + options=(hash + filterable=true + none="badges.none" + ) }}

    {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/modal/ignore-duration.hbs b/app/assets/javascripts/discourse/app/templates/modal/ignore-duration.hbs index cabbb66960..566b60f21a 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/ignore-duration.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/ignore-duration.hbs @@ -11,7 +11,7 @@ {{/d-modal-body}}
    @@ -171,8 +173,10 @@ {{flair-chooser value=newFlairGroupId content=model.availableFlairs - none="user.flair.none" onChange=(action (mut newFlairGroupId)) + options=(hash + none="user.flair.none" + ) }}
    @@ -188,7 +192,10 @@ {{combo-box value=newPrimaryGroupInput content=model.filteredGroups - none="user.primary_group.none"}} + options=(hash + none="user.primary_group.none" + ) + }}
    {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/preferences/emails.hbs b/app/assets/javascripts/discourse/app/templates/preferences/emails.hbs index 31bed6e611..0d7f54528a 100644 --- a/app/assets/javascripts/discourse/app/templates/preferences/emails.hbs +++ b/app/assets/javascripts/discourse/app/templates/preferences/emails.hbs @@ -58,10 +58,12 @@
    {{combo-box valueProperty="value" - filterable=true content=digestFrequencies value=model.user_option.digest_after_minutes onChange=(action (mut model.user_option.digest_after_minutes)) + options=(hash + filterable=true + ) }}
    {{preference-checkbox labelKey="user.include_tl0_in_digests" disabled=model.user_option.mailing_list_mode checked=model.user_option.include_tl0_in_digests}} diff --git a/app/assets/javascripts/discourse/app/templates/preferences/tags.hbs b/app/assets/javascripts/discourse/app/templates/preferences/tags.hbs index 59c5f2dbd3..e22d9c54d9 100644 --- a/app/assets/javascripts/discourse/app/templates/preferences/tags.hbs +++ b/app/assets/javascripts/discourse/app/templates/preferences/tags.hbs @@ -7,9 +7,11 @@ {{tag-chooser tags=model.watched_tags blockedTags=selectedTags - allowCreate=false everyTag=true unlimitedTagCount=true + options=(hash + allowAny=false + ) }}
    @@ -20,9 +22,12 @@ {{tag-chooser tags=model.tracked_tags blockedTags=selectedTags - allowCreate=false everyTag=true - unlimitedTagCount=true}} + unlimitedTagCount=true + options=(hash + allowAny=false + ) + }}
    {{i18n "user.tracked_tags_instructions"}}
    @@ -32,9 +37,12 @@ {{tag-chooser tags=model.watching_first_post_tags blockedTags=selectedTags - allowCreate=false everyTag=true - unlimitedTagCount=true}} + unlimitedTagCount=true + options=(hash + allowAny=false + ) + }}
    @@ -46,9 +54,12 @@ {{tag-chooser tags=model.muted_tags blockedTags=selectedTags - allowCreate=false everyTag=true - unlimitedTagCount=true}} + unlimitedTagCount=true + options=(hash + allowAny=false + ) + }}
    {{i18n "user.muted_tags_instructions"}}
    diff --git a/app/assets/javascripts/discourse/app/templates/review-index.hbs b/app/assets/javascripts/discourse/app/templates/review-index.hbs index b287e0c771..2a13c4772b 100644 --- a/app/assets/javascripts/discourse/app/templates/review-index.hbs +++ b/app/assets/javascripts/discourse/app/templates/review-index.hbs @@ -35,8 +35,10 @@ {{combo-box value=filterType content=allTypes - none="review.filters.type.all" onChange=(action (mut filterType)) + options=(hash + none="review.filters.type.all" + ) }}
    @@ -52,9 +54,11 @@
    {{category-chooser - none="review.filters.all_categories" value=filterCategoryId onChange=(action (mut filterCategoryId)) + options=(hash + none="review.filters.all_categories" + ) }}
    diff --git a/app/assets/javascripts/discourse/app/templates/tag/show.hbs b/app/assets/javascripts/discourse/app/templates/tag/show.hbs index 917209dd74..49dedfdf66 100644 --- a/app/assets/javascripts/discourse/app/templates/tag/show.hbs +++ b/app/assets/javascripts/discourse/app/templates/tag/show.hbs @@ -88,6 +88,7 @@ changeSort=(action "changeSort") onScroll=discoveryTopicList.saveScrollPosition scrollOnLoad=true + focusLastVisitedTopic=true }} {{/if}} {{/discovery-topics-list}} diff --git a/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr b/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr index 636225a3b1..cdca90b5c5 100644 --- a/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr +++ b/app/assets/javascripts/discourse/app/templates/topic-list-header.hbr @@ -10,13 +10,13 @@ {{#if showPosters}} {{raw "topic-list-header-column" order='posters' ariaLabel=(i18n "category.sort_options.posters")}} {{/if}} -{{raw "topic-list-header-column" sortable=sortable number='true' order='posts' name='replies'}} +{{raw "topic-list-header-column" sortable=sortable number='true' order='posts' name='replies' ariaLabel=(i18n "sr_replies")}} {{#if showLikes}} - {{raw "topic-list-header-column" sortable=sortable number='true' order='likes' name='likes'}} + {{raw "topic-list-header-column" sortable=sortable number='true' order='likes' name='likes' ariaLabel=(i18n "sr_likes")}} {{/if}} {{#if showOpLikes}} - {{raw "topic-list-header-column" sortable=sortable number='true' order='op_likes' name='likes'}} + {{raw "topic-list-header-column" sortable=sortable number='true' order='op_likes' name='likes' ariaLabel=(i18n "sr_op_likes")}} {{/if}} -{{raw "topic-list-header-column" sortable=sortable number='true' order='views' name='views'}} -{{raw "topic-list-header-column" sortable=sortable number='true' order='activity' name='activity'}} +{{raw "topic-list-header-column" sortable=sortable number='true' order='views' name='views' ariaLabel=(i18n "sr_views")}} +{{raw "topic-list-header-column" sortable=sortable number='true' order='activity' name='activity' ariaLabel=(i18n "sr_activity")}} {{~raw-plugin-outlet name="topic-list-header-after"~}} diff --git a/app/assets/javascripts/discourse/app/templates/topic.hbs b/app/assets/javascripts/discourse/app/templates/topic.hbs index dd268baff0..3b74568e5b 100644 --- a/app/assets/javascripts/discourse/app/templates/topic.hbs +++ b/app/assets/javascripts/discourse/app/templates/topic.hbs @@ -223,6 +223,7 @@ recoverPost=(action "recoverPost") expandHidden=(action "expandHidden") toggleBookmark=(action "toggleBookmark") + toggleBookmarkPolymorphic=(action "toggleBookmarkPolymorphic") togglePostType=(action "togglePostType") rebakePost=(action "rebakePost") changePostOwner=(action "changePostOwner") @@ -362,6 +363,7 @@ convertToPublicTopic=(action "convertToPublicTopic") convertToPrivateMessage=(action "convertToPrivateMessage") toggleBookmark=(action "toggleBookmark") + toggleBookmarkPolymorphic=(action "toggleBookmarkPolymorphic") showFlagTopic=(route-action "showFlagTopic") toggleArchiveMessage=(action "toggleArchiveMessage") editFirstPost=(action "editFirstPost") diff --git a/app/assets/javascripts/discourse/app/templates/user.hbs b/app/assets/javascripts/discourse/app/templates/user.hbs index 700e8d06b2..a95ce0cede 100644 --- a/app/assets/javascripts/discourse/app/templates/user.hbs +++ b/app/assets/javascripts/discourse/app/templates/user.hbs @@ -50,6 +50,7 @@ {{/unless}}
    + {{plugin-outlet name="before-user-profile-avatar" args=(hash model=model)}} {{user-profile-avatar user=model tagName=""}}
      diff --git a/app/assets/javascripts/discourse/app/templates/user/badges.hbs b/app/assets/javascripts/discourse/app/templates/user/badges.hbs index 8c20796569..2a865c1c3c 100644 --- a/app/assets/javascripts/discourse/app/templates/user/badges.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/badges.hbs @@ -16,5 +16,6 @@ filterUser="true" }} {{/each}} + {{plugin-outlet name="after-user-profile-badges" args=(hash user=user.model)}}
    {{/d-section}} diff --git a/app/assets/javascripts/discourse/app/templates/user/summary.hbs b/app/assets/javascripts/discourse/app/templates/user/summary.hbs index 2c72dc540f..2bdb0cdc1b 100644 --- a/app/assets/javascripts/discourse/app/templates/user/summary.hbs +++ b/app/assets/javascripts/discourse/app/templates/user/summary.hbs @@ -46,7 +46,7 @@ {{user-stat value=model.post_count label="user.summary.post_count"}} {{/link-to}} - {{plugin-outlet name="user-summary-stat" connectorTagName="li" args=(hash model=model)}} + {{plugin-outlet name="user-summary-stat" connectorTagName="li" args=(hash model=model user=user)}}
    {{/if}} @@ -157,6 +157,7 @@ {{#each model.badges as |badge|}} {{badge-card badge=badge count=badge.count username=user.username_lower}} {{/each}} + {{plugin-outlet name="after-user-summary-badges" args=(hash model=model user=user)}}
    {{else}}

    {{i18n "user.summary.no_badges"}}

    diff --git a/app/assets/javascripts/discourse/app/widgets/actions-summary.js b/app/assets/javascripts/discourse/app/widgets/actions-summary.js index 16f6be54ed..5ff7fdc2fb 100644 --- a/app/assets/javascripts/discourse/app/widgets/actions-summary.js +++ b/app/assets/javascripts/discourse/app/widgets/actions-summary.js @@ -23,6 +23,14 @@ createWidget("small-user-list", { return atts.listClassName; }, + buildAttributes(attrs) { + const attributes = { role: "list" }; + if (attrs.ariaLabel) { + attributes["aria-label"] = attrs.ariaLabel; + } + return attributes; + }, + html(atts) { let users = atts.users; if (users) { @@ -37,7 +45,11 @@ createWidget("small-user-list", { let description = null; if (atts.description) { - description = I18n.t(atts.description, { count: atts.count }); + description = h( + "span.list-description", + { attributes: { "aria-hidden": true } }, + I18n.t(atts.description, { count: atts.count }) + ); } // oddly post_url is on the user @@ -46,10 +58,16 @@ createWidget("small-user-list", { postUrl = postUrl || u.post_url; if (u.unknown) { return h("div.unknown", { - attributes: { title: I18n.t("post.unknown_user") }, + attributes: { + title: I18n.t("post.unknown_user"), + role: "listitem", + }, }); } else { - return avatarFor.call(this, "small", u); + return avatarFor.call(this, "small", u, { + role: "listitem", + "aria-hidden": false, + }); } }); diff --git a/app/assets/javascripts/discourse/app/widgets/button.js b/app/assets/javascripts/discourse/app/widgets/button.js index 242736aec1..acedbd3408 100644 --- a/app/assets/javascripts/discourse/app/widgets/button.js +++ b/app/assets/javascripts/discourse/app/widgets/button.js @@ -45,6 +45,14 @@ export const ButtonClass = { attributes["role"] = attrs.role; } + if (attrs.translatedAriaLabel) { + attributes["aria-label"] = attrs.translatedAriaLabel; + } + + if (attrs.ariaPressed) { + attributes["aria-pressed"] = attrs.ariaPressed; + } + if (attrs.tabAttrs) { const tab = attrs.tabAttrs; attributes["aria-selected"] = tab["aria-selected"]; diff --git a/app/assets/javascripts/discourse/app/widgets/embedded-post.js b/app/assets/javascripts/discourse/app/widgets/embedded-post.js index e5192f4486..a845fc70eb 100644 --- a/app/assets/javascripts/discourse/app/widgets/embedded-post.js +++ b/app/assets/javascripts/discourse/app/widgets/embedded-post.js @@ -21,24 +21,34 @@ createWidget("post-link-arrow", { }); export default createWidget("embedded-post", { + tagName: "div.reply", buildKey: (attrs) => `embedded-post-${attrs.id}`, + buildAttributes(attrs) { + const attributes = { "data-post-id": attrs.id }; + if (this.state.role) { + attributes.role = this.state.role; + } + if (this.state["aria-label"]) { + attributes["aria-label"] = this.state["aria-label"]; + } + return attributes; + }, + html(attrs, state) { attrs.embeddedPost = true; return [ - h("div.reply", { attributes: { "data-post-id": attrs.id } }, [ - h("div.row", [ - this.attach("post-avatar", attrs), - h("div.topic-body", [ - h("div.topic-meta-data.embedded-reply", [ - this.attach("poster-name", attrs), - this.attach("post-link-arrow", { - above: state.above, - shareUrl: attrs.customShare, - }), - ]), - new PostCooked(attrs, new DecoratorHelper(this), this.currentUser), + h("div.row", [ + this.attach("post-avatar", attrs), + h("div.topic-body", [ + h("div.topic-meta-data.embedded-reply", [ + this.attach("poster-name", attrs), + this.attach("post-link-arrow", { + above: state.above, + shareUrl: attrs.customShare, + }), ]), + new PostCooked(attrs, new DecoratorHelper(this), this.currentUser), ]), ]), ]; diff --git a/app/assets/javascripts/discourse/app/widgets/header.js b/app/assets/javascripts/discourse/app/widgets/header.js index 377e76304d..4885bbf198 100644 --- a/app/assets/javascripts/discourse/app/widgets/header.js +++ b/app/assets/javascripts/discourse/app/widgets/header.js @@ -10,6 +10,7 @@ import { iconNode } from "discourse-common/lib/icon-library"; import { schedule } from "@ember/runloop"; import { scrollTop } from "discourse/mixins/scroll-top"; import { wantsNewWindow } from "discourse/lib/intercept-click"; +import { logSearchLinkClick } from "discourse/lib/search"; const _extraHeaderIcons = []; @@ -426,14 +427,7 @@ export default createWidget("header", { const { searchLogId, searchResultId, searchResultType } = attrs; if (searchLogId && searchResultId && searchResultType) { - ajax("/search/click", { - type: "POST", - data: { - search_log_id: searchLogId, - search_result_id: searchResultId, - search_result_type: searchResultType, - }, - }); + logSearchLinkClick({ searchLogId, searchResultId, searchResultType }); } } diff --git a/app/assets/javascripts/discourse/app/widgets/post-menu.js b/app/assets/javascripts/discourse/app/widgets/post-menu.js index a468c69e6b..8b9272783e 100644 --- a/app/assets/javascripts/discourse/app/widgets/post-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/post-menu.js @@ -5,6 +5,7 @@ import { formattedReminderTime } from "discourse/lib/bookmark"; import { h } from "virtual-dom"; import showModal from "discourse/lib/show-modal"; import { smallUserAtts } from "discourse/widgets/actions-summary"; +import I18n from "I18n"; const LIKE_ACTION = 2; const VIBRATE_DURATION = 5; @@ -64,10 +65,14 @@ export function buildButton(name, widget) { } } -registerButton("read-count", (attrs) => { +registerButton("read-count", (attrs, state) => { if (attrs.showReadIndicator) { const count = attrs.readCount; if (count > 0) { + let ariaPressed = "false"; + if (state?.readers && state.readers.length > 0) { + ariaPressed = "true"; + } return { action: "toggleWhoRead", title: "post.controls.read_indicator", @@ -75,6 +80,10 @@ registerButton("read-count", (attrs) => { contents: count, iconRight: true, addContainer: false, + translatedAriaLabel: I18n.t("post.sr_post_read_count_button", { + count, + }), + ariaPressed, }; } } @@ -93,7 +102,7 @@ registerButton("read", (attrs) => { } }); -function likeCount(attrs) { +function likeCount(attrs, state) { const count = attrs.likeCount; if (count > 0) { @@ -111,6 +120,10 @@ function likeCount(attrs) { addContainer = true; } + let ariaPressed = "false"; + if (state?.likedUsers && state.likedUsers.length > 0) { + ariaPressed = "true"; + } return { action: "toggleWhoLiked", title, @@ -120,6 +133,8 @@ function likeCount(attrs) { iconRight: true, addContainer, titleOptions: { count: attrs.liked ? count - 1 : count }, + translatedAriaLabel: I18n.t("post.sr_post_like_count_button", { count }), + ariaPressed, }; } } @@ -255,6 +270,10 @@ registerButton("replies", (attrs, state, siteSettings) => { return; } + let ariaPressed; + if (!siteSettings.enable_filtered_replies_view) { + ariaPressed = state.repliesShown ? "true" : "false"; + } return { action, icon, @@ -268,6 +287,11 @@ registerButton("replies", (attrs, state, siteSettings) => { labelOptions: { count: replyCount }, label: attrs.mobileView ? "post.has_replies_count" : "post.has_replies", iconRight: !siteSettings.enable_filtered_replies_view || attrs.mobileView, + disabled: !!attrs.deleted, + translatedAriaLabel: I18n.t("post.sr_expand_replies", { + count: replyCount, + }), + ariaPressed, }; }); @@ -301,7 +325,7 @@ registerButton("reply", (attrs, state, siteSettings, postMenuSettings) => { registerButton( "bookmark", - (attrs, _state, _siteSettings, _settings, currentUser) => { + (attrs, _state, siteSettings, _settings, currentUser) => { if (!attrs.canBookmark) { return; } @@ -331,7 +355,9 @@ registerButton( return { id: attrs.bookmarked ? "unbookmark" : "bookmark", - action: "toggleBookmark", + action: siteSettings.use_polymorphic_bookmarks + ? "toggleBookmarkPolymorphic" + : "toggleBookmark", title, titleOptions, className: classNames.join(" "), @@ -627,6 +653,9 @@ export default createWidget("post-menu", { listClassName: "who-read", description, count, + ariaLabel: I18n.t( + "post.actions.people.sr_post_readers_list_description" + ), }) ); } @@ -646,6 +675,9 @@ export default createWidget("post-menu", { listClassName: "who-liked", description, count, + ariaLabel: I18n.t( + "post.actions.people.sr_post_likers_list_description" + ), }) ); } diff --git a/app/assets/javascripts/discourse/app/widgets/post.js b/app/assets/javascripts/discourse/app/widgets/post.js index 7de22a2634..3ba650dd68 100644 --- a/app/assets/javascripts/discourse/app/widgets/post.js +++ b/app/assets/javascripts/discourse/app/widgets/post.js @@ -68,16 +68,20 @@ export function avatarImg(wanted, attrs) { return h("img", properties); } -export function avatarFor(wanted, attrs) { +export function avatarFor(wanted, attrs, linkAttrs) { + const attributes = { + href: attrs.url, + "data-user-card": attrs.username, + "aria-hidden": true, + }; + if (linkAttrs) { + Object.assign(attributes, linkAttrs); + } return h( "a", { className: `trigger-user-card ${attrs.className || ""}`, - attributes: { - href: attrs.url, - "data-user-card": attrs.username, - "aria-hidden": true, - }, + attributes, }, avatarImg(wanted, attrs) ); @@ -180,14 +184,21 @@ createWidget("post-avatar", { if (!attrs.user_id) { body = iconNode("far-trash-alt", { class: "deleted-user-avatar" }); } else { - body = avatarFor.call(this, this.settings.size, { - template: attrs.avatar_template, - username: attrs.username, - name: attrs.name, - url: attrs.usernameUrl, - className: `main-avatar ${hideFromAnonUser ? "non-clickable" : ""}`, - hideTitle: true, - }); + body = avatarFor.call( + this, + this.settings.size, + { + template: attrs.avatar_template, + username: attrs.username, + name: attrs.name, + url: attrs.usernameUrl, + className: `main-avatar ${hideFromAnonUser ? "non-clickable" : ""}`, + hideTitle: true, + }, + { + tabindex: "-1", + } + ); } const postAvatarBody = [body]; @@ -341,18 +352,26 @@ createWidget("post-date", { tagName: "div.post-info.post-date", html(attrs) { - const attributes = { class: "post-date" }; - let date; + let date, + linkClassName = "post-date"; + if (attrs.wiki && attrs.lastWikiEdit) { - attributes["class"] += " last-wiki-edit"; + linkClassName += " last-wiki-edit"; date = new Date(attrs.lastWikiEdit); } else { date = new Date(attrs.created_at); } - return h("a", { attributes }, dateNode(date)); + return this.attach("link", { + rawLabel: dateNode(date), + className: linkClassName, + omitSpan: true, + title: "post.sr_date", + href: attrs.shareUrl, + action: "showShareModal", + }); }, - click() { + showShareModal() { const post = this.findAncestorModel(); const topic = post.topic; const controller = showModal("share-topic", { model: topic.category }); @@ -460,7 +479,16 @@ createWidget("post-contents", { result.push( h("section.embedded-posts.bottom", [ repliesBelow.map((p) => { - return this.attach("embedded-post", p, { model: p.asPost }); + return this.attach("embedded-post", p, { + model: p.asPost, + state: { + role: "region", + "aria-label": I18n.t("post.sr_embedded_reply_description", { + post_number: attrs.post_number, + username: p.username, + }), + }, + }); }), this.attach("button", { title: "post.collapse", @@ -468,6 +496,7 @@ createWidget("post-contents", { action: "toggleRepliesBelow", actionParam: "true", className: "btn collapse-up", + translatedAriaLabel: I18n.t("post.sr_collapse_replies"), }), ]) ); @@ -652,6 +681,7 @@ createWidget("post-article", { return { "aria-label": I18n.t("share.post", { postNumber: attrs.post_number, + username: attrs.username, }), role: "region", "data-post-id": attrs.id, @@ -662,8 +692,8 @@ createWidget("post-article", { html(attrs, state) { const rows = [ - h("a.tabLoc", { - attributes: { href: "", "aria-hidden": true, tabindex: -1 }, + h("span.tabLoc", { + attributes: { "aria-hidden": true, tabindex: -1 }, }), ]; if (state.repliesAbove.length) { @@ -827,6 +857,9 @@ export default createWidget("post", { } else { classNames.push("regular"); } + if (attrs.userSuspended) { + classNames.push("user-suspended"); + } if (addPostClassesCallbacks) { for (let i = 0; i < addPostClassesCallbacks.length; i++) { let pluginClasses = addPostClassesCallbacks[i].call(this, attrs); diff --git a/app/assets/javascripts/discourse/app/widgets/topic-map.js b/app/assets/javascripts/discourse/app/widgets/topic-map.js index 7f21df2aac..69439af54d 100644 --- a/app/assets/javascripts/discourse/app/widgets/topic-map.js +++ b/app/assets/javascripts/discourse/app/widgets/topic-map.js @@ -284,7 +284,7 @@ createWidget("topic-map-link", { const truncateLength = 85; if (content.length > truncateLength) { - content = `${content.substr(0, truncateLength).trim()}...`; + content = `${content.slice(0, truncateLength).trim()}...`; } return attrs.title ? replaceEmoji(content) : content; diff --git a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js index c7a5e649fe..c93b32b5d1 100644 --- a/app/assets/javascripts/discourse/lib/bootstrap-json/index.js +++ b/app/assets/javascripts/discourse/lib/bootstrap-json/index.js @@ -230,7 +230,7 @@ async function handleRequest(proxy, baseURL, req, res) { let url = `${proxy}${req.path}`; const queryLoc = req.url.indexOf("?"); if (queryLoc !== -1) { - url += req.url.substr(queryLoc); + url += req.url.slice(queryLoc); } if (req.method === "GET") { diff --git a/app/assets/javascripts/discourse/lib/rfc176-shims/index.js b/app/assets/javascripts/discourse/lib/rfc176-shims/index.js index df26adac62..cb0bb9460c 100644 --- a/app/assets/javascripts/discourse/lib/rfc176-shims/index.js +++ b/app/assets/javascripts/discourse/lib/rfc176-shims/index.js @@ -40,7 +40,11 @@ module.exports = { m = modules[entry.module] = []; } - m.push(entry); + if (entry.module === "@ember/test") { + m.push({ ...entry, global: `(Ember.Test && ${entry.global})` }); + } else { + m.push(entry); + } } let output = ""; diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 95f27709c9..f4896e1c3a 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -24,12 +24,12 @@ "@glimmer/component": "^1.0.4", "@glimmer/tracking": "^1.0.4", "@popperjs/core": "2.10.2", - "@uppy/aws-s3": "^2.0.4", - "@uppy/aws-s3-multipart": "^2.1.0", - "@uppy/core": "^2.1.0", - "@uppy/drop-target": "^1.1.0", - "@uppy/utils": "^4.0.3", - "@uppy/xhr-upload": "^2.0.4", + "@uppy/aws-s3": "^2.0.8", + "@uppy/aws-s3-multipart": "^2.2.1", + "@uppy/core": "^2.1.6", + "@uppy/drop-target": "^1.1.2", + "@uppy/utils": "^4.0.5", + "@uppy/xhr-upload": "^2.0.7", "admin": "^1.0.0", "broccoli-asset-rev": "^3.0.0", "deepmerge": "^4.2.2", diff --git a/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js b/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js index 1bc1786c8d..a456dff981 100644 --- a/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js +++ b/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js @@ -9,7 +9,7 @@ let len = prefix.length; Object.keys(requirejs.entries).forEach(function (key) { if (key.indexOf(prefix) === 0) { - Ember.TEMPLATES[key.substr(len)] = require(key).default; + Ember.TEMPLATES[key.slice(len)] = require(key).default; } else if (key.indexOf(adminPrefix) === 0) { Ember.TEMPLATES[key] = require(key).default; } diff --git a/app/assets/javascripts/discourse/testem.js b/app/assets/javascripts/discourse/testem.js index 8c5c5bf034..3a55f8e7a5 100644 --- a/app/assets/javascripts/discourse/testem.js +++ b/app/assets/javascripts/discourse/testem.js @@ -6,6 +6,8 @@ class Reporter { this._tapReporter = new TapReporter(...arguments); } + failReports = []; + reportMetadata(tag, metadata) { if (tag === "summary-line") { process.stdout.write(`\n${metadata.message}\n`); @@ -15,11 +17,24 @@ class Reporter { } report(prefix, data) { + if (data.failed) { + this.failReports.push([prefix, data]); + } this._tapReporter.report(prefix, data); } finish() { this._tapReporter.finish(); + + if (this.failReports.length > 0) { + process.stdout.write("\nFailures:\n\n"); + this.failReports.forEach(([prefix, data]) => { + if (process.env.GITHUB_ACTIONS) { + process.stdout.write(`::error ::QUnit Test Failure: ${data.name}\n`); + } + this.report(prefix, data); + }); + } } } diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js index 5c60ea6d19..09aebb1ff9 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-silence-user-test.js @@ -34,6 +34,7 @@ acceptance("Admin - Silence User", function (needs) { const expected = [ I18n.t("topic.auto_update_input.later_today"), I18n.t("topic.auto_update_input.tomorrow"), + I18n.t("topic.auto_update_input.later_this_week"), I18n.t("topic.auto_update_input.next_week"), I18n.t("topic.auto_update_input.two_weeks"), I18n.t("topic.auto_update_input.next_month"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-site-settings-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-site-settings-test.js index 8a1c4458cc..c931fa082e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-site-settings-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-site-settings-test.js @@ -72,46 +72,46 @@ acceptance("Admin - Site Settings", function (needs) { 1, "filter returns 1 site setting" ); - assert.ok(!exists(".row.setting.overridden"), "setting isn't overriden"); + assert.ok(!exists(".row.setting.overridden"), "setting isn't overridden"); await fillIn(".input-setting-string", "Test"); await click("button.cancel"); assert.ok( !exists(".row.setting.overridden"), - "canceling doesn't mark setting as overriden" + "canceling doesn't mark setting as overridden" ); await fillIn(".input-setting-string", "Test"); await click("button.ok"); assert.ok( exists(".row.setting.overridden"), - "saving marks setting as overriden" + "saving marks setting as overridden" ); await click("button.undo"); assert.ok( !exists(".row.setting.overridden"), - "setting isn't marked as overriden after undo" + "setting isn't marked as overridden after undo" ); await click("button.cancel"); assert.ok( exists(".row.setting.overridden"), - "setting is marked as overriden after cancel" + "setting is marked as overridden after cancel" ); await click("button.undo"); await click("button.ok"); assert.ok( !exists(".row.setting.overridden"), - "setting isn't marked as overriden after undo" + "setting isn't marked as overridden after undo" ); await fillIn(".input-setting-string", "Test"); await triggerKeyEvent(".input-setting-string", "keydown", ENTER_KEYCODE); assert.ok( exists(".row.setting.overridden"), - "saving via Enter key marks setting as overriden" + "saving via Enter key marks setting as overridden" ); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js index ade2b2cafa..50b099f64c 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-suspend-user-test.js @@ -133,6 +133,7 @@ acceptance("Admin - Suspend User - timeframe choosing", function (needs) { const expected = [ I18n.t("topic.auto_update_input.later_today"), I18n.t("topic.auto_update_input.tomorrow"), + I18n.t("topic.auto_update_input.later_this_week"), I18n.t("topic.auto_update_input.next_week"), I18n.t("topic.auto_update_input.two_weeks"), I18n.t("topic.auto_update_input.next_month"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-user-index-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-user-index-test.js index bb766e5ca8..e355b3c17f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-user-index-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-user-index-test.js @@ -8,7 +8,9 @@ import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import { test } from "qunit"; import I18n from "I18n"; +import { SECOND_FACTOR_METHODS } from "discourse/models/user"; +const { TOTP, BACKUP_CODE, SECURITY_KEY } = SECOND_FACTOR_METHODS; acceptance("Admin - User Index", function (needs) { needs.user(); needs.pretender((server, helper) => { @@ -83,17 +85,17 @@ acceptance("Admin - User Index", function (needs) { }); server.put("/admin/users/4/grant_admin", () => { - return helper.response({ - failed: "FAILED", - ok: false, - error: "The selected two-factor method is invalid.", - reason: "invalid_second_factor_method", - backup_enabled: true, - security_key_enabled: true, + return helper.response(403, { + second_factor_challenge_nonce: "somenonce", + }); + }); + + server.get("/session/2fa.json", () => { + return helper.response(200, { totp_enabled: true, - multiple_second_factor_methods: true, - allowed_credential_ids: ["allowed_credential_ids"], - challenge: "challenge", + backup_enabled: true, + security_keys_enabled: true, + allowed_methods: [TOTP, BACKUP_CODE, SECURITY_KEY], }); }); }); @@ -202,9 +204,13 @@ acceptance("Admin - User Index", function (needs) { await click(".bootbox .btn-primary"); }); - test("grant admin - shows the second factor modal", async function (assert) { + test("grant admin - redirects to the 2fa page", async function (assert) { await visit("/admin/users/4/user2"); await click(".grant-admin"); - assert.ok(exists(".grant-admin-second-factor-modal")); + assert.equal( + currentURL(), + "/session/2fa?nonce=somenonce", + "user is redirected to the 2FA page" + ); }); }); 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 606daae398..045b6e72a8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/category-edit-test.js @@ -1,5 +1,7 @@ import { acceptance, + count, + exists, queryAll, visible, } from "discourse/tests/helpers/qunit-helpers"; @@ -8,10 +10,11 @@ import DiscourseURL from "discourse/lib/url"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import sinon from "sinon"; import { test } from "qunit"; +import pretender from "discourse/tests/helpers/create-pretender"; acceptance("Category Edit", function (needs) { needs.user(); - needs.settings({ email_in: true }); + needs.settings({ email_in: true, tagging_enabled: true }); test("Editing the category", async function (assert) { await visit("/c/bug"); @@ -70,6 +73,73 @@ acceptance("Category Edit", function (needs) { ); }); + test("Editing required tag groups", async function (assert) { + await visit("/c/bug/edit/tags"); + + assert.ok(exists(".required-tag-groups")); + assert.strictEqual(count(".required-tag-group-row"), 0); + + await click(".add-required-tag-group"); + assert.strictEqual(count(".required-tag-group-row"), 1); + + await click(".add-required-tag-group"); + assert.strictEqual(count(".required-tag-group-row"), 2); + + await click(".delete-required-tag-group"); + assert.strictEqual(count(".required-tag-group-row"), 1); + + const tagGroupChooser = selectKit( + ".required-tag-group-row .tag-group-chooser" + ); + await tagGroupChooser.expand(); + await tagGroupChooser.selectRowByValue("TagGroup1"); + + await click("#save-category"); + assert.strictEqual(count(".required-tag-group-row"), 1); + + await click(".delete-required-tag-group"); + assert.strictEqual(count(".required-tag-group-row"), 0); + + await click("#save-category"); + assert.strictEqual(count(".required-tag-group-row"), 0); + }); + + test("Editing allowed tags and tag groups", async function (assert) { + await visit("/c/bug/edit/tags"); + + const allowedTagChooser = selectKit("#category-allowed-tags"); + await allowedTagChooser.expand(); + await allowedTagChooser.selectRowByValue("monkey"); + + const allowedTagGroupChooser = selectKit("#category-allowed-tag-groups"); + await allowedTagGroupChooser.expand(); + await allowedTagGroupChooser.selectRowByValue("TagGroup1"); + + await click("#save-category"); + + const payload = JSON.parse( + pretender.handledRequests[pretender.handledRequests.length - 1] + .requestBody + ); + assert.deepEqual(payload.allowed_tags, ["monkey"]); + assert.deepEqual(payload.allowed_tag_groups, ["TagGroup1"]); + + await allowedTagChooser.expand(); + await allowedTagChooser.deselectItemByValue("monkey"); + + await allowedTagGroupChooser.expand(); + await allowedTagGroupChooser.deselectItemByValue("TagGroup1"); + + await click("#save-category"); + + const removePayload = JSON.parse( + pretender.handledRequests[pretender.handledRequests.length - 1] + .requestBody + ); + assert.deepEqual(removePayload.allowed_tags, []); + assert.deepEqual(removePayload.allowed_tag_groups, []); + }); + test("Index Route", async function (assert) { await visit("/c/bug/edit"); assert.strictEqual( diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-tags-test.js index eeb74e7dac..1ed58bca8e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-tags-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-tags-test.js @@ -74,8 +74,7 @@ acceptance("Composer - Tags", function (needs) { await fillIn(".d-editor-input", "this is the *content* of a post"); Category.findById(2).setProperties({ - required_tag_groups: ["support tags"], - min_tags_from_required_group: 1, + required_tag_groups: [{ name: "support tags", min_count: 1 }], }); const categoryChooser = selectKit(".category-chooser"); diff --git a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js index f4e577baab..ea8c5c2f05 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/create-invite-modal-test.js @@ -213,6 +213,7 @@ acceptance( const expected = [ I18n.t("topic.auto_update_input.later_today"), I18n.t("topic.auto_update_input.tomorrow"), + I18n.t("topic.auto_update_input.later_this_week"), I18n.t("topic.auto_update_input.next_week"), I18n.t("topic.auto_update_input.two_weeks"), I18n.t("topic.auto_update_input.next_month"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js b/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js index a520901015..eb551cf2a8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/emoji-test.js @@ -1,5 +1,10 @@ -import { acceptance, queryAll } from "discourse/tests/helpers/qunit-helpers"; -import { click, fillIn, visit } from "@ember/test-helpers"; +import { + acceptance, + exists, + normalizeHtml, + queryAll, +} from "discourse/tests/helpers/qunit-helpers"; +import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; import { test } from "qunit"; import { IMAGE_VERSION as v } from "pretty-text/emoji/version"; @@ -12,8 +17,10 @@ acceptance("Emoji", function (needs) { await fillIn(".d-editor-input", "this is an emoji :blonde_woman:"); assert.strictEqual( - queryAll(".d-editor-preview:visible").html().trim(), - `

    this is an emoji :blonde_woman:

    ` + normalizeHtml(queryAll(".d-editor-preview:visible").html().trim()), + normalizeHtml( + `

    this is an emoji :blonde_woman:

    ` + ) ); }); @@ -22,9 +29,31 @@ acceptance("Emoji", function (needs) { await click("#topic-footer-buttons .btn.create"); await fillIn(".d-editor-input", "this is an emoji :blonde_woman:t5:"); + assert.strictEqual( - queryAll(".d-editor-preview:visible").html().trim(), - `

    this is an emoji :blonde_woman:t5:

    ` + normalizeHtml(queryAll(".d-editor-preview:visible").html().trim()), + normalizeHtml( + `

    this is an emoji :blonde_woman:t5:

    ` + ) ); }); + + needs.settings({ + emoji_autocomplete_min_chars: 2, + }); + + test("siteSetting:emoji_autocomplete_min_chars", async function (assert) { + await visit("/t/internationalization-localization/280"); + await click("#topic-footer-buttons .btn.create"); + + await fillIn(".d-editor-input", ":s"); + await triggerKeyEvent(".d-editor-input", "keyup", 40); // ensures a keyup is triggered + + assert.notOk(exists(".autocomplete.ac-emoji")); + + await fillIn(".d-editor-input", ":sw"); + await triggerKeyEvent(".d-editor-input", "keyup", 40); // ensures a keyup is triggered + + assert.ok(exists(".autocomplete.ac-emoji")); + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/image-aspect-ratio-test.js b/app/assets/javascripts/discourse/tests/acceptance/image-aspect-ratio-test.js new file mode 100644 index 0000000000..b5bbf09066 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/image-aspect-ratio-test.js @@ -0,0 +1,12 @@ +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { visit } from "@ember/test-helpers"; +import { test } from "qunit"; + +acceptance("Image aspect ratio", function () { + test("it applies the aspect ratio", async function (assert) { + await visit("/t/2480"); + const image = query("#post_3 img[src='/assets/logo.png']"); + + assert.strictEqual(image.style.aspectRatio, "690 / 388"); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js b/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js index 0247a8ea0b..64b9000802 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js @@ -119,6 +119,12 @@ acceptance("Invite accept", function (needs) { ); await fillIn("#new-account-email", "john.doe@example.com"); + assert.ok( + exists(".invites-show .btn-primary:disabled"), + "submit is disabled because password is not filled" + ); + + await fillIn("#new-account-password", "top$ecret"); assert.notOk( exists(".invites-show .btn-primary:disabled"), "submit is enabled" diff --git a/app/assets/javascripts/discourse/tests/acceptance/last-visited-topic-focus-test.js b/app/assets/javascripts/discourse/tests/acceptance/last-visited-topic-focus-test.js new file mode 100644 index 0000000000..b2456c72da --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/last-visited-topic-focus-test.js @@ -0,0 +1,26 @@ +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { test } from "qunit"; +import { visit } from "@ember/test-helpers"; +import { cloneJSON } from "discourse-common/lib/object"; +import topicFixtures from "discourse/tests/fixtures/topic"; + +acceptance("Last Visited Topic Focus", function (needs) { + needs.pretender((server, helper) => { + const fixture = cloneJSON(topicFixtures["/t/54077.json"]); + fixture.id = 11996; + fixture.slug = + "its-really-hard-to-navigate-the-create-topic-reply-pane-with-the-keyboard"; + server.get("/t/11996.json", () => helper.response(fixture)); + }); + test("last visited topic receives focus when you return back to the topic list", async function (assert) { + await visit("/"); + await visit( + "/t/its-really-hard-to-navigate-the-create-topic-reply-pane-with-the-keyboard/11996" + ); + await visit("/"); + const visitedTopicTitle = query( + '.topic-list-body tr[data-topic-id="11996"] .main-link' + ); + assert.ok(visitedTopicTitle.classList.contains("focused")); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/post-controls-test.js b/app/assets/javascripts/discourse/tests/acceptance/post-controls-test.js new file mode 100644 index 0000000000..8acc304c60 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/post-controls-test.js @@ -0,0 +1,116 @@ +import { + acceptance, + query, + queryAll, +} from "discourse/tests/helpers/qunit-helpers"; +import { click, visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import I18n from "I18n"; + +acceptance("Post controls", function () { + test("accessibility of the likes list below the post", async function (assert) { + await visit("/t/internationalization-localization/280"); + + const showLikesButton = query("#post_2 button.like-count"); + assert.equal( + showLikesButton.getAttribute("aria-pressed"), + "false", + "show likes button isn't pressed" + ); + assert.equal( + showLikesButton.getAttribute("aria-label"), + I18n.t("post.sr_post_like_count_button", { count: 4 }), + "show likes button has aria-label" + ); + + await click(showLikesButton); + assert.equal( + showLikesButton.getAttribute("aria-pressed"), + "true", + "show likes button is now pressed" + ); + + const likesContainer = query("#post_2 .small-user-list.who-liked"); + assert.equal( + likesContainer.getAttribute("role"), + "list", + "likes container has list role" + ); + assert.equal( + likesContainer.getAttribute("aria-label"), + I18n.t("post.actions.people.sr_post_likers_list_description"), + "likes container has aria-label" + ); + const likesAvatars = Array.from( + likesContainer.querySelectorAll("a.trigger-user-card") + ); + assert.ok(likesAvatars.length > 0, "avatars are rendered"); + likesAvatars.forEach((avatar) => { + assert.equal( + avatar.getAttribute("aria-hidden"), + "false", + "avatars are not aria-hidden" + ); + assert.equal( + avatar.getAttribute("role"), + "listitem", + "avatars have listitem role" + ); + }); + assert.equal( + likesContainer + .querySelector(".list-description") + .getAttribute("aria-hidden"), + "true", + "list description is aria-hidden" + ); + }); + + test("accessibility of the embedded replies below the post", async function (assert) { + await visit("/t/internationalization-localization/280"); + + const showRepliesButton = query("#post_1 button.show-replies"); + assert.equal( + showRepliesButton.getAttribute("aria-pressed"), + "false", + "show replies button isn't pressed" + ); + assert.equal( + showRepliesButton.getAttribute("aria-label"), + I18n.t("post.sr_expand_replies", { count: 1 }), + "show replies button has aria-label" + ); + + await click(showRepliesButton); + assert.equal( + showRepliesButton.getAttribute("aria-pressed"), + "true", + "show replies button is now pressed" + ); + + const replies = Array.from(queryAll("#post_1 .embedded-posts .reply")); + assert.equal(replies.length, 1, "replies are rendered"); + replies.forEach((reply) => { + assert.equal( + reply.getAttribute("role"), + "region", + "replies have region role" + ); + assert.equal( + reply.getAttribute("aria-label"), + I18n.t("post.sr_embedded_reply_description", { + post_number: 1, + username: "somebody", + }), + "replies have aria-label" + ); + }); + assert.equal( + query("#post_1 .embedded-posts .btn.collapse-up").getAttribute( + "aria-label" + ), + I18n.t("post.sr_collapse_replies"), + "collapse button has aria-label" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js index 8331ebec02..a4b1ff7302 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/search-full-test.js @@ -6,7 +6,7 @@ import { selectDate, visible, } from "discourse/tests/helpers/qunit-helpers"; -import { click, fillIn, visit } from "@ember/test-helpers"; +import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; import { test } from "qunit"; import { SEARCH_TYPE_CATS_TAGS, @@ -16,6 +16,7 @@ import { import selectKit from "discourse/tests/helpers/select-kit-helper"; let lastBody; +let searchResultClickTracked = false; acceptance("Search - Full Page", function (needs) { needs.user(); @@ -95,7 +96,16 @@ acceptance("Search - Full Page", function (needs) { server.put("/topics/bulk", (request) => { lastBody = helper.parsePostData(request.requestBody); - return helper.response({ topic_ids: [7] }); + return helper.response({ topic_ids: [130] }); + }); + + server.post("/search/click", () => { + searchResultClickTracked = true; + return helper.response({ success: "OK" }); + }); + + needs.hooks.afterEach(() => { + searchResultClickTracked = false; }); }); @@ -556,7 +566,7 @@ acceptance("Search - Full Page", function (needs) { await click(".bulk-select-visible .btn:nth-child(2)"); // select all await click(".bulk-select-btn"); // show bulk actions await click(".topic-bulk-actions-modal .btn:nth-child(2)"); // close topics - assert.equal(lastBody["topic_ids[]"], 7); + assert.equal(lastBody["topic_ids[]"], 130); }); test("adds visited class to visited topics", async function (assert) { @@ -570,4 +580,16 @@ acceptance("Search - Full Page", function (needs) { await click(".search-cta"); assert.equal(queryAll(".visited").length, 1); }); + + test("result link click tracking is invoked", async function (assert) { + await visit("/search"); + + await fillIn(".search-query", "discourse"); + await click(".search-cta"); + + await click("a.search-link:first-child"); + + assert.strictEqual(currentURL(), "/t/lorem-ipsum-dolor-sit-amet/130"); + assert.ok(searchResultClickTracked); + }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js index 68d122db3a..417a31ecf2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js @@ -87,7 +87,7 @@ acceptance("Second Factor Auth Page", function (needs) { ok: true, callback_method: "PUT", callback_path: "/callback-path", - redirect_path: "/", + redirect_url: "/", }, ]; } @@ -291,7 +291,7 @@ acceptance("Second Factor Auth Page", function (needs) { assert.equal( currentURL(), "/", - "user has been redirected to the redirect_path" + "user has been redirected to the redirect_url" ); assert.equal(callbackCount, 1, "callback request has been performed"); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/share-topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/share-topic-test.js index a22ec248ca..c18f96508d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/share-topic-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/share-topic-test.js @@ -1,6 +1,6 @@ import CategoryFixtures from "discourse/tests/fixtures/category-fixtures"; import I18n from "I18n"; -import { click, visit } from "@ember/test-helpers"; +import { click, currentURL, visit } from "@ember/test-helpers"; import { acceptance, exists, @@ -59,9 +59,19 @@ acceptance("Share and Invite modal", function (needs) { test("Post date link", async function (assert) { await visit("/t/short-topic-with-two-posts/54077"); - await click("#post_2 .post-info.post-date a"); + assert.ok( + query("#post_2 .post-info.post-date a").href.endsWith( + "/t/short-topic-with-two-posts/54077/2?u=eviltrout" + ) + ); + await click("#post_2 a.post-date"); assert.ok(exists(".share-topic-modal"), "it shows the share modal"); + assert.strictEqual( + currentURL(), + "/t/short-topic-with-two-posts/54077", + "it does not route to post #2" + ); }); test("Share topic in a restricted category", async function (assert) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js index a5ea8d1959..1d98b43a3f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/tags-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/tags-test.js @@ -499,7 +499,7 @@ acceptance("Tag info", function (needs) { await click(".category-breadcrumb li:nth-of-type(2) .category-drop-header"); await click( - '.category-breadcrumb li:nth-of-type(2) .category-row[data-name="none"]' + '.category-breadcrumb li:nth-of-type(2) .category-row[data-value="no-categories"]' ); assert.strictEqual(currentURL(), "/tags/c/feature/2/none/planters"); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-entrance-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-entrance-test.js new file mode 100644 index 0000000000..2839284f4e --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-entrance-test.js @@ -0,0 +1,26 @@ +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { click, triggerKeyEvent, visit } from "@ember/test-helpers"; +import { test } from "qunit"; + +const ESC_KEYCODE = 27; +acceptance("Topic Entrance Modal", function () { + test("can be closed with the esc key", async function (assert) { + await visit("/"); + await click(".topic-list-item button.posts-map"); + const topicEntrance = query("#topic-entrance"); + assert.ok( + !topicEntrance.classList.contains("hidden"), + "topic entrance modal appears" + ); + assert.equal( + document.activeElement, + topicEntrance.querySelector(".jump-top"), + "the jump top button has focus when the modal is shown" + ); + await triggerKeyEvent(topicEntrance, "keydown", ESC_KEYCODE); + assert.ok( + topicEntrance.classList.contains("hidden"), + "topic entrance modal disappears after pressing esc" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-list-plugin-api-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-list-plugin-api-test.js new file mode 100644 index 0000000000..e627fc9de9 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-list-plugin-api-test.js @@ -0,0 +1,29 @@ +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { clearCustomLastUnreadUrlCallbacks } from "discourse/models/topic"; +import { test } from "qunit"; +import { visit } from "@ember/test-helpers"; +import { withPluginApi } from "discourse/lib/plugin-api"; + +acceptance("Topic list plugin API", function () { + function customLastUnreadUrl(context) { + return `${context.urlForPostNumber(1)}?overridden`; + } + + test("Overrides lastUnreadUrl", async function (assert) { + try { + withPluginApi("1.2.0", (api) => { + api.registerCustomLastUnreadUrlCallback(customLastUnreadUrl); + }); + + await visit("/"); + assert.strictEqual( + query( + ".topic-list .topic-list-item:first-child a.raw-topic-link" + ).getAttribute("href"), + "/t/error-after-upgrade-to-0-9-7-9/11557/1?overridden" + ); + } finally { + clearCustomLastUnreadUrlCallbacks(); + } + }); +}); diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js index f2c445a907..b417ef12fa 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-set-slow-mode-test.js @@ -53,6 +53,7 @@ acceptance("Topic - Set Slow Mode", function (needs) { const expected = [ I18n.t("topic.auto_update_input.later_today"), I18n.t("topic.auto_update_input.tomorrow"), + I18n.t("topic.auto_update_input.later_this_week"), I18n.t("topic.auto_update_input.next_week"), I18n.t("topic.auto_update_input.two_weeks"), I18n.t("topic.auto_update_input.next_month"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js index 80970d7a59..bbbbeaf5fa 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-test.js @@ -246,6 +246,15 @@ acceptance("Topic", function (needs) { assert.ok(exists(".category-moderator"), "it has a class applied"); assert.ok(exists(".d-icon-shield-alt"), "it shows an icon"); }); + + test("Suspended user posts", async function (assert) { + await visit("/t/topic-from-suspended-user/54077"); + + assert.ok( + exists(".topic-post.user-suspended > #post_1"), + "it has a class applied" + ); + }); }); acceptance("Topic featured links", function (needs) { diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-activity-topic-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-activity-topic-test.js index e669d1e20b..673db76cbb 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-activity-topic-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-activity-topic-test.js @@ -1,14 +1,18 @@ -import { acceptance, exists, queryAll } from "../helpers/qunit-helpers"; +import { acceptance, exists, query, queryAll } from "../helpers/qunit-helpers"; import { test } from "qunit"; import { click, visit } from "@ember/test-helpers"; import userFixtures from "../fixtures/user-fixtures"; +import I18n from "I18n"; acceptance("User Activity / Topics - bulk actions", function (needs) { + const currentUser = "eviltrout"; needs.user(); needs.pretender((server, helper) => { - server.get("/topics/created-by/:username.json", () => { - return helper.response(userFixtures["/topics/created-by/eviltrout.json"]); + server.get(`/topics/created-by/${currentUser}.json`, () => { + return helper.response( + userFixtures[`/topics/created-by/${currentUser}.json`] + ); }); server.put("/topics/bulk", () => { @@ -17,7 +21,7 @@ acceptance("User Activity / Topics - bulk actions", function (needs) { }); test("bulk topic closing works", async function (assert) { - await visit("/u/charlie/activity/topics"); + await visit(`/u/${currentUser}/activity/topics`); await click("button.bulk-select"); await click(queryAll("input.bulk-select")[0]); @@ -34,6 +38,8 @@ acceptance("User Activity / Topics - bulk actions", function (needs) { }); acceptance("User Activity / Topics - empty state", function (needs) { + const currentUser = "eviltrout"; + const anotherUser = "charlie"; needs.user(); needs.pretender((server, helper) => { @@ -43,13 +49,28 @@ acceptance("User Activity / Topics - empty state", function (needs) { }, }; - server.get("/topics/created-by/:username.json", () => { + server.get(`/topics/created-by/${currentUser}.json`, () => { + return helper.response(emptyResponse); + }); + + server.get(`/topics/created-by/${anotherUser}.json`, () => { return helper.response(emptyResponse); }); }); - test("It renders the empty state panel", async function (assert) { - await visit("/u/charlie/activity/topics"); - assert.ok(exists("div.empty-state")); + test("When looking at the own activity page", async function (assert) { + await visit(`/u/${currentUser}/activity/topics`); + assert.equal( + query("div.empty-state span.empty-state-title").innerText, + I18n.t("user_activity.no_topics_title") + ); + }); + + test("When looking at another user's activity page", async function (assert) { + await visit(`/u/${anotherUser}/activity/topics`); + assert.equal( + query("div.empty-state span.empty-state-title").innerText, + I18n.t("user_activity.no_topics_title_others", { username: anotherUser }) + ); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js index ace7abce1d..a0602c737a 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-drafts-stream-test.js @@ -2,6 +2,7 @@ import { acceptance, count, exists, + normalizeHtml, query, queryAll, visible, @@ -54,8 +55,13 @@ acceptance("User Drafts", function (needs) { "meta" ); assert.strictEqual( - query(".user-stream-item:nth-child(3) .excerpt").innerHTML.trim(), - `here goes a reply to a PM :slight_smile:` + normalizeHtml( + query(".user-stream-item:nth-child(3) .excerpt").innerHTML.trim() + ), + normalizeHtml( + `here goes a reply to a PM :slight_smile:` + ), + "shows the excerpt" ); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js index a7bda10c13..b1c5593445 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-notifications-test.js @@ -138,6 +138,7 @@ acceptance("User Notifications - Users - Ignore User", function (needs) { const expected = [ I18n.t("topic.auto_update_input.later_today"), I18n.t("topic.auto_update_input.tomorrow"), + I18n.t("topic.auto_update_input.later_this_week"), I18n.t("topic.auto_update_input.this_weekend"), I18n.t("topic.auto_update_input.next_week"), I18n.t("topic.auto_update_input.two_weeks"), 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 b4a2fbee2d..8e97e08165 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 @@ -1,4 +1,4 @@ -import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; +import { click, currentURL, fillIn, settled, visit } from "@ember/test-helpers"; import { test } from "qunit"; import I18n from "I18n"; import { @@ -435,7 +435,7 @@ acceptance( publishNewToMessageBus({ topicId: 1, userId: 5 }); - await visit("/t/13"); // await re-render + await settled(); await visit("/u/charlie/messages"); assert.ok( @@ -613,7 +613,7 @@ acceptance( publishNewToMessageBus({ userId: 5, topicId: 1 }); - await visit("/t/12"); // await re-render + await settled(); assert.strictEqual( query(".suggested-topics-message").innerText.trim(), @@ -623,7 +623,7 @@ acceptance( publishUnreadToMessageBus({ userId: 5, topicId: 2 }); - await visit("/t/12"); // await re-render + await settled(); assert.strictEqual( query(".suggested-topics-message").innerText.trim(), @@ -633,7 +633,7 @@ acceptance( publishReadToMessageBus({ userId: 5, topicId: 2 }); - await visit("/t/12"); // await re-render + await settled(); assert.strictEqual( query(".suggested-topics-message").innerText.trim(), @@ -660,7 +660,7 @@ acceptance( publishGroupNewToMessageBus({ groupIds: [14], topicId: 1 }); - await visit("/t/13"); // await re-render + await settled(); assert.ok( query(".suggested-topics-message") @@ -673,7 +673,7 @@ acceptance( publishGroupUnreadToMessageBus({ groupIds: [14], topicId: 2 }); - await visit("/t/13"); // await re-render + await settled(); assert.ok( query(".suggested-topics-message") diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-test.js index 121ccb9e48..9d522d3d93 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-test.js @@ -2,13 +2,16 @@ import EmberObject from "@ember/object"; import User from "discourse/models/user"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import sinon from "sinon"; +import userFixtures from "discourse/tests/fixtures/user-fixtures"; import { acceptance, exists, query, queryAll, + updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; import { click, currentRouteName, visit } from "@ember/test-helpers"; +import { cloneJSON } from "discourse-common/lib/object"; import { test } from "qunit"; acceptance("User Routes", function (needs) { @@ -53,6 +56,20 @@ acceptance("User Routes", function (needs) { "/u/eviltrout/notifications/likes-received?acting_username=aquaman" ) ); + + updateCurrentUser({ moderator: true, admin: false }); + await visit("/u/charlie/summary"); + assert.notOk( + exists(".user-nav > .user-notifications"), + "does not have the notifications tab" + ); + + updateCurrentUser({ moderator: false, admin: true }); + await visit("/u/charlie/summary"); + assert.ok( + exists(".user-nav > .user-notifications"), + "has the notifications tab" + ); }); test("Root URL - Viewing Self", async function (assert) { @@ -177,3 +194,118 @@ acceptance("User - Saving user options", function (needs) { ); }); }); + +acceptance("User - Notification level dropdown visibility", function (needs) { + needs.user({ username: "eviltrout", id: 1, ignored_ids: [] }); + + needs.pretender((server, helper) => { + server.get("/u/charlie.json", () => { + const cloned = cloneJSON(userFixtures["/u/charlie.json"]); + cloned.user.can_ignore_user = false; + cloned.user.can_mute_user = false; + return helper.response(200, cloned); + }); + }); + + test("Notification level button is not rendered for user who cannot mute or ignore another user", async function (assert) { + await visit("/u/charlie"); + assert.notOk(exists(".user-notifications-dropdown")); + }); +}); + +acceptance( + "User - Muting other user with notification level dropdown", + function (needs) { + needs.user({ username: "eviltrout", id: 1, ignored_ids: [] }); + + needs.pretender((server, helper) => { + server.get("/u/charlie.json", () => { + const cloned = cloneJSON(userFixtures["/u/charlie.json"]); + cloned.user.can_mute_user = true; + return helper.response(200, cloned); + }); + + server.put("/u/charlie/notification_level.json", (request) => { + let requestParams = new URLSearchParams(request.requestBody); + // Ensure the correct `notification_level` param is sent to the server + if (requestParams.get("notification_level") === "mute") { + return helper.response(200, {}); + } else { + return helper.response(422, {}); + } + }); + }); + + test("Notification level is set to normal and can be changed to muted", async function (assert) { + await visit("/u/charlie"); + assert.ok( + exists(".user-notifications-dropdown"), + "Notification level dropdown is present" + ); + + const dropdown = selectKit(".user-notifications-dropdown"); + await dropdown.expand(); + assert.strictEqual(dropdown.selectedRow().value(), "changeToNormal"); + + await dropdown.selectRowByValue("changeToMuted"); + await dropdown.expand(); + assert.strictEqual(dropdown.selectedRow().value(), "changeToMuted"); + }); + } +); + +acceptance( + "User - Ignoring other user with notification level dropdown", + function (needs) { + needs.user({ username: "eviltrout", id: 1, ignored_ids: [] }); + + needs.pretender((server, helper) => { + server.get("/u/charlie.json", () => { + const cloned = cloneJSON(userFixtures["/u/charlie.json"]); + cloned.user.can_ignore_user = true; + return helper.response(200, cloned); + }); + + server.put("/u/charlie/notification_level.json", (request) => { + let requestParams = new URLSearchParams(request.requestBody); + // Ensure the correct `notification_level` param is sent to the server + if (requestParams.get("notification_level") === "ignore") { + return helper.response(200, {}); + } else { + return helper.response(422, {}); + } + }); + }); + test("Notification level can be changed to ignored", async function (assert) { + await visit("/u/charlie"); + assert.ok( + exists(".user-notifications-dropdown"), + "Notification level dropdown is present" + ); + + const notificationLevelDropdown = selectKit( + ".user-notifications-dropdown" + ); + await notificationLevelDropdown.expand(); + assert.strictEqual( + notificationLevelDropdown.selectedRow().value(), + "changeToNormal" + ); + + await notificationLevelDropdown.selectRowByValue("changeToIgnored"); + assert.ok(exists(".ignore-duration-modal")); + + const durationDropdown = selectKit( + ".ignore-duration-modal .future-date-input-selector" + ); + await durationDropdown.expand(); + await durationDropdown.selectRowByIndex(0); + await click(".modal-footer .ignore-duration-save"); + await notificationLevelDropdown.expand(); + assert.strictEqual( + notificationLevelDropdown.selectedRow().value(), + "changeToIgnored" + ); + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/fixtures/concerns/notification-types.js b/app/assets/javascripts/discourse/tests/fixtures/concerns/notification-types.js index 0817993cf5..a75d7721a1 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/concerns/notification-types.js +++ b/app/assets/javascripts/discourse/tests/fixtures/concerns/notification-types.js @@ -37,4 +37,5 @@ export const NOTIFICATION_TYPES = { chat_group_mention: 32, chat_quoted: 33, assigned: 34, + question_answer_user_commented: 35, }; diff --git a/app/assets/javascripts/discourse/tests/fixtures/search-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/search-fixtures.js index bfb8a44999..1251166c8c 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/search-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/search-fixtures.js @@ -11,12 +11,12 @@ export default { blurb: "The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It’s important! Edit this into a brief description of your community: Who is it for? What can they find here? Why should they come here? Where can they read more (links, resources, ...", post_number: 1, - topic_id: 7, + topic_id: 130, }, ], topics: [ { - id: 7, + id: 130, title: "Welcome to Discourse", fancy_title: "Welcome to Discourse", slug: "welcome-to-discourse", diff --git a/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js index a28ca8800f..cfc12850af 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js @@ -366,6 +366,7 @@ export default { can_edit: true, show_subcategory_list: false, default_view: "latest", + required_tag_groups: [], }, { id: 17, diff --git a/app/assets/javascripts/discourse/tests/fixtures/tag-group-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/tag-group-fixtures.js new file mode 100644 index 0000000000..07eb46530c --- /dev/null +++ b/app/assets/javascripts/discourse/tests/fixtures/tag-group-fixtures.js @@ -0,0 +1,8 @@ +export default { + "/tag_groups/filter/search": { + results: [ + { name: "TagGroup1", tag_names: ["alpha", "bravo", "charlie"] }, + { name: "TagGroup2", tag_names: ["delta", "echo"] }, + ], + }, +}; diff --git a/app/assets/javascripts/discourse/tests/fixtures/topic.js b/app/assets/javascripts/discourse/tests/fixtures/topic.js index 8fbd7aad06..909483a1b1 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/topic.js +++ b/app/assets/javascripts/discourse/tests/fixtures/topic.js @@ -6251,6 +6251,7 @@ export default { edit_reason: null, can_view_edit_history: true, wiki: false, + user_suspended: true, }, { id: 419, diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js index e5d69042d0..ef974b299f 100644 --- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js +++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js @@ -327,7 +327,7 @@ export function applyDefaultHandlers(pretender) { }); pretender.get("/posts/:id/replies", () => { - return response([{ id: 1234, cooked: "wat" }]); + return response([{ id: 1234, cooked: "wat", username: "somebody" }]); }); // TODO: Remove this old path when no longer using old ember @@ -356,7 +356,7 @@ export function applyDefaultHandlers(pretender) { ); pretender.put("/categories/:category_id", (request) => { - const category = parsePostData(request.requestBody); + const category = JSON.parse(request.requestBody); category.id = parseInt(request.params.category_id, 10); if (category.email_in === "duplicate@example.com") { @@ -1122,6 +1122,10 @@ export function applyDefaultHandlers(pretender) { }); }); + pretender.get("/tag_groups/filter/search", () => + response(fixturesByUrl["/tag_groups/filter/search"]) + ); + pretender.get("/c/:id/visible_groups.json", () => response({ groups: [] })); } diff --git a/app/assets/javascripts/discourse/tests/helpers/d-editor-helper.js b/app/assets/javascripts/discourse/tests/helpers/d-editor-helper.js index 88d6cb2df9..09d2f0aef3 100644 --- a/app/assets/javascripts/discourse/tests/helpers/d-editor-helper.js +++ b/app/assets/javascripts/discourse/tests/helpers/d-editor-helper.js @@ -1,11 +1,11 @@ export default function formatTextWithSelection(text, [start, len]) { return [ '"', - text.substr(0, start), + text.slice(0, start), "<", - text.substr(start, len), + text.slice(start, start + len), ">", - text.substr(start + len), + text.slice(start + len), '"', ].join(""); } diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 5065e7f057..d5c159776d 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -574,3 +574,12 @@ export async function paste(element, text, otherClipboardData = {}) { await settled(); return e; } + +// The order of attributes can vary in diffferent browsers. When comparing +// HTML strings from the DOM, this function helps to normalize them to make +// comparison work cross-browser +export function normalizeHtml(html) { + const resultElement = document.createElement("template"); + resultElement.innerHTML = html; + return resultElement.innerHTML; +} diff --git a/app/assets/javascripts/discourse/tests/helpers/site-settings.js b/app/assets/javascripts/discourse/tests/helpers/site-settings.js index d1e7ade6c6..6201942429 100644 --- a/app/assets/javascripts/discourse/tests/helpers/site-settings.js +++ b/app/assets/javascripts/discourse/tests/helpers/site-settings.js @@ -64,7 +64,6 @@ const ORIGINAL_SETTINGS = { allow_profile_backgrounds: true, allow_uploaded_avatars: "0", tl1_requires_read_posts: 30, - enable_long_polling: true, polling_interval: 3000, anon_polling_interval: 30000, flush_timings_secs: 5, diff --git a/app/assets/javascripts/discourse/tests/integration/components/pick-files-button-tests.js b/app/assets/javascripts/discourse/tests/integration/components/pick-files-button-tests.js deleted file mode 100644 index db622c99b4..0000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/pick-files-button-tests.js +++ /dev/null @@ -1,92 +0,0 @@ -import componentTest, { - setupRenderingTest, -} from "discourse/tests/helpers/component-test"; -import { discourseModule } from "discourse/tests/helpers/qunit-helpers"; -import hbs from "htmlbars-inline-precompile"; -import { triggerEvent } from "@ember/test-helpers"; -import sinon from "sinon"; -import bootbox from "bootbox"; - -function createBlob(mimeType, extension) { - const blob = new Blob(["content"], { - type: mimeType, - }); - blob.name = `filename${extension}`; - return blob; -} - -discourseModule( - "Integration | Component | pick-files-button", - function (hooks) { - const expectedExtension = ".json"; - const expectedMimeType = "text/json"; - - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.set("acceptedFileTypes", [expectedExtension, expectedMimeType]); - this.set("onFilesPicked", () => {}); - }); - - componentTest("it doesn't show alert if a file has a supported MIME type", { - skip: true, - template: hbs` - {{pick-files-button - acceptedFileTypes=this.acceptedFileTypes - onFilesPicked=this.onFilesPicked}}`, - - async test(assert) { - sinon.stub(bootbox, "alert"); - - const wrongExtension = ".txt"; - const file = createBlob(expectedMimeType, wrongExtension); - - await triggerEvent("input[type='file']", "change", { files: [file] }); - - assert.ok(bootbox.alert.notCalled); - }, - }); - - componentTest("it doesn't show alert if a file has a supported extension", { - skip: true, - template: hbs` - {{pick-files-button - acceptedFileTypes=this.acceptedFileTypes - onFilesPicked=this.onFilesPicked}}`, - - async test(assert) { - sinon.stub(bootbox, "alert"); - - const wrongMimeType = "text/plain"; - const file = createBlob(wrongMimeType, expectedExtension); - - await triggerEvent("input[type='file']", "change", { files: [file] }); - - assert.ok(bootbox.alert.notCalled); - }, - }); - - componentTest( - "it shows alert if a file has an unsupported extension and unsupported MIME type", - { - skip: true, - template: hbs` - {{pick-files-button - acceptedFileTypes=this.acceptedFileTypes - onFilesPicked=this.onFilesPicked}}`, - - async test(assert) { - sinon.stub(bootbox, "alert"); - - const wrongExtension = ".txt"; - const wrongMimeType = "text/plain"; - const file = createBlob(wrongMimeType, wrongExtension); - - await triggerEvent("input[type='file']", "change", { files: [file] }); - - assert.ok(bootbox.alert.calledOnce); - }, - } - ); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js index fbd64f9886..8ae75bbd32 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js @@ -141,7 +141,7 @@ discourseModule( }, }); - componentTest("with allowUncategorized=null rootNone=true", { + componentTest("with allowUncategorized=null none=true", { template: hbs` {{category-chooser value=value diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js index 1735d87166..d745244802 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js @@ -36,11 +36,7 @@ discourseModule( }; pretender.get("/tags/filter/search", (params) => { - if (params.queryParams.q === "rég") { - return response({ - results: [{ id: "régis", name: "régis", count: 2, pm_count: 0 }], - }); - } else if (params.queryParams.q === "dav") { + if (params.queryParams.q === "dav") { return response({ results: [{ id: "David", name: "David", count: 2, pm_count: 0 }], }); @@ -85,6 +81,32 @@ discourseModule( I18n.t("tagging.selector_all_tags"), "it has the correct label for all-tags" ); + + await this.subject.fillInFilter("dav"); + + assert.strictEqual( + this.subject.rows()[0].textContent.trim(), + "David", + "it has no tag count when filtering in a category context" + ); + }, + }); + + componentTest("default global (no category)", { + template: hbs`{{tag-drop}}`, + + async test(assert) { + await this.subject.expand(); + + assert.ok(true); + + await this.subject.fillInFilter("dav"); + + assert.strictEqual( + this.subject.rows()[0].textContent.trim(), + "David x2", + "it has the tag count" + ); }, }); } diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-info-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-info-test.js new file mode 100644 index 0000000000..0fc0ef0554 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/user-info-test.js @@ -0,0 +1,39 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import hbs from "htmlbars-inline-precompile"; +import { discourseModule, exists } from "discourse/tests/helpers/qunit-helpers"; + +discourseModule("Integration | Component | user-info", function (hooks) { + setupRenderingTest(hooks); + + componentTest("includeLink", { + template: hbs`{{user-info user=currentUser includeLink=includeLink}}`, + + async test(assert) { + this.set("includeLink", true); + + assert.ok(exists(`.username a[href="/u/${this.currentUser.username}"]`)); + + this.set("includeLink", false); + + assert.notOk( + exists(`.username a[href="/u/${this.currentUser.username}"]`) + ); + }, + }); + + componentTest("includeAvatar", { + template: hbs`{{user-info user=currentUser includeAvatar=includeAvatar}}`, + + async test(assert) { + this.set("includeAvatar", true); + + assert.ok(exists(".user-image")); + + this.set("includeAvatar", false); + + assert.notOk(exists(".user-image")); + }, + }); +}); diff --git a/app/assets/javascripts/discourse/tests/integration/widgets/widget-test.js b/app/assets/javascripts/discourse/tests/integration/widgets/widget-test.js index 6151faab17..465cf66be4 100644 --- a/app/assets/javascripts/discourse/tests/integration/widgets/widget-test.js +++ b/app/assets/javascripts/discourse/tests/integration/widgets/widget-test.js @@ -475,7 +475,7 @@ discourseModule("Integration | Component | Widget | base", function (hooks) { test(assert) { assert.ok( exists("section.override"), - "renders container with overrided tagName" + "renders container with overridden tagName" ); }, }); diff --git a/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js b/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js index 2135f692d0..4c347dd884 100644 --- a/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js +++ b/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js @@ -16,8 +16,10 @@ document.addEventListener("discourse-booted", () => { } let setupTests = require("discourse/tests/setup-tests").default; - const skippingCore = - new URLSearchParams(window.location.search).get("qunit_skip_core") === "1"; + const params = new URLSearchParams(window.location.search); + const skipCore = params.get("qunit_skip_core") === "1"; + const disableAutoStart = params.get("qunit_disable_auto_start") === "1"; + // eslint-disable-next-line no-undef Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION = false; @@ -43,10 +45,12 @@ document.addEventListener("discourse-booted", () => { } loader.loadModules(); + start({ setupTestContainer: false, loadTests: false, - setupEmberOnerrorValidation: !skippingCore, + startTests: !disableAutoStart, + setupEmberOnerrorValidation: !skipCore, }); }); window.EmberENV.TESTS_FILE_LOADED = true; diff --git a/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js b/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js index 4f2a337c46..25553f9fe1 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/get-url-test.js @@ -18,11 +18,20 @@ module("Unit | Utility | get-url", function () { }); test("getAbsoluteURL", function (assert) { - setupURL(null, "https://example.com", "/forum"); + setupURL(null, "https://example.com", null); assert.strictEqual( getAbsoluteURL("/cool/path"), "https://example.com/cool/path" ); + setupURL(null, "https://example.com/forum", "/forum"); + assert.strictEqual( + getAbsoluteURL("/cool/path"), + "https://example.com/forum/cool/path" + ); + assert.strictEqual( + getAbsoluteURL("/forum/cool/path"), + "https://example.com/forum/cool/path" + ); }); test("withoutPrefix", function (assert) { diff --git a/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js b/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js index 09dcecae46..46756c7f68 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/timeframes-builder-test.js @@ -32,6 +32,7 @@ module("Unit | Lib | timeframes-builder", function (hooks) { const expected = [ "later_today", "tomorrow", + "later_this_week", "next_week", "two_weeks", "next_month", @@ -112,9 +113,9 @@ module("Unit | Lib | timeframes-builder", function (hooks) { assert.ok(timeframes.includes("later_this_week")); }); - test("doesn't output 'Later This Week' on Tuesdays", function (assert) { + test("doesn't output 'Later This Week' on Thursdays", function (assert) { const timezone = moment.tz.guess(); - this.clock = fakeTime("2100-04-22 18:00:00", timezone, true); // Tuesday evening + this.clock = fakeTime("2100-04-22 18:00:00", timezone, true); // Thursday evening const timeframes = buildTimeframes(buildOptions(moment())).mapBy("id"); assert.notOk(timeframes.includes("later_this_week")); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js b/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js index ddca5c5578..ebc2f66d70 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/utilities-test.js @@ -1,7 +1,9 @@ +import { Promise } from "rsvp"; import { avatarImg, avatarUrl, caretRowCol, + clipboardCopyAsync, defaultHomepage, emailValid, escapeExpression, @@ -15,9 +17,13 @@ import { slugify, toAsciiPrintable, } from "discourse/lib/utilities"; +import sinon from "sinon"; import { test } from "qunit"; import Handlebars from "handlebars"; -import { discourseModule } from "discourse/tests/helpers/qunit-helpers"; +import { + chromeTest, + discourseModule, +} from "discourse/tests/helpers/qunit-helpers"; discourseModule("Unit | Utilities", function () { test("escapeExpression", function (assert) { @@ -283,3 +289,51 @@ discourseModule("Unit | Utilities", function () { }); }); }); + +discourseModule("Unit | Utilities | clipboard", function (hooks) { + let mockClipboard; + hooks.beforeEach(function () { + mockClipboard = { + writeText: sinon.stub().resolves(true), + write: sinon.stub().resolves(true), + }; + sinon.stub(window.navigator, "clipboard").get(() => mockClipboard); + }); + + function getPromiseFunction() { + return () => + new Promise((resolve) => { + resolve( + new Blob(["some text to copy"], { + type: "text/plain", + }) + ); + }); + } + + test("clipboardCopyAsync - browser does not support window.ClipboardItem", async function (assert) { + // without this check the stubbing will fail on Firefox + if (window.ClipboardItem) { + sinon.stub(window, "ClipboardItem").value(null); + } + + await clipboardCopyAsync(getPromiseFunction()); + assert.strictEqual( + mockClipboard.writeText.calledWith("some text to copy"), + true, + "it writes to the clipboard using writeText instead of write" + ); + }); + + chromeTest( + "clipboardCopyAsync - browser does support window.ClipboardItem", + async function (assert) { + await clipboardCopyAsync(getPromiseFunction()); + assert.strictEqual( + mockClipboard.write.called, + true, + "it writes to the clipboard using write" + ); + } + ); +}); diff --git a/app/assets/javascripts/discourse/tests/unit/models/category-test.js b/app/assets/javascripts/discourse/tests/unit/models/category-test.js index 27c4193c80..e8e88e67e3 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/category-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/category-test.js @@ -227,8 +227,7 @@ module("Unit | Model | category", function () { let foo = store.createRecord("category", { id: 1, slug: "foo", - required_tag_groups: ["bar"], - min_tags_from_required_group: 2, + required_tag_groups: [{ name: "bar", min_count: 2 }], }); assert.equal(foo.minimumRequiredTags, 2); @@ -259,7 +258,7 @@ module("Unit | Model | category", function () { foo = store.createRecord("category", { id: 5, slug: "foo", - min_tags_from_required_group: 2, + required_tag_groups: [], }); assert.equal(foo.minimumRequiredTags, null); diff --git a/app/assets/javascripts/locales/i18n.js b/app/assets/javascripts/locales/i18n.js index bde0d1a7bb..c930469c2e 100644 --- a/app/assets/javascripts/locales/i18n.js +++ b/app/assets/javascripts/locales/i18n.js @@ -201,8 +201,9 @@ I18n.toNumber = function(number, options) { number = parts[0]; while (number.length > 0) { - buffer.unshift(number.substr(Math.max(0, number.length - 3), 3)); - number = number.substr(0, number.length - 3); + var pos = Math.max(0, number.length - 3); + buffer.unshift(number.slice(pos, pos + 3)); + number = number.slice(0, -3); } formattedNumber = buffer.join(options.delimiter); diff --git a/app/assets/javascripts/polyfills.js b/app/assets/javascripts/polyfills.js index 93dc3b309c..381b769e39 100644 --- a/app/assets/javascripts/polyfills.js +++ b/app/assets/javascripts/polyfills.js @@ -1,5 +1,27 @@ /* 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 () { diff --git a/app/assets/javascripts/pretty-text-bundle.js b/app/assets/javascripts/pretty-text-bundle.js index 6602c63a6b..6f2ab7de7b 100644 --- a/app/assets/javascripts/pretty-text-bundle.js +++ b/app/assets/javascripts/pretty-text-bundle.js @@ -7,7 +7,6 @@ //= require ./pretty-text/addon/engines/discourse-markdown-it //= require xss.min //= require ./pretty-text/addon/allow-lister -//= require ./pretty-text/addon/white-lister //= require ./pretty-text/addon/sanitizer //= require ./pretty-text/addon/oneboxer //= require ./pretty-text/addon/oneboxer-cache diff --git a/app/assets/javascripts/pretty-text/addon/allow-lister.js b/app/assets/javascripts/pretty-text/addon/allow-lister.js index a6378ee61c..764eaa8188 100644 --- a/app/assets/javascripts/pretty-text/addon/allow-lister.js +++ b/app/assets/javascripts/pretty-text/addon/allow-lister.js @@ -1,4 +1,3 @@ -import deprecated from "discourse-common/lib/deprecated"; // to match: // abcd // abcd[test] @@ -27,14 +26,6 @@ export default class AllowLister { this._rawFeatures.push([feature, info]); } - whiteListFeature(feature, info) { - deprecated("`whiteListFeature` has been replaced with `allowListFeature`", { - since: "2.6.0.beta.4", - dropFrom: "2.7.0", - }); - this.allowListFeature(feature, info); - } - disable(feature) { this._enabled[feature] = false; this._cache = null; @@ -105,14 +96,6 @@ export default class AllowLister { return this._cache.allowList; } - getWhiteList() { - deprecated("`getWhiteList` has been replaced with `getAllowList`", { - since: "2.6.0.beta.4", - dropFrom: "2.7.0", - }); - return this.getAllowList(); - } - getCustom() { this._ensureCache(); return this._cache.custom; diff --git a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js index b9c9f4759a..80381858cd 100644 --- a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js +++ b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js @@ -426,12 +426,12 @@ export function extractDataAttribute(str) { return null; } - const key = `data-${str.substr(0, sep)}`.toLowerCase(); + const key = `data-${str.slice(0, sep)}`.toLowerCase(); if (!/^[A-Za-z]+[\w\-\:\.]*$/.test(key)) { return null; } - const value = str.substr(sep + 1); + const value = str.slice(sep + 1); return [key, value]; } diff --git a/app/assets/javascripts/pretty-text/addon/sanitizer.js b/app/assets/javascripts/pretty-text/addon/sanitizer.js index 19017184a9..492f0c4b16 100644 --- a/app/assets/javascripts/pretty-text/addon/sanitizer.js +++ b/app/assets/javascripts/pretty-text/addon/sanitizer.js @@ -76,7 +76,7 @@ export function sanitize(text, allowLister) { } let result = xss(text, { - whiteList: allowList.tagList, + allowList: allowList.tagList, stripIgnoreTag: true, stripIgnoreTagBody: ["script", "table"], diff --git a/app/assets/javascripts/pretty-text/addon/white-lister.js b/app/assets/javascripts/pretty-text/addon/white-lister.js deleted file mode 100644 index e07df502e4..0000000000 --- a/app/assets/javascripts/pretty-text/addon/white-lister.js +++ /dev/null @@ -1,16 +0,0 @@ -import AllowLister, { - DEFAULT_LIST as NEW_DEFAULT_LIST, -} from "pretty-text/allow-lister"; -import deprecated from "discourse-common/lib/deprecated"; - -export default class WhiteLister extends AllowLister { - constructor(options) { - deprecated("`WhiteLister` has been replaced with `AllowLister`", { - since: "2.6.0.beta.4", - dropFrom: "2.7.0", - }); - super(options); - } -} - -export const DEFAULT_LIST = NEW_DEFAULT_LIST; diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js index 6a68b19d9f..df735a5fd1 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js @@ -90,7 +90,7 @@ function getEmojiName(content, pos, state, inlineEmoji) { if (content.charCodeAt(pos + length) === 58) { // check for t2-t6 - if (content.substr(pos + length + 1, 3).match(/t[2-6]:/)) { + if (content.slice(pos + length + 1, pos + length + 4).match(/t[2-6]:/)) { length += 3; } break; @@ -105,7 +105,7 @@ function getEmojiName(content, pos, state, inlineEmoji) { return; } - return content.substr(pos, length); + return content.slice(pos, pos + length); } // straight forward :smile: to emoji image diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js index 069a88c2f3..17fa1e1c2c 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js @@ -38,13 +38,13 @@ export function textReplace(state, callback, skipAllLinks) { if (token.type === "link_open" || token.type === "link_close") { linkLevel -= token.nesting; } else if (token.type === "html_inline") { - const openLink = token.content.substr(0, 2).toLowerCase(); + const openLink = token.content.slice(0, 2).toLowerCase(); if (openLink === "/i)) { linkLevel++; } - } else if (token.content.substr(0, 4).toLowerCase() === "") { + } else if (token.content.slice(0, 4).toLowerCase() === "") { linkLevel--; } } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js index 28bd3ed7e9..037685f823 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js @@ -23,12 +23,12 @@ const rule = { let i; for (i = 1; i < split.length; i++) { if (split[i].indexOf("post:") === 0) { - postNumber = parseInt(split[i].substr(5), 10); + postNumber = parseInt(split[i].slice(5), 10); continue; } if (split[i].indexOf("topic:") === 0) { - topicId = parseInt(split[i].substr(6), 10); + topicId = parseInt(split[i].slice(6), 10); continue; } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/upload-protocol.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/upload-protocol.js index 4b99308a89..9fc600e70c 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/upload-protocol.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/upload-protocol.js @@ -1,88 +1,152 @@ +import xss from "xss"; + +const HTML_TYPES = ["html_block", "html_inline"]; + // add image to array if src has an upload function addImage(uploads, token) { if (token.attrs) { for (let i = 0; i < token.attrs.length; i++) { if (token.attrs[i][1].indexOf("upload://") === 0) { - uploads.push([token, i]); + uploads.push({ token, srcIndex: i, origSrc: token.attrs[i][1] }); break; } } } } +function attr(name, value) { + if (value) { + return `${name}="${xss.escapeAttrValue(value)}"`; + } + + return name; +} + +function uploadLocatorString(url) { + return `___REPLACE_UPLOAD_SRC_${url}___`; +} + +function findUploadsInHtml(uploads, blockToken) { + // Slightly misusing our HTML sanitizer to look for upload:// + // image src attributes, and replace them with a placeholder. + // Note that we can't use browser DOM APIs because this needs + // to run in mini-racer. + const fakeAllowList = {}; + + let foundImage = false; + const newContent = xss(blockToken.content, { + allowList: fakeAllowList, + allowCommentTag: true, + onTag(tag, html, options) { + // We're not using this for sanitizing, so allow all tags through + options.isWhite = true; + fakeAllowList[tag] = []; + }, + onTagAttr(tag, name, value) { + if (tag === "img" && name === "src" && value.startsWith("upload://")) { + uploads.push({ token: blockToken, srcIndex: null, origSrc: value }); + foundImage = true; + return uploadLocatorString(value); + } + return attr(name, value); + }, + }); + if (foundImage) { + blockToken.content = newContent; + } +} + +function processToken(uploads, token) { + if (token.tag === "img" || token.tag === "a") { + addImage(uploads, token); + } else if (HTML_TYPES.includes(token.type)) { + findUploadsInHtml(uploads, token); + } + + if (token.children) { + for (let j = 0; j < token.children.length; j++) { + const childToken = token.children[j]; + processToken(uploads, childToken); + } + } +} + function rule(state) { let uploads = []; for (let i = 0; i < state.tokens.length; i++) { let blockToken = state.tokens[i]; - if (blockToken.tag === "img" || blockToken.tag === "a") { - addImage(uploads, blockToken); - } - - if (!blockToken.children) { - continue; - } - - for (let j = 0; j < blockToken.children.length; j++) { - let token = blockToken.children[j]; - - if (token.tag === "img" || token.tag === "a") { - addImage(uploads, token); - } - } + processToken(uploads, blockToken); } if (uploads.length > 0) { - let srcList = uploads.map(([token, srcIndex]) => token.attrs[srcIndex][1]); + let srcList = uploads.map((u) => u.origSrc); + + // In client-side cooking, this lookup returns nothing + // This means we set data-orig-src, and let decorateCooked + // lookup the image URLs asynchronously let lookup = state.md.options.discourse.lookupUploadUrls; let longUrls = (lookup && lookup(srcList)) || {}; - uploads.forEach(([token, srcIndex]) => { - let origSrc = token.attrs[srcIndex][1]; + uploads.forEach(({ token, srcIndex, origSrc }) => { let mapped = longUrls[origSrc]; - switch (token.tag) { - case "img": - if (mapped) { - token.attrs[srcIndex][1] = mapped.url; - token.attrs.push(["data-base62-sha1", mapped.base62_sha1]); - } else { - // no point putting a transparent .png for audio/video - if (token.content.match(/\|video|\|audio/)) { - token.attrs[srcIndex][1] = state.md.options.discourse.getURL( - "/404" - ); - } else { - token.attrs[srcIndex][1] = state.md.options.discourse.getURL( - "/images/transparent.png" - ); - } + if (HTML_TYPES.includes(token.type)) { + const locator = uploadLocatorString(origSrc); + let attrs = []; - token.attrs.push(["data-orig-src", origSrc]); - } - break; - case "a": - if (mapped) { - // when secure media is enabled we want the full /secure-media-uploads/ - // url to take advantage of access control security - if ( - state.md.options.discourse.limitedSiteSettings.secureMedia && - mapped.url.indexOf("secure-media-uploads") > -1 - ) { - token.attrs[srcIndex][1] = mapped.url; - } else { - token.attrs[srcIndex][1] = mapped.short_path; - } - } else { + if (mapped) { + attrs.push( + attr("src", mapped.url), + attr("data-base62-sha1", mapped.base62_sha1) + ); + } else { + attrs.push( + attr( + "src", + state.md.options.discourse.getURL("/images/transparent.png") + ), + attr("data-orig-src", origSrc) + ); + } + + token.content = token.content.replace(locator, attrs.join(" ")); + } else if (token.tag === "img") { + if (mapped) { + token.attrs[srcIndex][1] = mapped.url; + token.attrs.push(["data-base62-sha1", mapped.base62_sha1]); + } else { + // no point putting a transparent .png for audio/video + if (token.content.match(/\|video|\|audio/)) { token.attrs[srcIndex][1] = state.md.options.discourse.getURL( "/404" ); - - token.attrs.push(["data-orig-href", origSrc]); + } else { + token.attrs[srcIndex][1] = state.md.options.discourse.getURL( + "/images/transparent.png" + ); } - break; + token.attrs.push(["data-orig-src", origSrc]); + } + } else if (token.tag === "a") { + if (mapped) { + // when secure media is enabled we want the full /secure-media-uploads/ + // url to take advantage of access control security + if ( + state.md.options.discourse.limitedSiteSettings.secureMedia && + mapped.url.indexOf("secure-media-uploads") > -1 + ) { + token.attrs[srcIndex][1] = mapped.url; + } else { + token.attrs[srcIndex][1] = mapped.short_path; + } + } else { + token.attrs[srcIndex][1] = state.md.options.discourse.getURL("/404"); + + token.attrs.push(["data-orig-href", origSrc]); + } } }); } diff --git a/app/assets/javascripts/pretty-text/package.json b/app/assets/javascripts/pretty-text/package.json index 59c35d79fa..e9ebc7e457 100644 --- a/app/assets/javascripts/pretty-text/package.json +++ b/app/assets/javascripts/pretty-text/package.json @@ -18,7 +18,7 @@ "ember-auto-import": "^2.2.4", "ember-cli-babel": "^7.13.0", "ember-cli-htmlbars": "^4.2.0", - "xss": "^1.0.8", + "xss": "^1.0.11", "webpack": "^5.67.0" }, "devDependencies": { diff --git a/app/assets/javascripts/select-kit/addon/components/category-row.js b/app/assets/javascripts/select-kit/addon/components/category-row.js index f6ca802b43..9ab473d382 100644 --- a/app/assets/javascripts/select-kit/addon/components/category-row.js +++ b/app/assets/javascripts/select-kit/addon/components/category-row.js @@ -110,7 +110,7 @@ export default SelectKitRowComponent.extend({ _formatDescription(description) { const limit = 200; - return `${description.substr(0, limit)}${ + return `${description.slice(0, limit)}${ description.length > limit ? "…" : "" }`; }, 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 f36a0db32f..3de48b094c 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit.js @@ -870,6 +870,21 @@ export default Component.extend( strategy, placement: this.selectKit.options.placement, modifiers: [ + { + name: "flip", + enabled: !inModal, + options: { + padding: { + top: + parseInt( + document.documentElement.style.getPropertyValue( + "--header-offset" + ), + 10 + ) || 0, + }, + }, + }, { name: "offset", options: { @@ -1103,6 +1118,7 @@ export default Component.extend( none: "options.none", rootNone: "options.none", disabled: "options.disabled", + isDisabled: "options.disabled", rootNoneLabel: "options.none", showFullTitle: "options.showFullTitle", title: "options.translatedNone", diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js index 689da33de0..5bd78815f7 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-header.js @@ -74,7 +74,7 @@ export default Component.extend(UtilsMixin, { }, keyDown(event) { - if (this.selectKit.isDisabled) { + if (this.selectKit.isDisabled || this.selectKit.options.disabled) { return; } diff --git a/app/assets/javascripts/select-kit/addon/components/tag-drop.js b/app/assets/javascripts/select-kit/addon/components/tag-drop.js index 9bd4bc5a01..971af05cf1 100644 --- a/app/assets/javascripts/select-kit/addon/components/tag-drop.js +++ b/app/assets/javascripts/select-kit/addon/components/tag-drop.js @@ -157,7 +157,9 @@ export default ComboBoxComponent.extend(TagsMixin, { .map((r) => { const content = context.defaultItem(r.id, r.text); content.targetTagId = r.target_tag || r.id; - content.count = r.count; + if (!context.currentCategory) { + content.count = r.count; + } content.pmCount = r.pm_count; return content; }); diff --git a/app/assets/javascripts/select-kit/addon/components/tag-group-chooser.js b/app/assets/javascripts/select-kit/addon/components/tag-group-chooser.js index 5abf91625a..8d2b8b999a 100644 --- a/app/assets/javascripts/select-kit/addon/components/tag-group-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/tag-group-chooser.js @@ -28,12 +28,6 @@ export default MultiSelectComponent.extend(TagsMixin, { .map((t) => this.defaultItem(t, t)); }), - actions: { - onChange(value) { - this.set("tagGroups", value); - }, - }, - search(query) { const data = { q: query, diff --git a/app/assets/javascripts/wizard/lib/preview.js b/app/assets/javascripts/wizard/lib/preview.js index 3ed162ccad..243f34e919 100644 --- a/app/assets/javascripts/wizard/lib/preview.js +++ b/app/assets/javascripts/wizard/lib/preview.js @@ -314,9 +314,9 @@ export function parseColor(color) { if (m) { const c = m[1]; return [ - parseInt(c.substr(0, 2), 16), - parseInt(c.substr(2, 2), 16), - parseInt(c.substr(4, 2), 16), + parseInt(c.slice(0, 2), 16), + parseInt(c.slice(2, 4), 16), + parseInt(c.slice(4, 6), 16), ]; } @@ -404,9 +404,9 @@ export function lighten(color, percent) { return ( "#" + - (0 | ((1 << 8) + color[0])).toString(16).substr(1) + - (0 | ((1 << 8) + color[1])).toString(16).substr(1) + - (0 | ((1 << 8) + color[2])).toString(16).substr(1) + (0 | ((1 << 8) + color[0])).toString(16).slice(1) + + (0 | ((1 << 8) + color[1])).toString(16).slice(1) + + (0 | ((1 << 8) + color[2])).toString(16).slice(1) ); } diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index cc6228b5bd..5689a8c977 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -2318,72 +2318,72 @@ resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz#4151a81b4052c80bc2becbae09f3a9ec010a9c7a" integrity sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg== -"@uppy/aws-s3-multipart@^2.1.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@uppy/aws-s3-multipart/-/aws-s3-multipart-2.1.1.tgz#7749491067ab72249dab201cc12409e57f2dbb1a" - integrity sha512-p+oFSCWEUc7ptv73sdZuWoq10hh0vzmP4cxwBEX/+nrplLFSuRUJ+z2XnNEigo8jXHWbA86k6tEX/3XIUsslgg== +"@uppy/aws-s3-multipart@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@uppy/aws-s3-multipart/-/aws-s3-multipart-2.2.1.tgz#385705b3ae6b56abee28fb086c046eeaeceee62a" + integrity sha512-57MZw2hxcBVeXp7xxdPNna+7HMckiJsrq/vwNM74aforLpNNSYE1B3JsiBeXU7fvIWx/W+5udtl8aAIKQxpJqw== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" + "@uppy/companion-client" "^2.0.5" + "@uppy/utils" "^4.0.5" -"@uppy/aws-s3@^2.0.4": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@uppy/aws-s3/-/aws-s3-2.0.5.tgz#dae2edb819b8e79119304a1659b931a862bf1e45" - integrity sha512-VWqVtmKtV/wSLCZdFbWlUt+CS7W/KZv20Pmm3JgcDLrQk3PdciYg3L9x65FTP8kSDsiXCwMg7uO5HfbspZWx9Q== +"@uppy/aws-s3@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@uppy/aws-s3/-/aws-s3-2.0.8.tgz#fd82b7db4d54ed118d369f856efe3d849e0482e3" + integrity sha512-ihZF3SpXZCZPxNapXshBhSC4TwNv0JlASZqd6T+u48Ojb6FZbYs7BgXLnLQooOGZPUx1UXtJREBh9SjXvn1lWw== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" - "@uppy/xhr-upload" "^2.0.5" + "@uppy/companion-client" "^2.0.5" + "@uppy/utils" "^4.0.5" + "@uppy/xhr-upload" "^2.0.7" nanoid "^3.1.25" -"@uppy/companion-client@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-2.0.3.tgz#d3cd30ebbc9f87d27374d13258b5d304366f10d5" - integrity sha512-I1baKKBpb3d//q3agRtNV3UD/sA7EecFOfoVSpMlPkFu6oQqxjSC5OFXTf3fa8X+wo4Lcutv1++3igPJ1zrgbA== +"@uppy/companion-client@^2.0.4", "@uppy/companion-client@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-2.0.5.tgz#ee0c177dac22afab132369f467d82e7017eeb468" + integrity sha512-yAeYbpQ+yHcklKVbkRy83V1Zv/0kvaTDTHaBvaaPmLtcKgeZE3pUjEI/7v2sTxvCVSy4cRjd9TRSXSSl5UCnuQ== dependencies: - "@uppy/utils" "^4.0.3" + "@uppy/utils" "^4.0.5" namespace-emitter "^2.0.1" -"@uppy/core@^2.1.0": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@uppy/core/-/core-2.1.1.tgz#503b3172ffe32e6cc7385f5b0c99f008ade815f1" - integrity sha512-dFlcy6+05zwsJk1KNeUKVWUyAfhOVwNpnPLaR1NX9Qsjv7KlYfUNRVW3uCCmIpd/EZsX44+haiqGrhLcYDAcxA== +"@uppy/core@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@uppy/core/-/core-2.1.6.tgz#8e3c6eca12c91118a6340a1aedc777c6b4f2f6f5" + integrity sha512-WTGthAAHMfB6uAtISbu+7jYh4opnBWHSf7A0jsPdREwXc4hrhC/z9lbejZfSLkVDXdbNwpWWH38EgOGCNQb5MQ== dependencies: "@transloadit/prettier-bytes" "0.0.7" - "@uppy/store-default" "^2.0.2" - "@uppy/utils" "^4.0.3" + "@uppy/store-default" "^2.0.3" + "@uppy/utils" "^4.0.5" lodash.throttle "^4.1.1" mime-match "^1.0.2" namespace-emitter "^2.0.1" nanoid "^3.1.25" preact "^10.5.13" -"@uppy/drop-target@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@uppy/drop-target/-/drop-target-1.1.1.tgz#9bfbcb7b284ef605d01fc24823f857cbad51377a" - integrity sha512-2MxNGEkI2vt1D6MEa0PNqR+VTMbuUzmiytHyy57phZNCNes8K4BdnneBwla2nG3LI0D1TURK7MKxaSjv93d3Vg== +"@uppy/drop-target@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@uppy/drop-target/-/drop-target-1.1.2.tgz#a78cc1c1947e55be4e16de71efdbacf9dfb7effd" + integrity sha512-iyLckwpxDqZr7ysH94cWwgta9P9SFus0sayXb9Lr/Kd0lk+tK/bcrJmsJHp4HYDFW7C8RphYdUu78C8NNXL09w== dependencies: - "@uppy/utils" "^4.0.3" + "@uppy/utils" "^4.0.5" -"@uppy/store-default@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-2.0.2.tgz#c0464e92452fdc7d4cd1548d2c7453017cad7a98" - integrity sha512-D9oz08EYBoc4fDotvaevd2Q7uVldS61HYFOXK20b5M/xXF/uxepapaqQnMu1DfCVsA77rhp7DMemxnWc9y8xTQ== +"@uppy/store-default@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-2.0.3.tgz#47ad4fc4816d21955ff37d6bb5096a93278c29e2" + integrity sha512-2BGlN1sW0cFv4rOqTK8dfSg579S984N1HxCJxLFqeW9nWD6zd/O8Omyd85tbxGQ+FLZLTmLOm/feD0YeCBMahg== -"@uppy/utils@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-4.0.3.tgz#181fdd161e1450d31af0cf7bc97946a99196a8fe" - integrity sha512-LApneC8lNvTonzSJFupxzuEvKhwp/Klc1otq8t+zXpdgjLVVSuW/rJBFfdIDrmDoqSzVLQKYjMy07CmhDAWfKg== +"@uppy/utils@^4.0.4", "@uppy/utils@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-4.0.5.tgz#0feda6e03d13af2fec969b146d7410462a8d2d48" + integrity sha512-uRv921A69UMjuWCLSC5tKXuIVoMOROVpFstIAQv5CoiCOCXyofcWpvAqELT7qlQJ5VRWha3uF5d/Z94SNnwxew== dependencies: lodash.throttle "^4.1.1" -"@uppy/xhr-upload@^2.0.4", "@uppy/xhr-upload@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-2.0.5.tgz#5792a7ff0bfb1503c8a9cccefb48ddb40deb11de" - integrity sha512-DkD6cRKrcI4oDmCimHAULb6rruyUt6SbH4/omhpvWILbG/mWV5vA39YLvYxCZ1FZbijJ4QkVTKEeOTLcmoljPg== +"@uppy/xhr-upload@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@uppy/xhr-upload/-/xhr-upload-2.0.7.tgz#d9128be1fdde78edc61878e23930a2855f607df5" + integrity sha512-bzCc654B0HfNmL4BIr7gGTvg2pQBucYgPmAb4ST7jGyWlEJWbSxMXR/19zvISQzpJ6v1uP6q2ppgxGMqNdj/rA== dependencies: - "@uppy/companion-client" "^2.0.3" - "@uppy/utils" "^4.0.3" + "@uppy/companion-client" "^2.0.4" + "@uppy/utils" "^4.0.4" nanoid "^3.1.25" "@webassemblyjs/ast@1.11.1": @@ -13864,6 +13864,14 @@ xmlhttprequest-ssl@~1.5.4: resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= +xss@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.11.tgz#211cb82e95b5071d4c75d597283c021157ebe46a" + integrity sha512-EimjrjThZeK2MO7WKR9mN5ZC1CSqivSl55wvUK5EtU6acf0rzEE1pN+9ZDrFXJ82BRp3JL38pPE6S4o/rpp1zQ== + dependencies: + commander "^2.20.3" + cssfilter "0.0.10" + xss@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.8.tgz#32feb87feb74b3dcd3d404b7a68ababf10700535" diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index f09583c1b3..8109dcf7f4 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -555,7 +555,7 @@ width: 80px; margin-bottom: 0; } - th.overriden { + th.overridden { text-align: right; } .color-input { diff --git a/app/assets/stylesheets/common/admin/emails.scss b/app/assets/stylesheets/common/admin/emails.scss index 29da56957d..58d1720a7a 100644 --- a/app/assets/stylesheets/common/admin/emails.scss +++ b/app/assets/stylesheets/common/admin/emails.scss @@ -32,6 +32,13 @@ max-width: 250px; @include ellipsis; } + + .email-details { + text-align: right; + a { + color: var(--primary-high); + } + } } .incoming-emails { diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index ed62a5c702..90646ddcac 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -234,6 +234,17 @@ .raw-topic-link > * { pointer-events: none; } + + &.focused { + box-shadow: inset 3px 0 0 var(--tertiary); + } + /* we have a custom focus indicator so we can remove the native one */ + .title:focus { + outline: none; + } + .title:focus-visible { + outline: none; + } } .unread-indicator { diff --git a/app/assets/stylesheets/common/base/crawler_layout.scss b/app/assets/stylesheets/common/base/crawler_layout.scss index fcf1e157cd..b8decdacd4 100644 --- a/app/assets/stylesheets/common/base/crawler_layout.scss +++ b/app/assets/stylesheets/common/base/crawler_layout.scss @@ -1,20 +1,37 @@ -body.crawler { +// IMPORTANT: This stylesheet needs to work for super old browsers, including those +// without support for `var()`. Therefore every color definition needs to define a simple +// value first, as a fallback + +body.crawler, +body > noscript { + font-family: serif; + a { + // we want all links to look like links + color: blue !important; + color: var(--tertiary) !important; + text-decoration: underline !important; + } > header { + // site header + box-sizing: border-box; width: 100%; top: 0; - z-index: z("max"); background-color: #fff; + background-color: var(--header_background); padding: 10px; box-shadow: none; - border-bottom: 1px solid var(--primary-low-mid); - box-sizing: border-box; + border-bottom: 1px solid #eee; + border-bottom: 1px solid var(--header_primary-medium); } .header-buttons { display: none; } + // topic list + div#main-outlet { + padding: 10px; div.post { word-break: break-word; img { @@ -27,8 +44,45 @@ body.crawler { .topic-list { table-layout: fixed; overflow: hidden; - margin-bottom: 1em; + margin: 2em 0; + + thead { + border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--primary_low); + th { + padding: 0 0 0.5em; + } + th:first-of-type { + width: 40%; + } + @media screen and (min-width: 500px) { + th { + &.replies, + &.views { + width: 10%; + } + } + th:first-of-type { + width: 50%; + } + th:last-of-type { + width: 15%; + } + } + } + + td { + padding: 10px 0; + &.posters { + padding: 10px 20px; + } + } + th:first-of-type { + padding-left: 0; + } + @media (max-width: 850px) { + table-layout: auto; td { word-break: break-all; &.posters { @@ -58,93 +112,159 @@ body.crawler { margin-top: 0.25em; } } - } - footer { - margin-top: 4em; + .topic-list-item { + border-bottom: 1px solid #eee; + border-bottom: 1px solid var(--primary-low); + > * { + padding: 0.75em 0; + } + td.main-link { + padding-right: 1em; + } + } } .topic-category { display: inline; } - .discourse-tags { - color: var(--primary-medium); + .topic-list-main-link a.title, + .topic-list .main-link a.title, + .latest-topic-list-item .main-link a.title { + padding: 0; } -} -.noscript-footer-nav { - clear: both; - margin-top: 4em; - a { - margin-right: 0.25em; - white-space: nowrap; + .topic-list .link-bottom-line { + margin-top: 0.25em; } -} -#noscript-footer { - padding: 0 10px; - text-align: center; -} + // topics -.crawler-post { - margin-top: 1em; - margin-bottom: 2em; - padding-top: 1.5em; - border-top: 1px solid var(--primary-low); -} - -.crawler-post-meta { - margin-bottom: 1em; - .creator { - word-break: break-all; - a { - font-weight: bold; + div#main-outlet { + div.post { + word-break: break-word; + img { + max-width: 100%; + height: auto; + } } - @include breakpoint(tablet) { - display: inline-block; + } + + .crawler-post { + margin-top: 1em; + margin-bottom: 2em; + padding-top: 1.5em; + border-top: 1px solid #eee; + border-top: 1px solid var(--primary-low); + } + + .crawler-post-meta { + margin-bottom: 1em; + .creator { + word-break: break-all; + a { + font-weight: bold; + } + @include breakpoint(tablet) { + display: inline-block; + margin-bottom: 0.25em; + } + } + } + + .crawler-post-infos { + color: #666; + display: inline-block; + @include breakpoint(tablet, min-width) { + float: right; + } + [itemprop="position"] { + float: left; + margin-right: 0.5em; + } + } + + .crawler-linkback-list { + margin-top: 1em; + a { + display: block; + padding: 0.5em 0; + border-top: 1px solid #ddd; + border-top: 1px solid var(--primary-low); + } + } + + #topic-title { + > * { + display: block; + } + h1 { + font-size: 2em; margin-bottom: 0.25em; } } -} -.crawler-post-infos { - color: var(--primary-high); - display: inline-block; - @include breakpoint(tablet, min-width) { - float: right; + .poll-info { + // crawler vote count always shows 0 + display: none; } - [itemprop="position"] { - float: left; - margin-right: 0.5em; - } -} -.crawler-linkback-list { - margin-top: 1em; - a { - display: block; - padding: 0.5em 0; - border-top: 1px solid var(--primary-low); + pre, + code, + blockquote, + aside.quote .title { + background: #eee; + background: var(--primary-low); } -} -.crawler-nav { - margin: 1em 0; - ul { - margin: 0; - list-style-type: none; + .md-table { + tr { + border: 1px solid #ddd; + border: 1px solid var(--primary-low); + } + th { + font-weight: bold; + } + td, + th { + padding: 0.25em; + border-right: 1px solid #ddd; + border-right: 1px solid var(--primary-low); + } } - li { - display: inline-block; - } - a { - display: inline-block; - padding: 0.5em 1em 0.5em 0; - } -} -.poll-info { - // crawler vote count always shows 0 - display: none; + // footer + + footer { + margin-top: 2em; + } + + .noscript-footer-nav { + margin-top: 4em; + a { + margin-right: 0.25em; + white-space: nowrap; + } + } + + #noscript-footer { + padding: 0 10px; + text-align: center; + } + + .crawler-nav { + margin: 1em 0; + ul { + margin: 0; + list-style-type: none; + } + li { + display: inline-block; + } + a { + display: inline-block; + padding: 0.5em 1em 0.5em 0; + } + } } diff --git a/app/assets/stylesheets/common/base/edit-category.scss b/app/assets/stylesheets/common/base/edit-category.scss index 0fcbd51823..45148d3798 100644 --- a/app/assets/stylesheets/common/base/edit-category.scss +++ b/app/assets/stylesheets/common/base/edit-category.scss @@ -162,6 +162,23 @@ div.edit-category { .category-default-slow-mode-seconds { width: 200px; } + + .required-tag-groups { + .required-tag-group-row { + display: flex; + gap: 0.5em; + + > * { + margin: 0; + } + + input[type="number"] { + width: 4em; + } + + margin-bottom: 1em; + } + } } .category-permissions-table { diff --git a/app/assets/stylesheets/common/base/login.scss b/app/assets/stylesheets/common/base/login.scss index e1828563de..a2110020be 100644 --- a/app/assets/stylesheets/common/base/login.scss +++ b/app/assets/stylesheets/common/base/login.scss @@ -389,6 +389,9 @@ button#new-account-link { padding: 1em 0.5em 1em 0; color: var(--tertiary); } + button { + margin-right: 1em; + } } .invite-error { diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 4b09e63783..f19d70c577 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -747,6 +747,17 @@ .btn-group { margin-top: 0; + display: flex; + align-items: stretch; + gap: 0.5em; + + .btn { + padding-block: 0.65em; + + .d-icon { + margin-right: 0; + } + } } .json-editor-btn-delete { diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index a99fd749d0..7362d75b20 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -192,6 +192,11 @@ $quote-share-maxwidth: 150px; img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji) { max-width: 100%; height: auto; + + @supports not (aspect-ratio: 1) { + // (see javascripts/discourse/app/initializers/image-aspect-ratio.js) + height: var(--calculated-height); + } } } @@ -388,6 +393,10 @@ nav.post-controls { color: currentColor; } } + &[disabled]:hover { + background-color: transparent; + cursor: not-allowed; + } &.fade-out { opacity: 1; } diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index 73d990f5d7..18d61d1c2e 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -197,16 +197,6 @@ } } - &.facebook { - .d-icon { - color: $facebook; - } - &:hover { - .d-icon { - color: $facebook; - } - } - } &.cas { .d-icon { color: var(--cas); diff --git a/app/assets/stylesheets/desktop/login.scss b/app/assets/stylesheets/desktop/login.scss index 93c6946501..1ee881f000 100644 --- a/app/assets/stylesheets/desktop/login.scss +++ b/app/assets/stylesheets/desktop/login.scss @@ -14,13 +14,16 @@ color: rgba(var(--primary-rgb), 0.85); } } + h2.login-with { + color: var(--secondary); + } #login-buttons:not(.hidden) { display: flex; flex: 0 1 auto; flex-direction: column; align-items: stretch; min-height: 75px; - min-width: 160px; + min-width: 200px; order: 2; &:focus-within, @@ -211,7 +214,7 @@ } .has-alt-auth .create-account-form { display: grid; - grid-template-columns: 65% 35%; + grid-template-columns: 60% 40%; grid-template-rows: auto 1fr; grid-template-areas: "header login-buttons" diff --git a/app/assets/stylesheets/mobile/components/user-card.scss b/app/assets/stylesheets/mobile/components/user-card.scss index 1e7b33e3d2..ec6c8d05f6 100644 --- a/app/assets/stylesheets/mobile/components/user-card.scss +++ b/app/assets/stylesheets/mobile/components/user-card.scss @@ -23,7 +23,7 @@ $avatar_width: 120px; flex-wrap: wrap; margin: 1em calc(var(--usercard-control-margin) * -1) 0; li { - flex: 1 0 auto; + flex: 1 0 calc(50% - (var(--usercard-control-margin) * 2)); min-width: 0; margin: 0 var(--usercard-control-margin); &:empty { @@ -31,6 +31,7 @@ $avatar_width: 120px; } button { @include ellipsis; + margin-bottom: calc(var(--usercard-control-margin) * 2); } } } diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index 9ad066ee28..09a090494d 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -155,3 +155,9 @@ sub sub { } } } + +.container.posts .topic-navigation { + // better positioning for the docked progress bar on large screens using mobile view + grid-area: posts; + grid-row: 2; +} diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 806bf4636d..8d54a2b4e4 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -209,6 +209,14 @@ class Admin::EmailController < Admin::AdminController incoming_email = IncomingEmail.find_by(to_addresses: bounced_to_address) end + # Temporary fix until all old format of emails has been purged via lib/email/cleaner.rb + if incoming_email.nil? + email_local_part, email_domain = SiteSetting.reply_by_email_address.split('@') + subdomain, root_domain, extension = email_domain&.split('.') + bounced_to_address = "#{subdomain}+verp-#{email_log.bounce_key}@#{root_domain}.#{extension}" + incoming_email = IncomingEmail.find_by(to_addresses: bounced_to_address) + end + raise Discourse::NotFound if incoming_email.nil? serializer = IncomingEmailDetailsSerializer.new(incoming_email, root: false) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f826361557..04b1dcceb4 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -455,6 +455,7 @@ class Admin::UsersController < Admin::AdminController begin user = sso.lookup_or_create_user + DiscourseEvent.trigger(:sync_sso, user) render_serialized(user, AdminDetailedUserSerializer, root: false) rescue ActiveRecord::RecordInvalid => ex render json: failed_json.merge(message: ex.message), status: 403 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97c8f72151..4e6b69a49c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -251,16 +251,20 @@ class ApplicationController < ActionController::Base end rescue_from SecondFactor::AuthManager::SecondFactorRequired do |e| - render json: { - second_factor_challenge_nonce: e.nonce - }, status: 403 + if request.xhr? + render json: { + second_factor_challenge_nonce: e.nonce + }, status: 403 + else + redirect_to session_2fa_path(nonce: e.nonce) + end end rescue_from SecondFactor::BadChallenge do |e| render json: { error: I18n.t(e.error_translation_key) }, status: e.status_code end - def redirect_with_client_support(url, options) + def redirect_with_client_support(url, options = {}) if request.xhr? response.headers['Discourse-Xhr-Redirect'] = 'true' render plain: url @@ -482,6 +486,11 @@ class ApplicationController < ActionController::Base end def guardian + # sometimes we log on a user in the middle of a request so we should throw + # away the cached guardian instance when we do that + if (@guardian&.user).blank? && current_user.present? + @guardian = Guardian.new(current_user, request) + end @guardian ||= Guardian.new(current_user, request) end @@ -978,13 +987,15 @@ class ApplicationController < ActionController::Base end end - def run_second_factor!(action_class) - action = action_class.new(guardian) + def run_second_factor!(action_class, action_data = nil) + action = action_class.new(guardian, request, action_data) manager = SecondFactor::AuthManager.new(guardian, action) yield(manager) if block_given? result = manager.run!(request, params, secure_session) - if !result.no_second_factors_enabled? && !result.second_factor_auth_completed? + if !result.no_second_factors_enabled? && + !result.second_factor_auth_completed? && + !result.second_factor_auth_skipped? # should never happen, but I want to know if somehow it does! (osama) raise "2fa process ended up in a bad state!" end diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index a71588f24b..9aabad449b 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -4,22 +4,42 @@ class BookmarksController < ApplicationController requires_login def create - params.require(:post_id) + if SiteSetting.use_polymorphic_bookmarks + params.require(:bookmarkable_id) + params.require(:bookmarkable_type) + else + params.require(:post_id) + end RateLimiter.new( current_user, "create_bookmark", SiteSetting.max_bookmarks_per_day, 1.day.to_i ).performed! bookmark_manager = BookmarkManager.new(current_user) - bookmark = bookmark_manager.create( - post_id: params[:post_id], + + create_params = { name: params[:name], reminder_at: params[:reminder_at], - for_topic: params[:for_topic] == "true", options: { auto_delete_preference: params[:auto_delete_preference] || 0 } - ) + } + + if SiteSetting.use_polymorphic_bookmarks + bookmark = bookmark_manager.create_for( + **create_params.merge( + bookmarkable_id: params[:bookmarkable_id], + bookmarkable_type: params[:bookmarkable_type] + ) + ) + else + bookmark = bookmark_manager.create( + **create_params.merge( + post_id: params[:post_id], + for_topic: params[:for_topic] == "true", + ) + ) + end if bookmark_manager.errors.empty? return render json: success_json.merge(id: bookmark.id) @@ -30,8 +50,8 @@ class BookmarksController < ApplicationController def destroy params.require(:id) - result = BookmarkManager.new(current_user).destroy(params[:id]) - render json: success_json.merge(result) + destroyed_bookmark = BookmarkManager.new(current_user).destroy(params[:id]) + render json: success_json.merge(BookmarkManager.bookmark_metadata(destroyed_bookmark, current_user)) end def update diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index aa64dcf30b..163734cf81 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -318,7 +318,7 @@ class CategoriesController < ApplicationController if SiteSetting.tagging_enabled params[:allowed_tags] = params[:allowed_tags].presence || [] if params[:allowed_tags] params[:allowed_tag_groups] = params[:allowed_tag_groups].presence || [] if params[:allowed_tag_groups] - params[:required_tag_group_name] = params[:required_tag_group_name].presence || '' if params[:required_tag_group_name] + params[:required_tag_groups] = params[:required_tag_groups].presence || [] if params[:required_tag_groups] end if SiteSetting.enable_category_group_moderation? @@ -357,8 +357,6 @@ class CategoriesController < ApplicationController :navigate_to_first_post_after_read, :search_priority, :allow_global_tags, - :required_tag_group_name, - :min_tags_from_required_group, :read_only_banner, :default_list_filter, :reviewable_by_group_id, @@ -366,8 +364,13 @@ class CategoriesController < ApplicationController permissions: [*p.try(:keys)], allowed_tags: [], allowed_tag_groups: [], + required_tag_groups: [:name, :min_count] ) + if result[:required_tag_groups] && !result[:required_tag_groups].is_a?(Array) + raise Discourse::InvalidParameters.new(:required_tag_groups) + end + result end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 153790cfbb..a5ad225ddb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -150,38 +150,32 @@ class GroupsController < ApplicationController def update group = Group.find(params[:id]) - guardian.ensure_can_edit!(group) unless guardian.can_admin_group?(group) + guardian.ensure_can_edit!(group) if !guardian.can_admin_group?(group) - params_with_permitted = group_params(automatic: group.automatic) - clear_disabled_email_settings(group, params_with_permitted) + group_attributes = group_params(automatic: group.automatic) + reset_group_email_settings_if_disabled!(group, group_attributes) categories, tags = [] if !group.automatic || current_user.admin - notification_level, categories, tags = user_default_notifications(group, params_with_permitted) + notification_level, categories, tags = user_default_notifications(group, group_attributes) if params[:update_existing_users].blank? user_count = count_existing_users(group.group_users, notification_level, categories, tags) - - if user_count > 0 - render json: { user_count: user_count } - return - end + return render status: 422, json: { user_count: user_count, errors: [I18n.t('invalid_params', message: :update_existing_users)] } if user_count > 0 end end - if group.update(params_with_permitted) + if group.update(group_attributes) GroupActionLogger.new(current_user, group).log_change_group_settings group.record_email_setting_changes!(current_user) group.expire_imap_mailbox_cache update_existing_users(group.group_users, notification_level, categories, tags) if params[:update_existing_users] == "true" AdminDashboardData.clear_found_problem("group_#{group.id}_email_credentials") - if guardian.can_see?(group) - render json: success_json - else - # They can no longer see the group after changing permissions - render json: { route_to: '/g' } - end + # Redirect user to groups index page if they can no longer see the group + return redirect_with_client_support groups_path if !guardian.can_see?(group) + + render json: success_json else render_json_error(group) end @@ -658,87 +652,74 @@ class GroupsController < ApplicationController end def group_params(automatic: false) - permitted_params = - if automatic - %i{ - visibility_level - mentionable_level - messageable_level - default_notification_level - bio_raw - flair_icon - flair_upload_id - flair_bg_color - flair_color - } - else - default_params = %i{ - mentionable_level - messageable_level - title - flair_icon - flair_upload_id - flair_bg_color - flair_color - bio_raw - public_admission - public_exit - allow_membership_requests - full_name - default_notification_level - membership_request_template - } + attributes = %i{ + bio_raw + default_notification_level + messageable_level + mentionable_level + flair_bg_color + flair_color + flair_icon + flair_upload_id + } - if current_user.staff? - default_params.push(*[ - :incoming_email, - :smtp_server, - :smtp_port, - :smtp_ssl, - :smtp_enabled, - :smtp_updated_by, - :smtp_updated_at, - :imap_server, - :imap_port, - :imap_ssl, - :imap_mailbox_name, - :imap_enabled, - :imap_updated_by, - :imap_updated_at, - :email_username, - :email_password, - :email_from_alias, - :primary_group, - :visibility_level, - :members_visibility_level, - :name, - :grant_trust_level, - :automatic_membership_email_domains, - :publish_read_state, - :allow_unknown_sender_topic_replies - ]) + if automatic + attributes.push(:visibility_level) + else + attributes.push( + :title, + :allow_membership_requests, + :full_name, + :public_exit, + :public_admission, + :membership_request_template + ) + end - custom_fields = DiscoursePluginRegistry.editable_group_custom_fields - default_params << { custom_fields: custom_fields } unless custom_fields.blank? - end + if !automatic && current_user.staff? + attributes.push( + :incoming_email, + :smtp_server, + :smtp_port, + :smtp_ssl, + :smtp_enabled, + :smtp_updated_by, + :smtp_updated_at, + :imap_server, + :imap_port, + :imap_ssl, + :imap_mailbox_name, + :imap_enabled, + :imap_updated_by, + :imap_updated_at, + :email_username, + :email_password, + :email_from_alias, + :primary_group, + :visibility_level, + :members_visibility_level, + :name, + :grant_trust_level, + :automatic_membership_email_domains, + :publish_read_state, + :allow_unknown_sender_topic_replies + ) - default_params - end + custom_fields = DiscoursePluginRegistry.editable_group_custom_fields + attributes << { custom_fields: custom_fields } if custom_fields.present? + end if !automatic || current_user.admin [:muted, :regular, :tracking, :watching, :watching_first_post].each do |level| - permitted_params << { "#{level}_category_ids" => [] } - permitted_params << { "#{level}_tags" => [] } + attributes << { "#{level}_category_ids" => [] } + attributes << { "#{level}_tags" => [] } end end - if guardian.can_associate_groups? - permitted_params << { associated_group_ids: [] } - end + attributes << { associated_group_ids: [] } if guardian.can_associate_groups? + attributes.concat(DiscoursePluginRegistry.group_params) - permitted_params = permitted_params | DiscoursePluginRegistry.group_params - - params.require(:group).permit(*permitted_params) + params.require(:group).permit(*attributes) end def find_group(param_name, ensure_can_see: true) @@ -767,23 +748,23 @@ class GroupsController < ApplicationController users end - def clear_disabled_email_settings(group, params_with_permitted) - should_clear_imap = group.imap_enabled && params_with_permitted.key?(:imap_enabled) && params_with_permitted[:imap_enabled] == "false" - should_clear_smtp = group.smtp_enabled && params_with_permitted.key?(:smtp_enabled) && params_with_permitted[:smtp_enabled] == "false" + def reset_group_email_settings_if_disabled!(group, attributes) + should_clear_imap = group.imap_enabled && attributes[:imap_enabled] == "false" + should_clear_smtp = group.smtp_enabled && attributes[:smtp_enabled] == "false" if should_clear_imap || should_clear_smtp - params_with_permitted[:imap_server] = nil - params_with_permitted[:imap_ssl] = false - params_with_permitted[:imap_port] = nil - params_with_permitted[:imap_mailbox_name] = "" + attributes[:imap_server] = nil + attributes[:imap_ssl] = false + attributes[:imap_port] = nil + attributes[:imap_mailbox_name] = "" end if should_clear_smtp - params_with_permitted[:smtp_server] = nil - params_with_permitted[:smtp_ssl] = false - params_with_permitted[:smtp_port] = nil - params_with_permitted[:email_username] = nil - params_with_permitted[:email_password] = nil + attributes[:smtp_server] = nil + attributes[:smtp_ssl] = false + attributes[:smtp_port] = nil + attributes[:email_username] = nil + attributes[:email_password] = nil end end diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 75d3ff1318..0ec590a3ea 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -245,8 +245,8 @@ class ListController < ApplicationController ensure_can_see_profile!(target_user) @title = "#{SiteSetting.title} - #{I18n.t("rss_description.user_topics", username: target_user.username)}" - @link = "#{Discourse.base_url}/u/#{target_user.username}/activity/topics" - @atom_link = "#{Discourse.base_url}/u/#{target_user.username}/activity/topics.rss" + @link = "#{target_user.full_url}/activity/topics" + @atom_link = "#{target_user.full_url}/activity/topics.rss" @description = I18n.t("rss_description.user_topics", username: target_user.username) @topic_list = TopicQuery diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index be388f7d7a..b7e0df39c5 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -132,7 +132,7 @@ class PostsController < ApplicationController format.rss do @posts = posts @title = "#{SiteSetting.title} - #{I18n.t("rss_description.user_posts", username: user.username)}" - @link = "#{Discourse.base_url}/u/#{user.username}/activity" + @link = "#{user.full_url}/activity" @description = I18n.t("rss_description.user_posts", username: user.username) render 'posts/latest', formats: [:rss] end @@ -537,9 +537,9 @@ class PostsController < ApplicationController params.require(:post_id) bookmark_id = Bookmark.where(post_id: params[:post_id], user_id: current_user.id).pluck_first(:id) - result = BookmarkManager.new(current_user).destroy(bookmark_id) + destroyed_bookmark = BookmarkManager.new(current_user).destroy(bookmark_id) - render json: success_json.merge(result) + render json: success_json.merge(BookmarkManager.bookmark_metadata(destroyed_bookmark, current_user)) end def wiki diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 585002b405..418de7a48d 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -39,77 +39,56 @@ class SessionController < ApplicationController end end - def sso_provider(payload = nil) - if SiteSetting.enable_discourse_connect_provider - begin - if !payload - params.require(:sso) - payload = request.query_string - end - sso = DiscourseConnectProvider.parse(payload) - rescue DiscourseConnectProvider::BlankSecret - render plain: I18n.t("discourse_connect.missing_secret"), status: 400 - return - rescue DiscourseConnectProvider::ParseError => e - if SiteSetting.verbose_discourse_connect_logging - Rails.logger.warn("Verbose SSO log: Signature parse error\n\n#{e.message}\n\n#{sso&.diagnostics}") - end + def sso_provider(payload = nil, confirmed_2fa_during_login = false) + if !SiteSetting.enable_discourse_connect_provider + render body: nil, status: 404 + return + end - # Do NOT pass the error text to the client, it would give them the correct signature - render plain: I18n.t("discourse_connect.login_error"), status: 422 - return - end + result = run_second_factor!( + SecondFactor::Actions::DiscourseConnectProvider, + payload: payload, + confirmed_2fa_during_login: confirmed_2fa_during_login + ) - if sso.return_sso_url.blank? - render plain: "return_sso_url is blank, it must be provided", status: 400 - return - end - - if sso.logout - params[:return_url] = sso.return_sso_url + if result.second_factor_auth_skipped? + data = result.data + if data[:logout] + params[:return_url] = data[:return_sso_url] destroy return end - if current_user - sso.name = current_user.name - sso.username = current_user.username - sso.email = current_user.email - sso.external_id = current_user.id.to_s - sso.admin = current_user.admin? - sso.moderator = current_user.moderator? - sso.groups = current_user.groups.pluck(:name).join(",") - - if current_user.uploaded_avatar.present? - base_url = Discourse.store.external? ? "#{Discourse.store.absolute_base_url}/" : Discourse.base_url - avatar_url = "#{base_url}#{Discourse.store.get_path_for_upload(current_user.uploaded_avatar)}" - sso.avatar_url = UrlHelper.absolute Discourse.store.cdn_url(avatar_url) - end - - if current_user.user_profile.profile_background_upload.present? - sso.profile_background_url = UrlHelper.absolute(upload_cdn_path( - current_user.user_profile.profile_background_upload.url - )) - end - - if current_user.user_profile.card_background_upload.present? - sso.card_background_url = UrlHelper.absolute(upload_cdn_path( - current_user.user_profile.card_background_upload.url - )) - end - - if request.xhr? - cookies[:sso_destination_url] = sso.to_url(sso.return_sso_url) - else - redirect_to sso.to_url(sso.return_sso_url) - end - else - cookies[:sso_payload] = request.query_string + if data[:no_current_user] + cookies[:sso_payload] = payload || request.query_string redirect_to path('/login') + return end - else - render body: nil, status: 404 + + if request.xhr? + # for the login modal + cookies[:sso_destination_url] = data[:sso_redirect_url] + else + redirect_to data[:sso_redirect_url] + end + elsif result.no_second_factors_enabled? + if request.xhr? + # for the login modal + cookies[:sso_destination_url] = result.data[:sso_redirect_url] + else + redirect_to result.data[:sso_redirect_url] + end + elsif result.second_factor_auth_completed? + redirect_url = result.data[:sso_redirect_url] + render json: success_json.merge(redirect_url: redirect_url) end + rescue DiscourseConnectProvider::BlankSecret + render plain: I18n.t("discourse_connect.missing_secret"), status: 400 + rescue DiscourseConnectProvider::ParseError => e + # Do NOT pass the error text to the client, it would give them the correct signature + render plain: I18n.t("discourse_connect.login_error"), status: 422 + rescue DiscourseConnectProvider::BlankReturnUrl + render plain: "return_sso_url is blank, it must be provided", status: 400 end # For use in development mode only when login options could be limited or disabled. @@ -334,11 +313,12 @@ class SessionController < ApplicationController return render json: payload end - if !authenticate_second_factor(user) + second_factor_auth_result = authenticate_second_factor(user) + if !second_factor_auth_result.ok return render(json: @second_factor_failure_payload) end - (user.active && user.email_confirmed?) ? login(user) : not_activated(user) + (user.active && user.email_confirmed?) ? login(user, second_factor_auth_result) : not_activated(user) end def email_login_info @@ -388,7 +368,7 @@ class SessionController < ApplicationController rate_limit_second_factor!(user) - if user.present? && !authenticate_second_factor(user) + if user.present? && !authenticate_second_factor(user).ok return render(json: @second_factor_failure_payload) end @@ -534,7 +514,7 @@ class SessionController < ApplicationController ok: true, callback_method: challenge[:callback_method], callback_path: challenge[:callback_path], - redirect_path: challenge[:redirect_path] + redirect_url: challenge[:redirect_url] }, status: 200 end @@ -651,10 +631,10 @@ class SessionController < ApplicationController failure_payload.merge!(Webauthn.allowed_credentials(user, secure_session)) end @second_factor_failure_payload = failed_json.merge(failure_payload) - return false + return second_factor_authentication_result end - true + second_factor_authentication_result end def login_error_check(user) @@ -706,13 +686,18 @@ class SessionController < ApplicationController } end - def login(user) + def login(user, second_factor_auth_result) session.delete(ACTIVATE_USER_KEY) user.update_timezone_if_missing(params[:timezone]) log_on_user(user) if payload = cookies.delete(:sso_payload) - sso_provider(payload) + confirmed_2fa_during_login = ( + second_factor_auth_result&.ok && + second_factor_auth_result.used_2fa_method.present? && + second_factor_auth_result.used_2fa_method != UserSecondFactor.methods[:backup_codes] + ) + sso_provider(payload, confirmed_2fa_during_login) else render_serialized(user, UserSerializer) end diff --git a/app/controllers/sitemap_controller.rb b/app/controllers/sitemap_controller.rb new file mode 100644 index 0000000000..3ae7efcde0 --- /dev/null +++ b/app/controllers/sitemap_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class SitemapController < ApplicationController + layout false + skip_before_action :preload_json, :check_xhr + before_action :check_sitemap_enabled + + def index + @sitemaps = Sitemap + .where(enabled: true) + .where.not(name: Sitemap::NEWS_SITEMAP_NAME) + + render :index + end + + def page + index = params.require(:page) + sitemap = Sitemap.find_by(enabled: true, name: index.to_s) + raise Discourse::NotFound if sitemap.nil? + + @output = Rails.cache.fetch("sitemap/#{sitemap.name}/#{sitemap.max_page_size}", expires_in: 24.hours) do + @topics = sitemap.topics + render :page, content_type: 'text/xml; charset=UTF-8' + end + + render plain: @output, content_type: 'text/xml; charset=UTF-8' unless performed? + end + + def recent + sitemap = Sitemap.touch(Sitemap::RECENT_SITEMAP_NAME) + + @output = Rails.cache.fetch("sitemap/recent/#{sitemap.last_posted_at.to_i}", expires_in: 1.hour) do + @topics = sitemap.topics + render :page, content_type: 'text/xml; charset=UTF-8' + end + + render plain: @output, content_type: 'text/xml; charset=UTF-8' unless performed? + end + + def news + sitemap = Sitemap.touch(Sitemap::NEWS_SITEMAP_NAME) + + @output = Rails.cache.fetch("sitemap/news", expires_in: 5.minutes) do + dlocale = SiteSetting.default_locale.downcase + @locale = dlocale.gsub(/_.*/, '') + @locale = dlocale.sub('_', '-') if @locale === "zh" + @topics = sitemap.topics + render :news, content_type: 'text/xml; charset=UTF-8' + end + + render plain: @output, content_type: 'text/xml; charset=UTF-8' unless performed? + end + + private + + def check_sitemap_enabled + raise Discourse::NotFound if !SiteSetting.enable_sitemap + end + + def build_sitemap_topic_url(slug, id, posts_count = nil) + base_url = [Discourse.base_url, 't', slug, id].join('/') + return base_url if posts_count.nil? + + page, mod = posts_count.divmod(TopicView.chunk_size) + page += 1 if mod > 0 + + page > 1 ? "#{base_url}?page=#{page}" : base_url + end + helper_method :build_sitemap_topic_url + +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 659a4cb221..768053e00a 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1065,10 +1065,27 @@ class UsersController < ApplicationController @user.enqueue_welcome_message('welcome_user') if @user.send_welcome_message log_on_user(@user) + # invites#perform_accept_invitation already sets destination_url, but + # sometimes it is lost (user changes browser, uses incognito, etc) + # + # The code below checks if the user was invited and redirects them to + # the topic they were originally invited to. + destination_url = cookies.delete(:destination_url) + if destination_url.blank? + topic = Invite + .joins(:invited_users) + .find_by(invited_users: { user_id: @user.id }) + &.topics + &.first + + if @user.guardian.can_see?(topic) + destination_url = path(topic.relative_url) + end + end + if Wizard.user_requires_completion?(@user) return redirect_to(wizard_path) - elsif destination_url = cookies[:destination_url] - cookies[:destination_url] = nil + elsif destination_url.present? return redirect_to(destination_url) elsif SiteSetting.enable_discourse_connect_provider && payload = cookies.delete(:sso_payload) return redirect_to(session_sso_provider_url + "?" + payload) @@ -1365,6 +1382,8 @@ class UsersController < ApplicationController elsif params[:notification_level] == "normal" MutedUser.where(user: acting_user, muted_user: target_user).delete_all IgnoredUser.where(user: acting_user, ignored_user: target_user).delete_all + else + return render_json_error(I18n.t("notification_level.invalid_value", value: params[:notification_level])) end render json: success_json diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c60e06cde0..dcca394602 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -393,7 +393,11 @@ module ApplicationHelper end def include_crawler_content? - crawler_layout? || !mobile_view? + crawler_layout? || !mobile_view? || !modern_mobile_device? + end + + def modern_mobile_device? + MobileDetection.modern_mobile_device?(request.user_agent) end def mobile_device? @@ -654,13 +658,7 @@ module ApplicationHelper end def rss_creator(user) - if user - if SiteSetting.prioritize_username_in_ux - "#{user.username}" - else - "#{user.name.presence || user.username }" - end - end + user&.display_name end def authentication_data diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb index f738ac4973..3f15a570a8 100644 --- a/app/helpers/email_helper.rb +++ b/app/helpers/email_helper.rb @@ -29,11 +29,70 @@ module EmailHelper EmailStyle.new.html .sub('%{email_content}') { capture { yield } } .gsub('%{html_lang}', html_lang) + .gsub('%{dark_mode_meta_tags}', SiteSetting.dark_mode_emails_active ? dark_mode_meta_tags : "") + .gsub('%{dark_mode_styles}', SiteSetting.dark_mode_emails_active ? dark_mode_styles : "") .html_safe end protected + def dark_mode_meta_tags + " + + + " + end + + def dark_mode_styles + " + + " + end + def extract_details(topic) if SiteSetting.private_email? [topic.slugless_url, private_topic_title(topic)] diff --git a/app/jobs/onceoff/onceoff.rb b/app/jobs/onceoff/onceoff.rb index 847779b817..d10b1f41a6 100644 --- a/app/jobs/onceoff/onceoff.rb +++ b/app/jobs/onceoff/onceoff.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require_relative '../base.rb' - class Jobs::Onceoff < ::Jobs::Base sidekiq_options retry: false diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 8ecade8206..eeac9ff89e 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -16,6 +16,7 @@ module Jobs post = Post.find_by(id: @post_id) return if post.blank? return if post.topic.blank? + return if post.cook_method == Post.cook_methods[:raw_html] raw = post.raw.dup start_raw = raw.dup diff --git a/app/jobs/regular/sync_acls_for_uploads.rb b/app/jobs/regular/sync_acls_for_uploads.rb new file mode 100644 index 0000000000..d3533ff5ab --- /dev/null +++ b/app/jobs/regular/sync_acls_for_uploads.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Jobs + # Sometimes we need to update a _lot_ of ACLs on S3 (such as when secure media + # is enabled), and since it takes ~1s per upload to update the ACL, this is + # best spread out over many jobs instead of having to do the whole thing serially. + class SyncAclsForUploads < ::Jobs::Base + def execute(args) + return if !Discourse.store.external? + return if !args.key?(:upload_ids) + + # TODO (martin) Change the logging here to debug after acl_stale implemented. + # + # Note...these log messages are set to warn to ensure this is working + # as intended in initial production trials, this will be set to debug + # after an acl_stale column is added to uploads. + time = Benchmark.measure do + Rails.logger.warn("Syncing ACL for upload ids: #{args[:upload_ids].join(", ")}") + Upload.includes(:optimized_images).where(id: args[:upload_ids]).find_in_batches do |uploads| + uploads.each do |upload| + Discourse.store.update_upload_ACL(upload, optimized_images_preloaded: true) + end + end + Rails.logger.warn("Completed syncing ACL for upload ids in #{time.to_s}. IDs: #{args[:upload_ids].join(", ")}") + end + end + end +end diff --git a/app/jobs/regular/sync_topic_user_bookmarked.rb b/app/jobs/regular/sync_topic_user_bookmarked.rb index bd630253e8..8c0621875f 100644 --- a/app/jobs/regular/sync_topic_user_bookmarked.rb +++ b/app/jobs/regular/sync_topic_user_bookmarked.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true module Jobs + + # TODO (martin) [POLYBOOK] This will need to be restructured for polymorphic + # bookmarks when edge cases are handled. class SyncTopicUserBookmarked < ::Jobs::Base def execute(args = {}) topic_id = args[:topic_id] diff --git a/app/jobs/scheduled/regenerate_sitemaps.rb b/app/jobs/scheduled/regenerate_sitemaps.rb new file mode 100644 index 0000000000..36b9ac9b67 --- /dev/null +++ b/app/jobs/scheduled/regenerate_sitemaps.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Jobs + class RegenerateSitemaps < ::Jobs::Scheduled + every 1.hour + + def execute(_args) + Sitemap.regenerate_sitemaps if SiteSetting.enable_sitemap? + end + end +end diff --git a/app/mailers/group_smtp_mailer.rb b/app/mailers/group_smtp_mailer.rb index 25bb887b0d..15e20959e1 100644 --- a/app/mailers/group_smtp_mailer.rb +++ b/app/mailers/group_smtp_mailer.rb @@ -77,24 +77,16 @@ class GroupSmtpMailer < ActionMailer::Base list = [] post.topic.allowed_groups.each do |g| - list.push("[#{g.name_full_preferred}](#{Discourse.base_url}/groups/#{g.name})") + list.push("[#{g.name_full_preferred}](#{g.full_url})") end post.topic.allowed_users.each do |u| next if u.id == recipient_user.id - if SiteSetting.prioritize_username_in_ux? - if u.staged? - list.push("#{u.email}") - else - list.push("[#{u.username}](#{Discourse.base_url}/u/#{u.username_lower})") - end + if u.staged? + list.push("#{u.email}") else - if u.staged? - list.push("#{u.email}") - else - list.push("[#{u.name.blank? ? u.username : u.name}](#{Discourse.base_url}/u/#{u.username_lower})") - end + list.push("[#{u.display_name}](#{u.full_url})") end end diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 5cca25310f..8ca231a5f3 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -556,17 +556,12 @@ class UserNotifications < ActionMailer::Base participant_list = [] post.topic.allowed_groups.each do |g| - participant_list.push "[#{g.name} (#{g.users.count})](#{Discourse.base_url}/groups/#{g.name})" + participant_list.push "[#{g.name} (#{g.users.count})](#{g.full_url})" end post.topic.allowed_users.each do |u| next if u.id == user.id - - if SiteSetting.prioritize_username_in_ux? - participant_list.push "[#{u.username}](#{Discourse.base_url}/u/#{u.username_lower})" - else - participant_list.push "[#{u.name.blank? ? u.username : u.name}](#{Discourse.base_url}/u/#{u.username_lower})" - end + participant_list.push "[#{u.display_name}](#{u.full_url})" end participants += participant_list.join(", ") diff --git a/app/models/api_key.rb b/app/models/api_key.rb index b55b496cee..99f8e209ac 100644 --- a/app/models/api_key.rb +++ b/app/models/api_key.rb @@ -67,6 +67,13 @@ class ApiKey < ActiveRecord::Base api_key_scopes.blank? || api_key_scopes.any? { |s| s.permits?(env) } end + + def update_last_used!(now = Time.zone.now) + return if last_used_at && (last_used_at > 1.minute.ago) + + # using update_column to avoid the AR transaction + update_column(:last_used_at, now) + end end # == Schema Information diff --git a/app/models/api_key_scope.rb b/app/models/api_key_scope.rb index a0b86bf590..07d532f22a 100644 --- a/app/models/api_key_scope.rb +++ b/app/models/api_key_scope.rb @@ -103,7 +103,7 @@ class ApiKeyScope < ActiveRecord::Base end def find_urls(actions:, methods:) - urls = [] + urls = Set.new if actions.present? route_sets = [Rails.application.routes] @@ -120,7 +120,11 @@ class ApiKeyScope < ActiveRecord::Base defaults = route.defaults action = "#{defaults[:controller].to_s}##{defaults[:action]}" path = route.path.spec.to_s.gsub(/\(\.:format\)/, '') - api_supported_path = path.end_with?('.rss') || route.path.requirements[:format]&.match?('json') + api_supported_path = ( + path.end_with?('.rss') || + !route.path.requirements[:format] || + route.path.requirements[:format].match?('json') + ) excluded_paths = %w[/new-topic /new-message /exception] if actions.include?(action) && api_supported_path && !excluded_paths.include?(path) @@ -136,7 +140,7 @@ class ApiKeyScope < ActiveRecord::Base end end - urls + urls.to_a end end diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index e2f413bf13..6007677cde 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -4,8 +4,6 @@ class Bookmark < ActiveRecord::Base # these columns were here for a very short amount of time, # hence the very short ignore time self.ignored_columns = [ - "bookmarkable_id", # TODO 2022-04-01 remove - "bookmarkable_type", # TODO 2022-04-01 remove "topic_id", # TODO 2022-04-01: remove "reminder_type" # TODO 2021-04-01: remove ] @@ -26,19 +24,46 @@ class Bookmark < ActiveRecord::Base ) end + # TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. validate :unique_per_post_for_user, on: [:create, :update], if: Proc.new { |b| b.will_save_change_to_post_id? || b.will_save_change_to_user_id? } + # TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. validate :for_topic_must_use_first_post, on: [:create, :update], if: Proc.new { |b| b.will_save_change_to_post_id? || b.will_save_change_to_for_topic? } + validate :polymorphic_columns_present, on: [:create, :update] + + validate :unique_per_bookmarkable, + on: [:create, :update], + if: Proc.new { |b| + b.will_save_change_to_bookmarkable_id? || b.will_save_change_to_bookmarkable_type? || b.will_save_change_to_user_id? + } + validate :ensure_sane_reminder_at_time, if: :will_save_change_to_reminder_at? validate :bookmark_limit_not_reached validates :name, length: { maximum: 100 } + def polymorphic_columns_present + return if !SiteSetting.use_polymorphic_bookmarks + return if self.bookmarkable_id.present? && self.bookmarkable_type.present? + + self.errors.add(:base, I18n.t("bookmarks.errors.bookmarkable_id_type_required")) + end + + def unique_per_bookmarkable + return if !SiteSetting.use_polymorphic_bookmarks + return if !Bookmark.exists?(user_id: user_id, bookmarkable_id: bookmarkable_id, bookmarkable_type: bookmarkable_type) + + self.errors.add(:base, I18n.t("bookmarks.errors.already_bookmarked", type: bookmarkable_type)) + end + + # TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. def unique_per_post_for_user + return if SiteSetting.use_polymorphic_bookmarks + exists = if is_for_first_post? Bookmark.exists?(user_id: user_id, post_id: post_id, for_topic: for_topic) else @@ -50,6 +75,7 @@ class Bookmark < ActiveRecord::Base end end + # TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. def for_topic_must_use_first_post if !is_for_first_post? && self.for_topic self.errors.add(:base, I18n.t("bookmarks.errors.for_topic_must_use_first_post")) @@ -80,6 +106,7 @@ class Bookmark < ActiveRecord::Base ) end + # TODO (martin) [POLYBOOK] Not relevant once polymorphic bookmarks are implemented. def is_for_first_post? @is_for_first_post ||= new_record? ? Post.exists?(id: post_id, post_number: 1) : post.post_number == 1 end @@ -88,6 +115,8 @@ class Bookmark < ActiveRecord::Base self.auto_delete_preference == Bookmark.auto_delete_preferences[:when_reminder_sent] end + # TODO (martin) [POLYBOOK] This is only relevant for post/topic bookmarkables, need to + # think of a way to do this gracefully. def auto_delete_on_owner_reply? self.auto_delete_preference == Bookmark.auto_delete_preferences[:on_owner_reply] end @@ -120,11 +149,24 @@ class Bookmark < ActiveRecord::Base end scope :for_user_in_topic, ->(user_id, topic_id) { - joins(:post).where(user_id: user_id, posts: { topic_id: topic_id }) + if SiteSetting.use_polymorphic_bookmarks + joins("LEFT JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'") + .joins("LEFT JOIN topics ON topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic'") + .where( + "bookmarks.user_id = :user_id AND (topics.id = :topic_id OR posts.topic_id = :topic_id)", + user_id: user_id, topic_id: topic_id + ) + else + joins(:post).where(user_id: user_id, posts: { topic_id: topic_id }) + end } def self.find_for_topic_by_user(topic_id, user_id) - for_user_in_topic(user_id, topic_id).where(for_topic: true).first + if SiteSetting.use_polymorphic_bookmarks + find_by(user_id: user_id, bookmarkable_id: topic_id, bookmarkable_type: "Topic") + else + for_user_in_topic(user_id, topic_id).where(for_topic: true).first + end end def self.count_per_day(opts = nil) @@ -171,7 +213,7 @@ end # # id :bigint not null, primary key # user_id :bigint not null -# post_id :bigint not null +# post_id :bigint # name :string(100) # reminder_at :datetime # created_at :datetime not null @@ -181,9 +223,12 @@ end # auto_delete_preference :integer default(0), not null # pinned :boolean default(FALSE) # for_topic :boolean default(FALSE), not null +# bookmarkable_id :integer +# bookmarkable_type :string # # Indexes # +# idx_bookmarks_user_polymorphic_unique (user_id,bookmarkable_type,bookmarkable_id) UNIQUE # index_bookmarks_on_post_id (post_id) # index_bookmarks_on_reminder_at (reminder_at) # index_bookmarks_on_reminder_set_at (reminder_set_at) diff --git a/app/models/category.rb b/app/models/category.rb index fc9d137757..3702a03bd6 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -5,10 +5,11 @@ class Category < ActiveRecord::Base 'none' ] - # TODO(2020-11-18): remove - self.ignored_columns = %w{ - suppress_from_latest - } + self.ignored_columns = [ + :suppress_from_latest, # TODO(2020-11-18): remove + :required_tag_group_id, # TODO(2023-04-01): remove + :min_tags_from_required_group, # TODO(2023-04-01): remove + ] include Searchable include Positionable @@ -56,7 +57,6 @@ class Category < ActiveRecord::Base validates :num_featured_topics, numericality: { only_integer: true, greater_than: 0 } validates :search_priority, inclusion: { in: Searchable::PRIORITIES.values } - validates :min_tags_from_required_group, numericality: { only_integer: true, greater_than: 0 } validate :parent_category_validator validate :email_in_validator @@ -103,7 +103,8 @@ class Category < ActiveRecord::Base has_many :tags, through: :category_tags has_many :category_tag_groups, dependent: :destroy has_many :tag_groups, through: :category_tag_groups - belongs_to :required_tag_group, class_name: 'TagGroup' + + has_many :category_required_tag_groups, -> { order(order: :asc) }, dependent: :destroy belongs_to :reviewable_by_group, class_name: 'Group' @@ -639,8 +640,14 @@ class Category < ActiveRecord::Base self.tag_groups = TagGroup.where(name: group_names).all.to_a end - def required_tag_group_name=(group_name) - self.required_tag_group = group_name.blank? ? nil : TagGroup.where(name: group_name).first + def required_tag_groups=(required_groups) + map = Array(required_groups).map.with_index { |rg, i| [rg["name"], { min_count: rg["min_count"].to_i, order: i }] }.to_h + tag_groups = TagGroup.where(name: map.keys) + + self.category_required_tag_groups = tag_groups.map do |tag_group| + attrs = map[tag_group.name] + CategoryRequiredTagGroup.new(tag_group: tag_group, **attrs) + end.sort_by(&:order) end def downcase_email @@ -933,6 +940,10 @@ class Category < ActiveRecord::Base end end + def has_restricted_tags? + tags.count > 0 || tag_groups.count > 0 + end + private def should_update_reviewables? @@ -1040,8 +1051,6 @@ end # search_priority :integer default(0) # allow_global_tags :boolean default(FALSE), not null # reviewable_by_group_id :integer -# required_tag_group_id :integer -# min_tags_from_required_group :integer default(1), not null # read_only_banner :string # default_list_filter :string(20) default("all") # allow_unlimited_owner_edits_on_first_post :boolean default(FALSE), not null diff --git a/app/models/category_required_tag_group.rb b/app/models/category_required_tag_group.rb new file mode 100644 index 0000000000..50d9ef6ab6 --- /dev/null +++ b/app/models/category_required_tag_group.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class CategoryRequiredTagGroup < ActiveRecord::Base + belongs_to :category + belongs_to :tag_group + + validates :min_count, numericality: { only_integer: true, greater_than: 0 } + + after_commit do + Site.clear_cache + end +end + +# == Schema Information +# +# Table name: category_required_tag_groups +# +# id :bigint not null, primary key +# category_id :bigint not null +# tag_group_id :bigint not null +# min_count :integer default(1), not null +# order :integer default(1), not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# idx_category_required_tag_groups (category_id,tag_group_id) UNIQUE +# diff --git a/app/models/concerns/second_factor_manager.rb b/app/models/concerns/second_factor_manager.rb index d1974f9449..e21957e75f 100644 --- a/app/models/concerns/second_factor_manager.rb +++ b/app/models/concerns/second_factor_manager.rb @@ -6,7 +6,14 @@ module SecondFactorManager extend ActiveSupport::Concern SecondFactorAuthenticationResult = Struct.new( - :ok, :error, :reason, :backup_enabled, :security_key_enabled, :totp_enabled, :multiple_second_factor_methods + :ok, + :error, + :reason, + :backup_enabled, + :security_key_enabled, + :totp_enabled, + :multiple_second_factor_methods, + :used_2fa_method, ) def create_totp(opts = {}) @@ -112,11 +119,26 @@ module SecondFactorManager case second_factor_method when UserSecondFactor.methods[:totp] - return authenticate_totp(second_factor_token) ? ok_result : invalid_totp_or_backup_code_result + if authenticate_totp(second_factor_token) + ok_result.used_2fa_method = UserSecondFactor.methods[:totp] + return ok_result + else + return invalid_totp_or_backup_code_result + end when UserSecondFactor.methods[:backup_codes] - return authenticate_backup_code(second_factor_token) ? ok_result : invalid_totp_or_backup_code_result + if authenticate_backup_code(second_factor_token) + ok_result.used_2fa_method = UserSecondFactor.methods[:backup_codes] + return ok_result + else + return invalid_totp_or_backup_code_result + end when UserSecondFactor.methods[:security_key] - return authenticate_security_key(secure_session, second_factor_token) ? ok_result : invalid_security_key_result + if authenticate_security_key(secure_session, second_factor_token) + ok_result.used_2fa_method = UserSecondFactor.methods[:security_key] + return ok_result + else + return invalid_security_key_result + end end # if we have gotten down to this point without being diff --git a/app/models/group.rb b/app/models/group.rb index 72eae6ab16..d0a401a8d6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -936,6 +936,10 @@ class Group < ActiveRecord::Base TopicAllowedGroup.where(group_id: self.id).joins(:topic).count end + def full_url + "#{Discourse.base_url}/g/#{UrlHelper.encode_component(self.name)}" + end + protected def name_format_validator diff --git a/app/models/javascript_cache.rb b/app/models/javascript_cache.rb index 20f8b63a84..b92509bb89 100644 --- a/app/models/javascript_cache.rb +++ b/app/models/javascript_cache.rb @@ -8,11 +8,19 @@ class JavascriptCache < ActiveRecord::Base before_save :update_digest def url - "#{GlobalSetting.cdn_url}#{Discourse.base_path}/theme-javascripts/#{digest}.js?__ws=#{Discourse.current_hostname}" + "#{GlobalSetting.cdn_url}#{Discourse.base_path}#{path}" + end + + def local_url + "#{Discourse.base_url}#{path}" end private + def path + "/theme-javascripts/#{digest}.js?__ws=#{Discourse.current_hostname}" + end + def update_digest self.digest = Digest::SHA1.hexdigest(content) if content_changed? end diff --git a/app/models/notification.rb b/app/models/notification.rb index 48dcb22958..168f6e40c7 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -64,7 +64,7 @@ class Notification < ActiveRecord::Base DELETE FROM notifications n WHERE high_priority - AND notification_type NOT IN (#{types[:chat_mention].to_i}, #{types[:chat_message].to_i}) + AND n.topic_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM posts p @@ -111,7 +111,8 @@ class Notification < ActiveRecord::Base chat_invitation: 31, chat_group_mention: 32, # March 2022 - This is obsolete, as all chat_mentions use `chat_mention` type chat_quoted: 33, - assigned: 34 + assigned: 34, + question_answer_user_commented: 35, # Used by https://github.com/discourse/discourse-question-answer ) end diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index bd8d58e79b..4030df5d59 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -31,7 +31,11 @@ class PostAnalyzer cooked = PrettyText.cook(raw, opts) end + limit = SiteSetting.max_oneboxes_per_post result = Oneboxer.apply(cooked) do |url| + next if limit <= 0 + limit -= 1 + @onebox_urls << url if opts[:invalidate_oneboxes] Oneboxer.invalidate(url) diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb index 945d547377..1a07f7f77f 100644 --- a/app/models/post_mover.rb +++ b/app/models/post_mover.rb @@ -178,6 +178,9 @@ class PostMover # we don't want to keep the old topic's OP bookmarked when we are # moving it into a new topic + # + # TODO (martin) [POLYBOOK] This will need to be restructured for polymorphic + # bookmarks when edge cases are handled. Bookmark.where(post_id: post.id).update_all(post_id: new_post.id) new_post diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index 10190ff064..21f6bef275 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -162,6 +162,11 @@ class RemoteTheme < ActiveRecord::Base new_path = "#{File.dirname(path)}/#{SecureRandom.hex}#{File.extname(path)}" File.rename(path, new_path) # OptimizedImage has strict file name restrictions, so rename temporarily upload = UploadCreator.new(File.open(new_path), File.basename(relative_path), for_theme: true).create_for(theme.user_id) + + if !upload.errors.empty? + raise ImportError, I18n.t("themes.import_error.upload", name: name, errors: upload.errors.full_messages.join(",")) + end + updated_fields << theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id) end end diff --git a/app/models/screened_ip_address.rb b/app/models/screened_ip_address.rb index 9d25a3c9a1..70d51c85ec 100644 --- a/app/models/screened_ip_address.rb +++ b/app/models/screened_ip_address.rb @@ -13,6 +13,17 @@ class ScreenedIpAddress < ActiveRecord::Base validates :ip_address, ip_address_format: true, presence: true after_validation :check_for_match + ROLLED_UP_BLOCKS = [ + # IPv4 + [4, 32, 24], + # IPv6 + [6, (65..128).to_a, 64], + [6, 64, 60], + [6, 60, 56], + [6, 56, 52], + [6, 52, 48], + ] + def self.watch(ip_address, opts = {}) match_for_ip_address(ip_address) || create(opts.slice(:action_type).merge(ip_address: ip_address)) end @@ -67,7 +78,8 @@ class ScreenedIpAddress < ActiveRecord::Base # # http://www.postgresql.org/docs/9.1/static/datatype-net-types.html # http://www.postgresql.org/docs/9.1/static/functions-net.html - order('masklen(ip_address) DESC').find_by("? <<= ip_address", ip_address.to_s) + ip_address = IPAddr === ip_address ? ip_address.to_cidr_s : ip_address.to_s + order('masklen(ip_address) DESC').find_by("? <<= ip_address", ip_address) end def self.should_block?(ip_address) @@ -94,82 +106,57 @@ class ScreenedIpAddress < ActiveRecord::Base !exists_for_ip_address_and_action?(ip_address, actions[:allow_admin], record_match: false) end - def self.star_subnets_query - @star_subnets_query ||= <<~SQL - SELECT network(inet(host(ip_address) || '/24'))::text AS ip_range + def self.subnets(family, from_masklen, to_masklen) + sql = <<~SQL + WITH ips_and_subnets AS ( + SELECT ip_address, + network(inet(host(ip_address) || '/' || :to_masklen))::text subnet FROM screened_ip_addresses - WHERE action_type = #{ScreenedIpAddress.actions[:block]} - AND family(ip_address) = 4 - AND masklen(ip_address) = 32 - GROUP BY ip_range - HAVING COUNT(*) >= :min_count - SQL - end - - def self.star_star_subnets_query - @star_star_subnets_query ||= <<~SQL - WITH weighted_subnets AS ( - SELECT network(inet(host(ip_address) || '/16'))::text AS ip_range, - CASE masklen(ip_address) - WHEN 32 THEN 1 - WHEN 24 THEN :roll_up_weight - ELSE 0 - END AS weight - FROM screened_ip_addresses - WHERE action_type = #{ScreenedIpAddress.actions[:block]} - AND family(ip_address) = 4 + WHERE family(ip_address) = :family AND + masklen(ip_address) IN (:from_masklen) AND + action_type = :blocked ) - SELECT ip_range - FROM weighted_subnets - GROUP BY ip_range - HAVING SUM(weight) >= :min_count + SELECT subnet + FROM ips_and_subnets + GROUP BY subnet + HAVING COUNT(*) >= :min_ban_entries_for_roll_up SQL - end - def self.star_subnets - min_count = SiteSetting.min_ban_entries_for_roll_up - DB.query_single(star_subnets_query, min_count: min_count) - end - - def self.star_star_subnets - weight = SiteSetting.min_ban_entries_for_roll_up - DB.query_single(star_star_subnets_query, min_count: 10, roll_up_weight: weight) + DB.query_single( + sql, + family: family, + from_masklen: from_masklen, + to_masklen: to_masklen, + blocked: ScreenedIpAddress.actions[:block], + min_ban_entries_for_roll_up: SiteSetting.min_ban_entries_for_roll_up, + ) end def self.roll_up(current_user = Discourse.system_user) - subnets = [star_subnets, star_star_subnets].flatten + ROLLED_UP_BLOCKS.each do |family, from_masklen, to_masklen| + ScreenedIpAddress.subnets(family, from_masklen, to_masklen).map do |subnet| + next if ScreenedIpAddress.where("? <<= ip_address", subnet).exists? - StaffActionLogger.new(current_user).log_roll_up(subnets) unless subnets.blank? + old_ips = ScreenedIpAddress + .where(action_type: ScreenedIpAddress.actions[:block]) + .where("ip_address << ?", subnet) + .where("family(ip_address) = ?", family) + .where("masklen(ip_address) IN (?)", from_masklen) - subnets.each do |subnet| - ScreenedIpAddress.create(ip_address: subnet) unless ScreenedIpAddress.where("? <<= ip_address", subnet).exists? + sum_match_count, max_last_match_at, min_created_at = + old_ips.pluck_first('SUM(match_count), MAX(last_match_at), MIN(created_at)') - sql = <<~SQL - UPDATE screened_ip_addresses - SET match_count = sum_match_count - , created_at = min_created_at - , last_match_at = max_last_match_at - FROM ( - SELECT SUM(match_count) AS sum_match_count - , MIN(created_at) AS min_created_at - , MAX(last_match_at) AS max_last_match_at - FROM screened_ip_addresses - WHERE action_type = #{ScreenedIpAddress.actions[:block]} - AND family(ip_address) = 4 - AND ip_address << :ip_address - ) s - WHERE ip_address = :ip_address - SQL + ScreenedIpAddress.create!( + ip_address: subnet, + match_count: sum_match_count, + last_match_at: max_last_match_at, + created_at: min_created_at, + ) - DB.exec(sql, ip_address: subnet) - - ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block]) - .where("family(ip_address) = 4") - .where("ip_address << ?", subnet) - .delete_all + StaffActionLogger.new(current_user).log_roll_up(subnet, old_ips.map(&:ip_address)) + old_ips.delete_all + end end - - subnets end end diff --git a/app/models/site.rb b/app/models/site.rb index 4442a12e27..12353dbd5f 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -65,7 +65,7 @@ class Site # corresponding ActiveRecord callback to clear the categories cache. Discourse.cache.fetch(categories_cache_key, expires_in: 30.minutes) do categories = Category - .includes(:uploaded_logo, :uploaded_background, :tags, :tag_groups, :required_tag_group) + .includes(:uploaded_logo, :uploaded_background, :tags, :tag_groups, category_required_tag_groups: :tag_group) .joins('LEFT JOIN topics t on t.id = categories.topic_id') .select('categories.*, t.slug topic_slug') .order(:position) diff --git a/app/models/sitemap.rb b/app/models/sitemap.rb new file mode 100644 index 0000000000..a6ead4692e --- /dev/null +++ b/app/models/sitemap.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +class Sitemap < ActiveRecord::Base + RECENT_SITEMAP_NAME = 'recent' + NEWS_SITEMAP_NAME = 'news' + + class << self + def regenerate_sitemaps + names_used = [RECENT_SITEMAP_NAME, NEWS_SITEMAP_NAME] + + names_used.each { |name| touch(name) } + + count = Category.where(read_restricted: false).sum(:topic_count) + max_page_size = SiteSetting.sitemap_page_size + size, mod = count.divmod(max_page_size) + size += 1 if mod > 0 + + size.times do |index| + page_name = (index + 1).to_s + touch(page_name) + names_used << page_name + end + + where.not(name: names_used).update_all(enabled: false) + end + + def touch(name) + find_or_initialize_by(name: name).tap do |sitemap| + sitemap.update!( + last_posted_at: sitemap.last_posted_topic || 3.days.ago, + enabled: true + ) + end + end + end + + def topics + if name == RECENT_SITEMAP_NAME + sitemap_topics.pluck(:id, :slug, :bumped_at, :updated_at, :posts_count) + elsif name == NEWS_SITEMAP_NAME + sitemap_topics.pluck(:id, :title, :slug, :created_at) + else + sitemap_topics.pluck(:id, :slug, :bumped_at, :updated_at) + end + end + + def last_posted_topic + sitemap_topics.maximum(:updated_at) + end + + def max_page_size + SiteSetting.sitemap_page_size + end + + private + + def sitemap_topics + indexable_topics = Topic + .where(visible: true) + .joins(:category) + .where(categories: { read_restricted: false }) + + if name == RECENT_SITEMAP_NAME + indexable_topics.where('bumped_at > ?', 3.days.ago).order(bumped_at: :desc) + elsif name == NEWS_SITEMAP_NAME + indexable_topics.where('bumped_at > ?', 72.hours.ago).order(bumped_at: :desc) + else + offset = (name.to_i - 1) * max_page_size + + indexable_topics.limit(max_page_size).offset(offset) + end + end +end + +# == Schema Information +# +# Table name: sitemaps +# +# id :bigint not null, primary key +# name :string not null +# last_posted_at :datetime not null +# enabled :boolean default(TRUE), not null +# +# Indexes +# +# index_sitemaps_on_name (name) UNIQUE +# diff --git a/app/models/tag_group.rb b/app/models/tag_group.rb index cf83c9ff2d..c56392ab77 100644 --- a/app/models/tag_group.rb +++ b/app/models/tag_group.rb @@ -6,6 +6,7 @@ class TagGroup < ActiveRecord::Base has_many :tag_group_memberships, dependent: :destroy has_many :tags, through: :tag_group_memberships has_many :category_tag_groups, dependent: :destroy + has_many :category_required_tag_groups, dependent: :destroy has_many :categories, through: :category_tag_groups has_many :tag_group_permissions, dependent: :destroy diff --git a/app/models/theme.rb b/app/models/theme.rb index b2d867509c..990b39c1e8 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -539,12 +539,19 @@ class Theme < ActiveRecord::Base end theme_uploads = {} + theme_uploads_local = {} + upload_fields.each do |field| if field.upload&.url theme_uploads[field.name] = Discourse.store.cdn_url(field.upload.url) end + if field.javascript_cache + theme_uploads_local[field.name] = field.javascript_cache.local_url + end end + hash['theme_uploads'] = theme_uploads if theme_uploads.present? + hash['theme_uploads_local'] = theme_uploads_local if theme_uploads_local.present? hash end @@ -652,7 +659,7 @@ class Theme < ActiveRecord::Base end settings_hash&.each do |name, value| - next if name == "theme_uploads" + next if name == "theme_uploads" || name == "theme_uploads_local" contents << to_scss_variable(name, value) end diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 22c03f6172..0e3c8663f3 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -184,7 +184,7 @@ class ThemeField < ActiveRecord::Base begin content = File.read(path) - svg_file = Nokogiri::XML(content) do |config| + Nokogiri::XML(content) do |config| config.options = Nokogiri::XML::ParseOptions::NOBLANKS end rescue => e @@ -568,6 +568,12 @@ class ThemeField < ActiveRecord::Base if (will_save_change_to_value? || will_save_change_to_upload_id?) && !will_save_change_to_value_baked? self.value_baked = nil end + if upload && upload.extension == "js" + if will_save_change_to_upload_id? || !javascript_cache + javascript_cache ||= build_javascript_cache + javascript_cache.content = upload.content + end + end end after_save do diff --git a/app/models/topic.rb b/app/models/topic.rb index bfbc2c1b92..afd37fb750 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -208,12 +208,16 @@ class Topic < ActiveRecord::Base has_many :category_users, through: :category has_many :posts + # TODO (martin): + # # When we are ready we can add as: :bookmarkable here to use the # polymorphic association. # # At that time we may also want to make another association for example # :topic_bookmarks that get all of the bookmarks for that topic's bookmarkable id # and type, because this one gets all of the post bookmarks. + # + # Note: We can use Bookmark#for_user_in_topic for this. has_many :bookmarks, through: :posts has_many :ordered_posts, -> { order(post_number: :asc) }, class_name: "Post" diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb index d3d1572c5d..18e154e977 100644 --- a/app/models/topic_embed.rb +++ b/app/models/topic_embed.rb @@ -132,7 +132,7 @@ class TopicEmbed < ActiveRecord::Base response = FetchResponse.new begin - html = uri.read(allow_redirections: :safe) + html = uri.read rescue OpenURI::HTTPError, Net::OpenTimeout return end diff --git a/app/models/topic_poster.rb b/app/models/topic_poster.rb index 3ff6a1e09e..762734799c 100644 --- a/app/models/topic_poster.rb +++ b/app/models/topic_poster.rb @@ -16,12 +16,6 @@ class TopicPoster < OpenStruct end def name_and_description - if SiteSetting.prioritize_username_in_ux? || user.name.blank? - name = user.username - else - name = user.name - end - - I18n.t("js.user.avatar.name_and_description", name: name, description: description) + I18n.t("js.user.avatar.name_and_description", name: user.display_name, description: description) end end diff --git a/app/models/upload.rb b/app/models/upload.rb index 061cc266af..b24044b7a6 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -171,6 +171,22 @@ class Upload < ActiveRecord::Base end end + def content + original_path = Discourse.store.path_for(self) + external_copy = nil + + if original_path.blank? + external_copy = Discourse.store.download(self) + original_path = external_copy.path + end + + File.read(original_path) + ensure + if external_copy + File.unlink(external_copy.path) + end + end + def fix_image_extension return false if extension == "unknown" diff --git a/app/models/user.rb b/app/models/user.rb index 0ab48be074..2e5bab0b85 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -273,7 +273,7 @@ class User < ActiveRecord::Base end def self.normalize_username(username) - username.unicode_normalize.downcase if username.present? + username.to_s.unicode_normalize.downcase if username.present? end def self.username_available?(username, email = nil, allow_reserved_username: false) @@ -428,7 +428,7 @@ class User < ActiveRecord::Base user_id: id, message_type: 'welcome_staff', message_options: { - role: role + role: role.to_s } ) end @@ -1461,6 +1461,18 @@ class User < ActiveRecord::Base username_lower == User.normalize_username(another_username) end + def full_url + "#{Discourse.base_url}/u/#{encoded_username}" + end + + def display_name + if SiteSetting.prioritize_username_in_ux? + username + else + name.presence || username + end + end + protected def badge_grant diff --git a/app/serializers/category_required_tag_group_serializer.rb b/app/serializers/category_required_tag_group_serializer.rb new file mode 100644 index 0000000000..c74c5d61e9 --- /dev/null +++ b/app/serializers/category_required_tag_group_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class CategoryRequiredTagGroupSerializer < ApplicationSerializer + attributes :name, :min_count + + def name + object.tag_group.name + end +end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 2a2eec117b..c03537c7c7 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -86,7 +86,8 @@ class PostSerializer < BasicPostSerializer :excerpt, :reviewable_id, :reviewable_score_count, - :reviewable_score_pending_count + :reviewable_score_pending_count, + :user_suspended def initialize(object, opts) super(object, opts) @@ -173,7 +174,7 @@ class PostSerializer < BasicPostSerializer end def include_can_permanently_delete? - SiteSetting.can_permanently_delete && object.deleted_at + SiteSetting.can_permanently_delete && scope.is_admin? && object.deleted_at end def can_recover @@ -367,10 +368,18 @@ class PostSerializer < BasicPostSerializer end def post_bookmark - if @topic_view.present? - @post_bookmark ||= @topic_view.user_post_bookmarks.find { |bookmark| bookmark.post_id == object.id && !bookmark.for_topic } + if SiteSetting.use_polymorphic_bookmarks + if @topic_view.present? + @post_bookmark ||= @topic_view.bookmarks.find { |bookmark| bookmark.bookmarkable == object } + else + @post_bookmark ||= Bookmark.find_by(user: scope.user, bookmarkable: object) + end else - @post_bookmark ||= object.bookmarks.find_by(user: scope.user, for_topic: false) + if @topic_view.present? + @post_bookmark ||= @topic_view.bookmarks.find { |bookmark| bookmark.post_id == object.id && !bookmark.for_topic } + else + @post_bookmark ||= object.bookmarks.find_by(user: scope.user, for_topic: false) + end end end @@ -541,6 +550,14 @@ class PostSerializer < BasicPostSerializer can_review_topic? end + def user_suspended + true + end + + def include_user_suspended? + object.user&.suspended? + end + private def can_review_topic? diff --git a/app/serializers/reviewable_score_serializer.rb b/app/serializers/reviewable_score_serializer.rb index a16bd752c8..549bd4536a 100644 --- a/app/serializers/reviewable_score_serializer.rb +++ b/app/serializers/reviewable_score_serializer.rb @@ -40,12 +40,6 @@ class ReviewableScoreSerializer < ApplicationSerializer text = I18n.t("reviewables.reasons.#{object.reason}", link: link, default: nil) else text = I18n.t("reviewables.reasons.#{object.reason}", default: nil) - - # TODO(roman): Remove after the 2.8 release. - # The discourse-antivirus and akismet plugins still use the backtick format for settings. - # It'll be hard to migrate them to the new format without breaking backwards compatibility, so I'm keeping the old behavior for now. - # Will remove after the 2.8 release. - linkify_backticks(object.reason, text) if text end text @@ -86,15 +80,4 @@ class ReviewableScoreSerializer < ApplicationSerializer "#{text.gsub('_', ' ')}" end - - def linkify_backticks(reason, text) - text.gsub!(/`[a-z_]+`/) do |m| - if scope.is_staff? - setting = m[1..-2] - "#{setting.gsub('_', ' ')}" - else - m.gsub('_', ' ') - end - end - end end diff --git a/app/serializers/site_category_serializer.rb b/app/serializers/site_category_serializer.rb index 8634ba3874..1d29b7c7af 100644 --- a/app/serializers/site_category_serializer.rb +++ b/app/serializers/site_category_serializer.rb @@ -5,10 +5,10 @@ class SiteCategorySerializer < BasicCategorySerializer attributes :allowed_tags, :allowed_tag_groups, :allow_global_tags, - :min_tags_from_required_group, - :required_tag_group_name, :read_only_banner + has_many :category_required_tag_groups, key: :required_tag_groups, embed: :objects + def include_allowed_tags? SiteSetting.tagging_enabled end @@ -29,7 +29,7 @@ class SiteCategorySerializer < BasicCategorySerializer SiteSetting.tagging_enabled end - def required_tag_group_name - object.required_tag_group&.name + def include_required_tag_groups? + SiteSetting.tagging_enabled end end diff --git a/app/serializers/topic_view_details_serializer.rb b/app/serializers/topic_view_details_serializer.rb index 78291bee09..ffe51d4db3 100644 --- a/app/serializers/topic_view_details_serializer.rb +++ b/app/serializers/topic_view_details_serializer.rb @@ -112,7 +112,7 @@ class TopicViewDetailsSerializer < ApplicationSerializer end def include_can_permanently_delete? - SiteSetting.can_permanently_delete && object.topic.deleted_at + SiteSetting.can_permanently_delete && scope.is_admin? && object.topic.deleted_at end def include_can_recover? diff --git a/app/services/inline_uploads.rb b/app/services/inline_uploads.rb index 598fdc9e36..55a162a8ea 100644 --- a/app/services/inline_uploads.rb +++ b/app/services/inline_uploads.rb @@ -204,16 +204,10 @@ class InlineUploads if src && (external_src || matched_uploads(src).present?) upload = uploads&.[](src) - - text = upload&.original_filename || node.attributes["alt"]&.value - width = (node.attributes["width"]&.value || upload&.width).to_i - height = (node.attributes["height"]&.value || upload&.height).to_i - title = node.attributes["title"]&.value - text = "#{text}|#{width}x#{height}" if width > 0 && height > 0 - url = upload&.short_url || PLACEHOLDER + node["src"] = upload&.short_url || PLACEHOLDER spaces_before = match[1].present? ? match[1][/ +$/].size : 0 - replacement = +"#{" " * spaces_before}![#{text}](#{url}#{title.present? ? " \"#{title}\"" : ""})" + replacement = +"#{" " * spaces_before}#{node.to_s}" yield(match[2], src, replacement, $~.offset(0)[0]) if block_given? end diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index ba091af0a0..8568d5fc5a 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -719,6 +719,13 @@ class PostAlerter # email to the user who was just added by CC. In this case the OP probably # replied and CC'd some people, and they are the only other topic users. return if post.incoming_email.cc_addresses_split.include?(to_address) + + # We don't want to create an email storm if someone emails the group and + # CC's 50 support addresses from various places, which all then respond + # with auto-responders saying they have received our email. Any auto-generated + # emails should not propagate notifications to anyone else, not even + # the regular topic user notifications. + return email_addresses.dup.uniq if post.incoming_email.is_auto_generated? end # Send a single email using group SMTP settings to cut down on the @@ -795,7 +802,12 @@ class PostAlerter LEFT JOIN tag_group_memberships tgm ON tag_users.tag_id = tgm.tag_id LEFT JOIN tag_group_permissions tgp ON tgm.tag_group_id = tgp.tag_group_id LEFT JOIN group_users gu ON gu.user_id = tag_users.user_id - WHERE (tgp.group_id IS NULL OR tgp.group_id = gu.group_id OR gu.group_id = :staff_group_id) + WHERE ( + tgp.group_id IS NULL OR + tgp.group_id = gu.group_id OR + tgp.group_id = :everyone_group_id OR + gu.group_id = :staff_group_id + ) AND (tag_users.notification_level = :watching AND tag_users.tag_id IN (:tag_ids) AND (tu.user_id IS NULL OR tu.notification_level = :watching)) @@ -807,7 +819,8 @@ class PostAlerter topic_id: post.topic_id, category_id: post.topic.category_id, tag_ids: tag_ids, - staff_group_id: Group::AUTO_GROUPS[:staff] + staff_group_id: Group::AUTO_GROUPS[:staff], + everyone_group_id: Group::AUTO_GROUPS[:everyone] ) if group_ids.present? diff --git a/app/services/post_owner_changer.rb b/app/services/post_owner_changer.rb index c8f8e8f7f4..702e6a8a17 100644 --- a/app/services/post_owner_changer.rb +++ b/app/services/post_owner_changer.rb @@ -28,7 +28,7 @@ class PostOwnerChanger PostActionDestroyer.destroy(@new_owner, post, :like, skip_delete_check: true) level = post.is_first_post? ? :watching : :tracking - TopicUser.change(@new_owner.id, @topic.id, notification_level: NotificationLevels.topic_levels[level]) + TopicUser.change(@new_owner.id, @topic.id, notification_level: NotificationLevels.topic_levels[level], posted: true) if post == @topic.posts.order("post_number DESC").where("NOT hidden AND posts.deleted_at IS NULL").first @topic.last_poster = @new_owner diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index c91519fa6d..33d18b41c9 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -18,12 +18,25 @@ class SearchIndexer end def self.update_index(table: , id: , a_weight: nil, b_weight: nil, c_weight: nil, d_weight: nil) - raw_data = [a_weight, b_weight, c_weight, d_weight] + raw_data = { + a: a_weight, + b: b_weight, + c: c_weight, + d: d_weight, + } - search_data = raw_data.map do |data| + # The version used in excerpts + search_data = raw_data.transform_values do |data| Search.prepare_data(data || "", :index) end + # The version used to build the index + indexed_data = search_data.transform_values do |data| + data.gsub(/\S+/) { |word| + word[0...SiteSetting.search_max_indexed_word_length] + } + end + table_name = "#{table}_search_data" foreign_key = "#{table}_id" @@ -37,14 +50,7 @@ class SearchIndexer setweight(to_tsvector('#{stemmer}', #{Search.wrap_unaccent("coalesce(:d,''))")}, 'D') SQL - ranked_params = { - a: search_data[0], - b: search_data[1], - c: search_data[2], - d: search_data[3], - } - - tsvector = DB.query_single("SELECT #{ranked_index}", ranked_params)[0] + tsvector = DB.query_single("SELECT #{ranked_index}", indexed_data)[0] additional_lexemes = [] tsvector.scan(/'(([a-zA-Z0-9]+\.)+[a-zA-Z0-9]+)'\:([\w+,]+)/).reduce(additional_lexemes) do |array, (lexeme, _, positions)| @@ -68,9 +74,9 @@ class SearchIndexer indexed_data = if table.to_s == "post" - clean_post_raw_data!(ranked_params[:d]) + clean_post_raw_data!(search_data[:d]) else - search_data.select { |d| d.length > 0 }.join(' ') + search_data.values.select { |d| d.length > 0 }.join(' ') end params = { @@ -331,6 +337,10 @@ class SearchIndexer if node["href"] == node.text || MENTION_CLASSES.include?(node["class"]) node.remove_attribute("href") end + + if node["class"] == "anchor" && node["href"].starts_with?("#") + node.remove_attribute("href") + end end html_scrubber = new diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index ecb126b2ce..823a2b6a8a 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -437,10 +437,10 @@ class StaffActionLogger )) end - def log_roll_up(subnets, opts = {}) + def log_roll_up(subnet, ips, opts = {}) UserHistory.create!(params(opts).merge( action: UserHistory.actions[:roll_up], - details: subnets.join(", ") + details: "#{subnet} from #{ips.join(", ")}" )) end diff --git a/app/services/user_destroyer.rb b/app/services/user_destroyer.rb index bc02627021..b191cbe3dc 100644 --- a/app/services/user_destroyer.rb +++ b/app/services/user_destroyer.rb @@ -40,14 +40,14 @@ class UserDestroyer delete_posts(user, category_topic_ids, opts) end - user.post_actions.each do |post_action| + user.post_actions.find_each do |post_action| post_action.remove_act!(Discourse.system_user) end # Add info about the user to staff action logs UserHistory.staff_action_records( Discourse.system_user, acting_user: user.username - ).each do |log| + ).unscope(:order).find_each do |log| log.details ||= '' log.details = (log.details.split("\n") + ["user_id: #{user.id}", "username: #{user.username}"] diff --git a/app/views/email/default_template.html b/app/views/email/default_template.html index 53e99ab184..f660cc46fd 100644 --- a/app/views/email/default_template.html +++ b/app/views/email/default_template.html @@ -10,6 +10,7 @@ name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width" /> + %{dark_mode_meta_tags} @@ -45,5 +46,6 @@                                        
    + %{dark_mode_styles} diff --git a/app/views/layouts/_noscript_footer.html.erb b/app/views/layouts/_noscript_footer.html.erb new file mode 100644 index 0000000000..76770aaa04 --- /dev/null +++ b/app/views/layouts/_noscript_footer.html.erb @@ -0,0 +1,32 @@ + diff --git a/app/views/layouts/_noscript_header.html.erb b/app/views/layouts/_noscript_header.html.erb new file mode 100644 index 0000000000..5085644f17 --- /dev/null +++ b/app/views/layouts/_noscript_header.html.erb @@ -0,0 +1,5 @@ +
    + "> +

    <%=SiteSetting.title%>

    +
    +
    diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 98b4fca1a5..35893cc195 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -73,25 +73,15 @@ <%= render_google_tag_manager_body_code %> <%- unless customization_disabled? %> diff --git a/app/views/layouts/crawler.html.erb b/app/views/layouts/crawler.html.erb index ec1f753d6e..cc8c28779d 100644 --- a/app/views/layouts/crawler.html.erb +++ b/app/views/layouts/crawler.html.erb @@ -5,14 +5,7 @@ <%= content_for?(:title) ? yield(:title) : SiteSetting.title %> <%= render partial: "layouts/head" %> - <%- if rtl? %> - <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_rtl : :desktop_rtl) %> - <%- else %> - <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile : :desktop) %> - <%- end %> - <%- if theme_id.present? %> - <%= discourse_stylesheet_link_tag(mobile_view? ? :mobile_theme : :desktop_theme) %> - <%- end %> + <%= render partial: "common/discourse_stylesheet" %> <%= theme_lookup("head_tag") %> <%= render_google_universal_analytics_code %> <%= yield :head %> @@ -23,50 +16,11 @@ <%= theme_lookup("header") %> -
    - "> - <%- if SiteSetting.site_logo_url.present? %> - - <%- else %> -

    <%=SiteSetting.title%>

    - <% end %> -
    -
    -
    + <%= render partial: "layouts/noscript_header" %> +
    <%= yield %>
    - + <%= render partial: "layouts/noscript_footer" %> <%= theme_lookup("footer") %> <%= theme_lookup("body_tag") %> <% if show_browser_update? %> diff --git a/app/views/robots_txt/index.erb b/app/views/robots_txt/index.erb index cb007ff69c..25e6816cdc 100644 --- a/app/views/robots_txt/index.erb +++ b/app/views/robots_txt/index.erb @@ -12,4 +12,8 @@ Disallow: <%= path %> <% end %> +<%- if SiteSetting.enable_sitemap? && !SiteSetting.login_required? %> +Sitemap: <%= request.protocol %><%= request.host_with_port %>/sitemap.xml +<% end %> + <%= server_plugin_outlet "robots_txt_index" %> diff --git a/app/views/sitemap/index.xml.erb b/app/views/sitemap/index.xml.erb new file mode 100644 index 0000000000..17ad425883 --- /dev/null +++ b/app/views/sitemap/index.xml.erb @@ -0,0 +1,9 @@ + + + <% @sitemaps.each do |sitemap| %> + + <%= Discourse.base_url %>/sitemap_<%= sitemap.name %>.xml + <%= sitemap.last_posted_at.xmlschema %> + + <% end %> + diff --git a/app/views/sitemap/news.xml.erb b/app/views/sitemap/news.xml.erb new file mode 100644 index 0000000000..1d83a614f4 --- /dev/null +++ b/app/views/sitemap/news.xml.erb @@ -0,0 +1,25 @@ + + + <% @topics.each do |(id, title, slug, created_at)| %> + + <%= build_sitemap_topic_url(slug, id) %> + + + <%= SiteSetting.title %> + <%= @locale %> + + <%= created_at.xmlschema %> + <%= title %> + + + <% end %> + diff --git a/app/views/sitemap/page.xml.erb b/app/views/sitemap/page.xml.erb new file mode 100644 index 0000000000..6f69c5bee3 --- /dev/null +++ b/app/views/sitemap/page.xml.erb @@ -0,0 +1,15 @@ + + + <% @topics.each do |(id, slug, bumped_at, updated_at, posts_count)| %> + + <%= build_sitemap_topic_url(slug, id, posts_count) %> + <%= (bumped_at || updated_at).xmlschema %> + + <% end %> + diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb index e40d9c050f..5580d1c5c2 100644 --- a/app/views/user_notifications/digest.html.erb +++ b/app/views/user_notifications/digest.html.erb @@ -104,7 +104,7 @@ - +
    @@ -121,7 +121,7 @@
    - +
    @@ -156,24 +156,19 @@
    <%- end %> - +
    - @@ -322,19 +318,14 @@ <%= category_badge(t.category, inline_style: true, absolute_url: true) %>

    - diff --git a/config/application.rb b/config/application.rb index 1d9600d817..fe08749c86 100644 --- a/config/application.rb +++ b/config/application.rb @@ -55,8 +55,6 @@ require 'pry-rails' if Rails.env.development? require 'discourse_fonts' -require_relative '../lib/zeitwerk_config.rb' - if defined?(Bundler) bundler_groups = [:default] @@ -69,6 +67,8 @@ if defined?(Bundler) Bundler.require(*bundler_groups) end +require_relative '../lib/require_dependency_backward_compatibility' + module Discourse class Application < Rails::Application @@ -110,9 +110,6 @@ module Discourse config.autoloader = :zeitwerk # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += Dir["#{config.root}/app"] - config.autoload_paths += Dir["#{config.root}/app/jobs"] - config.autoload_paths += Dir["#{config.root}/app/serializers"] config.autoload_paths += Dir["#{config.root}/lib"] config.autoload_paths += Dir["#{config.root}/lib/common_passwords"] config.autoload_paths += Dir["#{config.root}/lib/highlight_js"] @@ -145,11 +142,12 @@ module Discourse config.assets.skip_minification = [] # explicitly precompile any images in plugins ( /assets/images ) path - config.assets.precompile += [lambda do |filename, path| - path =~ /assets\/images/ && !%w(.js .css).include?(File.extname(filename)) - end] + Dir.glob("#{config.root}/plugins/*/assets/images/**/*").each do |filename| + config.assets.precompile << filename if !%w(.js .css).include?(File.extname(filename)) + end config.assets.precompile += %w{ + application.js vendor.js admin.js browser-detect.js @@ -196,25 +194,6 @@ module Discourse end end - # out of the box sprockets 3 grabs loose files that are hanging in assets, - # the exclusion list does not include hbs so you double compile all this stuff - initializer :fix_sprockets_loose_file_searcher, after: :set_default_precompile do |app| - app.config.assets.precompile.delete(Sprockets::Railtie::LOOSE_APP_ASSETS) - - # We don't want application from node_modules, only from the root - app.config.assets.precompile.delete(/(?:\/|\\|\A)application\.(css|js)$/) - app.config.assets.precompile += ['application.js'] - - start_path = ::Rails.root.join("app/assets").to_s - exclude = ['.es6', '.hbs', '.hbr', '.js', '.css', '.lock', '.json', '.log', '.html', ''] - app.config.assets.precompile << lambda do |logical_path, filename| - filename.start_with?(start_path) && - !filename.include?("/node_modules/") && - !filename.include?("/dist/") && - !exclude.include?(File.extname(logical_path)) - end - end - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. config.time_zone = 'UTC' @@ -293,6 +272,11 @@ module Discourse Sprockets.register_mime_type 'application/javascript', extensions: ['.js', '.es6', '.js.es6'], charset: :unicode Sprockets.register_postprocessor 'application/javascript', DiscourseJsProcessor + # This class doesn't exist in Sprockets 4, but ember-rails tries to 'autoload' it + # Define an empty class to prevent an error + class Sprockets::Engines + end + require 'discourse_redis' require 'logster/redis_store' # Use redis for our cache diff --git a/config/initializers/000-zeitwerk.rb b/config/initializers/000-zeitwerk.rb new file mode 100644 index 0000000000..7fa14e8bf4 --- /dev/null +++ b/config/initializers/000-zeitwerk.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# This custom inflector is needed because of our jobs directory structure. +# Ideally, we should not prefix our jobs with a `Jobs` namespace but instead +# have a `Job` suffix to follow the Rails conventions on naming. +# +# Based on: +# https://github.com/rails/rails/blob/75e6c0ac/railties/lib/rails/autoloaders/inflector.rb#L7-L19 +module DiscourseInflector + @overrides = {} + + def self.camelize(basename, abspath) + return basename.camelize if abspath.ends_with?("onceoff.rb") + return 'Jobs' if abspath.ends_with?("jobs/base.rb") + @overrides[basename] || basename.camelize + end + + def self.inflect(overrides) + @overrides.merge!(overrides) + end +end + +Rails.autoloaders.each do |autoloader| + autoloader.inflector = DiscourseInflector + + # We have filenames that do not follow Zeitwerk's camelization convention. Maintain an inflections for these files + # for now until we decide to fix them one day. + autoloader.inflector.inflect( + 'canonical_url' => 'CanonicalURL', + 'clean_up_unmatched_ips' => 'CleanUpUnmatchedIPs', + 'homepage_constraint' => 'HomePageConstraint', + 'ip_addr' => 'IPAddr', + 'onpdiff' => 'ONPDiff', + 'pop3_polling_enabled_setting_validator' => 'POP3PollingEnabledSettingValidator', + 'version' => 'Discourse', + 'onceoff' => 'Jobs', + 'regular' => 'Jobs', + 'scheduled' => 'Jobs', + ) +end diff --git a/config/initializers/003-sql_builder.rb b/config/initializers/003-sql_builder.rb deleted file mode 100644 index a7be88c294..0000000000 --- a/config/initializers/003-sql_builder.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require 'sql_builder' diff --git a/config/initializers/004-message_bus.rb b/config/initializers/004-message_bus.rb index 2f46aa04dc..46a531e3f7 100644 --- a/config/initializers/004-message_bus.rb +++ b/config/initializers/004-message_bus.rb @@ -119,16 +119,8 @@ end MessageBus.backend_instance.max_backlog_size = GlobalSetting.message_bus_max_backlog_size MessageBus.backend_instance.clear_every = GlobalSetting.message_bus_clear_every - -if SiteSetting.table_exists? && SiteSetting.where(name: ['enable_long_polling', 'long_polling_interval']).exists? - Discourse.deprecate("enable_long_polling/long_polling_interval have switched from site settings to global settings. Remove the override from the Site Settings UI, and use a config file or environment variables to set the global settings.", drop_from: '2.9.0') - - MessageBus.long_polling_enabled = SiteSetting.enable_long_polling - MessageBus.long_polling_interval = SiteSetting.long_polling_interval -else - MessageBus.long_polling_enabled = GlobalSetting.enable_long_polling.nil? ? true : GlobalSetting.enable_long_polling - MessageBus.long_polling_interval = GlobalSetting.long_polling_interval || 25000 -end +MessageBus.long_polling_enabled = GlobalSetting.enable_long_polling.nil? ? true : GlobalSetting.enable_long_polling +MessageBus.long_polling_interval = GlobalSetting.long_polling_interval || 25000 if Rails.env == "test" || $0 =~ /rake$/ # disable keepalive in testing diff --git a/config/initializers/100-sidekiq.rb b/config/initializers/100-sidekiq.rb index 4f7a77b69a..958d0b9144 100644 --- a/config/initializers/100-sidekiq.rb +++ b/config/initializers/100-sidekiq.rb @@ -1,14 +1,5 @@ # frozen_string_literal: true -# Ensure that scheduled jobs are loaded before mini_scheduler is configured. -if Rails.env == "development" - require "jobs/base" - - Dir.glob("#{Rails.root}/app/jobs/scheduled/*.rb") do |f| - require(f) - end -end - require "sidekiq/pausable" Sidekiq.configure_client do |config| @@ -23,28 +14,6 @@ Sidekiq.configure_server do |config| end end -MiniScheduler.configure do |config| - - config.redis = Discourse.redis - - config.job_exception_handler do |ex, context| - Discourse.handle_job_exception(ex, context) - end - - config.job_ran do |stat| - DiscourseEvent.trigger(:scheduled_job_ran, stat) - end - - config.skip_schedule { Sidekiq.paused? } - - config.before_sidekiq_web_request do - RailsMultisite::ConnectionManagement.establish_connection( - db: RailsMultisite::ConnectionManagement::DEFAULT - ) - end - -end - if Sidekiq.server? module Sidekiq @@ -124,3 +93,32 @@ Sidekiq.error_handlers.clear Sidekiq.error_handlers << SidekiqLogsterReporter.new Sidekiq.strict_args! + +Rails.application.config.to_prepare do + # Ensure that scheduled jobs are loaded before mini_scheduler is configured. + if Rails.env.development? + Dir.glob("#{Rails.root}/app/jobs/scheduled/*.rb") do |f| + require(f) + end + end + + MiniScheduler.configure do |config| + config.redis = Discourse.redis + + config.job_exception_handler do |ex, context| + Discourse.handle_job_exception(ex, context) + end + + config.job_ran do |stat| + DiscourseEvent.trigger(:scheduled_job_ran, stat) + end + + config.skip_schedule { Sidekiq.paused? } + + config.before_sidekiq_web_request do + RailsMultisite::ConnectionManagement.establish_connection( + db: RailsMultisite::ConnectionManagement::DEFAULT + ) + end + end +end diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 33c8599ff5..2aef6cd6a3 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -208,7 +208,6 @@ ar: to_placeholder: "إلى التاريخ" share: topic_html: 'الموضوع: %{topicTitle}' - post: "المنشور #%{postNumber}" close: "إغلاق" twitter: "المشاركة على Twitter" facebook: "المشاركة على Facebook" @@ -2030,27 +2029,23 @@ ar: not_approved: "لم تتم الموافقة على حسابك حتى الآن. سيتم إعلامك عبر البريد الإلكتروني عندما تكون مستعدًا لتسجيل الدخول." google_oauth2: name: "Google" - title: "باستخدام Google" - sr_title: "تسجيل الدخول باستخدام جوجل" twitter: name: "Twitter" - title: "باستخدام Twitter" - sr_title: "تسجيل الدخول باستخدام تويتر" instagram: name: "Instagram" - title: "باستخدام Instagram" + title: "تسجيل الدخول باستخدام Instagram" sr_title: "تسجيل الدخول باستخدام Instagram" facebook: name: "Facebook" - title: "باستخدام Facebook" + title: "تسجيل الدخول باستخدام فيسبوك" sr_title: "تسجيل الدخول باستخدام فيسبوك" github: name: "GitHub" - title: "باستخدام GitHub" + title: "تسجيل الدخول باستخدام GitHub" sr_title: "تسجيل الدخول باستخدام GitHub" discord: name: "Discord" - title: "باستخدام Discord" + title: "تسجيل الدخول باستخدام ديسكورد" sr_title: "تسجيل الدخول باستخدام ديسكورد" second_factor_toggle: totp: "استخدام تطبيق مصادقة بدلًا من ذلك" @@ -2066,7 +2061,6 @@ ar: success: "تم إنشاء حسابك وتسجيل دخولك." name_label: "الاسم" password_label: "كلمة المرور" - optional_description: "(اختياري)" password_reset: continue: "المتابعة إلى %{site_name}" emoji_set: @@ -3518,10 +3512,8 @@ ar: tag_groups_placeholder: "(اختياري) قائمة مجموعات الوسوم المسموح بها" manage_tag_groups_link: "إدارة مجموعات الوسوم" allow_global_tags_label: "السماح بالوسوم الأخرى أيضًا" - tag_group_selector_placeholder: "(اختياري) مجموعة وسوم" - required_tag_group_description: "طلب أن تحمل الموضوعات الجديدة وسومًا من مجموعة وسوم:" - min_tags_from_required_group_label: "عدد الوسوم:" - required_tag_group_label: "مجموعة الوسوم:" + required_tag_group: + delete: "حذف" topic_featured_link_allowed: "السماح بالروابط المميزة في هذه الفئة." delete: "حذف الفئة" create: "فئة جديدة" @@ -4251,7 +4243,6 @@ ar: unsupported_file_picked: "لقد اخترت ملفًا غير مدعوم. أنواع الملفات المدعومة - %{types}." user_activity: no_activity_title: "لا يوجد نشاط." - no_activity_body: "مرحبًا بكم في مجتمعنا. أنت جديد تمامًا هنا ولم تساهم بعد في المناقشات. كخطوة أولى، قم بزيارة أعلى أو فئات وابدأ القراءة فقط! حدّد %{heartIcon} على المشاركات التي تعجبك أو تريد معرفة المزيد عنها. إذا لم تكن قد فعلت ذلك فعلًا، ساعد الآخرين على التعرّف عليك عن طريق إضافة صورة وسيرة ذاتية في تفضيلات المستخدم." no_activity_others: "لا يوجد نشاط." no_replies_title: "لم ترد على أي مواضيع حتى الآن" no_replies_others: "لا توجد ردود." diff --git a/config/locales/client.be.yml b/config/locales/client.be.yml index 3eb094da9c..161951ab82 100644 --- a/config/locales/client.be.yml +++ b/config/locales/client.be.yml @@ -74,7 +74,6 @@ be: next_month: "наступны месяц" share: topic_html: 'Тэма: %{topicTitle}' - post: "паведамленне #%{postNumber}" close: "схаваць" emails_are_disabled: "Адпраўка паведамленняў па электроннай пошце было глабальна адключана адміністратарам. Ні адно апавяшчэнне па электроннай пошце не будзе даслана." s3: @@ -763,16 +762,8 @@ be: not_approved: "Ваш рахунак яшчэ не было прынята. Вы атрымаеце апавяшчэнне па электроннай пошце, калі зможаце ўвайсці ў сістэму." google_oauth2: name: "Google" - title: "з Google" twitter: name: "Twitter" - title: "праз Twitter" - instagram: - title: "праз Instagram" - facebook: - title: "праз Facebook" - github: - title: "праз GitHub" second_factor_toggle: totp: "Выкарыстоўвайце прыкладанне аутентификационное замест" backup_code: "Выкарыстоўваць рэзервовы код замест" @@ -1121,6 +1112,8 @@ be: general: "Агульныя" settings: "Налады" tags: "Тэгі" + required_tag_group: + delete: "Выдаліць" delete: "выдаліць катэгорыю" create: "Новая катэгорыя" create_long: "стварыць катэгорыю" diff --git a/config/locales/client.bg.yml b/config/locales/client.bg.yml index 70e9bbdaff..64ea9193a8 100644 --- a/config/locales/client.bg.yml +++ b/config/locales/client.bg.yml @@ -120,7 +120,6 @@ bg: to_placeholder: "до дата" share: topic_html: 'Тема: %{topicTitle}' - post: "публикации #%{postNumber}" close: "затвори" twitter: "Споделете в Twitter" facebook: "Споделете във Facebook" @@ -1682,27 +1681,23 @@ bg: not_approved: "Вашият профил все още не е одобрен. Ще бъдете уведомени с имейл, когато е възможно влизането Ви. " google_oauth2: name: "Google" - title: "с Google" - sr_title: "Влезте с Google" twitter: name: "Twitter" - title: "с Twitter" - sr_title: "Влезте с Twitter" instagram: name: "Instagram" - title: "чрез Instagram" + title: "Влезте с Instagram" sr_title: "Влезте с Instagram" facebook: name: "Facebook" - title: "със Facebook" + title: "Влезте с Facebook" sr_title: "Влезте с Facebook" github: name: "GitHub" - title: " с Github" + title: "Влезте с GitHub" sr_title: "Влезте с GitHub" discord: name: "Discord" - title: "с Discord" + title: "Влезте с Discord" sr_title: "Влезте с Discord" second_factor_toggle: totp: "Вместо това използвайте приложение за удостоверяване" @@ -1718,7 +1713,6 @@ bg: success: "Профилът ви е създаден и вече сте влезли в него." name_label: "Име" password_label: "Парола" - optional_description: "(по избор)" password_reset: continue: "Продължете към %{site_name}" emoji_set: @@ -2622,6 +2616,8 @@ bg: topic_template: "Шаблон на темата" tags: "Етикети" tags_placeholder: "(По желание) списък на позволените етикети" + required_tag_group: + delete: "Изтрий" topic_featured_link_allowed: "Разрешаване на включени връзки в тази категория" delete: "Изтрий категорията" create: "Нова категория" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 6d96c78e24..7333f68a59 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -140,7 +140,6 @@ bs_BA: to_placeholder: "do datuma" share: topic_html: 'Tema: %{topicTitle}' - post: "dijeli post #%{postNumber}" close: "zatvori" twitter: "Podijeli na Twitter" facebook: "Podijeli na Facebooku" @@ -1560,22 +1559,16 @@ bs_BA: not_approved: "Vaš korisnički račun nije još uvijek odobren od strane administratora. Dobiti ćete email kada dobijete mogućnost da se ulogirate." google_oauth2: name: "Google" - title: "koristeći Google" twitter: name: "Twitter" - title: "koristeći Twitter" instagram: name: "Instagram" - title: "koristeći Instagram" facebook: name: "Facebook" - title: "koristeći Facebook" github: name: "GitHub" - title: "koristeći GitHub" discord: name: "Discord" - title: "koristeći Discord" second_factor_toggle: totp: "Umjesto toga koristite aplikaciju za provjeru autentičnosti" backup_code: "Umjesto toga koristite sigurnosni kod" @@ -1589,7 +1582,6 @@ bs_BA: success: "Vaš račun je napravljen i sada ste prijavljeni." name_label: "Ime" password_label: "Lozinka" - optional_description: "(opciono)" password_reset: continue: "Nastavi ka %{site_name}" emoji_set: @@ -2525,6 +2517,8 @@ bs_BA: tags_placeholder: "(Opcionalno) lista dozvoljenih oznaka" tag_groups_placeholder: "(Opcionalno) lista dozvoljenih grupa oznaka" allow_global_tags_label: "Takođe dozvolite druge oznake" + required_tag_group: + delete: "Delete" topic_featured_link_allowed: "Omogućite istaknute veze u ovoj kategoriji" delete: "Delete Category" create: "Nova kategorija" diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index b7e7e6c4fd..fc6f40aa67 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -119,7 +119,6 @@ ca: to_placeholder: "fins a la data" share: topic_html: 'Tema: %{topicTitle}' - post: "publicació #%{postNumber}" close: "tanca" twitter: "Comparteix en Twitter" facebook: "Comparteix en Facebook" @@ -1482,22 +1481,16 @@ ca: not_approved: "El vostre compte encara no ha estat aprovat. Us notificarem per correu quan tingueu permís per a iniciar sessió." google_oauth2: name: "Google" - title: "amb Google" twitter: name: "Twitter" - title: "amb Twitter" instagram: name: "Instagram" - title: "amb Instagram" facebook: name: "Facebook" - title: "amb Facebook" github: name: "GitHub" - title: "amb GitHub" discord: name: "Discord" - title: "amb Discord" second_factor_toggle: totp: "Utilitzeu una aplicació d'autenticació en comptes d'això" backup_code: "Utilitzeu un codi de còpia de seguretat en comptes d'això" @@ -1511,7 +1504,6 @@ ca: success: "S'ha creat el compte i ara heu iniciat la sessió." name_label: "Nom" password_label: "Contrasenya" - optional_description: "(opcional)" password_reset: continue: "Continua a %{site_name}" emoji_set: @@ -2403,10 +2395,8 @@ ca: tags_placeholder: "Llista (opcional) d'etiquetes permeses" tag_groups_placeholder: "Llista (opcional) d'etiquetes de grup permeses" allow_global_tags_label: "Permet també altres etiquetes" - tag_group_selector_placeholder: "(Opcional) Grup d’etiquetes" - required_tag_group_description: "Requereix que els temes nous tinguin etiquetes d’un grup d’etiquetes:" - min_tags_from_required_group_label: "Etiquetes num.:" - required_tag_group_label: "Grup d'etiquetes:" + required_tag_group: + delete: "Suprimeix" topic_featured_link_allowed: "Permet enllaços destacats dins aquesta categoria" delete: "Suprimeix categoria" create: "Nova categoria" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index aff9980730..bc5a59089d 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -157,7 +157,6 @@ cs: to_placeholder: "do data" share: topic_html: 'Téma: %{topicTitle}' - post: "příspěvek #%{postNumber}" close: "zavřít" twitter: "Sdílet na Twitteru" facebook: "Sdílet na Facebooku" @@ -1441,19 +1440,14 @@ cs: not_approved: "Váš účet zatím nebyl schválen. Až se tak stane, obdržíte oznámení emailem a budete se moci přihlásit." google_oauth2: name: "Google" - title: "přes Google" twitter: name: "Twitter" - title: "přes Twitter" instagram: name: "Instagram" - title: "s Instagramem" facebook: name: "Facebook" - title: "přes Facebook" github: name: "GitHub" - title: "přes GitHub" discord: name: "Discord" invites: @@ -1466,7 +1460,6 @@ cs: success: "Váš účet byl vytvořen a nyní jste přihlášeni." name_label: "Jméno" password_label: "Heslo" - optional_description: "(volitelné)" password_reset: continue: "Pokračovat na %{site_name}" emoji_set: @@ -2334,6 +2327,8 @@ cs: tags: "Štítky" tags_placeholder: "(Volitelné) seznam dovolených štítků" tag_groups_placeholder: "(Volitelné) seznam dovolených skupin štítků" + required_tag_group: + delete: "Smazat" topic_featured_link_allowed: "Povolit zobrazené odkazy v této kategorii" delete: "Smazat kategorii" create: "Nová kategorie" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 13dc80e65b..32e1d73438 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -115,10 +115,11 @@ da: previous_month: "Forrige måned" next_month: "Næste måned" placeholder: dato + from_placeholder: "fra dato" to_placeholder: "til dato" share: topic_html: 'Emne: %{topicTitle}' - post: "indlæg #%{postNumber}" + post: "indlæg #%{postNumber} af @%{username}" close: "luk" twitter: "Del på Twitter" facebook: "Del på Facebook" @@ -126,6 +127,7 @@ da: url: "Kopiér og del URL" action_codes: public_topic: "offentliggjorde dette emne %{when}" + open_topic: "konverterede dette til et emne %{when}" private_topic: "gjorde dette emne til en privat besked %{when}" split_topic: "delte dette emne op %{when}" invited_user: "Inviterede %{who} %{when}" @@ -168,6 +170,8 @@ da: bootstrap_mode_disabled: "Bootstrap-tilstand deaktiveres inden for 24 timer." themes: default_description: "Standard" + broken_theme_alert: "Dit websted fungerer muligvis ikke, fordi et tema/en komponent har fejl." + only_admins: "(denne meddelelse vises kun til webstedsadministratorer)" s3: regions: ap_northeast_1: "Asien (Tokyo)" @@ -181,6 +185,7 @@ da: cn_northwest_1: "Kina (Ningxia)" eu_central_1: "EU (Frankfurt)" eu_north_1: "EU (Stockholm)" + eu_south_1: "EU (Milano)" eu_west_1: "EU (Irland)" eu_west_2: "EU (London)" eu_west_3: "EU (Paris)" @@ -238,6 +243,8 @@ da: character_count: one: "%{count} tegn" other: "%{count} tegn" + period_chooser: + aria_label: "Filtrer efter periode" related_messages: title: "Relaterede beskeder" see_all: 'Se alle beskeder fra @ %{username} ...' @@ -270,11 +277,15 @@ da: help: bookmark: "Klik for at sætte et bogmærke i det første indlæg i denne tråd" edit_bookmark: "Klik for at redigere bogmærket for dette emne" + edit_bookmark_for_topic: "Klik for at redigere bogmærket for dette emne" unbookmark: "Klik for at fjerne alle bogmærker i dette emne" unbookmark_with_reminder: "Klik for at fjerne alle bogmærker og påmindelser i dette emne." bookmarks: created: "Du har sat et bogmærke for dette indlæg. %{name}" + create_for: "Opret bogmærke for %{type}" + edit_for: "Rediger bogmærke for %{type}" not_bookmarked: "bogmærk dette indlæg" + remove_reminder_keep_bookmark: "Fjern påmindelse og behold bogmærke" created_with_reminder: "Du har sat et bogmærke med en påmindelse for dette indlæg %{date}. %{name}" remove: "Fjern bogmærke" delete: "Slet Bogmærke" @@ -286,7 +297,9 @@ da: list_permission_denied: "Du har ikke tilladelse til at se denne brugers bogmærker." no_user_bookmarks: "Du har ingen bogmærkede indlæg; bogmærker giver dig mulighed for hurtigt at henvise til bestemte indlæg." auto_delete_preference: + never: "Behold bogmærke" when_reminder_sent: "Slet bogmærke" + on_owner_reply: "Slet bogmærke, når jeg svarer" search_placeholder: "Søg i bogmærker efter navn, emnetitel eller indlægs indhold" search: "Søg" reminders: @@ -296,6 +309,8 @@ da: existing_reminder: "Du har en påmindelse sat for dette bogmærke, som vil blive sendt %{at_date_time}" copy_codeblock: copied: "kopieret!" + copy: "kopier kode til udklipsholder" + fullscreen: "vis kode i fuld skærm" drafts: label: "Kladder" label_with_count: "Kladder (%{count})" @@ -306,7 +321,7 @@ da: new_private_message: "Ny personlig meddelelseskladde" topic_reply: "Kladde til svar" abandon: - confirm: "Du har et udkast i gang for dette emne. Hvad vil du gerne gøre med den?" + confirm: "Du har et udkast i gang for dette emne. Hvad vil du gerne gøre med det?" yes_value: "Kassér" no_value: "Genoptag redigering" topic_count_categories: @@ -325,7 +340,7 @@ da: one: "Se %{count} nyt emne" other: "Se %{count} nye emner" preview: "forhåndsvising" - cancel: "annullér" + cancel: "annuller" deleting: "Sletter..." save: "Gem ændringer" saving: "Gemmer…" @@ -362,6 +377,7 @@ da: placeholder: "indtast beskeds titel, url eller id her" review: order_by: "Sorter efter" + date_filter: "Skrevet mellem" in_reply_to: "som svar til" explain: why: "forklar hvorfor dette punkt endte i køen" @@ -452,7 +468,7 @@ da: other: "%{count} svar" edit: "Rediger" save: "Gem" - cancel: "Afbryd" + cancel: "Annuller" new_topic: "Godkendelse af dette opretter et nyt emne" filters: all_categories: "(alle kategorier)" @@ -607,6 +623,8 @@ da: member_added: "Tilføjet" member_requested: "Anmodet kl" add_members: + title: "Tilføj brugere til %{group_name}" + description: "Indtast en liste over brugere, du vil invitere til gruppen, eller indsæt i en kommasepareret liste:" usernames_placeholder: "brugernavne" usernames_or_emails_placeholder: "brugernavne eller e-mail adresser" notify_users: "Giv brugerne besked" @@ -665,6 +683,7 @@ da: settings: title: "Indstillinger" allow_unknown_sender_topic_replies: "Tillad emnesvar fra ukendte afsendere" + from_alias: "Fra Alias" mailboxes: synchronized: "Synkroniseret postkasse" none_found: "Ingen postkasser blev fundet i denne e-mail-konto." @@ -961,7 +980,9 @@ da: use_current_timezone: "Brug den aktuelle tidszone" profile_hidden: "Denne brugers offentlige profil er skjult." expand_profile: "Udvid" + sr_expand_profile: "Udvid profildetaljer" collapse_profile: "Fold sammen" + sr_collapse_profile: "Skjul profildetaljer" bookmarks: "Bogmærker" bio: "Om mig" timezone: "Tidszone" @@ -1230,6 +1251,7 @@ da: upload_title: "Overfør dit profil billede" image_is_not_a_square: "Advarsel: vi har beskåret billedet; bredde og højde var ikke ens." logo_small: "Webstedets lille logo. Anvendes som standard." + use_custom: "Eller overfør en brugerdefineret avatar:" change_profile_background: title: "Profiloverskrift" instructions: "Profiloverskrifter vil blive centreret og har en standardbredde på 1110px." @@ -1261,6 +1283,7 @@ da: invalid: "Indtast venligst en gyldig email adresse" authenticated: "Din email er blevet bekræftet af %{provider}" invite_auth_email_invalid: "Din invitations e-mail matcher ikke den godkendte e-mail af %{provider}" + authenticated_by_invite: "Din e-mail er blevet godkendt af invitationen" frequency_immediately: "Vi sender dig en email med det samme, hvis du ikke har læst den ting vi emailer dig om." frequency: one: "Vi sender dig kun email, hvis vi ikke har set dig i det seneste minut." @@ -1269,7 +1292,7 @@ da: title: "Tilknyttede konti" connect: "Forbind" revoke: "Tilbagekald" - cancel: "Afbryd" + cancel: "Annuller" not_connected: "(ikke forbundet)" confirm_modal_title: "Forbind %{provider}-konto" confirm_description: @@ -1446,6 +1469,7 @@ da: edit_title: "Rediger Invitation" instructions: "Del dette link for øjeblikkeligt at give adgang til dette websted:" copy_link: "kopier link" + expires_in_time: "Udløber om %{time}" expired_at_time: "Udløb kl. %{time}" show_advanced: "Vis Avancerede Indstillinger" hide_advanced: "Skjul Avancerede Indstillinger" @@ -1453,6 +1477,7 @@ da: restrict_email: "Begræns til e-mail" restrict_domain: "Begræns til domæne" email_or_domain_placeholder: "navn@eksempel.dk eller eksempel.dk" + max_redemptions_allowed: "Maks. anvendelser" add_to_groups: "Tilføj til grupper" expires_at: "Udløber efter" custom_message: "Valgfri personlig meddelelse" @@ -1727,27 +1752,23 @@ da: not_approved: "Din konto er endnu ikke blevet godkendt. Du får besked via e-mail når du kan logge ind." google_oauth2: name: "Google" - title: "med google" - sr_title: "Log ind med Google" twitter: name: "Twitter" - title: "med Twitter" - sr_title: "Log ind med Twitter" instagram: name: "Instagram" - title: "med Instagram" + title: "Log ind med Instagram" sr_title: "Log ind med Instagram" facebook: name: "Facebook" - title: "med Facebook" + title: "Log ind med Facebook" sr_title: "Log ind med Facebook" github: name: "GitHub" - title: "med GitHub" + title: "Log ind med GitHub" sr_title: "Log ind med GitHub" discord: name: "Discord" - title: "med Discord" + title: "Log ind med Discord" sr_title: "Log ind med Discord" second_factor_toggle: totp: "Brug en godkendelsesapp i stedet" @@ -1763,7 +1784,6 @@ da: success: "Din konto er blevet oprettet, og du er nu logget ind." name_label: "Navn" password_label: "Adgangskode" - optional_description: "(valgfri)" password_reset: continue: "Fortsæt til %{site_name}" emoji_set: @@ -1894,7 +1914,7 @@ da: reply_original: "Svar på det oprindelige emne" reply_here: "Svar her" reply: "Svar" - cancel: "Annullér" + cancel: "Annuller" create_topic: "Opret emne" create_pm: "Besked" create_whisper: "Hvisk" @@ -2131,6 +2151,7 @@ da: category_tag: "filtrerer efter kategori eller mærke" in: "filtrerer efter metadata (f.eks. in:title, in:personal, in:pinned)" status: "filtrerer efter emnestatus" + full_search: "starter fuldsidesøgning" full_search_key: "%{modifier} + Enter" advanced: title: Avancerede filtre @@ -2213,6 +2234,7 @@ da: close_topics: "Luk indlæg" archive_topics: "Arkiver Emner" move_messages_to_inbox: "Flyt til indbakke" + notification_level: "Notifikationer..." change_notification_level: "Skift Notifikationsniveau" choose_new_category: "Vælg den nye kategori for emnerne:" selected: @@ -2311,6 +2333,7 @@ da: browse_all_categories: Vis alle kategorier browse_all_tags: Gennemse alle mærker view_latest_topics: vis seneste emner + suggest_create_topic: Klar til at starte en ny samtale? jump_reply_up: hop til tidligere svar jump_reply_down: hop til senere svar deleted: "Emnet er blevet slettet" @@ -2477,20 +2500,27 @@ da: invisible: "Gør ulistet" visible: "Gør listet" reset_read: "Glem hvilke emner jeg har læst" + make_public: "Gør til Offentligt Emne..." make_private: "Lav privat besked" reset_bump_date: "Nulstil 'Bump' Dato" feature: pin: "Fastgør Emne" unpin: "Fjern Fastgøring af Emne" pin_globally: "Fastgør emne globalt" + make_banner: "Lav Banner Emne" remove_banner: "Emnet skal ikke være banner længere" reply: title: "Svar" help: "start på et svar til dette emne" share: + title: "Del Emne" extended_title: "Del et link" help: "del et link til dette emne" + instructions: "Del et link til dette emne:" copied: "Emne link kopieret." + restricted_groups: + one: "Kun synlig for medlemmer af gruppen: %{groupNames}" + other: "Kun synlig for medlemmer af grupperne: %{groupNames}" invite_users: "Invitér" print: title: "Print" @@ -2662,7 +2692,9 @@ da: deleted_by_author_simple: "(emne slettet af forfatteren)" post: quote_reply: "Citér" + quote_reply_shortcut: "Eller tryk på q" quote_edit: "Rediger" + quote_edit_shortcut: "Eller tryk på e" quote_share: "Del" edit_reason: "Grund: " post_number: "indlæg %{number}" @@ -2763,6 +2795,7 @@ da: rebake: "Gendan HTML" publish_page: "Sideudgivelse" unhide: "Vis" + grant_badge: "Tildel Emblem..." lock_post: "Lås Indlæg" lock_post_description: "forhindr senderen i at redigere dette indlæg" unlock_post: "Lås indlæg op" @@ -2793,6 +2826,8 @@ da: read_capped: one: "og %{count} anden læste dette" other: "og %{count} andre læste dette" + sr_post_likers_list_description: "brugere, der kunne lide dette indlæg" + sr_post_readers_list_description: "brugere, der læste dette indlæg" by_you: off_topic: "Du flagede dette som off-topic" spam: "Du flagede dette som spam" @@ -2842,7 +2877,9 @@ da: button: "HTML" bookmarks: create: "Opret bogmærke" + create_for_topic: "Opret bogmærke til emnet" edit: "Rediger bogmærke" + edit_for_topic: "Rediger bogmærke til emnet" created: "Oprettet" updated: "Opdateret" name: "Navn" @@ -2856,6 +2893,8 @@ da: edit_bookmark: name: "Rediger bogmærke" description: "Rediger bogmærkenavnet eller ændre påmindelsesdato og -tidspunkt" + clear_bookmark_reminder: + name: "Ryd påmindelse" pin_bookmark: name: "Fastgør bogmærke" description: "Fastgør bogmærket. Dette vil få det til at stå øverst på din bogmærkeliste." @@ -2868,6 +2907,9 @@ da: viewing_summary: "Viser en oversigt over dette emne" post_number: "%{username}, indlæg #%{post_number}" show_all: "Vis alle" + share: + title: "Del indlæg #%{post_number}" + instructions: "Del et link til dette indlæg:" category: none: "(ingen kategori)" all: "Alle kategorier" @@ -2887,10 +2929,8 @@ da: tag_groups_placeholder: "(Valgfri) liste af tiladte mærkegrupper" manage_tag_groups_link: "Administrer mærkegrupper" allow_global_tags_label: "Tillad også andre mærker" - tag_group_selector_placeholder: "(Valgfrit) Mærkegruppe" - required_tag_group_description: "Kræv at nye emner skal have mærker fra en mærkegruppe:" - min_tags_from_required_group_label: "Antal Mærker:" - required_tag_group_label: "Mærkegruppe:" + required_tag_group: + delete: "Slet" topic_featured_link_allowed: "Tillad fremhævede links i denne kategori" delete: "Slet kategori" create: "Ny kategori" @@ -3128,6 +3168,7 @@ da: other: "brugere" category_title: "Kategori" history_capped_revisions: "Historik, seneste 100 revisioner" + history: "Historik" changed_by: "af %{author}" raw_email: title: "Indgående e-mail" @@ -3334,11 +3375,13 @@ da: name: Andre posting: name: Skriver - favorite_max_reached: "Du kan ikke favorisere flere badges." - favorite_max_not_reached: "Markér dette badge som favorit" + favorite_max_reached: "Du kan ikke favorisere flere emblemer." + favorite_max_not_reached: "Markér dette emblem som favorit" download_calendar: remember: "Spørg mig ikke igen" download: "Download" + add_to_calendar: "Føj til kalender" + google: "Google Kalender" ics: "ICS" tagging: all_tags: "Alle Mærker" @@ -3348,6 +3391,9 @@ da: changed: "mærker skiftet:" tags: "Mærker" choose_for_topic: "valgfrie mærker" + choose_for_topic_required: + one: "vælg mindst %{count} mærke..." + other: "vælg mindst %{count} mærker..." info: "Info" default_info: "Dette mærke er ikke begrænset til nogen kategorier og har ingen synonymer." category_restricted: "Dette mærke er begrænset til kategorier, som du ikke har adgang til." @@ -3359,6 +3405,7 @@ da: category_restrictions: one: "Den kan kun bruges i denne kategori:" other: "Det kan kun bruges i disse kategorier:" + edit_synonyms: "Rediger Synonymer" add_synonyms_label: "Tilføj synonymer:" add_synonyms: "Tilføj" add_synonyms_explanation: @@ -3375,6 +3422,7 @@ da: delete_confirm_synonyms: one: "Dets synonym slettes også." other: "Dets %{count} synonymer slettes også." + edit_tag: "Rediger mærke navn og beskrivelse" description: "Beskrivelse" sort_by: "Sortér efter:" sort_by_count: "antal" @@ -3395,7 +3443,7 @@ da: tag_list_joiner: ", " delete_unused: "Slet Ubrugte Mærker" delete_unused_description: "Slet alle mærker, der ikke er knyttet til nogen emner eller personlige beskeder" - cancel_delete_unused: "Afbryd" + cancel_delete_unused: "Annuller" filters: without_category: "%{filter} %{tag} emner" with_category: "%{filter} %{tag} emner i %{category}" @@ -3419,9 +3467,14 @@ da: description: "Du vil ikke blive underrettet om noget vedr. nye emner med dette mærke, og de vises ikke på din ulæste fane." groups: title: "Mærkegrupper" + about_heading: "Vælg en mærkegruppe eller opret en ny" + about_heading_empty: "Opret en ny mærkegruppe for at komme i gang" new: "Ny Gruppe" + new_title: "Opret Ny Gruppe" + edit_title: "Rediger Mærke Gruppe" tags_label: "Mærker i denne gruppe" parent_tag_label: "Overordnede mærke" + parent_tag_description: "Mærker fra denne gruppe kan kun bruges, hvis det overordnede mærke er til stede." one_per_topic_label: "Begræns ét mærke per emne fra denne gruppe" new_name: "Ny mærkegruppe" name_placeholder: "Navn" @@ -3431,6 +3484,7 @@ da: everyone_can_use: "Mærker kan bruges af alle" usable_only_by_groups: "Mærker er synlige for alle, men kun følgende grupper kan bruge dem" visible_only_to_groups: "Mærker er kun synlige for følgende grupper" + cannot_save: "Kan ikke gemme mærkegruppe. Sørg for, at der er mindst et mærke til stede, mærkegruppe navnet ikke er tomt, og en gruppe er valgt for mærke tilladelse." tags_placeholder: "Søg eller opret tags" parent_tag_placeholder: "Valgfri" select_groups_placeholder: "Vælg grupper ..." @@ -3483,9 +3537,25 @@ da: pick_files_button: unsupported_file_picked: "Du har valgt en fil, der ikke understøttes. Understøttede filtyper - %{types}." user_activity: + no_activity_title: "Ingen aktivitet endnu" + no_activity_body: "Velkommen til vores fællesskab! Du er helt ny her og har endnu ikke bidraget til diskussioner. Som et første skridt, besøg Top eller Kategorier og begynde bare at læse! Vælg %{heartIcon} på indlæg, som du kan lide eller ønsker at lære mere om. Når du deltager, vil din aktivitet blive vist her." no_activity_others: "Ingen aktivitet." + no_replies_title: "Du har ikke svaret på nogen emner endnu" no_replies_others: "Ingen svar." + no_drafts_title: "Du har ikke påbegyndt nogen kladder" + no_drafts_body: "Ikke helt klar til at skrive? Vi gemmer automatisk en ny kladde og viser den her, hver gang du begynder at skrive et emne, svar eller personlig besked. Vælg knappen Annuller for at kassere eller gemme din kladde for at fortsætte senere." + no_likes_title: "Du har ikke 'syntes godt om' nogen emner endnu" + no_likes_body: "En god måde at hoppe ind og begynde at bidrage på, er at begynde at læse samtaler, der allerede har fundet sted, og vælge %{heartIcon} på indlæg, som du kan lide!" no_likes_others: "Ingen 'synes om'-beskeder." + no_topics_title: "Du har ikke startet nogen emner endnu" + no_topics_title_others: "%{username} har endnu ikke startet nogen emner" + no_read_topics_title: "Du har ikke læst nogen emner endnu" + no_group_messages_title: "Ingen gruppemeddelelser fundet" + topic_entrance: + sr_jump_top_button: "Hop til det første indlæg" + sr_jump_bottom_button: "Hop til det sidste indlæg" + fullscreen_table: + expand_btn: "Udvid Tabel" admin_js: type_to_filter: "skriv for at filtrere…" admin: @@ -3582,6 +3652,7 @@ da: trending_search: more: 'Søg logninger' disabled: 'Rapport over søge tendenser er deaktiveret. Aktivér log søgeforespørgelser for at indsamle data.' + average_chart_label: Gennemsnit filters: file_extension: label: Filtypenavn @@ -3691,6 +3762,7 @@ da: Når du bruger anvendelsesområder, kan du begrænse en API-nøgle til et bestemt sæt endepunkter. Du kan også definere hvilke parametre der vil blive tilladt. Brug kommaer til at adskille flere værdier. title: Anvendelsesområder + read_only: Læs kun resource: Ressource action: Handling allowed_parameters: Tilladte parametre @@ -3703,6 +3775,9 @@ da: write: Opret et nyt emne eller et nyt indlæg i et eksisterende emne. read_lists: Læse emnelister som top, nye, nyeste osv. RSS understøttes også. wordpress: Nødvendigt for at WordPress wp-discourse-plugin fungerer. + categories: + list: Hent en liste over kategorier. + show: Hent en enkelt kategori efter id. users: bookmarks: Se liste over brugerbogmærker. Der returneres bogmærkepåmindelser, når du bruger ICS-format. sync_sso: Synkroniser en bruger ved hjælp af DiscourseConnect. @@ -3919,6 +3994,7 @@ da: delete_confirm: 'Er du sikker på, at du vil slette "%{theme_name}"?' color: "Farve" opacity: "Gennemsigtighed" + copy: "Dupliker" copy_to_clipboard: "Kopiér til udklipsholder" copied_to_clipboard: "Kopierede til udklipsholder" copy_to_clipboard_error: "Fejl ved kopiering til udklipsholder" @@ -4087,8 +4163,12 @@ da: %{example} Præfiksering af egenskabsnavne anbefales stærkt for at undgå konflikter med plugins og/eller kernen. + head_tag: + text: "Hoved" + title: "HTML, der vil blive indsat før head-tagget" body_tag: text: "Brødtekst" + title: "HTML, der vil blive indsat før body-tagget" yaml: text: "YAML" title: "Definér tema indstillinger i YAML-format" @@ -4186,6 +4266,7 @@ da: time: "Tidspunkt" user: "Bruger" email_type: "E-mail-type" + details_title: "Vis e-mail detaljer" to_address: "Modtager" test_email_address: "e-mail-adress der skal testes" send_test: "send test-e-mail" @@ -4435,10 +4516,13 @@ da: replace: "Erstat ord i indlæg med andre ord" tag: "Mærk automatisk emner baseret på første indlæg" form: + placeholder: "Indtast ord eller sætning (* er et jokertegn)" placeholder_regexp: "almindelig udtryk" replace_label: "Erstatning" + replace_placeholder: "eksempel" tag_label: "Mærke" link_label: "Link" + link_placeholder: "https://eksempel.dk" add: "Tilføj" success: "Succes" exists: "Eksisterer allerede" @@ -4551,6 +4635,7 @@ da: logged_out: "Bruger er logget ud på alle enheder" revoke_admin: "Fratag admin" grant_admin: "Tildel admin" + grant_admin_success: "Ny administrator blev bekræftet." grant_admin_confirm: "Vi har sendt dig en e-mail for at bekræfte den ny administrator. Åbn den venligst og følg instrukserne." revoke_moderation: "Fratag moderation" grant_moderation: "Tildel moderation" @@ -4590,6 +4675,9 @@ da: title: "Fremskridt med sletning af indlæg" description: "Sletter indlæg ..." confirmation: + title: "Slet alle indlæg af @%{username}" + text: "slet indlæg af @%{username}" + delete: "Slet indlæg af @%{username}" cancel: "Annuller" merge: button: "Flet" @@ -4910,6 +4998,7 @@ da: perform: "Tildel Emblem til Brugere" upload_csv: Overfør en CSV-fil med enten bruger-e-mails eller brugernavne aborted: Overfør venligst en CSV-fil, der indeholder enten bruger-e-mails eller brugernavne + success: Din CSV blev modtaget, og %{count} brugere vil modtage deres emblem inden længe. csv_has_unmatched_users: "Følgende poster er i CSV-filen, men de kunne ikke matches med eksisterende brugere, og vil derfor ikke modtage emblemet:" csv_has_unmatched_users_truncated_list: "Der var %{count} poster i CSV-filen, der ikke kunne matches med eksisterende brugere og modtager derfor ikke emblemet. På grund af det store antal umatchede poster vises kun de første 100:" replace_owners: Fjern emblemet fra tidligere ejere @@ -4917,6 +5006,7 @@ da: emoji: title: "Humørikon" add: "Tilføj ny emoji" + choose_files: "Vælg Filer" uploading: "Overfører…" name: "Navn" group: "Gruppe" @@ -4998,3 +5088,4 @@ da: previews: share_button: "Del" reply_button: "Svar" + topic_preview: "Forhåndsvisning af emne" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 2611a8b224..c3dafd3403 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -120,7 +120,7 @@ de: to_placeholder: "zu Datum" share: topic_html: 'Thema: %{topicTitle}' - post: "Beitrag #%{postNumber}" + post: "Beitrag Nr.%{postNumber} von @%{username}" close: "Schließen" twitter: "Auf Twitter teilen" facebook: "Auf Facebook teilen" @@ -287,6 +287,8 @@ de: unbookmark_with_reminder: "Klicke, um alle Lesezeichen und Erinnerungen in diesem Thema zu entfernen." bookmarks: created: "Du hast diesen Beitrag mit einem Lesezeichen versehen. %{name}" + create_for: "Lesezeichen für %{type} erstellen" + edit_for: "Lesezeichen für %{type} bearbeiten" not_bookmarked: "diesen Beitrag mit Lesezeichen versehen" remove_reminder_keep_bookmark: "Erinnerung entfernen und Lesezeichen behalten" created_with_reminder: "Du wirst %{date} an dieses Lesezeichen erinnert. %{name}" @@ -1780,27 +1782,23 @@ de: not_approved: "Dein Benutzerkonto wurde noch nicht genehmigt. Du wirst per E-Mail benachrichtigt, sobald du dich anmelden kannst." google_oauth2: name: "Google" - title: "mit Google" - sr_title: "Mit Google anmelden" twitter: name: "Twitter" - title: "mit Twitter" - sr_title: "Mit Twitter anmelden" instagram: name: "Instagram" - title: "mit Instagram" + title: "Mit Instagram anmelden" sr_title: "Mit Instagram anmelden" facebook: name: "Facebook" - title: "mit Facebook" + title: "Mit Facebook anmelden" sr_title: "Mit Facebook anmelden" github: name: "GitHub" - title: "mit GitHub" + title: "Mit GitHub anmelden" sr_title: "Mit GitHub anmelden" discord: name: "Discord" - title: "mit Discord" + title: "Mit Discord anmelden" sr_title: "Mit Discord anmelden" second_factor_toggle: totp: "Benutze stattdessen eine Authentifizierungs-App" @@ -1817,7 +1815,6 @@ de: success: "Dein Konto wurde erstellt und du bist jetzt angemeldet." name_label: "Name" password_label: "Passwort" - optional_description: "(optional)" password_reset: continue: "Weiter zu %{site_name}" emoji_set: @@ -2799,7 +2796,13 @@ de: show_hidden: "Ignorierte Inhalte anzeigen." deleted_by_author_simple: "(Beitrag vom Verfasser gelöscht)" collapse: "zuklappen" + sr_collapse_replies: "Eingebettete Antworten einklappen" + sr_expand_replies: + one: "Dieser Beitrag hat %{count} Antwort. Zum Erweitern klicken" + other: "Dieser Beitrag hat %{count} Antworten. Zum Erweitern klicken" expand_collapse: "erweitern/zuklappen" + sr_below_embedded_posts_description: "Beitrag #%{post_number} Antworten" + sr_embedded_reply_description: "Antwort von @%{username} auf Beitrag #%{post_number}" locked: "ein Team-Mitglied hat diesen Beitrag für die weitere Bearbeitung gesperrt" gap: one: "%{count} versteckten Beitrag anzeigen" @@ -2820,6 +2823,12 @@ de: has_likes_title_you: one: "dir und %{count} weiteren Person gefällt dieser Beitrag" other: "dir und %{count} weiteren Personen gefällt dieser Beitrag" + sr_post_like_count_button: + one: "%{count} Person mochte diesen Beitrag. Zum Ansehen klicken" + other: "%{count} Personen gefällt dieser Beitrag. Zum Anzeigen klicken" + sr_post_read_count_button: + one: "%{count} Person hat diesen Beitrag gelesen. Zur Ansicht klicken" + other: "%{count} Personen lesen diesen Beitrag. Zur Ansicht klicken" filtered_replies_hint: one: "Diesen Beitrag und seine Antwort ansehen" other: "Diesen Beitrag und seine %{count} Antworten ansehen" @@ -2921,6 +2930,8 @@ de: read_capped: one: "and %{count} anderer haben dies gelesen" other: "und %{count} andere haben dies gelesen" + sr_post_likers_list_description: "Benutzer, denen dieser Beitrag gefällt" + sr_post_readers_list_description: "Benutzer, die diesen Beitrag gelesen haben" by_you: off_topic: "Du hast das als „am Thema vorbei“ gemeldet" spam: "Du hast das als Spam gemeldet" @@ -3023,10 +3034,8 @@ de: tag_groups_placeholder: "(Optional) Liste erlaubter Schlagwortgruppen" manage_tag_groups_link: "Schlagwortgruppen verwalten" allow_global_tags_label: "Erlaube auch andere Schlagwörter." - tag_group_selector_placeholder: "(Optional) Schlagwortgruppe" - required_tag_group_description: "Neue Themen müssen Schlagwörter aus einer Schlagwortgruppe haben:" - min_tags_from_required_group_label: "Anz. Schlagwörter:" - required_tag_group_label: "Schlagwortgruppe:" + required_tag_group: + delete: "Löschen" topic_featured_link_allowed: "Erlaube hervorgehobene Links in dieser Kategorie" delete: "Kategorie löschen" create: "Neue Kategorie" @@ -3248,15 +3257,20 @@ de: other {}} original_post: "Original-Beitrag" views: "Aufrufe" + sr_views: "Nach Ansichten sortieren" views_lowercase: one: "Aufruf" other: "Aufrufe" replies: "Antworten" + sr_replies: "Nach Antworten sortieren" views_long: one: "dieses Thema wurde %{count}-mal betrachtet" other: "dieses Thema wurde %{number}-mal betrachtet" activity: "Aktivität" + sr_activity: "Nach Aktivität sortieren" likes: "„Gefällt mir“" + sr_likes: "Nach Likes sortieren" + sr_op_likes: "Nach Likes des Originalbeitrags sortieren" likes_lowercase: one: "„Gefällt mir“" other: "„Gefällt mir“" @@ -3650,7 +3664,7 @@ de: unsupported_file_picked: "Du hast eine nicht unterstützte Datei ausgewählt. Unterstützte Dateitypen – %{types}." user_activity: no_activity_title: "Noch keine Aktivität" - no_activity_body: "Willkommen in unserer Community! Du bist hier ganz neu und hast noch nicht an Diskussionen teilgenommen. Besuche als ersten Schritt Top oder Kategorien und beginne mit dem Lesen! Wähle %{heartIcon} auf Beiträgen, die dir gefallen oder über die du mehr erfahren möchtest. Wenn Du dies noch nicht getan hast, dann hilf bitte anderen Dich durch Hinzufügen eines Bildes und einer Bio in Deinen Benutzereinstellungen kennen zu lernen." + no_activity_body: "Willkommen in unserer Community! Du bist ganz neu hier und hast noch nicht an Diskussionen teilgenommen. Besuche als ersten Schritt Top oder Kategorien und fang einfach an zu lesen! Wähle %{heartIcon} bei Beiträgen aus, die Dir gefallen oder über die Du mehr erfahren möchtest. Wenn Du teilnimmst, wird Deine Aktivität hier aufgelistet." no_activity_others: "Keine Aktivität." no_replies_title: "Du hast noch keine Themen beantwortet" no_replies_others: "Keine Antworten." @@ -3660,9 +3674,13 @@ de: no_likes_body: "Eine großartige Möglichkeit einzusteigen und Beiträge zu leisten, besteht darin, bereits stattgefundene Konversationen zu lesen und die %{heartIcon} bei Beiträgen auszuwählen, die Dir gefallen!" no_likes_others: "Keine Beiträge mit „Gefällt mir“." no_topics_title: "Du hast noch keine Themen begonnen" + no_topics_title_others: "%{username} hat noch keine Themen begonnen" no_read_topics_title: "Du hast noch keine Themen gelesen" no_read_topics_body: "Sobald Du mit dem Lesen von Diskussionen beginnst, siehst Du hier eine Liste. Zum Lesen suche nach Themen, die Du in Top oder Kategorien oder suche nach dem Stichwort %{searchIcon}" no_group_messages_title: "Keine Gruppennachrichten gefunden" + topic_entrance: + sr_jump_top_button: "Zum ersten Beitrag springen" + sr_jump_bottom_button: "Zum letzten Beitrag springen" fullscreen_table: expand_btn: "Tabelle erweitern" second_factor_auth: @@ -4392,6 +4410,7 @@ de: time: "Zeit" user: "Benutzer" email_type: "E-Mail-Typ" + details_title: "E-Mail-Details anzeigen" to_address: "Empfänger" test_email_address: "E-Mail-Adresse für Test" send_test: "Test-E-Mail senden" diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 21fbc77c2c..44629ee5ff 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -118,7 +118,6 @@ el: to_placeholder: "μέχρι σήμερα" share: topic_html: 'Θέμα: %{topicTitle}' - post: "ανάρτηση #%{postNumber}" close: "κλείσιμο" twitter: "Κοινοποιήστε στο Twitter" facebook: "Μοιραστείτε στο Facebook" @@ -1516,22 +1515,16 @@ el: not_approved: "Ο λογαριασμός σας δεν έχει εγκριθεί ακόμα. Θα ειδοποιηθείτε με email όταν είστε έτοιμοι να συνδεθείτε." google_oauth2: name: "Google" - title: "μέσω της Google" twitter: name: "Twitter" - title: "μέσω του Twitter" instagram: name: "Instagram" - title: "μέσω του Instagram" facebook: name: "Facebook" - title: "μέσω του Facebook" github: name: "GitHub" - title: "μέσω του GitHub" discord: name: "Discord" - title: "με το Discord" second_factor_toggle: totp: "Χρησιμοποιήστε αντ 'αυτού μια εφαρμογή ελέγχου ταυτότητας" backup_code: "Χρησιμοποιήστε αντ 'αυτού έναν εφεδρικό κωδικό" @@ -1546,7 +1539,6 @@ el: success: "Ο λογαριασμός σας έχει δημιουργηθεί και έχετε συνδεθεί. " name_label: "Όνομα" password_label: "Κωδικός Πρόσβασης" - optional_description: "(προεραιτικό)" password_reset: continue: "Συνεχίστε στην %{site_name}" emoji_set: @@ -2529,10 +2521,8 @@ el: tags_tab_description: "Οι ετικέτες και οι ομάδες ετικετών που καθορίζονται παραπάνω θα είναι διαθέσιμες μόνο σε αυτήν την κατηγορία και άλλες κατηγορίες που τις καθορίζουν επίσης. Δε θα είναι διαθέσιμες για χρήση σε άλλες κατηγορίες." tag_groups_placeholder: "(Προαιρετική) λίστα επιτρεπόμενων ομάδων ετικετών" allow_global_tags_label: "Επιτρέψτε επίσης άλλες ετικέτες" - tag_group_selector_placeholder: "(Προαιρετικό) Ομάδα ετικετών" - required_tag_group_description: "Απαιτήστε τα νέα θέματα να έχουν ετικέτες από μια ομάδα ετικετών:" - min_tags_from_required_group_label: "Αριθμός ετικετών:" - required_tag_group_label: "Ομάδα ετικετών:" + required_tag_group: + delete: "Σβήσιμο" topic_featured_link_allowed: "Επίτρεψε προτεινόμενους συνδέσμους σε αυτή την κατηγορία" delete: "Διαγραφή Κατηγορίας" create: "Νέα Κατηγορία" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 9991936f0b..0b974911de 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -136,7 +136,7 @@ en: to_placeholder: "to date" share: topic_html: 'Topic: %{topicTitle}' - post: "post #%{postNumber}" + post: "post #%{postNumber} by @%{username}" close: "close" twitter: "Share on Twitter" facebook: "Share on Facebook" @@ -317,6 +317,8 @@ en: bookmarks: created: "You've bookmarked this post. %{name}" + create_for: "Create bookmark for %{type}" + edit_for: "Edit bookmark for %{type}" not_bookmarked: "bookmark this post" remove_reminder_keep_bookmark: "Remove reminder and keep bookmark" created_with_reminder: "You've bookmarked this post with a reminder %{date}. %{name}" @@ -393,6 +395,7 @@ en: upload: "Upload" uploading: "Uploading..." + processing: "Processing..." uploading_filename: "Uploading: %{filename}..." processing_filename: "Processing: %{filename}..." clipboard: "clipboard" @@ -1960,27 +1963,27 @@ en: not_approved: "Your account hasn't been approved yet. You will be notified by email when you are ready to log in." google_oauth2: name: "Google" - title: "with Google" - sr_title: "Login with Google" + title: "Sign in with Google" + sr_title: "Sign in with Google" twitter: name: "Twitter" - title: "with Twitter" - sr_title: "Login with Twitter" + title: "Sign in with Twitter" + sr_title: "Sign in with Twitter" instagram: name: "Instagram" - title: "with Instagram" + title: "Login with Instagram" sr_title: "Login with Instagram" facebook: name: "Facebook" - title: "with Facebook" + title: "Login with Facebook" sr_title: "Login with Facebook" github: name: "GitHub" - title: "with GitHub" + title: "Login with GitHub" sr_title: "Login with GitHub" discord: name: "Discord" - title: "with Discord" + title: "Login with Discord" sr_title: "Login with Discord" second_factor_toggle: totp: "Use an authenticator app instead" @@ -1997,7 +2000,6 @@ en: success: "Your account has been created and you're now logged in." name_label: "Name" password_label: "Password" - optional_description: "(optional)" password_reset: continue: "Continue to %{site_name}" @@ -3063,7 +3065,14 @@ en: show_hidden: "View ignored content." deleted_by_author_simple: "(post deleted by author)" collapse: "collapse" + sr_collapse_replies: "Collapse embedded replies" + sr_date: "Post date" + sr_expand_replies: + one: "This post has %{count} reply. Click to expand" + other: "This post has %{count} replies. Click to expand" expand_collapse: "expand/collapse" + sr_below_embedded_posts_description: "post #%{post_number} replies" + sr_embedded_reply_description: "reply by @%{username} to post #%{post_number}" locked: "a staff member has locked this post from being edited" gap: one: "view %{count} hidden reply" @@ -3089,6 +3098,12 @@ en: one: "you and %{count} other person liked this post" other: "you and %{count} other people liked this post" + sr_post_like_count_button: + one: "%{count} person liked this post. Click to view" + other: "%{count} people liked this post. Click to view" + sr_post_read_count_button: + one: "%{count} person read this post. Click to view" + other: "%{count} people read this post. Click to view" filtered_replies_hint: one: "View this post and its reply" other: "View this post and its %{count} replies" @@ -3199,6 +3214,8 @@ en: read_capped: one: "and %{count} other read this" other: "and %{count} others read this" + sr_post_likers_list_description: "users who liked this post" + sr_post_readers_list_description: "users who read this post" by_you: off_topic: "You flagged this as off-topic" spam: "You flagged this as spam" @@ -3309,10 +3326,11 @@ en: tag_groups_placeholder: "(Optional) list of allowed tag groups" manage_tag_groups_link: "Manage tag groups" allow_global_tags_label: "Also allow other tags" - tag_group_selector_placeholder: "(Optional) Tag group" - required_tag_group_description: "Require new topics to have tags from a tag group:" - min_tags_from_required_group_label: "Num Tags:" - required_tag_group_label: "Tag group:" + required_tag_group: + description: "Require new topics to have tags from tag groups:" + delete: "Delete" + add: "Add required tag group" + placeholder: "select tag group..." topic_featured_link_allowed: "Allow featured links in this category" delete: "Delete Category" create: "New Category" @@ -3542,15 +3560,20 @@ en: other {}} original_post: "Original Post" views: "Views" + sr_views: "Sort by views" views_lowercase: one: "view" other: "views" replies: "Replies" + sr_replies: "Sort by replies" views_long: one: "this topic has been viewed %{count} time" - other: "this topic has been viewed %{number} times" + other: "this topic has been viewed %{count} times" activity: "Activity" + sr_activity: "Sort by activity" likes: "Likes" + sr_likes: "Sort by likes" + sr_op_likes: "Sort by original post likes" likes_lowercase: one: "like" other: "likes" @@ -3967,7 +3990,7 @@ en: user_activity: no_activity_title: "No activity yet" - no_activity_body: "Welcome to our community! You are brand new here and have not yet contributed to discussions. As a first step, visit Top or Categories and just start reading! Select %{heartIcon} on posts that you like or want to learn more about. If you have not already done so, help others get to know you by adding a picture and bio in your user preferences." + no_activity_body: "Welcome to our community! You are brand new here and have not yet contributed to discussions. As a first step, visit Top or Categories and just start reading! Select %{heartIcon} on posts that you like or want to learn more about. As you participate, your activity will be listed here." no_activity_others: "No activity." no_replies_title: "You have not replied to any topics yet" no_replies_others: "No replies." @@ -3977,11 +4000,15 @@ en: no_likes_body: "A great way to jump in and start contributing is to start reading conversations that have already taken place, and select the %{heartIcon} on posts that you like!" no_likes_others: "No liked posts." no_topics_title: "You have not started any topics yet" + no_topics_title_others: "%{username} has not started any topics yet" no_read_topics_title: "You haven’t read any topics yet" no_read_topics_body: "Once you start reading discussions, you’ll see a list here. To start reading, look for topics that interest you in Top or Categories or search by keyword %{searchIcon}" no_group_messages_title: "No group messages found" + topic_entrance: + sr_jump_top_button: "Jump to the first post" + sr_jump_bottom_button: "Jump to the last post" fullscreen_table: expand_btn: "Expand Table" @@ -4733,6 +4760,7 @@ en: time: "Time" user: "User" email_type: "Email Type" + details_title: "Show email details" to_address: "To Address" test_email_address: "email address to test" send_test: "Send Test Email" diff --git a/config/locales/client.en_GB.yml b/config/locales/client.en_GB.yml index 1e81f3201d..745df224a3 100644 --- a/config/locales/client.en_GB.yml +++ b/config/locales/client.en_GB.yml @@ -6,6 +6,9 @@ en_GB: js: + number: + format: + separator: "." dates: time: "HH:mm" time_with_zone: "HH:mm (z)" @@ -113,6 +116,15 @@ en_GB: color_definitions: text: "Colour Definitions" title: "Enter custom colour definitions (advanced users only)" + placeholder: |2- + + Use this stylesheet to add custom colours to the list of CSS custom properties. + + Example: + + %{example} + + Prefixing the property names is highly recommended to avoid conflicts with plugins and/or core. colors: select_base: title: "Select base colour palette" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 521a3135fe..941f793150 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -120,7 +120,6 @@ es: to_placeholder: "hasta hoy" share: topic_html: 'Tema: %{topicTitle}' - post: "publicación núm. %{postNumber}" close: "cerrar" twitter: "Compartir en Twitter" facebook: "Compartir en Facebook" @@ -287,6 +286,8 @@ es: unbookmark_with_reminder: "Haz clic para eliminar todos los marcadores y recordatorios de este tema." bookmarks: created: "Has guardado esta publicación en marcadores. %{name}" + create_for: "Crear marcador para %{type}" + edit_for: "Editar marcador para %{type}" not_bookmarked: "guarda esta publicación en marcadores" remove_reminder_keep_bookmark: "Quitar recordatorio y mantener marcador" created_with_reminder: "Has marcado esta publicación con un recordatorio %{date}. %{name}" @@ -1778,27 +1779,23 @@ es: not_approved: "Tu cuenta aún no ha sido aprobada. Se te notificará por correo electrónico cuando todo esté listo para que inicies sesión." google_oauth2: name: "Google" - title: "con Google" - sr_title: "Iniciar sesión con Google" twitter: name: "Twitter" - title: "con Twitter" - sr_title: "Iniciar sesión con Twitter" instagram: name: "Instagram" - title: "con Instagram" + title: "Iniciar sesión con Instagram" sr_title: "Iniciar sesión con Instagram" facebook: name: "Facebook" - title: "con Facebook" + title: "Iniciar sesión con Facebook" sr_title: "Iniciar sesión con Facebook" github: name: "GitHub" - title: "con GitHub" + title: "Iniciar sesión con GitHub" sr_title: "Iniciar sesión con GitHub" discord: name: "Discord" - title: "con Discord" + title: "Iniciar sesión con Discord" sr_title: "Iniciar sesión con Discord" second_factor_toggle: totp: "Usar una aplicación de autenticación en su lugar" @@ -1815,7 +1812,6 @@ es: success: "Tu cuenta ha sido creada y has iniciado sesión." name_label: "Nombre" password_label: "Contraseña" - optional_description: "(opcional)" password_reset: continue: "Continuar a %{site_name}" emoji_set: @@ -3021,10 +3017,8 @@ es: tag_groups_placeholder: "(Opcional) lista de grupos de etiquetas permitidos" manage_tag_groups_link: "Gestionar grupos de etiquetas" allow_global_tags_label: "Permitir también otras etiquetas" - tag_group_selector_placeholder: "(Opcional) Grupo de etiquetas" - required_tag_group_description: "Requerir que los nuevos temas tengan etiquetas de un grupo de etiquetas:" - min_tags_from_required_group_label: "Número de etiquetas:" - required_tag_group_label: "Grupo de etiquetas:" + required_tag_group: + delete: "Eliminar" topic_featured_link_allowed: "Permitir enlaces destacados en esta categoría" delete: "Eliminar categoría" create: "Nueva categoría" @@ -3246,15 +3240,20 @@ es: other {}} original_post: "Publicación original" views: "Vistas" + sr_views: "Ordenar por núm. de visitas" views_lowercase: one: "visita" other: "vistas" replies: "Respuestas" + sr_replies: "Ordenar por núm. de respuestas" views_long: one: "este tema se ha visto %{count} vez" other: "este tema se ha visto %{number} veces" activity: "Actividad" + sr_activity: "Ordenar por fecha de actividad" likes: "Me gusta" + sr_likes: "Ordenar por núm. de me gusta" + sr_op_likes: "Ordenar por núm. de me gusta de la primera publicación" likes_lowercase: one: "me gusta" other: "me gusta" @@ -3648,7 +3647,6 @@ es: unsupported_file_picked: "Has elegido un tipo de archivo no admitido. Tipos de archivo admitidos: %{types}." user_activity: no_activity_title: "Todavía no hay actividad" - no_activity_body: "¡Te damos la bienvenida a la comunidad! Acabas de llegar y todavía no has empezado a participar. Como primer paso, echa un vistazo a los temas Destacados o las Categorías y lee un poco. Dale a %{heartIcon} en los mensajes que te gusten o interesen. Si todavía no lo has hecho, puedes ayudar a que los demás te reconozcan si subes una foto y escribes algo sobre ti en tus preferencias." no_activity_others: "Sin actividad." no_replies_title: "Todavía no has respondido a ningún tema" no_replies_others: "No hay respuestas." @@ -3658,9 +3656,13 @@ es: no_likes_body: "Una buena manera de empezar a contribuir es ir leyendo los temas ya publicados y darle a %{heartIcon} en lo que te guste" no_likes_others: "No le ha dado me gusta a ninguna publicación." no_topics_title: "Todavía no has empezado ningún tema" + no_topics_title_others: "%{username} no ha creado ningún tema todavía" no_read_topics_title: "Todavía no has leído ningún tema" no_read_topics_body: "Cuando empieces a leer temas, los verás aquí en una lista. Puedes empezar a buscar temas que te interesen en Destacados, la lista de Categorías o buscando palabras %{searchIcon}" no_group_messages_title: "No hay ningún mensaje en el grupo" + topic_entrance: + sr_jump_top_button: "Ir a la primera publicación" + sr_jump_bottom_button: "Ir a la última publicación" fullscreen_table: expand_btn: "Expandir tabla" second_factor_auth: diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 40867ec5f1..93efd5354e 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -113,7 +113,6 @@ et: placeholder: kuupäev share: topic_html: 'Teema: %{topicTitle}' - post: "post nr%{postNumber}" close: "Sulge" twitter: "Jaga Twitteris" facebook: "Jaga Facebookis" @@ -1267,19 +1266,14 @@ et: not_approved: "Kontot pole veel kinnitatud. Teid teavitatakse e-post teel, kui sisselogimine on võimaldatud." google_oauth2: name: "Google" - title: "Google abil" twitter: name: "Twitter" - title: "Twitteri abil" instagram: name: "Instagram" - title: "Instagram'iga" facebook: name: "Facebook" - title: "Facebooki abil" github: name: "GitHub" - title: "GitHub abil" invites: accept_title: "Kutse" welcome_to: "Tere tulemast lehele %{site_name}!" @@ -1290,7 +1284,6 @@ et: success: "Teie konto on loodud ning olete nüüd sisse logitud." name_label: "Nimi" password_label: "Parool" - optional_description: "(valikuline)" password_reset: continue: "Edasi saidile %{site_name}" emoji_set: @@ -2101,6 +2094,8 @@ et: tags: "Sildid" tags_placeholder: "(Valikuline) loetelu lubatud silte" tag_groups_placeholder: "(Valikuline) loetelu lubatud siltide gruppe" + required_tag_group: + delete: "Kustuta" topic_featured_link_allowed: "Luba selles foorumis esiletõstetud linke " delete: "Kustuta foorum" create: "Uus foorum" diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 6f300813f5..3cd6c187a8 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -120,7 +120,7 @@ fa_IR: to_placeholder: "تا تاریخ" share: topic_html: 'موضوع: %{topicTitle}' - post: "نوشته #%{postNumber}" + post: "نوشته #%{postNumber} توسط @%{username}" close: "بستن" twitter: "در توییتر به اشتراک بگذارید" facebook: "در فیس‌بوک به اشتراک بگذارید" @@ -337,6 +337,7 @@ fa_IR: saved: "ذخیره شد!" upload: "بارگذاری" uploading: "در حال بارگذاری..." + processing: "در حال پردازش..." uploading_filename: "بارگذاری %{filename}..." processing_filename: "در حال پردازش: %{filename}..." clipboard: "کلیپ بورد" @@ -1654,27 +1655,27 @@ fa_IR: not_approved: "حساب کاربری شما هنوز تایید نشده. بعد از تایید با ایمیل به شما اطلاع داده خواهد شد." google_oauth2: name: "گوگل" - title: "با گوگل" + title: "ورود با گوگل" sr_title: "ورود با گوگل" twitter: name: "توئیتر" - title: "با توییتر" + title: "ورود با توییتر" sr_title: "ورود با توییتر" instagram: name: "اینستاگرام" - title: "با اینستاگرام" + title: "ورود با اینستاگرام" sr_title: "ورود با اینستاگرام" facebook: name: "فیس بوک" - title: "با فیسبوک" + title: "ورود با فیسبوک" sr_title: "ورود با فیسبوک" github: name: "گیت هاب" - title: "با گیت‌هاب" + title: "ورود با گیت‌هاب" sr_title: "ورود با گیت‌هاب" discord: name: "دیسکورد" - title: "با دیسکورد" + title: "ورود با دیسکورد" sr_title: "ورود با دیسکورد" second_factor_toggle: totp: "استفاده از روش ورود با اپ کدساز" @@ -1690,7 +1691,6 @@ fa_IR: success: "حساب‌کاربری شما با موفقیت ایجاد شد و می‌توانید وارد سایت شوید." name_label: "نام" password_label: "رمزعبور" - optional_description: "(اختیاری)" password_reset: continue: "برو به %{site_name}" emoji_set: @@ -2650,6 +2650,9 @@ fa_IR: tags_placeholder: "(اختیاری) فهرست برچسب‌های قابل استفاده" tag_groups_placeholder: "(اختیاری) لیست برچسب‌های قابل استفاده‌ی گروه" allow_global_tags_label: "اجازه‌ی برچسب‌های دیگر را هم بده" + required_tag_group: + delete: "حذف" + placeholder: "انتخاب گروه برچسب..." topic_featured_link_allowed: "اجازه‌ی لینک‌های برجسته در این دسته‌بندی" delete: "حذف دسته‌بندی" create: "دسته‌بندی جدید" @@ -3668,6 +3671,7 @@ fa_IR: time: "زمان" user: "کاربر" email_type: "نوع ایمیل" + details_title: "نمایش جزئیات ایمیل" to_address: "به آدرس" test_email_address: "آدرس ایمیل برای آزمایش" send_test: "ارسال ایمیل آزمایشی" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 68b8a17463..bf0904a2c8 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -118,7 +118,6 @@ fi: to_placeholder: "päivämäärään" share: topic_html: 'Ketju: %{topicTitle}' - post: "%{postNumber}. viesti" close: "sulje" twitter: "Jaa Twitterissä" facebook: "Jaa Facebookissa" @@ -1727,22 +1726,16 @@ fi: not_approved: "Tiliäsi ei ole vielä hyväksytty. Saat ilmoituksen sähköpostilla, kun voit kirjautua sisään." google_oauth2: name: "Google" - title: "Googlella" twitter: name: "Twitter" - title: "Twitterillä" instagram: name: "Instagram" - title: "Instagramilla" facebook: name: "Facebook" - title: "Facebookilla" github: name: "GitHub" - title: "GitHubilla" discord: name: "Discord" - title: "Discordilla" second_factor_toggle: totp: "Käytä todentajasovellusta tämän sijaan" backup_code: "Käytä varakoodia tämän sijaan" @@ -1757,7 +1750,6 @@ fi: success: "Tili luotiin, ja olet nyt kirjautunut sisään." name_label: "Nimi" password_label: "Salasana" - optional_description: "(valinnainen)" password_reset: continue: "Jatka sivustolle %{site_name}" emoji_set: @@ -2882,10 +2874,8 @@ fi: tag_groups_placeholder: "(Valinnainen) lista sallituista tunnisteryhmistä" manage_tag_groups_link: "Hallitse tunnisteryhmiä" allow_global_tags_label: "Salli myös muut tunnisteet" - tag_group_selector_placeholder: "(Valinnainen) tunnisteryhmä" - required_tag_group_description: "Edellytä, että uudella ketjulla on tunnisteita tunnisteryhmästä:" - min_tags_from_required_group_label: "Tunnisteiden määrä:" - required_tag_group_label: "Tunnisteryhmä:" + required_tag_group: + delete: "Poista" topic_featured_link_allowed: "Salli ketjulinkit tällä alueella" delete: "Poista alue" create: "Uusi alue" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 1eeeb10f5e..f1bf799ff9 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -118,7 +118,6 @@ fr: to_placeholder: "à la date" share: topic_html: 'Sujet : %{topicTitle}' - post: "message #%{postNumber}" close: "fermer" twitter: "Partager sur Twitter" facebook: "Partager sur Facebook" @@ -157,8 +156,10 @@ fr: disabled: "a supprimé de la une %{when}. Il ne sera plus affiché en haut de chaque page." forwarded: "a transmis le courriel ci-dessus" topic_admin_menu: "actions du sujet" + skip_to_main_content: "Passer au contenu principal" wizard_required: "Bienvenue sur votre nouveau Discourse ! Démarrons par l'assistant de configuration ✨" emails_are_disabled: "Le courriel sortant a été désactivé par un administrateur. Aucune notification par courriel ne sera envoyée." + emails_are_disabled_non_staff: "L'envoi de courriel est désactivé pour les utilisateurs ne faisant pas partie des responsables." software_update_prompt: message: "Nous avons fait une mise à jour du site, nous vous invitons donc à actualiser cette page pour éviter d'éventuels dysfonctionnements." dismiss: "Ignorer" @@ -168,6 +169,10 @@ fr: bootstrap_mode_disabled: "Le mode d'amorçage sera désactivé dans les prochaines 24 heures." themes: default_description: "Par défaut" + broken_theme_alert: "Votre site risque de ne pas fonctionner car un thème ou un composant de thème a déclenché une ou plusieurs erreurs." + error_caused_by: "Déclenchée par '%{name}'. Cliquez ici pour mettre à jour, reconfigurer or désactiver." + only_admins: "(ce message ne s'affiche qu'aux administrateurs du site)" + broken_decorator_alert: "Les messages risquent de ne pas apparaître correctement car un décorateur de contenu de message de votre site a déclenché une erreur." s3: regions: ap_northeast_1: "Asie-Pacifique (Tokyo)" @@ -181,6 +186,7 @@ fr: cn_northwest_1: "Chine (Ningxia)" eu_central_1: "UE (Francfort)" eu_north_1: "UE (Stockholm)" + eu_south_1: "EU (Milan)" eu_west_1: "UE (Irlande)" eu_west_2: "UE (Londres)" eu_west_3: "UE (Paris)" @@ -238,6 +244,8 @@ fr: character_count: one: "%{count} caractère" other: "%{count} caractères" + period_chooser: + aria_label: "Filtrer par période" related_messages: title: "Messages connexes" see_all: 'Voir tous les messages de @%{username}…' @@ -270,11 +278,15 @@ fr: help: bookmark: "Cliquez pour mettre un signet sur le premier message de ce sujet" edit_bookmark: "Cliquez pour modifier le signet sur ce sujet" + edit_bookmark_for_topic: "Cliquez pour modifier le signet associé à ce sujet" unbookmark: "Cliquez pour retirer tous les signets de ce sujet" unbookmark_with_reminder: "Cliquez pour retirer tous les signets et rappels de ce sujet." bookmarks: created: "Vous avez mis un signet à ce message. %{name}" + create_for: "Créer un signet pour %{type}" + edit_for: "Modifier le signet pour %{type}" not_bookmarked: "mettre un signet à ce message" + remove_reminder_keep_bookmark: "Supprimer le rappel et conserver le signet" created_with_reminder: "Vous avez mis un signet à ce message avec un rappel pour le %{date}. %{name}" remove: "Retirer le signet" delete: "Supprimer le signet" @@ -289,6 +301,8 @@ fr: label: "Après avoir été notifié" never: "Conserver le signet" when_reminder_sent: "Supprimer le signet" + on_owner_reply: "Supprimer le signet quand j'aurai répondu" + clear_reminder: "Conserver le signet et supprimer le rappel" search_placeholder: "Rechercher des signets par nom, titre de sujet ou contenu de message" search: "Recherche" reminders: @@ -298,12 +312,16 @@ fr: existing_reminder: "Vous avez configuré un rappel pour ce signet qui sera envoyé %{at_date_time}" copy_codeblock: copied: "copié !" + copy: "Copier ce code dans le presse-papier" + fullscreen: "afficher ce code en plein écran" drafts: label: "Brouillons" + label_with_count: "Brouillons (%{count})" resume: "Reprendre" remove: "Supprimer" remove_confirmation: "Voulez-vous vraiment supprimer ce brouillon ?" new_topic: "Nouveau brouillon de sujet" + new_private_message: "Nouveau brouillon de message direct" topic_reply: "Créer un brouillon de réponse" abandon: confirm: "Vous avez un brouillon en cours pour ce sujet. Que voulez-vous en faire ?" @@ -362,6 +380,7 @@ fr: placeholder: "saisissez ici l'intitulé du sujet, son URL ou son ID" review: order_by: "Trier par" + date_filter: "Publié entre" in_reply_to: "en réponse à" explain: why: "expliquer pourquoi cet élément s'est retrouvé dans la file d'attente" @@ -383,6 +402,7 @@ fr: type_bonus: name: "indiquer le bonus" title: "Certains types d'éléments à examiner peuvent se voir attribuer un bonus par les responsables pour en élever la priorité." + stale_help: "Cet élément a fait l'objet d'une décision prise par %{username}." claim_help: optional: "Vous pouvez vous réserver cet élément pour empêcher d'autres responsables de l'examiner." required: "Vous devez réserver un élément avant de l'examiner." @@ -674,6 +694,8 @@ fr: title: "Paramètres" allow_unknown_sender_topic_replies: "Autoriser les réponses par un expéditeur inconnu." allow_unknown_sender_topic_replies_hint: "Permet aux expéditeurs inconnus de répondre aux sujets du groupe. Si cette option n'est pas activée, les réponses provenant d'adresses courriel qui ne sont pas encore invitées dans le sujet créeront un nouveau sujet." + from_alias: "Alias de l'expéditeur" + from_alias_hint: "Alias à utiliser comme adresse d'expédition lors de l'envoi d'e-mails SMTP de groupe. Veuillez noter que cela peut ne pas être pris en charge par tous les prestataires de messagerie, nous vous invitons à consulter la documentation de votre prestataire." mailboxes: synchronized: "Boîte de réception à synchroniser" none_found: "Aucune boîte de réception n'a été trouvée pour ce compte de courriel." @@ -790,6 +812,7 @@ fr: owner: "Propriétaire" primary: "Principal" forbidden: "Vous n'avez pas l'autorisation de voir les membres." + no_filter_matches: "Aucun membre ne correspond à ces critères." topics: "Sujets" posts: "Messages" mentions: "Mentions" @@ -832,6 +855,7 @@ fr: icon: "Sélectionner une icône" image: "Insérer une image" default_notifications: + modal_title: "Notifications par défaut de l'utilisateur" modal_description: "Voulez-vous appliquer cette modification rétroactivement ? Cela affectera les préférences de %{count} utilisateurs existants." modal_yes: "Oui" modal_no: "Non, appliquer ce changement à partir de maintenant" @@ -967,7 +991,9 @@ fr: use_current_timezone: "Utiliser le fuseau horaire actuel" profile_hidden: "Le profil public de cet utilisateur est caché." expand_profile: "Développer" + sr_expand_profile: "Afficher les détails du profil" collapse_profile: "Réduire" + sr_collapse_profile: "Masquer les détails du profil" bookmarks: "Signets" bio: "À propos de moi" timezone: "Fuseau horaire" @@ -1101,7 +1127,9 @@ fr: warnings_received: "avertissements" rejected_posts: "messages rejetés" messages: + all: "toutes les boîtes de réception" inbox: "Boîte de réception" + personal: "Personnels" latest: "Récents" sent: "Envoyés" unread: "Non lues" @@ -1119,6 +1147,8 @@ fr: failed_to_move: "Impossible de déplacer les messages sélectionnés (votre connexion est peut-être en panne)" tags: "Étiquettes" warnings: "Avertissements officiels" + read_more_in_group: "Envie d'en lire encore ? Consultez d'autres messages dans %{groupLink}." + read_more: "Envie d'en lire encore ? Consultez d'autres messages dans vos messages personnels." preferences_nav: account: "Compte" security: "Sécurité" @@ -1234,6 +1264,7 @@ fr: upload_title: "Envoyer votre photo" image_is_not_a_square: "Attention : nous avons découpé votre image. La largeur et la hauteur n'étaient pas égales." logo_small: "Logo du site en petit format. Sera utilisé par défaut." + use_custom: "Ou importez un avatar personnalisé :" change_profile_background: title: "Arrière-plan du profil" instructions: "Les arrière-plans du profil seront centrés avec une largeur par défaut de 1110 pixels." @@ -1278,6 +1309,7 @@ fr: not_connected: "(non connecté)" confirm_modal_title: "Connectez le compte %{provider}" confirm_description: + disconnect: "Votre compte %{provider} existant '%{account_description}' sera déconnecté." account_specific: "Votre compte %{provider} « %{account_description} » sera utilisé pour l'authentification." generic: "Votre compte %{provider} sera utilisé pour l'authentification." name: @@ -1456,6 +1488,7 @@ fr: expired_at_time: "Arrivée à expiration le %{time}" show_advanced: "Afficher les options avancées" hide_advanced: "Masquer les options avancées" + email_or_domain_placeholder: "nom@exemple.com ou exemple.com" max_redemptions_allowed: "Nombre max. d'utilisations" add_to_groups: "Ajouter la personne invitée à ces groupes" expires_at: "Ce lien expirera dans" @@ -1544,6 +1577,7 @@ fr: instructions: "Apparaît après votre nom d'utilisateur" flair: none: "(aucun)" + instructions: "icône affichée à côté de votre image de profil" primary_group: title: "Groupe principal" none: "(aucun)" @@ -1626,6 +1660,8 @@ fr: description_time_MF: "Il y a {replyCount, plural, one {# réponse} other {# réponses}} avec un temps de lecture estimé à {readingTime, plural, one {# minute} other {# minutes}}." enable: "Résumer ce sujet" disable: "Afficher tous les messages" + short_label: "Résumer" + short_title: "Afficher un résumé de ce sujet : les articles les plus intéressants tels que déterminés par la communauté" deleted_filter: enabled_description: "Ce sujet contient des messages supprimés, qui ont été masqués." disabled_description: "Les messages supprimés du sujet sont visibles." @@ -1653,10 +1689,12 @@ fr: disclaimer: "En vous inscrivant, vous acceptez la politique de confidentialité et les conditions générales d'utilisation." title: "Créer votre compte" failed: "Un problème est survenu. Cette adresse courriel est peut-être déjà enregistrée. Essayez le lien d'oubli du mot de passe." + associate: "Vous avez déjà un compte ? Connectez-vous pour lier votre compte %{provider}." forgot_password: title: "Réinitialisation du mot de passe" action: "J'ai oublié mon mot de passe" invite: "Saisissez votre nom d'utilisateur ou votre adresse courriel et vous recevrez un nouveau mot de passe par courriel." + invite_no_username: "Saisissez votre adresse de courriel et nous vous enverrons un courriel de réinitialisation du mot de passe." reset: "Réinitialiser votre mot de passe" complete_username: "Si un compte correspond au nom d'utilisateur %{username}, vous devriez recevoir rapidement un courriel contenant les instructions permettant de réinitialiser votre mot de passe." complete_email: "Si un compte correspond à l'adresse courriel %{email}, vous devriez recevoir rapidement un courriel contenant les instructions permettant de réinitialiser votre mot de passe." @@ -1729,22 +1767,24 @@ fr: not_approved: "Votre compte n'a pas encore été approuvé. Vous recevrez une notification par courriel lorsque vous pourrez vous connecter." google_oauth2: name: "Google" - title: "via Google" twitter: name: "Twitter" - title: "via Twitter" instagram: name: "Instagram" - title: "via Instagram" + title: "Se connecter avec Instagram" + sr_title: "Se connecter avec Instagram" facebook: name: "Facebook" - title: "via Facebook" + title: "Se connecter avec Facebook" + sr_title: "Se connecter avec Facebook" github: name: "GitHub" - title: "via GitHub" + title: "Se connecter avec GitHub" + sr_title: "Se connecter avec GitHub" discord: name: "Discord" - title: "via Discord" + title: "Se connecter avec Discord" + sr_title: "Se connecter avec Discord" second_factor_toggle: totp: "Utilisez plutôt une application d'authentification" backup_code: "Utilisez plutôt un code de secours" @@ -1759,7 +1799,6 @@ fr: success: "Votre compte a été créé et vous êtes maintenant connecté(e)." name_label: "Nom" password_label: "Mot de passe" - optional_description: "(facultatif)" password_reset: continue: "Continuer vers %{site_name}" emoji_set: @@ -1776,6 +1815,7 @@ fr: categories_and_top_topics: "Catégories et meilleurs sujets" categories_boxes: "Boîtes avec sous-catégories" categories_boxes_with_topics: "Boîtes avec sujets à la une" + subcategories_with_featured_topics: "Sous-catégories avec sujets en vedette" shortcut_modifier_key: shift: "Maj" ctrl: "Ctrl" @@ -2108,15 +2148,24 @@ fr: search_button: "Recherche" categories: "Catégories" tags: "Étiquettes" + browser_tip_description: "à nouveau pour utiliser la fonction de recherche de votre navigateur" + recent: "Recherches récentes" + clear_recent: "Effacer les recherches récentes" type: + default: "Sujets/messages" users: "Utilisateurs" categories: "Catégories" + categories_and_tags: "Catégories/étiquettes" context: user: "Rechercher dans les messages de @%{username}" category: "Rechercher dans la catégorie #%{category}" tag: "Rechercher dans l'étiquette #%{tag}" topic: "Rechercher dans ce sujet" private_messages: "Rechercher dans les messages directs" + tips: + category_tag: "filtres par catégorie ou par étiquette" + author: "filtres par auteur de message" + in: "filtres par métadonnées (ex : in:title, in:personal, in:pinned)" advanced: posted_by: label: Auteur @@ -2201,9 +2250,11 @@ fr: other: "Marquer les nouveaux sujets comme lus (%{count})" toggle: "activer/désactiver la sélection multiple de sujets" actions: "Actions sur la sélection" + change_category: "Définir la catégorie..." close_topics: "Fermer les sujets" archive_topics: "Archiver les sujets" move_messages_to_inbox: "Déplacer dans la boîte de réception" + notification_level: "Notifications..." change_notification_level: "Modifier le niveau de notification" choose_new_category: "Choisissez la nouvelle catégorie pour ces sujets :" selected: @@ -2466,6 +2517,7 @@ fr: open: "Ouvrir le sujet" close: "Fermer le sujet" multi_select: "Sélectionner des messages…" + slow_mode: "Activer le mode ralenti..." timed_update: "Planifier une action…" pin: "Épingler le sujet…" unpin: "Désépingler le sujet…" @@ -2489,6 +2541,9 @@ fr: help: "partager ce sujet" instructions: "Partager ce sujet avec un lien :" copied: "Le lien vers ce sujet a été copié." + restricted_groups: + one: "Visible uniquement par les membres du groupe: %{groupNames}" + other: "Visible uniquement par les membres des groupes : %{groupNames}" invite_users: "Inviter" print: title: "Imprimer" @@ -2660,7 +2715,9 @@ fr: deleted_by_author_simple: "(sujet supprimé par son auteur)" post: quote_reply: "Citer" + quote_reply_shortcut: "Ou appuyez sur q" quote_edit: "Modifier" + quote_edit_shortcut: "Ou appuyez sur e" quote_share: "Partager" edit_reason: "Raison : " post_number: "message %{number}" @@ -2675,6 +2732,10 @@ fr: show_hidden: "Afficher le contenu ignoré." deleted_by_author_simple: "(message supprimé par son auteur)" collapse: "réduire" + sr_collapse_replies: "Masquer les réponses" + sr_expand_replies: + one: "Ce message a reçu %{count} réponse. Cliquez pour développer l'affichage" + other: "Ce message a reçu %{count} réponses. Cliquez pour développer l'affichage" expand_collapse: "développer/réduire" locked: "un responsable a verrouillé ce message pour empêcher sa modification" gap: @@ -2696,6 +2757,12 @@ fr: has_likes_title_you: one: "vous et %{count} autre personne avez aimé ce message" other: "vous et %{count} autres personnes avez aimé ce message" + sr_post_like_count_button: + one: "%{count} personne a aimé ce post. Cliquez pour voir" + other: "%{count} personnes ont aimé ce message. Cliquez pour voir" + sr_post_read_count_button: + one: "%{count} personne a lu ce message. Cliquez pour voir" + other: "%{count} personnes ont lu ce message. Cliquez pour voir" filtered_replies_hint: one: "Voir ce message et sa réponse" other: "Voir ce message et ses %{count} réponses" @@ -2709,6 +2776,8 @@ fr: edit: "Nous sommes désolés, une erreur est survenue lors de la modification de votre message. Veuillez réessayer." upload: "Nous sommes désolés, une erreur est survenue lors de l'envoi du fichier. Veuillez réessayer." file_too_large: "Nous sommes désolés, ce fichier est trop volumineux (la taille maximale autorisée est de %{max_size_kb} Ko). Nous vous suggérons de stocker votre fichier sur un service d'hébergement extérieur au forum et de coller ensuite un lien dans votre message." + file_size_zero: "Navré, un problème est survenu : le fichier reçu semble être vide. Nous vous invitons à réessayer l'envoi." + file_too_large_humanized: "Navré, ce fichier est trop lourd (la taille maximale est de %{max_size}). Pourquoi ne pas le copier sur un service de partage de fichier, puis coller le lien ?" too_many_uploads: "Nous sommes désolés, vous ne pouvez envoyer qu'un seul fichier à la fois." too_many_dragged_and_dropped_files: one: "Nous sommes désolés, vous ne pouvez envoyer que %{count} fichier à la fois." @@ -2753,6 +2822,7 @@ fr: just_the_post: "Non, uniquement ce message" admin: "actions d'administration sur le message" permanently_delete: "Supprimer définitivement" + permanently_delete_confirmation: "Êtes-vous certain de vouloir supprimer définitivement ce message ? Vous ne pourrez plus le récupérer." wiki: "Basculer en mode wiki" unwiki: "Retirer le mode wiki" convert_to_moderator: "Ajouter la couleur modérateur" @@ -2760,6 +2830,8 @@ fr: rebake: "Reconstruire le HTML" publish_page: "Publication de pages" unhide: "Afficher" + change_owner: "Changer de propriétaire..." + grant_badge: "Attribuer un badge..." lock_post: "Verrouiller le message" lock_post_description: "empêcher l'utilisateur de modifier ce message" unlock_post: "Déverrouiller le message" @@ -2773,6 +2845,8 @@ fr: delete_topic_confirm_modal_no: "Non, conserver ce sujet" delete_topic_error: "Une erreur s'est produite lors de la suppression de ce sujet" delete_topic: "supprimer le sujet" + add_post_notice: "Afficher une note pour les responsables..." + change_post_notice: "Modifier la note pour les responsables..." delete_post_notice: "Supprimer la note pour les responsables" remove_timer: "supprimer la planification" edit_timer: "modifier le minuteur" @@ -2790,6 +2864,8 @@ fr: read_capped: one: "et %{count} autre a lu ceci" other: "et %{count} autres ont lu ceci" + sr_post_likers_list_description: "utilisateurs ayant aimé ce message" + sr_post_readers_list_description: "utilisateurs ayant lu ce message" by_you: off_topic: "Vous avez signalé ceci comme étant hors-sujet" spam: "Vous avez signalé ceci comme étant du spam" @@ -2839,7 +2915,9 @@ fr: button: "HTML" bookmarks: create: "Ajouter un signet" + create_for_topic: "Créer un signet associé à ce sujet." edit: "Modifier le signet" + edit_for_topic: "Modifier le signet associé à ce sujet" created: "Créé" updated: "Mis à jour" name: "Nom" @@ -2853,6 +2931,9 @@ fr: edit_bookmark: name: "Modifier le signet" description: "Modifier le nom du signet ou changer la date et l'heure du rappel" + clear_bookmark_reminder: + name: "Supprimer le rappel" + description: "Supprimer la date et l'heure du rappel" pin_bookmark: name: "Épingler le signet" description: "Épingler le signet. Ceci permet de le faire figurer au sommet de votre liste de signets." @@ -2865,6 +2946,9 @@ fr: viewing_summary: "Affichage d'un résumé de ce sujet" post_number: "%{username}, message #%{post_number}" show_all: "Tout afficher" + share: + title: "Partager ce message #%{post_number}" + instructions: "Partager un lien vers ce message :" category: none: "(aucune catégorie)" all: "Toutes les catégories" @@ -2884,10 +2968,8 @@ fr: tag_groups_placeholder: "(Facultatif) liste des groupes d'étiquettes autorisées" manage_tag_groups_link: "Gérer les groupes d'étiquettes" allow_global_tags_label: "Permettre aussi d'autres étiquettes" - tag_group_selector_placeholder: "(Facultatif) Groupe d'étiquettes" - required_tag_group_description: "Exiger que les nouveaux sujets aient des étiquettes d'un groupe d’étiquettes :" - min_tags_from_required_group_label: "Nombre d'étiquettes :" - required_tag_group_label: "Groupe d'étiquettes :" + required_tag_group: + delete: "Supprimer" topic_featured_link_allowed: "Autoriser les liens à la une dans cette catégorie" delete: "Supprimer la catégorie" create: "Nouvelle catégorie" @@ -2947,6 +3029,7 @@ fr: default_list_filter: "Filtre de liste par défaut :" allow_badges_label: "Autoriser les badges à être accordés dans cette catégorie" edit_permissions: "Modifier les permissions" + reviewable_by_group: "En plus des responsables, le contenu de cette catégorie peut également être examiné par :" review_group_name: "nom du groupe" require_topic_approval: "Nécessiter l'approbation pour chaque nouveau sujet" require_reply_approval: "Nécessiter l'approbation pour chaque nouvelle réponse" @@ -2956,10 +3039,12 @@ fr: position_disabled: "Les catégories seront affichées dans l'ordre d'activité. Pour contrôler l'ordre des catégories dans la liste, " position_disabled_click: 'activez le paramètre « positions de catégorie fixes ».' minimum_required_tags: "Nombre minimal d'étiquettes requises dans un sujet :" + default_slow_mode: 'Activer le mode ralenti pour tous les nouveaux sujets dans cette catégorie' parent: "Catégorie parente" num_auto_bump_daily: "Nombre de sujets ouverts à remonter dans la liste automatiquement et quotidiennement :" navigate_to_first_post_after_read: "Naviguer vers le premier message après avoir lu un sujet" notifications: + title: "modifier le niveau de notification de cette catégorie" watching: title: "Surveiller" description: "Vous surveillerez automatiquement tous les sujets de cette catégorie. Vous recevrez une notification pour tous les nouveaux messages de chaque sujet, et le nombre de nouvelles réponses sera affiché." @@ -3106,15 +3191,20 @@ fr: other {}} original_post: "Message original" views: "Vues" + sr_views: "Trier par vues" views_lowercase: one: "vue" other: "vues" replies: "Réponses" + sr_replies: "Trier par réponses" views_long: one: "ce sujet a été vu %{count} fois" other: "ce sujet a été vu %{number} fois" activity: "Activité" + sr_activity: "Trier par activité" likes: "J'aime" + sr_likes: "Trier par J'aime" + sr_op_likes: "Trier par J'aime sur le premier message du sujet" likes_lowercase: one: "J'aime" other: "J'aime" @@ -3124,6 +3214,7 @@ fr: other: "utilisateurs" category_title: "Catégorie" history_capped_revisions: "Historique des 100 dernières modifications" + history: "Historique" changed_by: "par %{author}" raw_email: title: "Courriel entrant" @@ -3333,7 +3424,17 @@ fr: favorite_max_not_reached: "Faire de ce badge un de mes préférés" favorite_count: "%{count} badge(s) sur %{max} préféré(s)" download_calendar: + title: "Télécharger le calendrier" + save_ics: "Télécharger le fichier .ics" + save_google: "Inscrire dans le calendrier Google" + remember: "Ne plus me le proposer" + remember_explanation: "(vous pouvez changer ce réglage dans vos préférences utilisateur)" download: "Télécharger" + default_calendar: "Calendrier par défaut" + default_calendar_instruction: "Définir le calendrier à utiliser lorsqu'une date est enregistrée" + add_to_calendar: "Inscrire au calendrier" + google: "Calendrier Google" + ics: "ICS" tagging: all_tags: "Toutes les étiquettes" other_tags: "Autres étiquettes" @@ -3342,17 +3443,23 @@ fr: changed: "étiquettes modifiées :" tags: "Étiquettes" choose_for_topic: "étiquettes optionnelles" + choose_for_topic_required: + one: "sélectionnez au moins %{count} étiquette..." + other: "sélectionnez au moins %{count} étiquettes..." info: "Détails" default_info: "Cette étiquette n'est pas limitée à une catégorie et n'a aucun synonyme." + staff_info: "Pour y définir des restrictions d'utilisation, placez cette étiquette dans un groupe d'étiquettes." category_restricted: "Cette étiquette est limitée à des catégories auxquelles vous n'avez pas la permission d'accéder." synonyms: "Synonymes" synonyms_description: "Quand les étiquettes suivantes sont utilisées, elles seront remplacées par %{base_tag_name}." + save: "Enregistrer le nom et la description de l'étiquette" tag_groups_info: one: 'Cette étiquette appartient au groupe « %{tag_groups} ».' other: "Cette étiquette appartient aux groupes : %{tag_groups}." category_restrictions: one: "Elle ne peut être utilisée que dans cette catégorie :" other: "Elle ne peut être utilisée que dans ces catégories :" + edit_synonyms: "Modifier les synonymes" add_synonyms_label: "Ajoutez des synonymes :" add_synonyms: "Ajouter" add_synonyms_explanation: @@ -3369,6 +3476,7 @@ fr: delete_confirm_synonyms: one: "Son synonyme sera aussi supprimé." other: "Ses %{count} synonymes seront aussi supprimés." + edit_tag: "Modifier le nom et la description de l'étiquette" description: "Description" sort_by: "Trier par :" sort_by_count: "nombre" @@ -3480,10 +3588,19 @@ fr: regular: "habitué" leader: "meneur" detailed_name: "%{level} : %{name}" + pick_files_button: + unsupported_file_picked: "Vous avez sélectionné un fichier non pris en charge. Types de fichiers pris en charge : %{types}." user_activity: + no_activity_body: "Bienvenue dans notre communauté ! Vous êtes tout nouveau ici et n'avez pas encore contribué aux discussions. Dans un premier temps, visitez Top ou Catégories et commencez simplement à lire ! Sélectionnez %{heartIcon} sur les publications que vous aimez ou sur lesquelles vous souhaitez en savoir plus. Au fur et à mesure de votre participation, votre activité sera listée ici." no_activity_others: "Aucune activité." + no_replies_title: "Vous n'avez publié de réponse à aucun sujet pour le moment" no_replies_others: "Aucune réponse." + no_drafts_title: "Vous n'avez enregistré aucun brouillon" no_likes_others: "Aucun message aimé." + no_topics_title_others: "%{username} n'a pas encore commencé de sujets." + topic_entrance: + sr_jump_top_button: "Aller au premier message" + sr_jump_bottom_button: "Aller au dernier message" admin_js: type_to_filter: "commencez votre saisie pour filtrer…" admin: @@ -3703,6 +3820,9 @@ fr: wordpress: Nécessaire pour le fonctionnement de l'extension WordPress wp-discourse. posts: edit: Modifiez n'importe quel message ou un message spécifique. + categories: + list: Obtenir une liste de catégories. + show: Obtenir une seule catégorie par id. users: bookmarks: Lister les signets des utilisateurs. Les rappels de signet sont retournés lorsque le format ICS est utilisé. sync_sso: Synchroniser un utilisateur en utilisant DiscourseConnect. @@ -3714,6 +3834,14 @@ fr: delete: Supprimer les comptes utilisateurs. email: receive_emails: Combiner ces permissions au service de réception de courriel pour traiter les courriels entrants. + badges: + create: Créer un nouveau badge. + show: Obtenir des informations sur un badge. + update: Mettre à jour un badge. + delete: Supprimer un badge. + list_user_badges: Lister les badges de l'utilisateur. + assign_badge_to_user: Attribuer un badge à un utilisateur. + revoke_badge_from_user: Révoquer le badge d'un utilisateur. web_hooks: title: "Webhooks" none: "Il n'y a aucun Webhook actuellement." @@ -4177,6 +4305,7 @@ fr: time: "Heure" user: "Utilisateur" email_type: "Type de courriel" + details_title: "Afficher les détails de l'e-mail" to_address: "À l'adresse" test_email_address: "adresse courriel à tester" send_test: "Envoyer un courriel de test" @@ -4911,7 +5040,9 @@ fr: replace_owners: Retirer le badge des propriétaires précédents emoji: title: "Émoji" + help: "Ajoutez de nouveaux émojis que tout le monde pourra utiliser. Vous pouvez glisser-déposer plusieurs fichiers en une seule fois pour créer des émojis qui hériteront du nom de leurs fichiers d'origine respectifs. Vous pouvez aussi utiliser le bouton \"Ajouter un nouvel émoji\" pour ouvrir une boite de dialogue de sélection de fichiers." add: "Ajouter un nouvel émoji" + choose_files: "Choisissez des fichiers" uploading: "Envoi en cours…" name: "Nom" group: "Groupe" @@ -4921,6 +5052,7 @@ fr: embedding: get_started: "Si vous souhaitez intégrer Discourse dans un autre site, commencez par ajouter son hôte." confirm_delete: "Voulez-vous vraiment supprimer cet hôte ?" + sample: "Collez le code HTML qui suit dans votre site pour y créer et y intégrer des sujets Discourse. Remplacez REPLACE_ME par l'URL canonique de la page dans laquelle vous souhaitez l'intégrer." title: "Intégration externe" host: "Hôtes autorisés" class_name: "Nom de classe" @@ -4991,5 +5123,8 @@ fr: moderator: "Modérateur" regular: "Utilisateur normal" previews: + topic_title: "Titre d'un sujet de discussion" share_button: "Partager" reply_button: "Répondre" + topic_preview: "Aperçu du sujet" + homepage_preview: "Aperçu de la page d'accueil" diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index 041854b920..4f597f9641 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -118,7 +118,6 @@ gl: to_placeholder: "ata a data" share: topic_html: 'Tema: %{topicTitle}' - post: "publicación #%{postNumber}" close: "pechar" twitter: "Compartir no Twitter" facebook: "Compartir no Facebook" @@ -1668,22 +1667,16 @@ gl: not_approved: "A súa conta non foi aínda aprobada. Notificaráselle por correo electrónico cando poida iniciar a sesión." google_oauth2: name: "Google" - title: "con Google" twitter: name: "Twitter" - title: "co Twitter" instagram: name: "Instagram" - title: "con Instagram" facebook: name: "Facebook" - title: "co Facebook" github: name: "GitHub" - title: "con GitHub" discord: name: "Discord" - title: "con Discord" second_factor_toggle: totp: "Usar unha app autenticadora no seu lugar" backup_code: "Usar no seu lugar un código de copia de seguranza" @@ -1698,7 +1691,6 @@ gl: success: "A súa conta acaba de ser creada e ten a sesión iniciada." name_label: "Nome" password_label: "Contrasinal" - optional_description: "(opcional)" password_reset: continue: "Continuar en %{site_name}" emoji_set: @@ -2792,10 +2784,8 @@ gl: tag_groups_placeholder: "lista de grupos de etiquetas permitidos (opcional)" manage_tag_groups_link: "Xestionar grupos de etiquetas" allow_global_tags_label: "Permitir tamén outras etiquetas" - tag_group_selector_placeholder: "Grupo de etiquetas (opcional)" - required_tag_group_description: "Requirir que os novos temas teñan etiquetas dun grupo de etiquetas:" - min_tags_from_required_group_label: "Número de etiquetas:" - required_tag_group_label: "Grupo de etiquetas:" + required_tag_group: + delete: "Eliminar" topic_featured_link_allowed: "Permitir ligazóns de actualidade nesta categoría" delete: "Eliminar categoría" create: "Nova categoría" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index bc8df879c9..41cd535cab 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -164,7 +164,7 @@ he: to_placeholder: "עד תאריך" share: topic_html: 'נושא: %{topicTitle}' - post: "פוסט מס׳ %{postNumber}" + post: "פוסט מס׳ %{postNumber} מאת ‎@%{username}" close: "סגירה" twitter: "שיתוף בטוויטר" facebook: "שיתוף בפייסבוק" @@ -339,6 +339,8 @@ he: unbookmark_with_reminder: "יש ללחוץ כדי להסיר את כל הסימניות והתזכורות בנושא זה." bookmarks: created: "הוספת את הפוסט הזה לסימניות. %{name}" + create_for: "יצירת סימנייה אל %{type}" + edit_for: "עריכת סימנייה אל %{type}" not_bookmarked: "סמנו פוסט זה עם סימנייה" remove_reminder_keep_bookmark: "הסרת תזכורת ושמירת הסימנייה" created_with_reminder: "סימנת את הפוסט הזה עם תזכורת %{date}. %{name}" @@ -414,6 +416,7 @@ he: saved: "נשמר!" upload: "העלאה" uploading: "בהליכי העלאה..." + processing: "מתבצע עיבוד…" uploading_filename: "מעלה: %{filename}..." processing_filename: "מתבצע עיבוד: %{filename}…" clipboard: "לוח" @@ -1914,27 +1917,27 @@ he: not_approved: "החשבון שלך עדיין לא אושר. תישלח אליך הודעה בדוא״ל כשיתאפשר לך להיכנס למערכת." google_oauth2: name: "Google" - title: "עם Google" + title: "כניסה עם Google" sr_title: "כניסה עם Google" twitter: name: "Twitter" - title: "עם Twitter" + title: "כניסה עם טוויטר" sr_title: "כניסה עם טוויטר" instagram: name: "Instagram" - title: "עם אינסטגרם" + title: "כניסה עם אינסטגרם" sr_title: "כניסה עם אינסטגרם" facebook: name: "Facebook" - title: "עם Facebook" + title: "כניסה עם פייסבוק" sr_title: "כניסה עם פייסבוק" github: name: "GitHub" - title: "עם GitHub" + title: "כניסה עם GitHub" sr_title: "כניסה עם GitHub" discord: name: "Discord" - title: "עם Discord" + title: "כניסה עם Discord" sr_title: "כניסה עם Discord" second_factor_toggle: totp: "להשתמש ביישומון אימות במקום" @@ -1951,7 +1954,6 @@ he: success: "החשבון נוצר ונכנסת אליו." name_label: "שם" password_label: "ססמה" - optional_description: "(רשות)" password_reset: continue: "המשיכו ל-%{site_name}" emoji_set: @@ -3030,7 +3032,15 @@ he: show_hidden: "צפייה בתוכן שמיועד להתעלמות." deleted_by_author_simple: "(הפוסט נמחק על ידי הכותב)" collapse: "צמצום" + sr_collapse_replies: "צמצום תשובות מוטמעות" + sr_expand_replies: + one: "לפוסט הזה יש תגובה %{count}. יורחב בלחיצה" + two: "לפוסט הזה יש %{count} תגובות. יורחב בלחיצה" + many: "לפוסט הזה יש %{count} תגובות. יורחב בלחיצה" + other: "לפוסט הזה יש %{count} תגובות. יורחב בלחיצה" expand_collapse: "הרחב/צמצם" + sr_below_embedded_posts_description: "תגובות לפוסט מס׳ %{post_number}" + sr_embedded_reply_description: "תגובה מאת ‎@%{username} לפוסט %{post_number}" locked: "חבר סגל נעל את האפשרות לערוך את הפוסט הזה" gap: one: "הצג תגובה אחת שהוסתרה" @@ -3039,7 +3049,7 @@ he: other: "הצגת %{count} תגובות שהוסתרו" notice: new_user: "זהו הפרסום הראשון מאת %{user} - הבה נקבל את פניו/ה לקהילה שלנו!" - returning_user: "עבר זמן מה מאז שראינו במחוזותינו את %{user} - הפרסום האחרון שלו/ה היה ב־%{time}." + returning_user: "עבר זמן מה מאז שראינו במחוזותינו את %{user} - הפרסום האחרון שלו/ה היה %{time}." unread: "הפוסט טרם נקרא" has_replies: one: "תגובה אחת" @@ -3059,6 +3069,16 @@ he: two: "אתם ו %{count} אנשים אחרים אהבתם את הפוסט הזה" many: "אתם ו %{count} אנשים אחרים אהבתם את הפוסט הזה" other: "אתם ו %{count} אנשים אחרים אהבתם את הפוסט הזה" + sr_post_like_count_button: + one: "מישהו או מישהי (%{count}) אהבו את הפוסט הזה. לחיצה תציג מי" + two: "%{count} אנשים אהבו את הפוסט הזה. לחיצה תציג מי" + many: "%{count} אנשים אהבו את הפוסט הזה. לחיצה תציג מי" + other: "%{count} אנשים אהבו את הפוסט הזה. לחיצה תציג מי" + sr_post_read_count_button: + one: "מישהו או מישהי (%{count}) קראו את הפוסט הזה. לחיצה תציג מי" + two: "%{count} אנשים קראו את הפוסט הזה. לחיצה תציג מי" + many: "%{count} אנשים קראו את הפוסט הזה. לחיצה תציג מי" + other: "%{count} אנשים קראו את הפוסט הזה. לחיצה תציג מי" filtered_replies_hint: one: "הצגת הפוסט הזה ואת התגובה עליו" two: "הצגת הפוסט הזה ואת %{count} התגובות עליו" @@ -3180,6 +3200,8 @@ he: two: "ו־%{count} נוספים קראו את זה" many: "ו־%{count} נוספים קראו את זה" other: "ו־%{count} נוספים קראו את זה" + sr_post_likers_list_description: "משתמשים שאהבו את הפוסט הזה" + sr_post_readers_list_description: "משתמשים שקראו את הפוסט הזה" by_you: off_topic: "דיגלתם פרסום זה כאוף-טופיק" spam: "דיגלתם את זה כספאם" @@ -3286,10 +3308,11 @@ he: tag_groups_placeholder: "(רשות) רשימת קבוצות תגיות" manage_tag_groups_link: "ניהול קבוצות תגיות" allow_global_tags_label: "לאפשר גם תגיות אחרות" - tag_group_selector_placeholder: "(רשות) קבוצת תגיות" - required_tag_group_description: "לדרוש שלנושאים חדשים יהיו תגיות מקבוצת תגיות:" - min_tags_from_required_group_label: "מס׳ תגיות:" - required_tag_group_label: "קבוצת תגיות:" + required_tag_group: + description: "לדרוש שלנושאים חדשים יהיו תגיות מקבוצות תגיות:" + delete: "הסרה" + add: "הוספת קבוצת תגיות נדרשת" + placeholder: "בחירת קבוצת תגיות…" topic_featured_link_allowed: "אפשרו קישורים מומלצים בקטגוריה זו" delete: "מחיקת קטגוריה" create: "קטגוריה חדשה" @@ -3521,19 +3544,24 @@ he: other {}} original_post: "פוסט מקורי" views: "צפיות" + sr_views: "מיון לפי תצוגות" views_lowercase: one: "צפיה" two: "צפיות" many: "צפיות" other: "צפיות" replies: "תגובות" + sr_replies: "מיון לפי תגובות" views_long: one: "נושא זה נצפה פעם %{count}" two: "נושא זה נצפה %{number} פעמים" many: "נושא זה נצפה %{number} פעמים" other: "נושא זה נצפה %{number} פעמים" activity: "פעילות" + sr_activity: "מיון לפי פעילות" likes: "לייקים" + sr_likes: "מיון לפי לייקים" + sr_op_likes: "מיון לפי לייקים בפוסט המקורי" likes_lowercase: one: "לייק" two: "לייקים" @@ -3965,7 +3993,7 @@ he: unsupported_file_picked: "בחרת קובץ שאינו נתמך. סוגי קבצים נתמכים - %{types}." user_activity: no_activity_title: "אין פעילות עדיין" - no_activity_body: "ברוך בואך לקהילה שלנו! הצטרפת ממש לא מזמן וטרם תרמת לדיונים. כצעד ראשון, כדאי לבקר במובילים או בקטגוריות ופשוט להתחיל לקרוא. ניתן לבחור ב־%{heartIcon} בפוסטים שנשאו חן בעיניך או שמעניין אותך ללמוד עליהם יותר. אם טרם עשית זאת, ניתן לסייע לאחרים להכיר אותך על ידי הוספת תמונה ותיאור עצמי בהעדפות המשתמש שלך." + no_activity_body: "ברוך בואך לקהילה שלנו! הצטרפת ממש לא מזמן וטרם תרמת לדיונים. כצעד ראשון, כדאי לבקר במובילים או בקטגוריות ופשוט להתחיל לקרוא. ניתן לבחור ב־%{heartIcon} בפוסטים שנשאו חן בעיניך או שמעניין אותך ללמוד עליהם יותר. הפעילות שלך תופיע כאן." no_activity_others: "אין פעילות." no_replies_title: "עדיין לא הגבת לאף נושא" no_replies_others: "אין תגובות." @@ -3975,9 +4003,13 @@ he: no_likes_body: "דרך נפלאה לקפוץ ישירות פנימה ולהתחיל לתרום היא להתחיל לקרוא דיונים שכבר נערכו ולבחור בסמל %{heartIcon} על פוסטים שאהבת!" no_likes_others: "אין פוסטים שנעשה להם לייק." no_topics_title: "עדיין לא פתחת אף נושא" + no_topics_title_others: "לא נפתח עוד אף נושא על ידי %{username}" no_read_topics_title: "טרם קראת נושאים" no_read_topics_body: "עם תחילת קריאת הדיונים, תופיע כאן רשימה. כדי להתחיל לקרוא, כדאי לחפש נושאים שמעניינים אותך תחת עליונים או קטגוריות או לחפש אחר מילת מפתח %{searchIcon}" no_group_messages_title: "לא נמצאו הודעות קבוצתיות" + topic_entrance: + sr_jump_top_button: "קפיצה לפוסט הראשון" + sr_jump_bottom_button: "קפיצה לפוסט האחרון" fullscreen_table: expand_btn: "הרחבת טבלה" second_factor_auth: @@ -4719,6 +4751,7 @@ he: time: "זמן" user: "משתמש" email_type: "סוג דוא״ל" + details_title: "הצגת פרטי דוא״ל" to_address: "לכתובת" test_email_address: "כתובת דוא״ל לבדיקה" send_test: "שליחת הודעת דוא״ל בדיקה" diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index 549fee4181..9194b06519 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -120,7 +120,6 @@ hu: to_placeholder: "eddig:" share: topic_html: 'Téma: %{topicTitle}' - post: "bejegyzés #%{postNumber}" close: "bezárás" twitter: "Megosztás Twitteren" facebook: "Megosztás Facebookon" @@ -174,6 +173,7 @@ hu: themes: default_description: "Alapértelmezett" broken_theme_alert: "Lehet, hogy webhelye nem működik, mert a téma/összetevő hibába ütközött." + error_caused_by: "A(z) „%{name}” okozta. Kattintson ide a frissítéshez, újrakonfiguráláshoz vagy letiltáshoz." only_admins: "(ez az üzenet csak a webhely rendszergazdáinak jelenik meg)" broken_decorator_alert: "Előfordulhat, hogy a bejegyzések nem jelennek meg megfelelően, mert a webhely egyik bejegyzéstartalmának dekorátora hibát dobott." s3: @@ -299,7 +299,11 @@ hu: list_permission_denied: "Nincs engedélye a felhasználó könyvjelzőinek megtekintéséhez." no_user_bookmarks: "Nincsenek könyvjelzőzött bejegyzései; a könyvjelzők segítségével gyorsan hivatkozhat bizonyos bejegyzésekre." auto_delete_preference: + label: "Miután értesítést kapott" + never: "Könyvjelző megtartása" when_reminder_sent: "Könyvjelző törlése" + on_owner_reply: "Könyvjelző törlése, amint válaszolok" + clear_reminder: "Könyvjelző megtartása és az emlékeztető törlése" search_placeholder: "Könyvjelzők keresése név, téma címe vagy tartalom alapján" search: "Keresés" reminders: @@ -309,6 +313,8 @@ hu: existing_reminder: "Beállított egy emlékeztetőt erre a könyvjelzőre, amely %{at_date_time}-kor lesz kiküldve." copy_codeblock: copied: "másolva!" + copy: "kód másolása a vágólapra" + fullscreen: "kód megjelenítése teljes képernyőn" drafts: label: "Vázlatok" label_with_count: "Vázlatok (%{count})" @@ -690,6 +696,7 @@ hu: allow_unknown_sender_topic_replies: "Engedélyezze az ismeretlen feladóktól érkező témaválaszokat." allow_unknown_sender_topic_replies_hint: "Lehetővé teszi az ismeretlen feladók számára, hogy válaszoljanak a csoport témáira. Ha ez nincs engedélyezve, akkor az e-mail szálban még nem szereplő címekről érkező válaszok új témát hoznak létre." from_alias: "Álnévből" + from_alias_hint: "A csoportos SMTP e-mailek küldésekor a \"from\" címként használandó álnév. Megjegyzés: ezt nem minden e-mail szolgáltató támogatja, tekintse meg a szolgáltató dokumentációját." mailboxes: synchronized: "Szinkronizált postafiók" none_found: "Ebben az e-mail fiókban nem találhatók postaládák." @@ -1090,6 +1097,7 @@ hu: muted_categories_instructions: "Semmilyen értesítést nem fog kapni a kategória témaköreiről, és a kategóriák vagy a legújabbak között sem fog megjelenni." muted_categories_instructions_dont_hide: "Nem fog értesítést kapni az új témákról ezekben a kategóriákban." regular_categories: "Átlagos" + regular_categories_instructions: "Ezek a kategóriák a „Legújabb” és a „Legnépszerűbb” témák között lesznek láthatóak." no_category_access: "Moderátorként korlátozott kategória-hozzáférése van, a mentés tiltott." delete_account: "Saját fiók törlése" delete_account_confirm: "Biztos, hogy végleg törli a fiókját? Ez a művelet nem vonható vissza." @@ -1113,6 +1121,7 @@ hu: api_approved: "Jóváhagyva:" api_last_used_at: "Utoljára használva:" theme: "Téma" + save_to_change_theme: 'A téma a „%{save_text}” gombra kattintás után frissül' home: "Alapértelmezett főoldal" staged: "Lépcsőzetes" staff_counters: @@ -1759,27 +1768,23 @@ hu: not_approved: "A fiók még nem lett elfogadva. E-mailben értesítjük, ha bejelentkezhet." google_oauth2: name: "Google" - title: "Google-lel" - sr_title: "Bejelentkezés a Google-lel" twitter: name: "Twitter" - title: "Twitterrel" - sr_title: "Bejelentkezés a Twitterrel" instagram: name: "Instagram" - title: "Instagrammal" + title: "Bejelentkezés az Instagrammal" sr_title: "Bejelentkezés az Instagrammal" facebook: name: "Facebook" - title: "Facebookkal" + title: "Bejelentkezés a Facebookkal" sr_title: "Bejelentkezés a Facebookkal" github: name: "GitHub" - title: "GitHubbal" + title: "Bejelentkezés a GitHubbal" sr_title: "Bejelentkezés a GitHubbal" discord: name: "Discord" - title: "Discorddal" + title: "Bejelentkezés a Discorddal" sr_title: "Bejelentkezés a Discorddal" second_factor_toggle: totp: "Helyette hitelesítő alkalmazás használata" @@ -1795,7 +1800,6 @@ hu: success: "A fiók elkészült, és most már be van jelentkezve." name_label: "Név" password_label: "Jelszó" - optional_description: "(nem kötelező)" password_reset: continue: "Tovább a(z) %{site_name} oldalra" emoji_set: @@ -2528,6 +2532,7 @@ hu: open: "Téma megnyitása" close: "Téma lezárása" multi_select: "Bejegyzések Kiválasztása" + slow_mode: "Lassú mód beállítása…" timed_update: "Téma időzítőjének beállítása" pin: "Témakör kiemelése..." unpin: "Témakör kiemelésének megszüntetése..." @@ -2536,11 +2541,13 @@ hu: invisible: "Listázás törlése" visible: "Listázás" reset_read: "Olvasási adatok visszaállítása" + make_public: "Téma nyilvánossá tétele" make_private: "Személyes üzenet írása" feature: pin: "Témakör kiemelése" unpin: "Témakör kiemelésének megszüntetése" pin_globally: "Témakör globális kiemelése" + make_banner: "Kiemelt téma létrehozása" remove_banner: "Kiemelt Téma eltávolítása" reply: title: "Válasz" @@ -2549,6 +2556,7 @@ hu: title: "Téma megosztása" extended_title: "Link megosztása" help: "a témakör hivatkozásának megosztása" + instructions: "Ossza meg a témához tartozó hivatkozást:" copied: "Témahivatkozás másolva." restricted_groups: one: "Csak a csoport tagjai számára látható: %{groupNames}" @@ -2828,6 +2836,8 @@ hu: settings: "Beállítások" topic_template: "Téma sablonja" tags: "Címkék" + required_tag_group: + delete: "Törlés" topic_featured_link_allowed: "Kiemelt hivatkozások engedélyezése ebben a kategóriában" delete: "Kategória törlése" create: "Új kategória" diff --git a/config/locales/client.hy.yml b/config/locales/client.hy.yml index c1d9cdf76f..a884a9b564 100644 --- a/config/locales/client.hy.yml +++ b/config/locales/client.hy.yml @@ -118,7 +118,6 @@ hy: to_placeholder: "դեպի ամսաթիվ" share: topic_html: 'Թեմա՝ %{topicTitle}' - post: "գրառում #%{postNumber}" close: "փակել" action_codes: public_topic: "այս թեման դարձրել է հրապարակային %{when}" @@ -1405,22 +1404,16 @@ hy: not_approved: "Ձեր հաշիվը դեռևս չի հաստատվել: Դուք կստանաք ծանուցում էլ. նամակի միջոցով, երբ այն հաստատվի:" google_oauth2: name: "Google" - title: "Google-ով" twitter: name: "Twitter" - title: "Twitter-ով" instagram: name: "Instagram" - title: "Instagram-ով" facebook: name: "Facebook" - title: "Facebook-ով" github: name: "GitHub" - title: "GitHub-ով" discord: name: "Discord" - title: "Discord-ով" second_factor_toggle: totp: "Փոխարենը օգտագործել նույնականացման հավելվածը" backup_code: "Փոխարենը օգտագործել պահուստային կոդը" @@ -1435,7 +1428,6 @@ hy: success: "Ձեր հաշիվը ստեղծված է, և այժմ Դուք մուտք եք գործել:" name_label: "Անուն" password_label: "Գաղտնաբառ" - optional_description: "(ընտրովի)" password_reset: continue: "Շարունակել դեպի %{site_name}" emoji_set: @@ -2360,10 +2352,8 @@ hy: tags_tab_description: "Այստեղ սահմանված թեգերը և թեգերի խմբերը հասանելի կլինեն միայն այս կատեգորիայում և այլ կատեգորիաներում, որոնք նույնպես սահմանում են դրանք: Դրանք հասանելի չեն լինի օգտագործման համար այլ կատեգորիաներում:" tag_groups_placeholder: "(Ընտրովի) թույլատրված թեգերի խմբերի ցանկը" allow_global_tags_label: "Նաև թույլ տալ այլ տեգեր " - tag_group_selector_placeholder: "(Ընտրովի) Թեգերի խումբ" - required_tag_group_description: "Պահանջել, որ նոր թեմաների տեգերը լինեն տեգերի խմբից:" - min_tags_from_required_group_label: "Թեգի Համարը." - required_tag_group_label: "Թեգերի խումբ." + required_tag_group: + delete: "Ջնջել" topic_featured_link_allowed: "Թույլատրել հանրահայտ հղումները այս կատեգորիայում" delete: "Ջնջել Կատեգորիան" create: "Նոր Կատեգորիա" diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index 443e5247ba..cc1ff324d8 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -98,7 +98,6 @@ id: to_placeholder: "sampai saat ini" share: topic_html: 'Topik: %{topicTitle}' - post: "post #%{postNumber}" close: "tutup" twitter: "Bagikan di Twitter" facebook: "Bagikan di Facebook" @@ -1262,19 +1261,14 @@ id: submit_new_email: "Perbarui Alamat Email" google_oauth2: name: "Google" - title: "dengan Google" twitter: name: "Twitter" - title: "dengan Twitter" instagram: name: "Instagram" - title: "dengan Instagram" facebook: name: "Facebook" - title: "dengan Facebook" github: name: "GitHub" - title: "dengan GitHub" discord: name: "Discord" invites: @@ -1284,7 +1278,6 @@ id: accept_invite: "Terima Undangan" name_label: "Nama" password_label: "Kata Sandi" - optional_description: "(opsional)" password_reset: continue: "Lanjut ke %{site_name}" emoji_set: @@ -1584,6 +1577,8 @@ id: edit: "Ubah" settings: "Pengaturan" tags: "Label" + required_tag_group: + delete: "Hapus" name: "Nama Kategori" permissions: reply: "Balas" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index e69814d151..0125286569 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -120,7 +120,7 @@ it: to_placeholder: "ad oggi" share: topic_html: 'Argomento: %{topicTitle}' - post: "messaggio n°%{postNumber}" + post: "messaggio n°%{postNumber} di @%{username}" close: "chiudi" twitter: "Condividi su Twitter" facebook: "Condividi su Facebook" @@ -287,6 +287,8 @@ it: unbookmark_with_reminder: "Clicca per rimuovere tutti i segnalibri e i promemoria a questo argomento." bookmarks: created: "Hai aggiunto questo messaggio ai segnalibri %{name}" + create_for: "Crea un segnalibro per %{type}" + edit_for: "Modifica il segnalibro per %{type}" not_bookmarked: "aggiungi messaggio ai segnalibri" remove_reminder_keep_bookmark: "Rimuovi il promemoria e mantieni il segnalibro" created_with_reminder: "Hai aggiunto ai segnalibri questo messaggio con un promemoria %{date}. %{name}" @@ -348,6 +350,7 @@ it: saved: "Salvato!" upload: "Carica" uploading: "In caricamento..." + processing: "Elaborazione..." uploading_filename: "In caricamento: %{filename}..." processing_filename: "In elaborazione: %{filename}..." clipboard: "appunti" @@ -1774,27 +1777,23 @@ it: not_approved: "Il tuo account non è ancora stato approvato. Verrai avvertito via email quando potrai collegarti." google_oauth2: name: "Google" - title: "con Google" - sr_title: "Accedi con Google" twitter: name: "Twitter" - title: "con Twitter" - sr_title: "Accedi con Twitter" instagram: name: "Instagram" - title: "con Instagram" + title: "Accedi con Instagram" sr_title: "Accedi con Instagram" facebook: name: "Facebook" - title: "con Facebook" + title: "Accedi con Facebook" sr_title: "Accedi con Facebook" github: name: "GitHub" - title: "con GitHub" + title: "Accedi con GitHub" sr_title: "Accedi con GitHub" discord: name: "Discord" - title: "con Discord" + title: "Accedi con Discord" sr_title: "Accedi con Discord" second_factor_toggle: totp: "Utilizzare un'app di autenticazione" @@ -1811,7 +1810,6 @@ it: success: "Il tuo account è stato creato e ora hai effettuato l'accesso." name_label: "Nome" password_label: "Password" - optional_description: "(opzionale)" password_reset: continue: "Procedi su %{site_name}" emoji_set: @@ -2790,7 +2788,10 @@ it: show_hidden: "Visualizza contenuto ignorato." deleted_by_author_simple: "(messaggio eliminato dall'autore)" collapse: "comprimi" + sr_collapse_replies: "Comprimi risposte incorporate" expand_collapse: "espandi/comprimi" + sr_below_embedded_posts_description: "risposte al messaggio n°%{post_number}" + sr_embedded_reply_description: "risposta da @%{username} al messaggio n°%{post_number}" locked: "un membro dello Staff ha bloccato le modifiche a questo messaggio" gap: one: "visualizza %{count} risposta nascosta" @@ -2912,6 +2913,8 @@ it: read_capped: one: "e %{count} ha letto" other: "e altri %{count} hanno letto" + sr_post_likers_list_description: "utenti a cui è piaciuto questo messaggio" + sr_post_readers_list_description: "utenti che hanno letto questo messaggio" by_you: off_topic: "L'hai segnalato come fuori tema" spam: "L'hai segnalato come spam" @@ -3011,10 +3014,11 @@ it: tag_groups_placeholder: "Elenco (opzionale) dei gruppi di etichette permessi" manage_tag_groups_link: "Gestisci gruppi di etichette" allow_global_tags_label: "Consenti anche ulteriori Etichette" - tag_group_selector_placeholder: "(Facoltativo) Gruppo di Etichette" - required_tag_group_description: "Rendi obbligatorio per i nuovi argomenti avere un'Etichetta da un Gruppo di Etichette:" - min_tags_from_required_group_label: "Numero Etichette:" - required_tag_group_label: "Gruppo di Etichette:" + required_tag_group: + description: "Richiedi che i nuovi argomenti abbiano etichette dai seguenti gruppi:" + delete: "Cancella" + add: "Aggiungi il gruppo di etichette richiesto" + placeholder: "seleziona il gruppo di etichette" topic_featured_link_allowed: "Consenti collegamenti in primo piano in questa categoria" delete: "Elimina Categoria" create: "Crea Categoria" @@ -3236,15 +3240,20 @@ it: other {}} original_post: "Messaggio Originale" views: "Visualizzazioni" + sr_views: "Ordina per visualizzazioni" views_lowercase: one: "visita" other: "visite" replies: "Risposte" + sr_replies: "Ordina per risposte" views_long: one: "questo argomento è stato visto una volta" other: "questo argomento è stato visto %{number} volte" activity: "Attività" + sr_activity: "Ordina per attività" likes: "Mi piace" + sr_likes: "Ordina per n° di \"mi piace\"" + sr_op_likes: "Ordina per n° di \"mi piace\" al post originale" likes_lowercase: one: "mi piace" other: "mi piace" @@ -3634,7 +3643,6 @@ it: unsupported_file_picked: "Hai scelto un file non supportato. Tipi di file supportati: %{types}." user_activity: no_activity_title: "Ancora nessuna attività" - no_activity_body: "Ti diamo il benvenuto nella nostra comunità! Sei nuovo/a qui e non hai ancora contribuito alle discussioni. Per cominciare, visita Top o Categorie e inizia a leggere! Seleziona %{heartIcon} sui post che ti piacciono o che vuoi approfondire. Se non lo hai già fatto, fatti conoscere aggiungendo una foto e una biografia nelle tue preferenze utente." no_activity_others: "Nessuna attività." no_replies_title: "Non hai ancora risposto a nessun argomento" no_replies_others: "Nessuna risposta." @@ -3647,6 +3655,9 @@ it: no_read_topics_title: "Non hai ancora letto nessun argomento" no_read_topics_body: "Una volta che avrai iniziato a leggere discussioni, ne vedrai un elenco qui. Per cominciare a leggere, cerca argomenti di tuo interesse alle pagine Popolari o Categorie; oppure effettua una ricerca per parola chiave %{searchIcon}" no_group_messages_title: "Nessun messaggio di gruppo trovato" + topic_entrance: + sr_jump_top_button: "Vai al primo post" + sr_jump_bottom_button: "Vai all'ultimo post" fullscreen_table: expand_btn: "Espandi Tabella" second_factor_auth: @@ -4374,6 +4385,7 @@ it: time: "Orario" user: "Utente" email_type: "Tipo di Email" + details_title: "Mostra dettagli email" to_address: "Indirizzo Destinazione" test_email_address: "indirizzo email da testare" send_test: "Invia una email di prova" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 81d8553262..509da17c9b 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -98,7 +98,6 @@ ja: to_placeholder: "日付" share: topic_html: 'トピック: %{topicTitle}' - post: "投稿 #%{postNumber}" close: "閉じる" twitter: "Twitter で共有" facebook: "Facebook で共有" @@ -1692,27 +1691,23 @@ ja: not_approved: "あなたのアカウントはまだ承認されていません。ログインできるようになったら、メールで通知します。" google_oauth2: name: "Google" - title: "Google" - sr_title: "Google でログイン" twitter: name: "Twitter" - title: "Twitter" - sr_title: "Twitter でログイン" instagram: name: "Instagram" - title: "Instagram" + title: "Instagram でログイン" sr_title: "Instagram でログイン" facebook: name: "Facebook" - title: "Facebook" + title: "Facebook でログイン" sr_title: "Facebook でログイン" github: name: "GitHub" - title: "GitHub" + title: "GitHub でログイン" sr_title: "GitHub でログイン" discord: name: "Discord" - title: "Discord" + title: "Discord でログイン" sr_title: "Discord でログイン" second_factor_toggle: totp: "代わりに認証アプリを使用する" @@ -1728,7 +1723,6 @@ ja: success: "あなたのアカウントが作成され、ログインしました。" name_label: "名前" password_label: "パスワード" - optional_description: "(オプション)" password_reset: continue: "%{site_name} に進む" emoji_set: @@ -2812,10 +2806,8 @@ ja: tag_groups_placeholder: "(オプション) 許可されたタググループのリスト" manage_tag_groups_link: "タググループの管理" allow_global_tags_label: "他のタグも許可" - tag_group_selector_placeholder: "(オプション) タググループ" - required_tag_group_description: "新しいトピックでタググループのタグの使用を必須とする:" - min_tags_from_required_group_label: "タグ数:" - required_tag_group_label: "タググループ:" + required_tag_group: + delete: "削除" topic_featured_link_allowed: "このカテゴリに注目のリンクを許可する" delete: "カテゴリを削除" create: "新規カテゴリ" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index b29092f204..47fc82d13a 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -98,7 +98,6 @@ ko: to_placeholder: "현재까지" share: topic_html: '글: %{topicTitle}' - post: "댓글 #%{postNumber}" close: "닫기" twitter: "트위터에 공유" facebook: "페이스북에 공유" @@ -1697,27 +1696,23 @@ ko: not_approved: "계정이 아직 승인되지 않았습니다. 승인 되면 이메일로 알림을 받게 됩니다." google_oauth2: name: "구글" - title: "구글 사용" - sr_title: "구글로 로그인" twitter: name: "트위터" - title: "트위터 사용" - sr_title: "트위터로 로그인" instagram: name: "인스타그램" - title: "인스타그램 사용" + title: "인스타그램으로 로그인" sr_title: "인스타그램으로 로그인" facebook: name: "페이스북" - title: "페이스북 사용" + title: "페이스북으로 로그인" sr_title: "페이스북으로 로그인" github: name: "GitHub" - title: "GitHub 사용" + title: "GitHub로 로그인" sr_title: "GitHub로 로그인" discord: name: "디스코드" - title: "디스코드 사용" + title: "디스코드로 로그인" sr_title: "디스코드로 로그인" second_factor_toggle: totp: "대신 인증 자 앱을 사용하십시오." @@ -1734,7 +1729,6 @@ ko: success: "사용자님의 계정이 생성되었으며 이제 로그인 되었습니다." name_label: "이름" password_label: "비밀번호" - optional_description: "(선택 사항)" password_reset: continue: "%{site_name}으로 가기" emoji_set: @@ -2864,10 +2858,8 @@ ko: tag_groups_placeholder: "(선택사항) 허용된 태그 그룹 목록" manage_tag_groups_link: "태그 그룹 관리" allow_global_tags_label: "다른 태그도 허용" - tag_group_selector_placeholder: "(선택 사항) 태그 그룹" - required_tag_group_description: "새 글은 태그 그룹의 태그를 포함:" - min_tags_from_required_group_label: "태그 수:" - required_tag_group_label: "태그 그룹 :" + required_tag_group: + delete: "삭제" topic_featured_link_allowed: "이 카테고리에 주요 링크 허용" delete: "카테고리 삭제" create: "새 카테고리" diff --git a/config/locales/client.lt.yml b/config/locales/client.lt.yml index 711adb9b15..ee9aff7496 100644 --- a/config/locales/client.lt.yml +++ b/config/locales/client.lt.yml @@ -159,7 +159,6 @@ lt: to_placeholder: "iki šiol" share: topic_html: 'Tema: %{topicTitle}' - post: "įrašas #%{postNumber}" close: "uždaryti" twitter: "Bendrinkite „Twitter“" facebook: "Dalintis „Facebook“" @@ -1700,27 +1699,23 @@ lt: not_approved: "Jūsų paskyra dar nepatvirtinta. Jums bus pranešta elektroniniu paštu, kai būsite pasiruošę prisijungti." google_oauth2: name: "Google" - title: "per Google" - sr_title: "Prisijungti su Google" twitter: name: "Twitter" - title: "per Twitter" - sr_title: "Prisijungti su Twitter" instagram: name: "Instagram" - title: "su Instagram" + title: "Prisijungti su Instagram" sr_title: "Prisijungti su Instagram" facebook: name: "Facebook" - title: "per Facebook" + title: "Prisijungti su Facebook" sr_title: "Prisijungti su Facebook" github: name: "GitHub" - title: "per GitHub" + title: "Prisijunkite su GitHub" sr_title: "Prisijunkite su GitHub" discord: name: "Discord" - title: "su Discord" + title: "Prisijungti su Discord" sr_title: "Prisijungti su Discord" second_factor_toggle: totp: "Vietoj to naudokite autentifikavimo programą" @@ -1734,7 +1729,6 @@ lt: success: "Jūsų paskyra sukurta ir dabar esate prisijungę." name_label: "Vardas" password_label: "Slaptažodis" - optional_description: "(pasoronktinai)" password_reset: continue: "Tėsti į %{site_name}" emoji_set: @@ -2829,10 +2823,8 @@ lt: tag_groups_placeholder: "(Neprivaloma) leidžiamų žymų grupių sąrašas" manage_tag_groups_link: "Tvarkykite žymų grupes" allow_global_tags_label: "Taip pat leiskite kitas žymas" - tag_group_selector_placeholder: "(Neprivaloma) Žymų grupė" - required_tag_group_description: "Reikalauti, kad naujose temose būtų žymų iš žymų grupės:" - min_tags_from_required_group_label: "Žymų skaičius:" - required_tag_group_label: "Žymų grupė:" + required_tag_group: + delete: "Pašalinti" topic_featured_link_allowed: "Leisti svarbiausias šios kategorijos nuorodas" delete: "Ištrinti kategoriją" create: "Nauja Kategorija" diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml index 30b51fec20..89034f75ac 100644 --- a/config/locales/client.lv.yml +++ b/config/locales/client.lv.yml @@ -128,7 +128,6 @@ lv: placeholder: datums share: topic_html: 'Temats: %{topicTitle}' - post: "ieraksts #%{postNumber}" close: "aizvērt" twitter: "Kopīgojiet vietnē Twitter" facebook: "Dalīties Facebook" @@ -1628,22 +1627,16 @@ lv: not_approved: "Jūsu konts vēl nav apstiprināts. Jums tiks paziņots e-pastā, kad varēsiet pieslēgties." google_oauth2: name: "Google" - title: "ar Google" twitter: name: "Twitter" - title: "ar Twitter" instagram: name: "Instagram" - title: "ar Instagram" facebook: name: "Facebook" - title: "ar Facebook" github: name: "GitHub" - title: "ar GitHub" discord: name: "Discord" - title: "ar Discord" second_factor_toggle: totp: "Tā vietā izmantojiet autentifikatora lietotni" backup_code: "Tā vietā izmantojiet rezerves kodu" @@ -1658,7 +1651,6 @@ lv: success: "Jūsu konts ir izveidots un jūs esat ielogojies " name_label: "Vārds" password_label: "Parole" - optional_description: "(pēc izvēles)" password_reset: continue: "Turpināt %{site_name}" emoji_set: @@ -2390,6 +2382,8 @@ lv: tags: "Tagi" tags_placeholder: "(Pēc izvēles) atļauto tagu saraksts" tag_groups_placeholder: "(Pēc izvēles) atļauto tagu grupu saraksts" + required_tag_group: + delete: "Dzēst" delete: "Dzēst sadaļu" create: "Jauna sadaļa" create_long: "Izveidot jaunu sadaļu" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index c0f974f44d..a48229d425 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -120,7 +120,6 @@ nb_NO: to_placeholder: "til dato" share: topic_html: 'Emne: %{topicTitle}' - post: "innlegg #%{postNumber}" close: "lukk" twitter: "Del på Twitter" facebook: "Del på Facebook" @@ -1713,22 +1712,16 @@ nb_NO: not_approved: "Kontoen din har ikke blitt godkjent ennå. Du vil få beskjed via e-post når du er klar til å logge inn." google_oauth2: name: "Google" - title: "med Google" twitter: name: "Twitter" - title: "med Twitter" instagram: name: "Instagram" - title: "med Instagram" facebook: name: "Facebook" - title: "med Facebook" github: name: "GitHub" - title: "med GitHub" discord: name: "Discord" - title: "med Discord" second_factor_toggle: totp: "Bruk en autentiseringsapp i stedet" backup_code: "Bruk en sikkerhetskode i stedet" @@ -1743,7 +1736,6 @@ nb_NO: success: "Kontoen din har blitt opprettet og du er nå logget inn." name_label: "Navn" password_label: "Passord" - optional_description: "(valgfritt)" password_reset: continue: "Fortsett til %{site_name}" emoji_set: @@ -2836,10 +2828,8 @@ nb_NO: tag_groups_placeholder: "(Valgfritt) liste over tillatte stikkordgrupper" manage_tag_groups_link: "Administrere tagg-grupper" allow_global_tags_label: "Tillat også andre stikkord" - tag_group_selector_placeholder: "(Valgfritt) stikkord gruppe" - required_tag_group_description: "Krev nye emner for å ha tagger fra en stikkord-gruppe:" - min_tags_from_required_group_label: "Antall stikkord:" - required_tag_group_label: "Stikkordgrupper:" + required_tag_group: + delete: "Slett" topic_featured_link_allowed: "Tillat fremhevede lenker i denne kategorien" delete: "Slett kategori" create: "Ny Kategori" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 1dc65db7f4..b8f7538417 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -120,7 +120,6 @@ nl: to_placeholder: "tot datum" share: topic_html: 'Topic: %{topicTitle}' - post: "bericht #%{postNumber}" close: "sluiten" twitter: "Delen op Twitter" facebook: "Delen op Facebook" @@ -1722,27 +1721,23 @@ nl: not_approved: "Uw account is nog niet goedgekeurd. U ontvangt een melding via e-mail zodra u zich kunt aanmelden." google_oauth2: name: "Google" - title: "met Google" - sr_title: "Inloggen met Google" twitter: name: "Twitter" - title: "met Twitter" - sr_title: "Inloggen met Twitter" instagram: name: "Instagram" - title: "met Instagram" + title: "Inloggen met Instagram" sr_title: "Inloggen met Instagram" facebook: name: "Facebook" - title: "met Facebook" + title: "Inloggen met Facebook" sr_title: "Inloggen met Facebook" github: name: "GitHub" - title: "met GitHub" + title: "Inloggen met GitHub" sr_title: "Inloggen met GitHub" discord: name: "Discord" - title: "met Discord" + title: "Inloggen met Discord" sr_title: "Inloggen met Discord" second_factor_toggle: totp: "Een authenticator-app gebruiken" @@ -1758,7 +1753,6 @@ nl: success: "Uw account is gemaakt en u bent nu aangemeld." name_label: "Naam" password_label: "Wachtwoord" - optional_description: "(optioneel)" password_reset: continue: "Doorgaan naar %{site_name}" emoji_set: @@ -2905,10 +2899,8 @@ nl: tag_groups_placeholder: "(Optioneel) lijst van toegestane tag-groepen" manage_tag_groups_link: "Tag-groepen beheren" allow_global_tags_label: "Ook andere tags toestaan" - tag_group_selector_placeholder: "(Optioneel) Tag-groep" - required_tag_group_description: "Nieuwe topics moeten tags hebben uit een tag-groep:" - min_tags_from_required_group_label: "Aantal tags:" - required_tag_group_label: "Tag-groep:" + required_tag_group: + delete: "Verwijderen" topic_featured_link_allowed: "Aanbevolen koppelingen in deze categorie toestaan" delete: "Categorie verwijderen" create: "Nieuwe categorie" @@ -3511,7 +3503,6 @@ nl: leader: "leider" user_activity: no_activity_title: "Nog geen activiteit" - no_activity_body: "Welkom bij onze gemeenschap! U bent hier gloednieuw en hebt nog niet bijgedragen aan discussies. Als een eerste stap, bezoek Top of Categorieën en begin maar met lezen! Selecteer %{heartIcon} op berichten die u leuk vindt of waarover u meer wilt weten. Indien u dat nog niet gedaan hebt, help anderen u beter te leren kennen door een foto en een bio toe te voegen in uw gebruikersvoorkeuren." no_activity_others: "Geen activiteit." no_replies_others: "Geen antwoorden." no_likes_others: "Geen gelikete berichten." diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 93467ef416..9b5e9f2d34 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -164,7 +164,6 @@ pl_PL: to_placeholder: "do daty" share: topic_html: 'Temat: %{topicTitle}' - post: "post #%{postNumber}" close: "zamknij" twitter: "Udostępnij na Twitterze" facebook: "Udostępnij na Facebooku" @@ -1904,27 +1903,23 @@ pl_PL: not_approved: "Twoje konto nie zostało jeszcze aktywowane. Zostaniesz powiadomiony emailem gdy będziesz mógł/mogła się zalogować." google_oauth2: name: "Google" - title: "przez Google" - sr_title: "Zaloguj się przez Google" twitter: name: "Twitter" - title: "przez Twitter" - sr_title: "Zaloguj się przez Twitter" instagram: name: "Instagram" - title: "z Instagramem" + title: "Zaloguj się przez Instagram" sr_title: "Zaloguj się przez Instagram" facebook: name: "Facebook" - title: "przez Facebook" + title: "Zaloguj się przez facebook" sr_title: "Zaloguj się przez facebook" github: name: "GitHub" - title: "przez GitHub" + title: "Zaloguj się przez GitHub" sr_title: "Zaloguj się przez GitHub" discord: name: "Discord" - title: "z Discordem" + title: "Zaloguj się przez Discord" sr_title: "Zaloguj się przez Discord" second_factor_toggle: totp: "Zamiast tego użyj aplikacji uwierzytelniającej" @@ -1941,7 +1936,6 @@ pl_PL: success: "Twoje konto zostało utworzone i jesteś teraz zalogowany." name_label: "Nazwa" password_label: "Hasło" - optional_description: "(opcjonalne)" password_reset: continue: "Przejdź do %{site_name}" emoji_set: @@ -2800,6 +2794,7 @@ pl_PL: title: "Odpowiedz" help: "zacznij tworzyć odpowiedź do tego tematu" share: + title: "Udostępnij temat" extended_title: "Udostępnij link" help: "udostępnij odnośnik do tego tematu" instructions: "Udostępnij link do tego tematu:" @@ -3268,10 +3263,8 @@ pl_PL: tag_groups_placeholder: "(Opcjonalnie) lista dozwolonych grup tagów" manage_tag_groups_link: "Zarządzaj grupami tagów" allow_global_tags_label: "Zezwól dodatkowo na inne tagi" - tag_group_selector_placeholder: "(Opcjonalnie) Grupa znaczników" - required_tag_group_description: "Wymagaj, aby nowe tematy miały znaczniki z grupy znaczników:" - min_tags_from_required_group_label: "Liczba tagów:" - required_tag_group_label: "Grupa tagów:" + required_tag_group: + delete: "Usuń" topic_featured_link_allowed: "Zezwól na wybrane linki w tej kategorii" delete: "Usuń kategorię" create: "Nowa kategoria" @@ -3627,6 +3620,7 @@ pl_PL: readonly: "przeglądać" lightbox: download: "pobierz" + open: "oryginalny obraz" previous: "Poprzedni (klawisz strzałki w lewo)" next: "Następny (klawisz strzałki w prawo)" counter: "%curr% z %total%" @@ -3948,7 +3942,6 @@ pl_PL: unsupported_file_picked: "Wybrałeś nieobsługiwany plik. Obsługiwane typy plików – %{types}." user_activity: no_activity_title: "Brak aktywności" - no_activity_body: "Witaj w naszej społeczności! Jesteś tu zupełnie nowy i nie przyczyniłeś się jeszcze do dyskusji. W pierwszym kroku odwiedź Popularne lub Kategorie i zacznij czytać! Kliknij %{heartIcon} dla wpisów, które lubisz lub chcesz dowiedzieć się więcej. Jeśli jeszcze tego nie zrobiłeś, pomóż innym cię poznać, dodając zdjęcie i bio w swoich preferencjach użytkownika." no_activity_others: "Brak aktywności" no_replies_title: "Nie odpowiedziałeś jeszcze na żadne tematy" no_replies_others: "Brak odpowiedzi." diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index d64ff6c78a..febb143c08 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -120,7 +120,6 @@ pt: to_placeholder: "ir para data" share: topic_html: 'Tópico: %{topicTitle}' - post: "publicação #%{postNumber}" close: "fechar" twitter: "Partilhar no Twitter" facebook: "Partilhar no Facebook" @@ -1727,27 +1726,23 @@ pt: not_approved: "A sua conta ainda não foi aprovada. Será notificado por mensagem quando estiver pronto para iniciar a sessão." google_oauth2: name: "Google" - title: "com Google" - sr_title: "Iniciar sessão com o GitHub" twitter: name: "Twitter" - title: "com Twitter" - sr_title: "Iniciar sessão com o Twitter" instagram: name: "Instagram" - title: "com Instagram" + title: "Iniciar sessão com Instagram" sr_title: "Iniciar sessão com Instagram" facebook: name: "Facebook" - title: "com Facebook" + title: "Iniciar sessão com o Facebook" sr_title: "Iniciar sessão com o Facebook" github: name: "GitHub" - title: "com GitHub" + title: "Iniciar sessão com o GitHub" sr_title: "Iniciar sessão com o GitHub" discord: name: "Discord" - title: "com Discord" + title: "Iniciar sessão com o Discord" sr_title: "Iniciar sessão com o Discord" second_factor_toggle: totp: "Use um aplicativo de autenticação" @@ -1763,7 +1758,6 @@ pt: success: "A sua conta foi criada e está agora com a sessão iniciada." name_label: "Nome" password_label: "Palavra-passe" - optional_description: "(opcional)" password_reset: continue: "Continuar para %{site_name}" emoji_set: @@ -2886,10 +2880,8 @@ pt: tag_groups_placeholder: "(Opcional) lista de grupos de etiquetas permitidos" manage_tag_groups_link: "Gerenciar grupos de tags" allow_global_tags_label: "Permita também outras tags" - tag_group_selector_placeholder: "(Opcional) Grupo de tags" - required_tag_group_description: "Exigir que novos tópicos tenham tags de um grupo de tags:" - min_tags_from_required_group_label: "Número de Tags:" - required_tag_group_label: "Grupo de tags:" + required_tag_group: + delete: "Eliminar" topic_featured_link_allowed: "Permitir links em destaque nesta categoria" delete: "Eliminar Categoria" create: "Nova Categoria" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 2193e1014f..1854b61c52 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -120,7 +120,6 @@ pt_BR: to_placeholder: "até agora" share: topic_html: 'Tópico: %{topicTitle}' - post: "postagem #%{postNumber}" close: "fechar" twitter: "Compartilhar no Twitter" facebook: "Compartilhar no Facebook" @@ -1766,27 +1765,23 @@ pt_BR: not_approved: "Sua conta ainda não foi aprovada. Você será notificado(a) por e-mail quando tudo estiver pronto para entrar." google_oauth2: name: "Google" - title: "com o Google" - sr_title: "Entrar com o Google" twitter: name: "Twitter" - title: "com o Twitter" - sr_title: "Entrar com o Twitter" instagram: name: "Instagram" - title: "com o Instagram" + title: "Entrar com o Instagram" sr_title: "Entrar com o Instagram" facebook: name: "Facebook" - title: "com o Facebook" + title: "Entrar com o Facebook" sr_title: "Entrar com o Facebook" github: name: "GitHub" - title: "com o GitHub" + title: "Entrar com o GitHub" sr_title: "Entrar com o GitHub" discord: name: "Discord" - title: "com o Discord" + title: "Entrar com o Discord" sr_title: "Entrar com o Discord" second_factor_toggle: totp: "Use um aplicativo autenticador" @@ -1802,7 +1797,6 @@ pt_BR: success: "Sua conta foi criada e você já entrou." name_label: "Nome" password_label: "Senha" - optional_description: "(opcional)" password_reset: continue: "Continuar para %{site_name}" emoji_set: @@ -2999,10 +2993,8 @@ pt_BR: tag_groups_placeholder: "(Opcional) lista de grupos de etiquetas permitidos" manage_tag_groups_link: "Gerenciar grupos de etiquetas" allow_global_tags_label: "Permitir também outras etiquetas" - tag_group_selector_placeholder: "(Opcional) grupo de etiquetas" - required_tag_group_description: "Exigir que novos tópicos tenham etiquetas de um grupo de etiquetas:" - min_tags_from_required_group_label: "Número de etiquetas:" - required_tag_group_label: "Grupo de etiquetas:" + required_tag_group: + delete: "Excluir" topic_featured_link_allowed: "Permitir links em destaque nesta categoria" delete: "Excluir categoria" create: "Nova categoria" @@ -3624,7 +3616,6 @@ pt_BR: unsupported_file_picked: "Você escolheu um arquivo incompatível. Tipos de arquivos compatíveis: %{types}." user_activity: no_activity_title: "Nenhuma atividade ainda." - no_activity_body: "Boas-vindas à nossa comunidade! É a sua primeira vez aqui, e você ainda não contribuiu para nenhuma discussão. Para começar, acesse Melhores ou Categorias e comece a ler! Selecione %{heartIcon} nas postagens de que você gosta ou quer saber mais. Se ainda não tiver feito isso, ajude os(as) outros(as) a conhecê-lo(a) ao adicionar uma imagem e biografia nas suas preferências de usuário(a)." no_activity_others: "Nenhuma atividade." no_replies_title: "Você ainda não respondeu a nenhum tópico." no_replies_others: "Sem respostas." diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 92c378fccb..22a7d79abe 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -135,7 +135,6 @@ ro: placeholder: dată share: topic_html: 'Subiect: %{topicTitle}' - post: "Distribuie postarea #%{postNumber}" close: "Închide" twitter: "Distribuie pe Twitter" facebook: "Distribuie pe Facebook" @@ -1486,22 +1485,16 @@ ro: not_approved: "Contul tău încă nu a fost aprobat. Vei primi notificarea prin email când ești gata de logare." google_oauth2: name: "Google" - title: "Google" twitter: name: "Twitter" - title: "Twitter" instagram: name: "Instagram" - title: "Instagram" facebook: name: "Facebook" - title: "Facebook" github: name: "GitHub" - title: "GitHub" discord: name: "Discord" - title: "cu Discord" invites: accept_title: "Initație" welcome_to: "Bine ai venit la %{site_name}!" @@ -1512,7 +1505,6 @@ ro: success: "Contul tău a fost creat și acum ești logat." name_label: "Nume" password_label: "Parolă" - optional_description: "(opțional)" password_reset: continue: "Continuă la %{site_name}" emoji_set: @@ -2303,6 +2295,8 @@ ro: tags: "Etichete" tags_placeholder: "(Opțional) lista etichetelor permise" tag_groups_placeholder: "(Opțional) lista grupurilor de etichete permise" + required_tag_group: + delete: "Șterge" topic_featured_link_allowed: "Permite link-uri promovate în această categorie." delete: "Șterge categorie" create: "Categorie nouă" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 23778a9d02..d0cdbcc7a8 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -164,7 +164,7 @@ ru: to_placeholder: "по дату" share: topic_html: 'Тема: %{topicTitle}' - post: "сообщение #%{postNumber}" + post: "Сообщение #%{postNumber} от пользователя %{username}" close: "закрыть" twitter: "Поделиться в Твиттере" facebook: "Поделиться в Фейсбуке" @@ -339,6 +339,8 @@ ru: unbookmark_with_reminder: "Удалить все закладки и напоминания из этой темы." bookmarks: created: "Вы добавили это сообщение в закладки под именем '%{name}'" + create_for: "Создать закладку для %{type}" + edit_for: "Изменить закладку для %{type}" not_bookmarked: "Добавить сообщение в закладки" remove_reminder_keep_bookmark: "Удалить напоминание, но оставить закладку" created_with_reminder: "Вы добавили это сообщение в закладки под именем '%{name}' с последующим напоминанием %{date}" @@ -414,6 +416,7 @@ ru: saved: "Сохранено!" upload: "Загрузить" uploading: "Загрузка..." + processing: "Обработка..." uploading_filename: "Загрузка %{filename}" processing_filename: "Обработка: %{filename}..." clipboard: "буфер обмена" @@ -1914,27 +1917,27 @@ ru: not_approved: "Ваша учётная запись ещё не прошла проверку. После успешной проверки мы отправим вам письмо с уведомлением, и вы сможете войти в свою учётную запись." google_oauth2: name: "Google" - title: "Google" + title: "Войти через Google" sr_title: "Войти через Google" twitter: name: "Twitter" - title: "Twitter" + title: "Войти через Twitter" sr_title: "Войти через Twitter" instagram: name: "Instagram" - title: "Instagram" + title: "Войти через Instagram" sr_title: "Войти через Instagram" facebook: name: "Facebook" - title: "Facebook" + title: "Войти через Facebook" sr_title: "Войти через Facebook" github: name: "GitHub" - title: "GitHub" + title: "Войти через GitHub" sr_title: "Войти через GitHub" discord: name: "Discord" - title: "Discord" + title: "Войти через Discord" sr_title: "Войти через Discord" second_factor_toggle: totp: "Вместо этого используйте приложение для проверки подлинности" @@ -1951,7 +1954,6 @@ ru: success: "Ваш аккаунт создан и теперь вы можете войти." name_label: "Имя" password_label: "Пароль" - optional_description: "(необязательно)" password_reset: continue: "Далее на %{site_name}" emoji_set: @@ -3031,7 +3033,15 @@ ru: show_hidden: "Просмотр игнорируемого содержимого." deleted_by_author_simple: "(сообщение удалено автором)" collapse: "Свернуть" + sr_collapse_replies: "Свернуть ответы на сообщение" + sr_expand_replies: + one: "На это сообщение есть %{count} ответ. Нажмите для просмотра." + few: "На это сообщение есть %{count} ответа. Нажмите для просмотра." + many: "На это сообщение есть %{count} ответов. Нажмите для просмотра." + other: "На это сообщение есть %{count} ответов. Нажмите для просмотра." expand_collapse: "Развернуть/Свернуть" + sr_below_embedded_posts_description: "Ответы на сообщение #%{post_number}" + sr_embedded_reply_description: "Ответ пользователя @%{username} на сообщение #%{post_number}" locked: "Модератор заблокировал это сообщение для редактирования" gap: one: "Просмотреть %{count} скрытый ответ" @@ -3060,6 +3070,16 @@ ru: few: "Вам и ещё %{count} людям понравилось это сообщение" many: "Вам и ещё %{count} людям понравилось это сообщение" other: "Вам и ещё %{count} людям понравилось это сообщение" + sr_post_like_count_button: + one: "%{count} пользователю понравилось это сообщение. Нажмите для просмотра." + few: "%{count} пользователям понравилось это сообщение. Нажмите для просмотра." + many: "%{count} пользователям понравилось это сообщение. Нажмите для просмотра." + other: "%{count} пользователям понравилось это сообщение. Нажмите для просмотра." + sr_post_read_count_button: + one: "%{count} пользователь прочитал это сообщение. Нажмите для просмотра." + few: "%{count} пользователя прочитали это сообщение. Нажмите для просмотра." + many: "%{count} пользователей прочитали это сообщение. Нажмите для просмотра." + other: "%{count} пользователей прочитали это сообщение. Нажмите для просмотра." filtered_replies_hint: one: "Посмотреть это сообщение и ответ на него" few: "Посмотреть это сообщение и %{count} ответа" @@ -3181,6 +3201,8 @@ ru: few: "и ещё %{count} прочитали" many: "и ещё %{count} прочитали" other: "и ещё %{count} прочитали" + sr_post_likers_list_description: "Пользователи, которым понравилось это сообщение" + sr_post_readers_list_description: "Пользователи, прочитавшие это сообщение" by_you: off_topic: "Помечено вами как оффтопик" spam: "Помечено вами как спам" @@ -3287,10 +3309,11 @@ ru: tag_groups_placeholder: "(Необязат.) Доступные группы тегов" manage_tag_groups_link: "Управление группами тегов" allow_global_tags_label: "Также разрешить другие теги" - tag_group_selector_placeholder: "(Необязат.) Группа тегов" - required_tag_group_description: "Требовать, чтобы новые темы имели теги из группы тегов:" - min_tags_from_required_group_label: "Номер тега:" - required_tag_group_label: "Группа тегов:" + required_tag_group: + description: "Требовать, чтобы новые темы имели теги из группы тегов:" + delete: "Удалить" + add: "Добавить необходимую группу тегов" + placeholder: "Выберите группу тегов..." topic_featured_link_allowed: "Разрешить избранные ссылки в этом разделе" delete: "Удалить раздел" create: "Создать раздел" @@ -3522,19 +3545,24 @@ ru: other {}} original_post: "Начальное сообщение" views: "Просм." + sr_views: "Сортировать по просмотрам" views_lowercase: one: "просмотр" few: "просмотра" many: "просмотров" other: "просмотров" replies: "Ответов" + sr_replies: "Сортировать по ответам" views_long: one: "Тема просмотрена %{count} раз" few: "Тема просмотрена %{number} раза" many: "Тема просмотрена %{number} раз" other: "Тема просмотрена %{number} раз" activity: "Активность" + sr_activity: "Сортировать по активности" likes: "Нрав." + sr_likes: "Сортировать по симпатиям" + sr_op_likes: "Сортировать по симпатиям первого сообщения" likes_lowercase: one: "симпатия" few: "симпатии" @@ -3968,7 +3996,7 @@ ru: unsupported_file_picked: "Вы выбрали неподдерживаемый тип файла. Поддерживаемые типы файлов: %{types}." user_activity: no_activity_title: "Пока нет активности" - no_activity_body: "Добро пожаловать в наше сообщество! Вы здесь пока новичок и ещё не участвовали в обсуждениях. Для начала посмотрите самые обсуждаемые темы или ознакомьтесь с разделами форума! Отметьте понравившиеся сообщения значком %{heartIcon}. И, если вы ещё этого не сделали, помогите другим участникам форума познакомиться с вами, добавив изображение и краткую информацию о себе в профиль пользователя." + no_activity_body: "Добро пожаловать в наше сообщество! Вы здесь пока новичок и ещё не участвовали в обсуждениях. Для начала посмотрите самые обсуждаемые темы или ознакомьтесь с разделами форума! Отметьте наиболее полезные или понравившиеся сообщения значком %{heartIcon}. Статистика Вашей активности будет отображаться на этой странице." no_activity_others: "Нет активности." no_replies_title: "Вы пока не ответили ни на какие сообщения" no_replies_others: "Нет ответов." @@ -3978,9 +4006,13 @@ ru: no_likes_body: "Отличный способ принять участие и внести свой вклад - это начать читать обсуждения, и отмечать значком %{heartIcon} сообщения, которые вам понравились!" no_likes_others: "Симпатий пока нет." no_topics_title: "Вы пока ещё не начали ни одной темы" + no_topics_title_others: "Пользователь %{username} ещё не создал ни одной темы" no_read_topics_title: "Вы пока ещё не прочли ни одной темы" no_read_topics_body: "Когда вы начнете читать обсуждения, список читаемых тем появится здесь. Чтобы начать читать, поищите интересующие вас темы в разделеОбсуждаемые, в других разделах форума, либо выполните поиск по ключевому слову %{searchIcon}" no_group_messages_title: "Групповые сообщения не найдены" + topic_entrance: + sr_jump_top_button: "Перейти к первому сообщению" + sr_jump_bottom_button: "Перейти к последнему сообщению" fullscreen_table: expand_btn: "Развернуть таблицу" second_factor_auth: @@ -4714,6 +4746,7 @@ ru: time: "Время" user: "Пользователь" email_type: "Тип письма" + details_title: "Показать данные электронной почты" to_address: "Адресат" test_email_address: "Email-адрес тестового письма" send_test: "Отправить тестовое письмо" diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 12b7931e27..feca45f4eb 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -152,7 +152,6 @@ sk: next_month: "Nasledujúci mesiac" placeholder: Dátum share: - post: "príspevok #%{postNumber}" close: "zatvoriť" action_codes: public_topic: "téma je verejná%{when}" @@ -1221,16 +1220,8 @@ sk: not_approved: "Váš účet zatiaľ nebol schválený. Ak bude pripravený na prihlásenie, dostanete upozorňujúci email." google_oauth2: name: "Google" - title: "pomocou Google" twitter: name: "Twitter" - title: "pomocou Twitter účtu" - instagram: - title: "so službou Instagram" - facebook: - title: "pomocou stránky Facebook" - github: - title: "pomocou GitHub" discord: name: "Discord" invites: @@ -1240,7 +1231,6 @@ sk: success: "Váš účet bol vytvorený a ste prihlásený." name_label: "Meno" password_label: "Heslo" - optional_description: "(nepovinné)" password_reset: continue: "Pokračujte na %{site_name}" emoji_set: @@ -1866,6 +1856,8 @@ sk: tags: "Štítky" tags_placeholder: "(Voliteľné) zoznam povolených štítkov" tag_groups_placeholder: "(Voliteľné) zoznam povolených skupín štítkov" + required_tag_group: + delete: "Odstrániť" delete: "Odstrániť kategóriu" create: "Nová kategória" create_long: "Vytvoriť novú kategóriu" diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index eb581658fb..7392919c07 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -162,7 +162,6 @@ sl: to_placeholder: "na datum" share: topic_html: 'Tema: %{topicTitle}' - post: "prispevek #%{postNumber}" close: "zapri" twitter: "Deli na Twitterju" facebook: "Deli na Facebooku" @@ -1685,22 +1684,16 @@ sl: not_approved: "Vaš uporabniški račun še ni bil potrjen. Ko bo pripravljen za prijavo boste obveščeni preko e-sporočila." google_oauth2: name: "Google" - title: "Google" twitter: name: "Twitter" - title: "Twitter" instagram: name: "Instagram" - title: "z Instagramom" facebook: name: "Facebook" - title: "s Facebookom" github: name: "GitHub" - title: "z GitHubom" discord: name: "Discord" - title: "z Discordom" second_factor_toggle: totp: "Namesto tega uporabite authenticator aplikacijo" backup_code: "Namesto tega uporabite rezervno potrditveno kodo" @@ -1715,7 +1708,6 @@ sl: success: "Vaš račun je ustvarjen in vi ste prijavljeni." name_label: "Ime" password_label: "Geslo" - optional_description: "(neobvezno)" password_reset: continue: "Nadaljujte na %{site_name}" emoji_set: @@ -2787,6 +2779,8 @@ sl: tags_placeholder: "(neobvezno) seznam dovoljenih oznak" tag_groups_placeholder: "(neobvezno) seznam dovoljenih oznak skupin" allow_global_tags_label: "Dovoli tudi druge oznake" + required_tag_group: + delete: "Izbriši" topic_featured_link_allowed: "Dovoli najboljše povezave v tej kategoriji" delete: "Izbriši kategorijo" create: "Nova kategorija" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 9ff6af5abd..490312c91a 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -109,7 +109,6 @@ sq: previous_month: "Muaji i kaluar" next_month: "Muaji i ardhshëm" share: - post: "postim #%{postNumber}" close: "mbylle" action_codes: public_topic: "e bëri këtë temë publike %{when}" @@ -969,16 +968,8 @@ sq: not_approved: "Llogaria juaj nuk është aprovuar ende. Do t'ju dërgojmë një email kur të aprovohet. " google_oauth2: name: "Google" - title: "me Google" twitter: name: "Twitter" - title: "me Twitter" - instagram: - title: "me Instagram" - facebook: - title: "me Facebook" - github: - title: "me GitHub" invites: accept_title: "Ftesë" welcome_to: "Mirë se vini tek %{site_name}!" @@ -1541,6 +1532,8 @@ sq: tags: "Etiketat" tags_placeholder: "(Opsionale) lista e etiketave të lejuara" tag_groups_placeholder: "(Opsionale) lista e grupeve të etiketave" + required_tag_group: + delete: "Fshij" delete: "Fshini kategorinë" create: "Krijo kategorinë e re" create_long: "Krijo një kategori të re" diff --git a/config/locales/client.sr.yml b/config/locales/client.sr.yml index 5db726dfc3..4424c6650e 100644 --- a/config/locales/client.sr.yml +++ b/config/locales/client.sr.yml @@ -134,7 +134,6 @@ sr: to_placeholder: "до датума" share: topic_html: 'Tema: %{topicTitle}' - post: "poruka #%{postNumber}" close: "zatvori" twitter: "Podeli na Twitteru" facebook: "Podeli na Facebooku" @@ -1036,14 +1035,8 @@ sr: sent_activation_email_again: "Poslali smo vam drugi aktivacijski e-mail na: %{currentEmail}. Možda zatreba par minuta da stigne; svakako proverite nepoželjnu poštu." google_oauth2: name: "Google" - title: "pomoću Google-a" twitter: name: "Twitter" - title: "pomoću Twitter-a" - facebook: - title: "pomoću Facebook-a" - github: - title: "pomoću GitHuba" invites: welcome_to: "Dobrodošao na %{site_name}!" success: "Tvoj nalog je kreiran i sada si ulogovan." @@ -1469,6 +1462,8 @@ sr: view: "Prikaži Teme u Kategoriji" general: "Opšti" settings: "Podešavanje" + required_tag_group: + delete: "Obriši" delete: "Obriši Kategoriju" create: "Nova Kategorija" save: "Sačuvaj Kategoriju" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index a98dc8801c..e536444430 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -120,7 +120,7 @@ sv: to_placeholder: "till-datum" share: topic_html: 'Ämne: %{topicTitle}' - post: "inlägg #%{postNumber}" + post: "inlägg #%{postNumber} av %{username}" close: "stäng" twitter: "Dela på Twitter" facebook: "Dela på Facebook" @@ -163,7 +163,7 @@ sv: skip_to_main_content: "Hoppa till huvudinnehållet" wizard_required: "Välkommen till din nya Discourse! Låt oss komma igång med hjälp av konfigureringsguiden ✨" emails_are_disabled: "All utgående e-post har blivit globalt inaktiverad av en administratör. Inga e-postaviseringar av något slag kommer att skickas ut." - emails_are_disabled_non_staff: "Utgående e-post har inaktiverats för icke-anställda användare." + emails_are_disabled_non_staff: "Utgående e-post har inaktiverats för användare som inte är anställda." software_update_prompt: message: "Vi har uppdaterat den här webbplatsen, och ber dig uppdatera sidan, annars kan du uppleva oväntad funktion." dismiss: "Avfärda" @@ -287,6 +287,8 @@ sv: unbookmark_with_reminder: "Klicka för att ta bort alla bokmärken och påminnelser i detta ämne." bookmarks: created: "Du har bokmärkt detta inlägg. %{name}" + create_for: "Skapa bokmärke för %{type}" + edit_for: "Redigera bokmärke för %{type}" not_bookmarked: "bokmärk detta inlägg" remove_reminder_keep_bookmark: "Ta bort påminnelsen och behåll bokmärket" created_with_reminder: "Du har bokmärkt detta inlägg med en påminnelse %{date}. %{name}" @@ -352,6 +354,7 @@ sv: saved: "Sparat!" upload: "Ladda upp" uploading: "Laddar upp..." + processing: "Bearbetar..." uploading_filename: "Laddar upp: %{filename}..." processing_filename: "Bearbetar: %{filename}..." clipboard: "urklipp" @@ -366,6 +369,7 @@ sv: switch_to_anon: "Starta anonymt läge" switch_from_anon: "Avsluta anonymt läge" banner: + close: "Avfärda denna banderoll" edit: "Redigera" pwa: install_banner: "Vill du installera %{title} på denna enhet?" @@ -1779,27 +1783,23 @@ sv: not_approved: "Ditt konto har inte godkänts än. Du kommer att aviseras via e-post när det är klart att logga in." google_oauth2: name: "Google" - title: "med Google" - sr_title: "Logga in med Google" twitter: name: "Twitter" - title: "med Twitter" - sr_title: "Logga in med Twitter" instagram: name: "Instagram" - title: "med Instagram" + title: "Logga in med Instagram" sr_title: "Logga in med Instagram" facebook: name: "Facebook" - title: "med Facebook" + title: "Logga in med Facebook" sr_title: "Logga in med Facebook" github: name: "GitHub" - title: "med GitHub" + title: "Logga in med GitHub" sr_title: "Logga in med GitHub" discord: name: "Discord" - title: "med Discord" + title: "Logga in med Discord" sr_title: "Logga in med Discord" second_factor_toggle: totp: "Använd en autentiseringsapp istället" @@ -1816,7 +1816,6 @@ sv: success: "Ditt konto har skapats och du är nu inloggad." name_label: "Namn" password_label: "Lösenord" - optional_description: "(valfri)" password_reset: continue: "Fortsätt till %{site_name}" emoji_set: @@ -2798,7 +2797,13 @@ sv: show_hidden: "Visa ignorerat innehåll" deleted_by_author_simple: "(inlägg raderat av författaren)" collapse: "förminska" + sr_collapse_replies: "Förminska inbäddade svar" + sr_expand_replies: + one: "Detta inlägg har %{count} svar. Klicka för att utvidga" + other: "Detta inlägg har %{count} svar. Klicka för att utvidga" expand_collapse: "utvidga/förminska" + sr_below_embedded_posts_description: "inlägg #%{post_number} svar" + sr_embedded_reply_description: "svar från @%{username} på inlägg #%{post_number}" locked: "en i personalen har låst detta inlägg från att redigeras" gap: one: "visa %{count} dolt svar" @@ -2819,6 +2824,12 @@ sv: has_likes_title_you: one: "du och %{count} annan person gillade det här inlägget" other: "du och %{count} andra personer gillade det här inlägget" + sr_post_like_count_button: + one: "%{count} person gillade det här inlägget. Klicka för att se" + other: "%{count} personer gillade det här inlägget. Klicka för att se" + sr_post_read_count_button: + one: "%{count} person läste det här inlägget. Klicka för att se" + other: "%{count} personer läste det här inlägget. Klicka för att se" filtered_replies_hint: one: "Visa detta inlägg och dess svar" other: "Visa detta inlägg och dess %{count} svar" @@ -2920,6 +2931,8 @@ sv: read_capped: one: "och %{count} annan läste detta" other: "och %{count} andra läste detta" + sr_post_likers_list_description: "användare som gillade detta inlägg" + sr_post_readers_list_description: "användare som läste detta inlägg" by_you: off_topic: "Du flaggade detta som irrelevant" spam: "Du flaggade detta som skräppost" @@ -3022,10 +3035,11 @@ sv: tag_groups_placeholder: "(Valfri) lista över tillåtna grupptaggar" manage_tag_groups_link: "Hantera tagg-grupper" allow_global_tags_label: "Tillåt även andra taggar" - tag_group_selector_placeholder: "(Valfri) tagg-grupp" - required_tag_group_description: "Kräv att nya ämnen har taggar från en tagg-grupp:" - min_tags_from_required_group_label: "Nummer-taggar:" - required_tag_group_label: "Tagg-grupp:" + required_tag_group: + description: "Kräv att nya ämnen har taggar från tagg-grupper:" + delete: "Radera" + add: "Lägg till obligatorisk tagg-grupp" + placeholder: "välj tagg-grupp..." topic_featured_link_allowed: "Tillåt utvalda länkar i denna kategori" delete: "Radera kategori" create: "Ny kategori" @@ -3247,15 +3261,20 @@ sv: other {}} original_post: "Originalinlägg" views: "Visningar" + sr_views: "Sortera efter vyer" views_lowercase: one: "visning" other: "visningar" replies: "Svar" + sr_replies: "Sortera efter svar" views_long: one: "Detta ämnet har visats %{count} gång" other: "Detta ämne har visats %{number} gånger" activity: "Aktivitet" + sr_activity: "Sortera efter aktivitet" likes: "Gillningar" + sr_likes: "Sortera efter gillningar" + sr_op_likes: "Sortera efter originalinläggets gillningar" likes_lowercase: one: "gillar" other: "gillar" @@ -3649,7 +3668,7 @@ sv: unsupported_file_picked: "Du har valt en fil som inte stöds. Filtyper som stöds: %{types}." user_activity: no_activity_title: "Ingen aktivitet än" - no_activity_body: "Välkommen till vårt forum! Du är helt ny här och har ännu inte bidragit till diskussioner. Du kan börja med att gå till Topp eller Kategorier och sätta igång att läsa! Välj %{heartIcon} på inlägg som du gillar eller vill lära dig mer om. Om du inte redan har gjort det kan du underlätta för andra att lära känna dig genom att lägga till en bild och en biografi i dina användarinställningar." + no_activity_body: "Välkommen till vårt forum! Du är helt ny här och har ännu inte bidragit till diskussioner. Du kan börja med att gå till Topp eller Kategorier och sätta igång att läsa! Välj %{heartIcon} på inlägg som du gillar eller vill lära dig mer om. I takt med att du deltar kommer din aktivitet att visas här." no_activity_others: "Ingen aktivitet." no_replies_title: "Du har inte svarat på några ämnen än" no_replies_others: "Inga svar." @@ -3659,9 +3678,13 @@ sv: no_likes_body: "Ett bra sätt att hoppa in och börja bidra är att börja läsa samtal som redan har ägt rum, och välja %{heartIcon} på inlägg som du gillar!" no_likes_others: "Inga gillade inlägg." no_topics_title: "Du har inte startat några ämnen än" + no_topics_title_others: "%{username} har inte startat några ämnen än" no_read_topics_title: "Du har inte läst några ämnen än" no_read_topics_body: "När du börjar läsa diskussioner ser du en lista här. Börja läsa genom att leta efter ämnen som du tycker är intressanta i Topp eller Kategorier eller sök efter sökord %{searchIcon}" no_group_messages_title: "Inga gruppmeddelanden hittades" + topic_entrance: + sr_jump_top_button: "Hoppa till första inlägget" + sr_jump_bottom_button: "Hoppa till sista inlägget" fullscreen_table: expand_btn: "Expandera tabell" second_factor_auth: @@ -4119,6 +4142,7 @@ sv: delete_confirm: 'Är du säker på att du vill radera "%{theme_name}"?' color: "Färg" opacity: "Opacitet" + copy: "Duplicera" copy_to_clipboard: "Kopiera till urklipp" copied_to_clipboard: "Kopierad till urklipp" copy_to_clipboard_error: "Fel vid kopiering av data till urklipp" @@ -4390,6 +4414,7 @@ sv: time: "Tid" user: "Användare" email_type: "E-posttyp" + details_title: "Visa e-postdetaljer" to_address: "Till-adress" test_email_address: "e-postadress att testa" send_test: "Skicka e-posttest" diff --git a/config/locales/client.sw.yml b/config/locales/client.sw.yml index 5d6e5d9fc9..446f3be3ac 100644 --- a/config/locales/client.sw.yml +++ b/config/locales/client.sw.yml @@ -107,7 +107,6 @@ sw: next_month: "Mwezi Ujao" placeholder: tarehe share: - post: "taarifa #%{postNumber}" close: "funga" action_codes: public_topic: "ameifanya hii mada isiwe ya siri %{when}" @@ -1110,19 +1109,14 @@ sw: not_approved: "Akaunti yako bado haijathibitishwa. Utapata ujumbe kwa barua pepe ukiwa tayari kuingia." google_oauth2: name: "Google" - title: "na Google" twitter: name: "Twitter" - title: "na Twitter" instagram: name: "Instagram" - title: "na Instagram" facebook: name: "Facebook" - title: "na Facebook" github: name: "GitHub" - title: "na Github" discord: name: "Matatizo" invites: @@ -1135,7 +1129,6 @@ sw: success: "Akaunti yako imetengenezwa na sasa unaweza kuingia." name_label: "Jina" password_label: "Nywila" - optional_description: "(sio muhimu)" password_reset: continue: "endelea kwenye %{site_name}" emoji_set: @@ -1881,6 +1874,8 @@ sw: tags: "Lebo" tags_placeholder: "(Sio muhimu) orodha ya lebo zilizoruhusiwa." tag_groups_placeholder: "(Sio muhimu) orodha ya vikundi vyenye lebo zilizoruhusiwa." + required_tag_group: + delete: "Futa" delete: "Futa Kategoria" create: "Kategoria Mpya" create_long: "Tengeneza kategoria mpya" diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index a8776fc1b2..9f772cd7fe 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -98,7 +98,6 @@ te: next_month: "తరువాత నెల" placeholder: తేదీ share: - post: "#%{postNumber} టపా" close: "మూసివేయి" emails_are_disabled: "బయటకు వెళ్లే అన్ని ఈమెయిల్లూ అధికారి నిశేధించాడు. ఇప్పుడు ఎటువంటి ఈమెయిల్ ప్రకటనలూ పంపవీలవదు." themes: @@ -695,20 +694,13 @@ te: sent_activation_email_again: "మీకు %{currentEmail} మరో చేతన ఈమెయిల్ పంపాము. అది చేరుకోడానికి కొద్ది నిమిషాలు పట్టవచ్చు. ఇంకా స్పామ్ ఫోల్డరు చూడటం మర్చిపోకండి సుమా. " google_oauth2: name: "గూగుల్" - title: "గూగుల్ తో" twitter: name: "ట్విట్టర్" - title: "ట్విట్టరు తో" - facebook: - title: "ఫేస్ బుక్ తో" - github: - title: "గిట్ హబ్ తో" invites: accept_title: "ఆహ్వానం" welcome_to: "%{site_name} కు సుస్వాగతం!" name_label: "పేరు" password_label: "సంకేతపదం" - optional_description: "(ఐచ్ఛికం)" password_reset: continue: "%{site_name} కు కొనసాగండి" emoji_set: @@ -1083,6 +1075,8 @@ te: general: "సాధారణ" settings: "అమరికలు" tags: "ట్యాగులు" + required_tag_group: + delete: "తొలగించు" delete: "వర్గం తొలగించు" create: "కొత్త వర్గం" save: "వర్గం దాచు" diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml index 2a05943571..0c36b7dee8 100644 --- a/config/locales/client.th.yml +++ b/config/locales/client.th.yml @@ -96,7 +96,6 @@ th: to_placeholder: "ไปวันที่" share: topic_html: 'กระทู้: %{topicTitle}' - post: "โพสต์ #%{postNumber}" close: "ปิด" action_codes: public_topic: "ทำให้กระทู้นี้เป็นสาธารณะ %{when} " @@ -1231,21 +1230,14 @@ th: not_approved: "บัญชีของคุณยังไม่ได้รับการยืนยัน คุณจะได้รับการแจ้งเตือนทางอีเมลเมื่อคุณสามารถเข้าสู่ระบบได้" google_oauth2: name: "กูเกิล" - title: "ด้วยกูเกิล" twitter: name: "ทวิตเตอร์" - title: "ด้วยทวิตเตอร์" instagram: name: "อินสตาแกรม" - title: "ด้วยอินสตาแกรม" facebook: name: "เฟซบุ๊ก" - title: "ด้วยเฟซบุ๊ก" github: name: "กิตฮับ" - title: "ด้วยกิตฮับ" - discord: - title: "โดยดิสคอร์ด" invites: accept_title: "คำเชิญ" welcome_to: "ยินดีต้อนรับสู่ %{site_name}!" @@ -1255,7 +1247,6 @@ th: success: "บัญชีของคุณถูกสร้าง คุณได้เข้าสู่ระบบแล้ว" name_label: "ชื่อ" password_label: "รหัสผ่าน" - optional_description: "(ทางเลือก)" password_reset: continue: "ดำเนินการต่อไปยัง %{site_name}" emoji_set: @@ -2039,6 +2030,8 @@ th: topic_template: "รูปแบบกระทู้" tags: "แท็ก" allow_global_tags_label: "อนุญาตแท็กอื่นด้วย" + required_tag_group: + delete: "ลบ" delete: "ลบหมวดหมู่" create: "หมวดหมู่ใหม่" create_long: "สร้างหมวดหมู่ใหม่" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 7425230c75..36f5f5f0c5 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -120,7 +120,7 @@ tr_TR: to_placeholder: "şimdiye kadar" share: topic_html: 'Konu: %{topicTitle}' - post: "gönderi #%{postNumber}" + post: "%{username} kullanıcısının #%{postNumber} numaralı gönderisi" close: "kapat" twitter: "Twitter'da paylaş" facebook: "Facebook'ta paylaş" @@ -274,7 +274,7 @@ tr_TR: contact: "Bize Ulaşın" contact_info: "Bu siteyi etkileyen ciddi bir sorun veya acil bir durum oluştuğunda, lütfen %{contact_info} adresi üzerinden bizimle iletişime geçin." bookmarked: - title: "İşaret" + title: "Yer İmi" edit_bookmark: "Yer İmini Düzenle" clear_bookmarks: "Yer İmlerini Temizle" help: @@ -285,6 +285,8 @@ tr_TR: unbookmark_with_reminder: "Bu konudaki tüm yer imlerini ve anımsatıcıları kaldırmak için tıklayın." bookmarks: created: "Bu gönderiyi yer imlerine eklediniz. %{name}" + create_for: "%{type} için yer işareti oluştur" + edit_for: "%{type} için yer işaretini düzenle" not_bookmarked: "bu gönderiyi yer imlerine ekle" remove_reminder_keep_bookmark: "Anımsatıcıyı kaldır ve yer imini tut" created_with_reminder: "Bu gönderiye %{date} hatırlatıcısı ile yer imlerine eklediniz. %{name}" @@ -350,6 +352,7 @@ tr_TR: saved: "Kaydedildi!" upload: "Yükle" uploading: "Yükleniyor..." + processing: "İşleniyor..." uploading_filename: "Yükleniyor: %{filename}..." processing_filename: "İşleniyor: %{filename}..." clipboard: "pano" @@ -1770,27 +1773,27 @@ tr_TR: not_approved: "Hesabın henüz onaylanmış değil. Onaylandığında e--posta ile bilgilendirileceksin." google_oauth2: name: "Google" - title: "Google ile" + title: "Google ile giriş yapın" sr_title: "Google ile giriş yapın" twitter: name: "Twitter" - title: "Twitter ile" + title: "Twitter ile giriş yapın" sr_title: "Twitter ile giriş yapın" instagram: name: "Instagram" - title: "Instagram ile" + title: "Instagram ile giriş yapın" sr_title: "Instagram ile giriş yapın" facebook: name: "Facebook" - title: "Facebook ile" + title: "Facebook ile giriş yapın" sr_title: "Facebook ile giriş yapın" github: name: "GitHub" - title: "GitHub ile" + title: "GitHub ile Giriş Yapın" sr_title: "GitHub ile Giriş Yapın" discord: name: "Discord" - title: "Discord ile" + title: "Discord ile Giriş Yapın" sr_title: "Discord ile Giriş Yapın" second_factor_toggle: totp: "Bunun yerine bir doğrulama uygulaması kullanın" @@ -1806,7 +1809,6 @@ tr_TR: success: "Hesabın oluşturuldu ve şimdi giriş yaptın." name_label: "Ad" password_label: "Parola" - optional_description: "(isteğe bağlı)" password_reset: continue: "%{site_name} sitesine gitmeyi sürdür" emoji_set: @@ -2592,8 +2594,8 @@ tr_TR: title: "Yazdır" help: "Bu konunun yazıcı dostu olan sürümünü aç" flag_topic: - title: "Bayrakla işaretle" - help: "bu gönderiyi denetlenmesi için özel olarak bayrakla imle veya bununla ilgili özel bir bildirim gönder" + title: "Bayrak Koy" + help: "Bu gönderiyi denetlenmesi için özel olarak bayrakla imle veya bununla ilgili özel bir bildirim gönder" success_message: "Bu konuyu başarıyla bayrakla işaretledin" make_public: title: "Herkese Açık Konuya Dönüştür" @@ -2775,7 +2777,13 @@ tr_TR: show_hidden: "Yok sayılan içeriği görüntüleyin." deleted_by_author_simple: "(gönderi yazarı tarafından silindi)" collapse: "daralt" + sr_collapse_replies: "Gömülü yanıtları kapat" + sr_expand_replies: + one: "Bu gönderide %{count} yanıt var. Genişletmek için tıklayın" + other: "Bu gönderide %{count} yanıt var. Genişletmek için tıklayın" expand_collapse: "genişlet/daralt" + sr_below_embedded_posts_description: "#%{post_number} numaralı gönderiye gelen yanıtlar" + sr_embedded_reply_description: "#%{post_number} numaralı gönderiye @%{username} kullanıcısının yanıtı" locked: "personel bu yayının düzenlenmesini kilitledi" gap: one: "gizlenen %{count} yorumu gör" @@ -2796,6 +2804,12 @@ tr_TR: has_likes_title_you: one: "siz ve %{count} diğer kişi bu gönderiyi beğendi" other: "sen ve %{count} kişi bu gönderiyi beğendi" + sr_post_like_count_button: + one: "%{count} kişi bu gönderiyi beğendi. Görüntülemek için tıklayın" + other: "%{count} kişi bu gönderiyi beğendi. Görüntülemek için tıklayın" + sr_post_read_count_button: + one: "%{count} kişi bu yazıyı okudu. Görüntülemek için tıklayın" + other: "%{count} kişi bu yazıyı okudu. Görüntülemek için tıklayın" filtered_replies_hint: one: "Bu gönderiyi ve yanıtını görüntüleyin" other: "Bu gönderiyi ve %{count} yanıtını görüntüleyin" @@ -2897,6 +2911,8 @@ tr_TR: read_capped: one: "ve %{count} tanesini daha okuyun" other: "ve %{count}tanesini daha okuyun" + sr_post_likers_list_description: "bu gönderiyi beğenen kullanıcılar" + sr_post_readers_list_description: "bu gönderiyi okuyan kullanıcılar" by_you: off_topic: "Bunu bayrakla \"konu dışı\" olarak işaretledin" spam: "Bunu bayrakla \"istenmeyen e-posta\" olarak işaretledin" @@ -2996,10 +3012,11 @@ tr_TR: tag_groups_placeholder: "(Seçmeli) izin verilen etiket gruplarının listesi" manage_tag_groups_link: "Etiket gruplarını yönetin" allow_global_tags_label: "Diğer etiketlere de izin ver" - tag_group_selector_placeholder: "(Opsiyonel) Etiket grubu" - required_tag_group_description: "Bir etiket grubundaki etiketleresahip olmak için için yeni konular gerekiyor:" - min_tags_from_required_group_label: "Etiketler:" - required_tag_group_label: "Etiket grubu:" + required_tag_group: + description: "Yeni konuların etiket gruplarından bir etiket bulundurmasını zorunlu yap" + delete: "Sil" + add: "Zorunlu etiket grubu ekle" + placeholder: "bir etiket grubu seçin..." topic_featured_link_allowed: "Bu kategoride özellikli bağlantılara izin ver" delete: "Kategoriyi Sil" create: "Yeni Kategori" @@ -3127,7 +3144,7 @@ tr_TR: colors_disabled: "Category style olarak none belirlenmiş olduğundan renk seçemezsiniz." flagging: title: "Topluluğumuzun nezaket kuralları içerisinde kalmasına sağladığın destek için teşekkürler!" - action: "Gönderiyi Bayrakla İşaretle" + action: "Gönderiye Bayrak Koy" take_action: "Harekete Geç..." take_action_options: default: @@ -3169,7 +3186,7 @@ tr_TR: other: "%{count} kaldı" flagging_topic: title: "Topluluğumuzun nezaket kuralları içerisinde kalmasına verdiğin destek için teşekkürler!" - action: "Konuyu Bayrakla İşaretle" + action: "Konuya Bayrak Koy" notify_action: "Mesaj" topic_map: title: "Konu Özeti" @@ -3221,15 +3238,20 @@ tr_TR: other {}} {count, plural, one {1 yanıt} other {# yanıt}} var original_post: "Orjinal Gönderi" views: "Görüntüleme" + sr_views: "Görüntüleme sayısına göre sırala" views_lowercase: one: "gösterim" other: "görüntülenenler" replies: "Yanıtlar" + sr_replies: "Cevap sayısına göre sırala" views_long: one: "bu konu %{count} defa görüntülendi" other: "bu konu %{number} defa görüntülendi" activity: "Etkinlik" + sr_activity: "Aktiviteye göre sırala" likes: "Beğeni" + sr_likes: "Beğeni sayısına göre sırala" + sr_op_likes: "Orijinal gönderi beğenilerine göre sırala" likes_lowercase: one: "beğeni" other: "beğeni" @@ -3619,6 +3641,7 @@ tr_TR: unsupported_file_picked: "Desteklenmeyen bir dosya seçtiniz. Desteklenen dosya türleri şunlardır– %{types}." user_activity: no_activity_title: "Henüz etkinlik yok" + no_activity_body: "Topluluğumuza hoş geldiniz! Burada çok yenisiniz ve tartışmalara henüz katkıda bulunmadınız. İlk adım olarak en çok okunanları veya kategorileri ziyaret edebilir ve okumaya başlayabilirsiniz! Beğendiğiniz veya hakkında daha fazla bilgi edinmek istediğiniz gönderilerde lütfen %{heartIcon} kullanarak beğeninizi belirtin. Paylaşımlara katıldıkça, etkinliğiniz burada listelenecek." no_activity_others: "Etkinlik yok." no_replies_title: "Henüz hiçbir konuyu yanıtlamadınız." no_replies_others: "Yanıt yok." @@ -3626,8 +3649,12 @@ tr_TR: no_likes_title: "Henüz hiçbir konuyu beğenmedin" no_likes_others: "Beğenilen gönderi yok." no_topics_title: "Henüz herhangi bir konu başlatmadın" + no_topics_title_others: "%{username} kullanıcısı henüz herhangi bir konu başlatmadı" no_read_topics_title: "Henüz herhangi bir konu okumadın" no_group_messages_title: "Grup mesajı bulunamadı" + topic_entrance: + sr_jump_top_button: "İlk gönderiye atla" + sr_jump_bottom_button: "Son gönderiye atla" fullscreen_table: expand_btn: "Tabloyu Genişlet" admin_js: @@ -4342,6 +4369,7 @@ tr_TR: time: "Saat" user: "Kullanıcı" email_type: "E-posta Türü" + details_title: "E-posta detaylarını göster" to_address: "Gönderi Adresi" test_email_address: "test için e-posta adresi" send_test: "Test E-postası Gönder" @@ -4579,7 +4607,7 @@ tr_TR: block: "Engelle" censor: "Sansür" require_approval: "Onay gerektir" - flag: "Bayrakla işaretle" + flag: "Bayrak Koy" replace: "Değiştir" tag: "Etiket" silence: "Sustur" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index e275a8f9bc..943c81fcf3 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -164,7 +164,6 @@ uk: to_placeholder: "дата" share: topic_html: 'Тема: %{topicTitle}' - post: "допис #%{postNumber}" close: "сховати" twitter: "Поділитися в Twitter" facebook: "Поділитися у Facebook" @@ -1900,27 +1899,23 @@ uk: not_approved: "Ваш обліковий запис ще не було схвалено. Ви отримаєте сповіщення на електронну скриньку, коли зможете увійти." google_oauth2: name: "Google" - title: "з Google" - sr_title: "Увійти через Google" twitter: name: "Twitter" - title: "через Twitter" - sr_title: "Увійти через Twitter" instagram: name: "Instagram" - title: "через Instagram" + title: "Увійти через Instagram" sr_title: "Увійти через Instagram" facebook: name: "Facebook" - title: "через Facebook" + title: "Увійти через Facebook" sr_title: "Увійти через Facebook" github: name: "GitHub" - title: "через GitHub" + title: "Увійти через GitHub" sr_title: "Увійти через GitHub" discord: name: "Discord" - title: "з Discord" + title: "Увійти за допомогою Discord" sr_title: "Увійти за допомогою Discord" second_factor_toggle: totp: "Замість цього використовуйте додаток для перевірки автентичності" @@ -1936,7 +1931,6 @@ uk: success: "Ваш аккаунт створений та ви можете тепер увійти." name_label: "Ім'я" password_label: "Пароль" - optional_description: "(опціонально)" password_reset: continue: "Продовжити на %{site_name}" emoji_set: @@ -3263,10 +3257,8 @@ uk: tag_groups_placeholder: "(Необов'язково) список дозволених груп міток" manage_tag_groups_link: "Керування групами тегів" allow_global_tags_label: "Також дозволити інші мітки" - tag_group_selector_placeholder: "(Необов’язково) Група тегів" - required_tag_group_description: "Потрібні нові теми, щоб мати теги з групи тегів:" - min_tags_from_required_group_label: "Кількість тегів:" - required_tag_group_label: "Група тегів:" + required_tag_group: + delete: "Вилучити" topic_featured_link_allowed: "Дозволити популярні посилання в цій категорії" delete: "Видалити категорію" create: "Нова категорія" @@ -3943,7 +3935,6 @@ uk: unsupported_file_picked: "Ви вибрали непідтримуваний файл. Підтримуються такі типи файлів – %{types}." user_activity: no_activity_title: "Ще немає активності" - no_activity_body: "Ласкаво просимо до нашої спільноти! Ви тут зовсім новачок і ще не долучилися до обговорень. Як перший крок, відвідайте Топ або Категорії і просто почніть читати! Позначте %{heartIcon} публікації, які вам подобаються або про які потрібно дізнатися більше. Якщо ви цього ще не зробили, допоможіть іншим познайомитися з вами, додавши зображення та коротку інформацію про себе у профілі користувача." no_activity_others: "Немає активностей." no_replies_title: "Ви ще не відповіли на жодну тему" no_replies_others: "Немає відповідей" diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 7c61438079..dc6f687560 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -26,7 +26,10 @@ ur: millions: "%{number}M" dates: time: "h:mm a" + time_with_zone: "hh:mm a (z)" + time_short_day: "ddd، h:mm a" timeline_date: "MMM YYYY" + long_no_year: "MMM D، h:mm a" long_no_year_no_time: "MMM D" full_no_year_no_time: "MMMM Do" long_with_year: "MMM D, YYYY h:mm a" @@ -38,6 +41,7 @@ ur: long_date_without_year_with_linebreak: "MMM D
    LT" long_date_with_year_with_linebreak: "MMM D, 'YY
    LT" wrap_ago: "%{date} قبل" + wrap_on: "%{date}پر" tiny: half_a_minute: "ایک لحظہ قبل" less_than_x_seconds: @@ -47,8 +51,8 @@ ur: one: "%{count} سیکنڈ" other: "%{count} سیکنڈ" less_than_x_minutes: - one: "< %{count}منٹ" - other: "< %{count}منٹ " + one: "< %{count}m" + other: "< %{count}m" x_minutes: one: "%{count} منٹ" other: "%{count} منٹس" @@ -88,14 +92,14 @@ ur: one: "%{count} منٹ قبل" other: "%{count} منٹ قبل" x_hours: - one: "%{count} گھنٹہ قبل " - other: " %{count} گھنٹے قبل " + one: "%{count} گھنٹہ قبل" + other: "%{count} گھنٹے قبل" x_days: one: "%{count} دن قبل" other: "%{count} دن قبل" x_months: one: "%{count} ماہ قبل" - other: " %{count} مہینے قبل" + other: "%{count} مہینے قبل" x_years: one: "%{count} سال قبل" other: "%{count} سال قبل" @@ -112,10 +116,10 @@ ur: previous_month: "پچھلے ماہ" next_month: "اگلے ماہ" placeholder: تاریخ + from_placeholder: "تاریخ سے" to_placeholder: "اِس تاریخ تک" share: topic_html: 'ٹاپک: %{topicTitle}' - post: "پوسٹ #%{postNumber}" close: "بند کریں" twitter: "ٹویٹر پر شیئر کریں" facebook: "فیس بک پر شیئر کریں" @@ -123,11 +127,12 @@ ur: url: "یو آر ایل کو کاپی اور شیئر کریں" action_codes: public_topic: "اس ٹاپک کو پبلک بنایا گیا %{when}" + open_topic: "اسے %{when}پر عنوان میں تبدیل کر دیا" private_topic: "اس ٹاپک کو ذاتی پیغام بنایا گیا %{when}" split_topic: "اس ٹاپک کو تقسیم کیا گیا %{when}" invited_user: "دعوت دی %{who} %{when}" invited_group: "دعوت دی %{who} %{when}" - user_left: "%{who}نے خود کواِس پیغام سے ہٹا دیا %{when} " + user_left: "%{who}نے خود کواِس پیغام سے ہٹا دیا %{when}" removed_user: "ہٹایا %{who} %{when}" removed_group: "ہٹایا %{who} %{when}" autobumped: "خود کار طریقے سے بَمپ کریں %{when}" @@ -154,14 +159,23 @@ ur: disabled: "اِس بینر کو ہٹا دیا %{when}۔ یہ اب ہر صفحے کے سب سے اوپر دکھایا نہیں جائے گا۔" forwarded: "مندرجہ بالا ای میل بھیجیں" topic_admin_menu: "موضوع کے ایکشنز" + skip_to_main_content: "مرکزی مواد پر جائیں" wizard_required: "اپنے نئے ڈِسکورس پر خوش آمدید! سیٹ اَپ وزرڈ سے آغاز کرتے ہیں۔✨" - emails_are_disabled: " ای میل کو منتظم کی طرف سے غیر فعال کر دیا گیا ہے. کسی بھی قسم کی ای میل نہیں بھیجی جائیں گی۔" + emails_are_disabled: "ای میل کو منتظم کی طرف سے غیر فعال کر دیا گیا ہے. کسی بھی قسم کی ای میل نہیں بھیجی جائیں گی۔" + emails_are_disabled_non_staff: "باہر جانے والی ای میل غیر عملہ صارفین کے لیے غیر فعال کر دی گئی ہیں" software_update_prompt: message: "ہم نے اس سائٹ کواپڈیٹ کردیا براہ مہربانی ریفریش کریں ، یا آپ کا تجربہ غیر متوقع ہو سکتا ہے۔" dismiss: "بر خاست کریں" + bootstrap_mode_enabled: + one: "اپنی نئی سائٹ کو شروع کرنے کوآسان بنانے کے لیے، آپ بوٹسٹریپ موڈ میں ہیں۔ تمام نئے صارفین کو اعتماد کی سطح 1 دی جائے گی اور روزانہ ای میل سمری ای میلز کو فعال کیا جائے گا۔ %{count} صارف کے شامل ہونے پر یہ خود بخود بند ہو جائے گا۔" + other: "اپنی نئی سائٹ کو شروع کرنے کوآسان بنانے کے لیے، آپ بوٹسٹریپ موڈ میں ہیں۔ تمام نئے صارفین کو اعتماد کی سطح 1 دی جائے گی اور روزانہ ای میل سمری ای میلز کو فعال کیا جائے گا۔ %{count} صارفین کے شامل ہونے پر یہ خود بخود بند ہو جائے گا۔" bootstrap_mode_disabled: "بُوٹسٹرَیپ مَوڈ 24 گھنٹوں کے اندر غیر فعال کر دیا جائے گا۔" themes: default_description: "ڈِیفالٹ" + broken_theme_alert: "ہو سکتا ہے آپ کی سائٹ کام نہ کرے کیونکہ تھیم/جزو میں خامیاں ہیں۔" + error_caused_by: "'%{name}' کی وجہ سے۔اپ ڈیٹ کرنے، دوبارہ ترتیب دینے یا غیر فعال کرنے کے لیے یہاں کلک کریں۔" + only_admins: "(یہ پیغام صرف سائٹ کے منتظمین کو دکھایا گیا ہے)" + broken_decorator_alert: "ہو سکتا ہے کہ پوسٹس درست طریقے سے ظاہر نہ ہوں کیونکہ آپ کی سائٹ پر پوسٹ کے مواد کو جاچنے والوں میں سے ایک نے ایک غلطی کی طرف اشارہ کیا ہے۔" s3: regions: ap_northeast_1: "ایشیا پیسفک (ٹوکیو)" @@ -175,6 +189,7 @@ ur: cn_northwest_1: "چین (ننگزیا)" eu_central_1: "یورپی یونین (فرینکفرٹ)" eu_north_1: "یورپی یونین (اسٹاک ہوم)" + eu_south_1: "ای یو (میلان)" eu_west_1: "یورپی یونین (آئر لینڈ)" eu_west_2: "یورپی یونین (لندن)" eu_west_3: "یورپی یونین (پیرس)" @@ -189,17 +204,17 @@ ur: edit: "اس ٹاپک کے عنوان اور زمرے میں ترمیم کریں" expand: "مزید کھولیں" not_implemented: "یہ خصوصیت ابھی تک لاگو نہیں کی گئی، معضرت!" - no_value: "نہیں " - yes_value: "ہاں " + no_value: "نہیں" + yes_value: "جی ہاں" submit: "شائع" generic_error: "معذرت، ایک تکنیکی خرابی کا سامنا کرنا پڑا ہے۔" generic_error_with_reason: "ایک تکنیکی خرابی پیش آئی: %{error}" sign_up: "سائن اپ" log_in: "لاگ ان" - age: "عمر " + age: "عمر" joined: "شمولیت اختیار کر لی" admin_title: "منتظم" - show_more: "مزید " + show_more: "مزید دکھائیں" show_help: "اختیارات" links: "لنکس" links_lowercase: @@ -214,24 +229,26 @@ ur: conduct: "ضابطہ اخلاق" mobile_view: "موبائل وِیو" desktop_view: "ڈیسک ٹاپ وِیو" - or: "یا " - now: "ابھی " - read_more: "مزید پڑھیں " - more: "مزید " + or: "یا" + now: "ابھی ابھی" + read_more: "مزید پڑھیں" + more: "مزید" x_more: one: "%{count} مزید" other: "%{count} مزید" - never: "کبھی نہیں " + never: "کبھی نہیں" every_30_minutes: "ہر 30 منٹ" every_hour: "ہر گھنٹے" - daily: "روزانہ " - weekly: "ہفتہ وار " + daily: "روزانہ" + weekly: "ہفتہ وار" every_month: "ہر مہینے" every_six_months: "ہر چھ ماہ" max_of_count: "زیادہ سے زیادہ %{count}" character_count: one: "%{count} حرف" other: "%{count} حروف" + period_chooser: + aria_label: "مدت کے لحاظ سے فلٹر کریں" related_messages: title: "متعلقہ پیغامات" see_all: '@%{username} کی طرف سے تمام پیغامات دیکھیں...' @@ -259,23 +276,34 @@ ur: contact_info: "اس سائٹ کے بارے میں کسی اہم مسئلہ یا فوری معاملہ کی صورت میں، براہ مہربانی %{contact_info} پر رابطہ کریں۔" bookmarked: title: "بُک مارک" + edit_bookmark: "بک مارک میں ترمیم کریں" clear_bookmarks: "بک مارکس ختم کریں" help: bookmark: "اِس ٹاپک کی پہلی پوسٹ بُک مارک کرنے کے لئے کلک کریں" + edit_bookmark: "اس موضوع پر بک مارک میں ترمیم کرنے کے لیے کلک کریں" + edit_bookmark_for_topic: "اس موضوع پر بک مارک میں ترمیم کرنے کے لیے کلک کریں" unbookmark: "اِس ٹاپک کے تمام بُک مارک ہٹانے کے لئے کلک کریں" + unbookmark_with_reminder: "اس موضوع میں تمام بک مارکس اور یاد دہانیوں کو مٹانے کے لیے کلک کریں۔" bookmarks: created: "آپ نے اس %{name} پوسٹ کو بک مارک کیا ہے۔" not_bookmarked: "اِس پوسٹ کو بُک مارک کریں" + remove_reminder_keep_bookmark: "یاد دہانی کو ہٹا دیں اور بک مارک کو بچا کر رکھیں" created_with_reminder: "آپ نے اس پوسٹ کو ایک یاد دہانی %{date}, %{name} کے ساتھ بک مارک کیا ہے۔" remove: "بُک مارک ہٹائیں" - delete: "بک مارک کو حذف کریں" - confirm_delete: "کیا آپ واقعی اس بُک مارک کو حذف کرنا چاہتے ہیں؟ یاد دہانی بھی حذف کردی جائے گی۔" + delete: "بک مارک مٹائیں" + confirm_delete: "کیا آپ واقعی اس بُک مارک کو مٹانا چاہتے ہیں؟ یاد دہانی بھی مٹادی جائے گی۔" confirm_clear: "کیا آپ کو یقین ہے کہ آپ اِس ٹاپک سے اپنے تمام بُک مارکس ہٹانا چاہتے ہیں؟" save: "محفوظ کریں" no_timezone: 'آپ نے ابھی تک ٹائم زون متعین نہیں کیا ہے۔ آپ یاد دہانیوں کا تعین نہیں کرسکیں گے۔ اپنے پروفائل میں دیے گئے لنک میں ایک مرتب کریں۔.' invalid_custom_datetime: "آپ کی فراہم کردہ تاریخ اور وقت غلط ہے ، براہ کرم دوبارہ کوشش کریں۔" list_permission_denied: "آپ کو اس صارف کے بُک مارکس کو دیکھنے کی اجازت نہیں ہے۔" no_user_bookmarks: "آپ کے پاس کوئی بُک مارک کی ہوئی پوسٹس نہیں ہیں؛ بُک مارکس آپ کو مخصوص پوسٹس کے فوری حوالہ فراہم کرتے ہیں۔" + auto_delete_preference: + label: "آپ کو اطلاع دینے کے بعد" + never: "بک مارک کو بچا کر رکھیں" + when_reminder_sent: "بک مارک مٹائیں" + on_owner_reply: "ایک بارمیں جواب دوں،بک مارک مٹائیں" + clear_reminder: "بک مارک بچا کر اور یاد دہانی مٹائیں" search_placeholder: "نام ، عنوان عنوان ، یا پوسٹ پوسٹ کے ذریعہ بُک مارکس تلاش کریں" search: "تلاش کریں" reminders: @@ -285,12 +313,16 @@ ur: existing_reminder: "آپ نے اس بُک مارک کے لئے ایک یاد دہانی ترتیب ہے جو %{at_date_time} بھیجی جائے گی ـ" copy_codeblock: copied: "کاپی کر لیا!" + copy: "کوڈ کو کلپ بورڈ میں کاپی کریں" + fullscreen: "پوری سکرین میں کوڈ دکھائیں" drafts: label: "ڈرافٹس" + label_with_count: "ڈرافٹ (%{count})" resume: "جاری رکھیں" remove: "خارج کریں" remove_confirmation: "کیا آپ واقعی یہ مسودہ حذف کرنا چاہتے ہیں؟" new_topic: "نئے ٹاپک کا ڈرافٹ" + new_private_message: "نیے ذاتی پیغام کا مسودہ" topic_reply: "جواب ڈرافٹ کریں" abandon: confirm: "آپ کے پاس اس عنوان کے لئے ایک مسودہ ہے۔ آپ اس کے ساتھ کیا کرنا چاہیں گے؟" @@ -333,6 +365,7 @@ ur: switch_to_anon: "گمنام موڈ میں داخل ہوں" switch_from_anon: "گمنام موڈ سے باہر نکلیں" banner: + close: "اس بینر کو مسترد کریں" edit: "ترمیم" pwa: install_banner: "کیا آپ اِس ڈیوائس پر %{title} انسٹال کرنا چاہتے ہیں؟" @@ -348,6 +381,7 @@ ur: placeholder: "پیغام کا عنوان، یو آر ایل یا آئی ڈی یہاں ٹائپ کریں" review: order_by: "کے حساب سے آرڈر" + date_filter: "کے درمیان پوسٹ کیا گیا" in_reply_to: "کے جواب میں" explain: why: "وضاحت کریں کہ یہ شے آخر میں قطار میں کیوں داخل ہو گئی" @@ -369,6 +403,7 @@ ur: type_bonus: name: "قِسم بَونَس" title: "سٹاف کی طرف سے کچھ قابل تجدید اقسام کو بَونَس تفویض کیا جاسکتا ہے تاکہ ان کو اعلیٰ ترجیح بنایا جاسکے۔" + stale_help: "اس نظر ثانی کا حل %{username}نے کیا ہے۔" claim_help: optional: "آپ اس چیز کو کََلیم کرسکتے ہیں کہ دوسروں کو اِسکا جائزہ لینے سے روکا جا سکے۔" required: "اشیاء کا جائزہ لینے سے پہلے آپ کا اُن کو کََلیم کرنا ضروری ہے۔" @@ -379,7 +414,7 @@ ur: unclaim: help: "اس کََلیم کو ہٹائیں" awaiting_approval: "منظوری کا منتظر" - delete: "حذف کریں" + delete: "مٹائیں" settings: saved: "محفوظ کر لیا گیا" save_changes: "تبدیلیاں محفوظ کریں" @@ -400,8 +435,8 @@ ur: filtered_user: "صارف" filtered_reviewed_by: "کی طرف سے جائزہ کردہ" show_all_topics: "تمام ٹاپکس دکھائیں" - deleted_post: "(پوسٹ حذف کر دی گئی)" - deleted_user: "(صارف حذف کر دیا گیا)" + deleted_post: "(پوسٹ مٹادی گئی)" + deleted_user: "(صارف مٹادیا گیا)" user: bio: "بائیو" website: "ویب سائٹ" @@ -427,7 +462,7 @@ ur: topic: "ٹاپک" reviewable_count: "شمار" reported_by: "کی طرف سے رپورٹ کردہ" - deleted: "[ٹاپک حذف کر دیا گیا]" + deleted: "[ٹاپک مٹادیا گیا]" original: "(حقیقی ٹاپک)" details: "تفصیلات" unique_users: @@ -480,7 +515,7 @@ ur: ignored: title: "نظر انداز کردہ" deleted: - title: "حذف کردہ" + title: "مٹائیں" reviewed: title: "(تمام جائزہ کردہ)" all: @@ -518,18 +553,29 @@ ur: days: one: "دن" other: "دن" + months: + one: "مہینہ" + other: "مہینے" + years: + one: "سال" + other: "سال" relative: "تعلق" time_shortcut: later_today: "آج بعد میں" + next_business_day: "اگلے کاروباری دن" tomorrow: "کَل" + post_local_date: "پوسٹ میں تاریخ" later_this_week: "اِس ہفتے بعد میں" - this_weekend: "اِس ہفتےکےآخر میں" + this_weekend: "اس ہفتے کے آخر میں" start_of_next_business_week: "پیر" start_of_next_business_week_alt: "اگلے پیر" two_weeks: "دو ہفتے" next_month: "اگلے ماہ" six_months: "چھ مہینے" custom: "حسب ضرورت تاریخ اور وقت" + relative: "وابستہ وقت" + none: "کسی کی ضرورت نہیں" + last_custom: "آخری نظامی تاریخ وقت" user_action: user_posted_topic: "%{user} نے ٹاپک پوسٹ کیا" you_posted_topic: "آپ نے ٹاپک پوسٹ کیا" @@ -562,10 +608,12 @@ ur: days_visited_long: "زیارت کے دن" posts_read: "پڑھ لیا گیا" posts_read_long: "پڑھی گئیں پوسٹس" + last_updated: "آخری دفہ اپ ڈیٹ:" total_rows: one: "%{count} صارف" other: "%{count} صارفین" edit_columns: + title: "ڈائرکٹری کالموں میں ترمیم کریں" save: "محفوظ کریں" reset_to_default: "دوبارہ پہلے جیسا کر دیں" group: @@ -581,7 +629,12 @@ ur: member_added: "شامل کر دیا گیا" member_requested: "درخواست کیا گیا" add_members: + title: "صارفین کو %{group_name}میں شامل کریں" + description: "ان صارفین کی فہرست درج کریں جنہیں آپ گروپ میں مدعو کرنا چاہتے ہیں،کوما سے الگ الگ کردہ فہرست میں:" usernames_placeholder: "صارف نام" + usernames_or_emails_placeholder: "صارفین کے نام یا ای میلز" + notify_users: "صارفین کو مطلع کریں" + set_owner: "صارفین کو اس گروپ کے مالکان کے طور پر مرتب کریں" requests: title: "درخواستیں" reason: "وجہ" @@ -595,6 +648,7 @@ ur: title: "مَینَیج" name: "نام" full_name: "پورا نام" + add_members: "صارفین کو شامل کریں" invite_members: "دعوت دیں" delete_member_confirm: "'%{username}' کو '%{group}' گروپ سے ہٹائیں؟" profile: @@ -605,19 +659,54 @@ ur: notification: اطلاع email: title: "اِی میل" + status: "IMAP کے ذریعے ای میلز کو ہم آہنگ کیا %{old_emails} / %{total_emails}" + enable_smtp: "SMTP فعال کریں" + enable_imap: "IMAP فعال کریں" + test_settings: "ترتیبات کو ٹیسٹ کریں" + save_settings: "ترتیبات کو محفوظ کریں" + last_updated: "آخری دفہ اپ ڈیٹ:" last_updated_by: "تک" + settings_required: "تمام ترتیبات درکار ہیں، براہ کرم توثیق سے پہلے تمام فیلڈز کو پُر کریں۔" + smtp_settings_valid: "SMTP ترتیبات درست ہیں۔" + smtp_title: "SMTP" + smtp_instructions: "جب آپ گروپ کے لیے SMTP کو فعال کرتے ہیں، تو گروپ کے ان باکس سے بھیجے گئے تمام آؤٹ باؤنڈ ای میلز آپ کے فورم کے ذریعے بھیجے گئے دیگر ای میلز کے لیے ترتیب کردہ میل سرور کے بجائے یہاں بیان کردہ SMTP سیٹنگز کے ذریعے بھیجے جائیں گے۔" + imap_title: "IMAP" + imap_additional_settings: "اضافی ترتیبات" + imap_instructions: 'جب آپ گروپ کے لیے IMAP کو فعال کرتے ہیں، تو ای میلز گروپ ان باکس اور فراہم کردہ IMAP سرور اور میل باکس کے درمیان مطابقت پذیر ہو جاتی ہیں۔ IMAP کو فعال کرنے سے پہلے SMTP کو درست اور جانچ شدہ اسناد کے ساتھ فعال کیا جانا چاہیے۔ SMTP کے لیے استعمال ہونے والا ای میل صارف نام اور پاس ورڈ IMAP کے لیے استعمال کیا جائے گا۔ مزید معلومات کے لیے ڈسکورس میٹافیچر کےاعلان پر دیکھیں ۔' + imap_alpha_warning: "انتباہ: یہ الفا اسٹیج کی خصوصیت ہے۔ صرف Gmail باضابطہ طور پر تعاون یافتہ ہے۔ اپنے خطرے پر استعمال کریں!" + imap_settings_valid: "SMTP ترتیبات درست ہیں۔" + smtp_disable_confirm: "اگر آپ SMTP کو غیر فعال کرتے ہیں، تو تمام SMTP اور IMAP ترتیبات دوبارہ ترتیب دی جائیں گی اور متعلقہ فعالیت کو غیر فعال کر دیا جائے گا۔ کیا آپ واقعی جاری رکھنا چاہتے ہیں؟" + imap_disable_confirm: "اگر آپ IMAP کو غیر فعال کرتے ہیں تو تمام IMAP ترتیبات دوبارہ ترتیب دی جائیں گی اور متعلقہ فعالیت کو غیر فعال کر دیا جائے گا۔ کیا آپ واقعی جاری رکھنا چاہتے ہیں؟" + imap_mailbox_not_selected: "آپ کو اس IMAP کنفیگریشن کے لیے ایک میل باکس کا انتخاب کرنا چاہیے ورنہ کوئی میل باکس مطابقت پذیر نہیں ہوگا!" + prefill: + title: "ترتیبات کے ساتھ پہلے سے بھریں:" + gmail: "جی میل" credentials: + title: "شہادت" + smtp_server: "SMTP سرور" + smtp_port: "SMTP پورٹ" + smtp_ssl: "SMTP کے لیے SSL استعمال کریں" + imap_server: "IMAP سرور" + imap_port: "IMAP پورٹ" + imap_ssl: "IMAP کے لیے SSL استعمال کریں" username: "صارف کا نام" password: "پاسورڈ" settings: title: "سیٹِنگ" + allow_unknown_sender_topic_replies: "نامعلوم ارسال کنندہ کے عنوان کے جوابات کی اجازت دیں۔" + allow_unknown_sender_topic_replies_hint: "نامعلوم بھیجنے والوں کو گروپ کے عنوانات کا جواب دینے کی اجازت دیتا ہے۔ اگر یہ فعال نہیں ہے تو، ای میل پتوں کے جوابات جو پہلے ہی موضوع پر مدعو نہیں کیے گئے ہیں ایک نیا موضوع بنائیں گے۔" + from_alias: "عرف سے" + from_alias_hint: "گروپ SMTP ای میلز بھیجتے وقت فرم ایڈریس کے طور پر استعمال کرنے کے لیے عرف نوٹ کریں کہ یہ تمام میل فراہم کنندگان کے ذریعہ تعاون یافتہ نہیں ہوسکتا ہے، براہ کرم اپنے میل فراہم کنندہ کی دستاویزات سے مشورہ کریں۔" mailboxes: + synchronized: "مطابقت پذیر میل باکس" + none_found: "اس ای میل اکاؤنٹ میں کوئی میل باکس نہیں ملا۔" disabled: "غیر فعال" membership: - title: ممبرشپ + title: رکنیت access: رسائی categories: title: زُمرَہ جات + long_title: "زمرہ کی ڈیفالٹ اطلاعات" description: "جب صارفین کو اس گروپ میں شامل کیا جائے گا، تو ان کی کیٹیگری کی اطلاع کی ترتیبات ان ڈیفالٹس پر سیٹ کردی جائیں گی۔ اس کے بعد وہ انہیں تبدیل کرسکتے ہیں۔" watched_categories_instructions: "ان موضوعات میں خود بخود تمام عنوانات دیکھیں۔ گروپ ممبران کو تمام نئی پوسٹس اور عنوانات سے آگاہ کیا جائے گا ، اور نئی پوسٹوں کی گنتی بھی عنوان کے ساتھ سامنے آئے گی۔" tracked_categories_instructions: "ان موضوعات میں تمام عنوانات کو خود بخود ٹریک کریں۔ عنوان کے ساتھ ہی نئی پوسٹوں کی گنتی ظاہر ہوگی۔" @@ -669,7 +758,7 @@ ur: submit: "درخواست بھیجیں" title: "@%{group_name} میں شمولیت کی درخواست" reason: "گروپ مالکان کو بتائیے کہ آپ اس گروپ میں شامل ہونے کے کیوں مستحق ہیں" - membership: "ممبرشپ " + membership: "رکنیت" name: "نام" group_name: "گروپ نام" user_count: "صارفین" @@ -724,6 +813,7 @@ ur: owner: "مالِک" primary: "بنیادی" forbidden: "آپ کو ممبران دیکھنے کی اجازت نہیں ہے۔" + no_filter_matches: "کوئی ممبر اس تلاش سے مماثل نہیں ہے۔" topics: "ٹاپک" posts: "پوسٹ" mentions: "ذکر" @@ -766,7 +856,10 @@ ur: icon: "ایک آئکن منتخب کریں" image: "تصویر اپ لوڈ کریں" default_notifications: - modal_yes: "ہاں " + modal_title: "صارف کی ڈیفالٹ اطلاعات" + modal_description: "کیا آپ اس تبدیلی کو تاریخی طور پر لاگو کرنا چاہیں گے؟ اس سے %{count} موجودہ صارفین کی ترجیحات بدل جائیں گی۔" + modal_yes: "جی ہاں" + modal_no: "نہیں، صرف آگے بڑھ کر تبدیلی کا اطلاق کریں" user_action_groups: "1": "لائیکس دیے گئے" "2": "لائیکس موصول ہوے" @@ -829,10 +922,10 @@ ur: username: "صارف کا نام" trust_level: "TL" read_time: "پڑھنے کیلئے اِستعمال ہونے والا وقت" - topics_entered: " داخل ہوئے ٹاپک" + topics_entered: "درج کردہ موضوعات" post_count: "# پوسٹ" - confirm_delete_other_accounts: "کیا آپ واقعی یہ اکاؤنٹس حذف کرنا چاہتے ہیں؟" - powered_by: "MaxMindDBکا استعمال کرتے ہوئے " + confirm_delete_other_accounts: "کیا آپ واقعی یہ اکاؤنٹس مٹانا چاہتے ہیں؟" + powered_by: "MaxMindDBکا استعمال کرتے ہوئے" copied: "کاپی کر لیا" user_fields: none: "(ایک آپشن منتخب کریں)" @@ -856,6 +949,7 @@ ur: all: "تمام" read: "پڑھ لیا گیا" unread: "بغیر پڑھے" + unseen: "ان دیکھا" ignore_duration_title: "صارف کو نظر انداز کریں" ignore_duration_username: "صارف کا نام" ignore_duration_when: "دورانیہ:" @@ -881,17 +975,27 @@ ur: wednesday: "بدھ" thursday: "جمعرات" friday: "جمعہ" + saturday: "ہفتہ" + sunday: "اتوار" to: "کیلئے" activity_stream: "سرگرمی" read: "پڑھ لیا گیا" + read_help: "حال ہی میں پڑھے گئے موضوعات" preferences: "ترجیحات" feature_topic_on_profile: + open_search: "ایک نیا موضوع منتخب کریں" + title: "ایک موضوع منتخب کریں" + search_label: "عنوان کے لحاظ سے موضوع تلاش کریں" save: "محفوظ کریں" clear: title: "صاف کریں" + warning: "کیا آپ واقعی اپنا نمایاں موضوع صاف کرنا چاہتے ہیں؟" + use_current_timezone: "موجودہ ٹائم زون کا استعمال کریں" profile_hidden: "اِس صارف کی پبلک پروفائل پوشیدہ ہے۔" expand_profile: "مزید کھولیں" + sr_expand_profile: "پروفائل کی تفصیلات کو پھیلائیں" collapse_profile: "بند کریں" + sr_collapse_profile: "پروفائل کی تفصیلات کو سکیڑیں" bookmarks: "بُکمارکس" bio: "سائٹ کے بارے میں" timezone: "ٹائم زون" @@ -907,16 +1011,44 @@ ur: perm_denied_expl: "آپ نے اطلاعات کے لئے اجازت دینے سے انکار کر دیا۔ اپنے براؤزر کی سیٹِنگ سے اطلاعات کی اجازت دیں۔" disable: "اطلاعات غیر فعال کریں" enable: "اطلاعات فعال کریں" + each_browser_note: 'نوٹ: آپ کو اپنے استعمال کردہ ہر براؤزر پر اس ترتیب کو تبدیل کرنا ہوگا۔ اس ترتیب سے قطع نظر "ڈسٹرب نہ کریں" میں ہونے پر تمام اطلاعات کو غیر فعال کر دیا جائے گا۔' consent_prompt: "جب لوگ آپ کی پوسٹس کا جواب دیں تو کیا آپ لائیو اطلاعات چاہتے ہیں؟" dismiss: "بر خاست کریں" dismiss_notifications: "سب بر خاست کریں" dismiss_notifications_tooltip: "تمام بغیر پڑھی اطلاعات کو پڑھی جا چکی اطلاعات کے طور پر مارک کریں" + no_messages_title: "آپ کےل یے کوئی پیغامات نہیں ہیں۔" + no_messages_body: > + عام بات چیت کے بہاؤ سے باہر کسی کے ساتھ براہ راست ذاتی بات چیت کرنے کی ضرورت ہے؟ ان کا اوتار منتخب کرکے اور %{icon} میسج بٹن کا استعمال کرکے انہیں میسج کریں۔

    اگر آپ کو مدد کی ضرورت ہو تو آپ عملے کے ممبر کو میسج کر سکتے ہیں۔ + no_bookmarks_title: "آپ نے ابھی تک کچھ بھی بک مارک نہیں کیا" + no_bookmarks_body: > + %{icon} بٹن کے ساتھ پوسٹس کو بُک مارک کرنا شروع کریں اور انہیں آسان حوالہ کے لیے یہاں درج کیا جائے گا۔ آپ ایک یاد دہانی بھی شیڈول کر سکتے ہیں! + no_bookmarks_search: "فراہم کردہ تلاش کے استفسار کے ساتھ کوئی بک مارکس نہیں ملے۔" + no_notifications_title: "آپ کے لئے ابھی تک کوئی اطلاع نہیں" + no_notifications_body: > + آپ کو اس پینل میں آپ سے براہ راست متعلقہ سرگرمی کے بارے میں مطلع کیا جائے گا، بشمول آپ کے عنوانات اور پوسٹس کے جوابات، جب کوئی آپ کا تذکرہ کرتا ہے یا آپ کا حوالہ دیتا ہے، اور آپ جو موضوعات دیکھ رہے ہیں ان کا جواب دیتا ہے۔ جب آپ نے تھوڑی دیر سے لاگ ان نہیں کیا ہو گا تو آپ کے ای میل پر بھی اطلاعات بھیجی جائیں گی۔

    یہ فیصلہ کرنے کے لیے %{icon} کو تلاش کریں کہ آپ کن مخصوص عنوانات، زمروں اور ٹیگز کے بارے میں مطلع کرنا چاہتے ہیں۔ مزید کے لیے، اپنی اطلاع کی ترجیحات دیکھیں۔ + no_notifications_page_title: "آپ کے لئے ابھی تک کوئی اطلاع نہیں" + no_notifications_page_body: > + آپ کو براہ راست آپ سے متعلقہ سرگرمی کے بارے میں مطلع کیا جائے گا، بشمول آپ کے عنوانات اور پوسٹس کے جوابات، جب کوئی آپ کا تذکرہ کرتا ہے یا آپ کا حوالہ دیتا ہے، اور آپ جو عنوانات دیکھ رہے ہیں ان کا جواب دیتا ہے۔ جب آپ نے تھوڑی دیر سے لاگ ان نہیں کیا ہو گا تو آپ کے ای میل پر بھی اطلاعات بھیجی جائیں گی۔

    یہ فیصلہ کرنے کے لیے %{icon} کو تلاش کریں کہ آپ کن مخصوص عنوانات، زمروں اور ٹیگز کے بارے میں مطلع کرنا چاہتے ہیں۔ مزید کے لیے، اپنی اطلاع کی ترجیحات دیکھیں۔ first_notification: "آپ کی پہلی نوٹیفکیشن! شروع کرنے کے لئے اسے منتخب کریں۔" dynamic_favicon: "براؤزر آئکن پر پر شمار دکھائیں" + skip_new_user_tips: + description: "نئے صارف آن بورڈنگ تجاویز اور بیجز کو چھوڑ دیں" + not_first_time: "آپ کےلیے پہلی بار نہیں؟" + skip_link: "ان تجاویز کو چھوڑ دیں" + read_later: "میں اسے بعد میں پڑھوں گا." theme_default_on_all_devices: "میری تمام ڈیوائسز پر اِس کو ڈیفالٹ تھیم بنائیں" + color_scheme_default_on_all_devices: "میرے تمام آلات پر ڈیفالٹ رنگ کی سکیم (سکیمیں)سیٹ کریں۔" + color_scheme: "رنگ کی سکیم" color_schemes: + default_description: "تھیم ڈیفالٹ" + disable_dark_scheme: "باقاعدہ طور پر" + dark_instructions: "آپ اپنے آلے کے ڈارک موڈ کو ٹوگل کرکے ڈارک موڈ کلر سکیم کا جائزہ لے سکتے ہیں۔" undo: "رِی سَیٹ" regular: "معمولی" + dark: "ڈارک موڈ" + default_dark_scheme: "(سائٹ ڈیفالٹ)" + dark_mode: "سیاہ موڈ" + dark_mode_enable: "خودکار ڈارک موڈ کلر سکیم کو فعال کریں۔" text_size_default_on_all_devices: "میری تمام ڈیوائسز پر اِس کو ڈیفالٹ ٹیکسٹ سائز بنائیں" allow_private_messages: "دوسرے صارفین کو مجھے ذاتی پیغامات بھیجنے کی اجازت دیں" external_links_in_new_tab: "تمام بیرونی ویب سائٹ کے لنکس ایک نئے ٹیب میں کھولیں" @@ -931,7 +1063,7 @@ ur: silenced_tooltip: "یہ صارف خاموش کیا ہوا ہے" suspended_notice: "یہ صارف %{date} تک معطل ہے۔" suspended_permanently: "یہ صارف معطل ہے۔" - suspended_reason: "وجہ:" + suspended_reason: "وجہ: " github_profile: "گِٹ ہَب" email_activity_summary: "سرگرمی کا خلاصہ" mailing_list_mode: @@ -964,16 +1096,22 @@ ur: muted_categories_instructions: "آپ کو اِن زمرہ جات میں موجود نئے ٹاپکس کی کسی بھی چیز کے بارے میں مطلع نہیں کیا جائے گا، اور یہ زمرہ جات یا تازہ ترین صفحات پر نظر نہیں آئیں گے۔" muted_categories_instructions_dont_hide: "آپ کو اِن زمرہ جات میں نئے ٹاپک کی کسی بھی چیز کے بارے میں مطلع نہیں کیا جائے گا۔" regular_categories: "معمولی" + regular_categories_instructions: "آپ ان اقسام کو “تازہ ترین” اور “سرفہرست” موضوع کی فہرست میں دیکھیں گے." no_category_access: "ایک ماڈریٹر کے طور پر آپ کو زمرہ پر محدود رسائی حاصل ہے، محفوظ کرنا غیر فعال ہے۔" - delete_account: "میرا اکاؤنٹ حذف کریں" - delete_account_confirm: "کیا آپ واقعی مستقل طور پر اپنا اکاؤنٹ حذف کرنا چاہتے ہیں؟ اس عمل کو کالعدم نہیں کیا جا سکتا!" - deleted_yourself: "آپ کے اکاؤنٹ کو کامیابی سے حزف کر دیا گیا ہے۔" - delete_yourself_not_allowed: "اگر آپ چاہتے ہیں کہ آپ کا اکاؤنٹ حذف کر دیا جائے تو براہ کرم سٹاف کے کسی رکن سے رابطہ کریں۔" + delete_account: "میرا اکاؤنٹ مٹائیں" + delete_account_confirm: "کیا آپ واقعی مستقل طور پر اپنا اکاؤنٹ مٹانا چاہتے ہیں؟ اس عمل کو کالعدم نہیں کیا جا سکتا!" + deleted_yourself: "آپ کے اکاؤنٹ کو کامیابی سے مٹادیا گیا ہے۔" + delete_yourself_not_allowed: "اگر آپ چاہتے ہیں کہ آپ کا اکاؤنٹ مٹادیا جائے تو براہ کرم سٹاف کے کسی رکن سے رابطہ کریں۔" unread_message_count: "پیغامات" - admin_delete: "حذف کریں" + admin_delete: "مٹائیں" users: "صارفین" muted_users: "خاموش کِیا ہوا" + muted_users_instructions: "ان صارفین کی تمام اطلاعات اور پی ایم کو دبا دیں۔" + allowed_pm_users: "اجازت یافتہ" + allowed_pm_users_instructions: "صرف ان صارفین سے PM کی اجازت دیں۔" + allow_private_messages_from_specific_users: "صرف مخصوص صارفین کو مجھے ذاتی پیغامات بھیجنے کی اجازت دیں۔" ignored_users: "نظر انداز کردہ" + ignored_users_instructions: "ان صارفین کی تمام پوسٹس، نوٹیفیکیشنز اور PM کو دبا دیں۔" tracked_topics_link: "دکھائیں" automatically_unpin_topics: "جب میں سب سے نیچے تک پہنچوں تو خود کار طریقے سے ٹاپک سے پن ہٹایں۔" apps: "ایپس" @@ -982,16 +1120,20 @@ ur: api_approved: "منظورشدہ" api_last_used_at: "آخری بار استعمال کیا:" theme: "تھیم" + save_to_change_theme: 'آپ کے "%{save_text}" پر کلک کرنے کے بعد تھیم کو اپ ڈیٹ کر دیا جائے گا۔' home: "ڈیفالٹ ہوم پیج" staged: "سٹَیجڈ" staff_counters: flags_given: "مدد گار فلَیگ" flagged_posts: "فلَیگ کی گئی پوسٹس" - deleted_posts: "حذف کی گئی پوسٹس" + deleted_posts: "مٹائی گئی پوسٹس" suspensions: "معطلیاں" warnings_received: "تنبیہات" + rejected_posts: "مسترد شدہ پوسٹس" messages: + all: "تمام ان باکسز" inbox: "اِن باکس" + personal: "ذاتی" latest: "تازہ ترین" sent: "بھیجا جا چکا" unread: "بغیر پڑھے" @@ -1008,6 +1150,9 @@ ur: move_to_archive: "آر کائیو" failed_to_move: "منتخب شدہ پیغامات کو منتقل کرنے میں ناکامی (شاید آپ کے نیٹ ورک بند ہے)" tags: "ٹیگز" + warnings: "آفیشل انتباہات" + read_more_in_group: "مزید پڑھنا چاہتے ہیں؟ دوسرے پیغامات کو %{groupLink}میں براؤز کریں۔" + read_more: "مزید پڑھنا چاہتے ہیں؟ دوسرے پیغامات کو ذاتی پیغاماتمیں براؤز کریں۔" preferences_nav: account: "اکاؤنٹ" security: "سیکورٹی" @@ -1024,23 +1169,36 @@ ur: in_progress: "(اِی میل بھیجی جا رہی ہے)" error: "(خرابی)" emoji: "لاک ایموجی" - action: " پاس ورڈ دوبارہ سیٹ کرنے کی اِی میل بھیجیں" + action: "پاس ورڈ دوبارہ سیٹ کرنے کی اِی میل بھیجیں" set_password: "پاس ورڈ رکھیں" choose_new: "نیا پاس ورڈ منتخب کریں" choose: "پاس ورڈ منتخب کریں" second_factor_backup: + title: "دو فیکٹر بیک اپ کوڈز" regenerate: "دوبارہ تخلیق کریں" disable: "غیر فعال کریں" enable: "فعال کریں" enable_long: "بیک اپ کوڈ فعال کریں" + manage: + one: "بیک اپ کوڈز کا نظم کریں۔ آپ کے پاس %{count} بیک اپ کوڈ باقی ہے۔" + other: "بیک اپ کوڈز کا نظم کریں۔ آپ کے پاس %{count} بیک اپ کوڈز باقی ہیں۔" copy_to_clipboard: "کلِپ بورڈ میں کاپی کریں" copy_to_clipboard_error: "کلِپ بورڈ میں ڈیٹا کاپی کرنے پر خرابی کا سامنا کر نا پرا" copied_to_clipboard: "کلِپ بورڈ میں کاپی کر لیا گیا" + download_backup_codes: "بیک اپ کوڈ ڈاؤن لوڈ کریں" + remaining_codes: + one: "آپ کے %{count} بیک اپ کوڈ باقی ہے۔" + other: "آپ کے %{count} بیک اپ کوڈز باقی ہیں۔" + use: "بیک اپ کوڈ استعمال کریں" + enable_prerequisites: "بیک اپ کوڈز بنانے سے پہلے آپ کو ایک بنیادی دو فیکٹر طریقہ کو فعال کرنا ہوگا۔" codes: title: "بیک اپ کوڈز تیار" description: "اِن بیک اپ کوڈز میں سے ہر ایک کو صرف ایک بار استعمال کیا جاسکتا ہے۔ انہیں کہیں محفوظ لیکن قابل رسائی رکھیں۔" second_factor: title: "دو عنصری کی تصدیق" + enable: "دو فیکٹر توثیق انتظام" + disable_all: "سبھی کو غیر فعال کریں۔" + forgot_password: "پاس ورڈ بھول گئے؟" confirm_password_description: "جاری رکھنے کیلئے براہ کرم اپنے پاسوَرڈ کی تصدیق کریں" name: "نام" label: "کَوڈ" @@ -1051,6 +1209,11 @@ ur: show_key_description: "دستی طور پر درج کریں" short_description: | ایک بار استعمال والے سیکورٹی کوڈز کے ساتھ اپنے اکاؤنٹ کی حفاظت کریں۔ + extended_description: | + دو عنصر کی توثیق آپ کے اکاؤنٹ میں آپ کے پاس ورڈ کے علاوہ ون ٹائم ٹوکن کی ضرورت کے ذریعے اضافی سیکیورٹی کا اضافہ کرتی ہے۔ ٹوکنز Android اور iOS آلات پر بنائے جا سکتے ہیں۔ + oauth_enabled_warning: "براہ کرم نوٹ کریں کہ آپ کے اکاؤنٹ پر دو فیکٹر تصدیق کے فعال ہونے کے بعد سوشل لاگ ان کو غیر فعال کر دیا جائے گا۔" + use: "تصدیق کنندہ ایپ استعمال کریں" + enforced_notice: "اس سائٹ تک رسائی سے پہلے آپ کو دو عنصر کی تصدیق کو فعال کرنے کی ضرورت ہے۔" disable: "غیر فعال کریں" disable_confirm: "کیا آپ واقعی تمام دو عنصر والے طریقوں کو غیر فعال کرنا چاہتے ہیں؟" save: "محفوظ کریں" @@ -1105,6 +1268,7 @@ ur: upload_title: "آپنی تصویر اَپ لوڈ کریں" image_is_not_a_square: "انتباہ: ہم نے آپ کی تصویر کو کراپ کیا ہے؛ چوڑائی اور اونچائی برابر نہیں تھے۔" logo_small: "سائٹ کے چھوٹے لوگو کو طے شدہ طور پر استعمال کیا جاتا ہے۔" + use_custom: "یا حسب ضرورت اوتار اپ لوڈ کریں:" change_profile_background: title: "پروفائل ہیڈر" instructions: "پروفائل ہیڈرز درمیان میں ہوں گے اور ان کی طے شدہ چوڑائی 1110px ہوگی۔" @@ -1149,6 +1313,7 @@ ur: not_connected: "(غیر کنَیکٹ شدہ)" confirm_modal_title: "%{provider} اکاؤنٹ کنیکٹ کریں" confirm_description: + disconnect: "آپ کا موجودہ %{provider} اکاؤنٹ '%{account_description}' منقطع ہو جائے گا۔" account_specific: "آپ کا %{provider} اکاؤنٹ '%{account_description}' تصدیق کیلئے استعمال کیا جائے گا۔" generic: "آپ کا %{provider} اکاؤنٹ تصدیق کیلئے استعمال کیا جائے گا۔" name: @@ -1221,24 +1386,26 @@ ur: always: "ہمیشہ" first_time_and_daily: "پہلی بار پوسٹ کو لائک کیا جائے اور روزانہ" first_time: "پہلی بار پوسٹ کو لائک کیا جائے" - never: "کبھی نہیں " + never: "کبھی نہیں" email_previous_replies: title: "اِی میل کے نچلے حصے میں پچھلے جوابات شامل کریں" unless_emailed: "اگر ماضی میں بھیجا گیا ہو، تو نہیں" always: "ہمیشہ" - never: "کبھی نہیں " + never: "کبھی نہیں" email_digests: title: "جب میں یہاں کا دورا نہ کروں، مجھے مقبول ٹاپک اور جوابات کا ایک اِی میل خلاصہ بھیجیں" every_30_minutes: "ہر 30 منٹ" every_hour: "گھنٹہ وار" - daily: "روزانہ " + daily: "روزانہ" weekly: "ہفتہ وار" every_month: "ہر مہینے" every_six_months: "ہر چھ ماہ" email_level: + title: "جب میرا حوالہ دیا جائے، جواب دیا جائے، میرے @username کا ذکر کیا جائے، یا جب میرے دیکھے گئے زمروں، ٹیگز یا عنوانات میں کوئی نئی سرگرمی ہو تو مجھے ای میل کریں۔" always: "ہمیشہ" only_when_away: "صرف جب غیر حاضر" - never: "کبھی نہیں " + never: "کبھی نہیں" + email_messages_level: "جب مجھے ذاتی پیغام بھیجا جائے تو مجھے ای میل کریں" include_tl0_in_digests: "اِی میل خلاصہ میں نئے صارفین سے مواد شامل کریں" email_in_reply_to: "ای میل میں پوسٹ کے جواب کا اقتباس شامل کریں" other_settings: "دیگر" @@ -1253,7 +1420,7 @@ ur: after_2_weeks: "پچھلے 2 ہفتوں میں بنائے گئے" auto_track_topics: "جس ٹاپک میں مَیں داخل ہوں اُسے خود کار طریقے سے ٹریک کریں" auto_track_options: - never: "کبھی نہیں " + never: "کبھی نہیں" immediately: "ابھی اِسی وقت" after_30_seconds: "30 سیکنڈ کے بعد" after_1_minute: "1 منٹ کے بعد" @@ -1276,9 +1443,13 @@ ur: groups: "گروپس" topic: "ٹاپک" sent: "تخلیق کی گذار/آخری ارسال" + expires_at: "میعاد ختم" edit: "ترمیم" remove: "خارج کریں" + copy_link: "لنک حاصل کریں" + reinvite: "ای میل دوبارہ بھیجیں" reinvited: "دعوت دوبارہ بھیج دی گئی" + removed: "ہٹا دیا" search: "دعوتیں تلاش کرنے کے لئے ٹائپ کریں..." user: "مدعو کیا گیا صارف" none: "ظاہر کرنے کے لئے کوئی دعوتیں نہیں ہیں۔" @@ -1288,20 +1459,61 @@ ur: redeemed: "دعوتیں جِن سے فائدہ اٹھا لیا گیا ہے" redeemed_at: "فائدہ اٹھا لیا گیا" pending: "زیرِاِلتوَاء دعوتیں" - topics_entered: " دیکھ لیے گئے ٹاپک" + topics_entered: "دیکھے گئے موضوعات" posts_read_count: "پڑھی گئیں پوسٹس" expired: "اِس دعوت کی میعاد ختم ہو چکی ہے۔" + remove_all: "میعاد ختم ہونے والے دعوت نامے کو ہٹا دیں" removed_all: "تمام میعاد ختم ہوئی دعوتیں منسوخ کر دی گئیں!" remove_all_confirm: "کیا آپ واقعی تمام میعاد ختم ہوئی دعوتیں منسوخ کر دینا چاہتے ہیں؟" + reinvite_all: "تمام دعوتیں دوبارہ بھیجیں" reinvite_all_confirm: "کیا آپ واقعی تمام دعوتیں دوبارہ بھیجنا چاہتے ہیں؟" + reinvited_all: "تمام دعوت نامے بھیجے گئے!" time_read: "پڑھنے کیلئے اِستعمال ہونے والا وقت" days_visited: "دورہ کیے گئے دن" account_age_days: "دنوں میں اکاؤنٹ کی عمر" create: "دعوت دیں" + generate_link: "انوائٹ لنک بنائیں" + link_generated: "یہاں آپ کی دعوت کا لنک ہے!" valid_for: "دعوت لنک صرف اِس اِی میل اِیڈریس کے لئے درست ہے:%{email}" + single_user: "ای میل کے ذریعے مدعو کریں" + multiple_user: "لنک کے ذریعے مدعو کریں" invite_link: + title: "مدعوکرنےکا لنک" success: "دعوت لنک کامیابی سے بن گیا!" + error: "مدعو کرنے کا لنک بنانے میں ایک خرابی تھی" + max_redemptions_allowed_label: "اس لنک کا استعمال کرتے ہوئے کتنے لوگوں کو رجسٹر کرنے کی اجازت ہے؟" + expires_at: "اس دعوتی لنک کی میعاد کب ختم ہوگی؟" + invite: + new_title: "دعوت نامہ بنائیں" + edit_title: "دعوت میں ترمیم کریں" + instructions: "اس سائٹ تک فوری رسائی دینے کے لیے اس لنک کا اشتراک کریں:" + copy_link: "لنک کاپی کریں" + expires_in_time: "%{time}میں ختم ہو گا" + expired_at_time: "%{time}پر ختم ہو گا" + show_advanced: "ایڈوانس آپشنز دکھائیں" + hide_advanced: "ایڈوانس آپشنز چھپائیں" + restrict: "تک محدود" + restrict_email: "ای میل تک محدود" + restrict_domain: "ڈومین تک محدود" + email_or_domain_placeholder: "name@example.com یا example.com" + max_redemptions_allowed: "زیادہ سے زیادہ استعمال" + add_to_groups: "گروپوں میں شامل" + invite_to_topic: "موضوع پر پہنچیں" + expires_at: "کے بعدختم" + custom_message: "اختیاری ذاتی پیغام" + send_invite_email: "محفوظ کریں اور ای میل بھیجیں" + send_invite_email_instructions: "ای میل پر مدعوکے لئے دعوت نامہ کی ای میل کو محدود کریں" + save_invite: "دعوت نامہ محفوظ کریں" + invite_saved: "دعوت نامہ محفوظ ہے" bulk_invite: + none: "اس صفحہ پر دکھانے کے لیے کوئی دعوت نہیں ہے۔" + text: "مجموئی دعوت نامہ" + instructions: | +

    اپنی کمیونٹی کو تیزی سے آگے بڑھانے کے لیے صارفین کی فہرست کو مدعو کریں۔ CSV فائل تیار کریں جس میں کم از کم ایک قطار فی ای میل ایڈریس پر مشتمل ہو جس کو آپ مدعو کرنا چاہتے ہیں۔ اگر آپ لوگوں کو گروپوں میں + کرنا چاہتے ہیں یا پہلی بار سائن ان کرتے ہیں تو انہیں کسی مخصوص موضوع

    بھیجنا چاہتے ہیں تو درج

    سے الگ

    معلومات فراہم کی جاسکتی ہیں
    +            آپ کی اپ لوڈ کردہ CSV فائل میں ایک دعوت نامہ بھیجا جائے گا، اور آپ بعد میں اس کا انتظام کر سکیں گے۔

    + progress: "اپ لوڈ %{progress} فیصد..." + success: "فائل کامیابی سے اپ لوڈ ہو گئی۔ عمل مکمل ہونے پر آپ کو پیغام کے ذریعے مطلع کیا جائے گا۔" error: "معذرت، فائل CSV فارمیٹ میں ہونا ضروری ہے۔" password: title: "پاسورڈ" @@ -1311,6 +1523,7 @@ ur: same_as_email: "آپ کا پاسورڈ وہی ہے جو آپ کا اِی میل ہے۔" ok: "آپ کا پاسورڈ صحیح لگ رہا ہے" instructions: "کم از کم %{count} حروف" + required: "براہ کرم پاس ورڈ درج کریں" summary: title: "خلاصہ" stats: "اعدادوشمار" @@ -1365,11 +1578,16 @@ ur: avatar: title: "پروفائل تصویر" header_title: "پروفائل، پیغامات، بک مارکس اور ترجیحات" + name_and_description: "%{name} - %{description}" + edit: "پروفائل تصویر میں ترمیم کریں" title: title: "عنوان" none: "(کوئی نہیں)" + instructions: "آپ کے صارف نام کے بعد ظاہر ہوتا ہے" flair: + title: "فلئیر" none: "(کوئی نہیں)" + instructions: "آپ کی پروفائل تصویر کے ساتھ ظاہر ہونے والا آئیکن" primary_group: title: "پرائمری گروپ" none: "(کوئی نہیں)" @@ -1390,7 +1608,7 @@ ur: unknown: "خرابی" not_found: "صفحہ نہیں ملا" desc: - network: " براہِ مہربانی اپنا کنکشن چیک کریں" + network: "براہ کرم اپنا کنکشن چیک کریں۔" network_fixed: "لگتا ہے یہ وآپس آ گیا" server: "خرابی کوڈ: %{status}" forbidden: "آپ کو یہ دیکھنے کی اجازت نہیں ہے۔" @@ -1402,7 +1620,9 @@ ur: fixed: "پیج لوڈ کریں" modal: close: "بند کریں" + dismiss_error: "غلطی کو مسترد کریں" close: "بند کریں" + assets_changed_confirm: "اس سائٹ کو ابھی ایک سافٹ ویئر اپ گریڈ ملا ہے۔ ابھی تازہ ترین ورژن حاصل کریں؟" logout: "آپ لاگ آؤٹ ہو گئے تھے۔" refresh: "رِیفریش" home: "ہَوم" @@ -1419,7 +1639,7 @@ ur: time_read: پڑھا time_read_recently: "حال ہی میں %{time_read}" time_read_tooltip: "کُل وقت پڑھا %{time_read}" - time_read_recently_tooltip: "کُل وقت پڑھنے میں لگا %{time_read}(%{recent_time_read} گزشتہ 60 دنوں میں) " + time_read_recently_tooltip: "%{time_read} کل پڑھنے کا وقت (گزشتہ 60 دنوں میں%{recent_time_read})" last_reply_lowercase: آخری جواب replies_lowercase: one: جواب @@ -1433,14 +1653,19 @@ ur: value_prop: "جب آپ ایک اکاؤنٹ بناتے ہیں، ہم بالکل یاد رکھتے ہیں کہ آپ نے کیا پڑھا ہے، تاکہ جہاں سے آپ نے پڑھنا چھوڑا ہو بلکل وہاں ہی پر آپ کو ہمیشہ واپس پہنچایا جا سکے۔ جب بھی کوئی آپ کو جواب دیتا ہے، آپ کو یہاں اور اِی میل پر اطلاع بھی دے دی جاتی ہے۔ اور آپ محبت کو بانٹںے کیلئے پوسٹ لائیک کر سکتے ہیں۔ :heartpulse:" summary: enabled_description: "آپ اس ٹاپک کا خلاصہ ملاحظہ کررہے ہیں: سب سے دلچسپ پوسٹ کمیونٹی کی طرف سے متعین کی جاتی ہیں۔" + description: + one: "یہاں%{count} جواب ہے۔" + other: "یہاں%{count} جوابات ہیں۔" description_time_MF: "وہاں {replyCount, plural, one { # جواب ہے} other { # جوابات ہیں}} ایک متوقع مطالعہ {readingTime, plural, one {# منٹ} other {# منٹ}}۔" enable: "اس ڑاپک کا خلاصہ کریں" disable: "تمام پوسٹیں دکھائیں" + short_label: "خلاصہ" + short_title: "اس موضوع کا خلاصہ دکھائیں: سب سے دلچسپ پوسٹس جیسا کہ کمیونٹی کے ذریعہ طے کیا گیا ہے۔" deleted_filter: enabled_description: "اِس ٹاپک میں حذف شدہ پوسٹ شامل ہیں، جو چھپا دی گئی ہیں۔" disabled_description: "ٹاپک کی حذف شدہ پوسٹ دکھائی گئی ہیں۔" enable: "حذف شدہ پوسٹس کو چھپائیں" - disable: "حذف شدہ پوسٹس کو ظاہر کریں" + disable: "حذف شدہ پوسٹس دکھائیں" private_message_info: title: "پیغام" invite: "دوسروں کو مدعو کریں..." @@ -1463,10 +1688,12 @@ ur: disclaimer: "رجسٹر کرنے پر آپ پرائیوِیسی پالیسی اور سروس کی شرائط سے اتفاق کرتے ہیں۔" title: "اپنا اکاؤنٹ بنائیں" failed: "کچھ غلط ہو گیا، شاید یہ ای میل پہلے ہی سے رجسٹرڈ ہے، پاسورڈ بھول جانے والا لنک اِستعمال کر کے دیکھیں" + associate: "پہلے سے ہی اکاؤنٹ ہے؟ اپنے %{provider} اکاؤنٹ کو لنک کرنے کے لیے لاگ ان کریں۔" forgot_password: title: "پاسورڈ رِی سَیٹ" action: "میں اپنا پاسورڈ بھول گیا" invite: "اپنا صارف نام یا اِی میل ایڈریس درج کریں، اور ہم آپ کو ایک پاسورڈ رِی سَیٹ ایمیل بھیج دیں گے۔" + invite_no_username: "اپنا ای میل ایڈریس درج کریں، اور ہم آپ کو پاس ورڈ دوبارہ ترتیب دینے کا ای میل بھیجیں گے۔" reset: "پاسورڈ رِی سَیٹ کریں" complete_username: "اگر کوئی اکاؤنٹ %{username} سے میچ کرتا ہو گا، تو آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" complete_email: "اگر کوئی اکاؤنٹ %{email} سے ملتا ہو گا، تو آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" @@ -1539,25 +1766,28 @@ ur: not_approved: "آپ کا اکاؤنٹ ابھی تک منظور نہیں کیا گیا ہے۔ جب آپ لاگ اِن کر سکیں گو اِی میل کے ذریعے آپ کو مطلع کر دیا جائے گا۔" google_oauth2: name: "گُوگَل" - title: "گوگل سے" twitter: name: "Twitter" - title: "ٹویٹر سے" instagram: name: "اِنسٹاگرام" - title: "اِنسٹاگرام سے" + title: "انسٹاگرام کے ساتھ لاگ ان کریں" + sr_title: "انسٹاگرام کے ساتھ لاگ ان کریں" facebook: name: "فَیسبُک" - title: "فیس بک سے" + title: "فیس بک کے ساتھ لاگ ان کریں" + sr_title: "فیس بک کے ساتھ لاگ ان کریں" github: name: "گِٹ ہَب" - title: "گِٹ ہَب سے" + title: "گٹ ہب کے ساتھ لاگ ان کریں" + sr_title: "گٹ ہب کے ساتھ لاگ ان کریں" discord: name: "ڈِسکَورڈ" - title: "ڈِسکَورڈ کے ساتھ" + title: "ڈسکورڈ کے ساتھ لاگ ان کریں" + sr_title: "ڈسکورڈ کے ساتھ لاگ ان کریں" second_factor_toggle: totp: "بجائے ایک اَوتھینٹیکَیٹر اَیپ کا استعمال کریں" backup_code: "بجائے ایک بیک اپ کوڈ استعمال کریں" + security_key: "اس کے بجائے سیکیورٹی کی استعمال کریں۔" invites: accept_title: "دعوت نامہ" emoji: "لفافہ اموجی" @@ -1569,7 +1799,6 @@ ur: success: "آپ کا اکاؤنٹ بنا دیا گیا ہے اور اب آپ لاگ اِن کر سکتے ہیں۔" name_label: "نام" password_label: "پاسورڈ" - optional_description: "(اختیاری)" password_reset: continue: "%{site_name} پر جاری رکھیں" emoji_set: @@ -1586,6 +1815,7 @@ ur: categories_and_top_topics: "زمرہ جات اور ٹاپ ٹاپکس" categories_boxes: "ذیلی زمرے والے خانے" categories_boxes_with_topics: "نمایاں ٹاپک والے خانے" + subcategories_with_featured_topics: "نمایاں عنوانات کے ساتھ ذیلی زمرہ جات" shortcut_modifier_key: shift: "شفٹ" ctrl: "Ctrl" @@ -1594,6 +1824,9 @@ ur: conditional_loading_section: loading: لوڈ ہو رہا ہے... category_row: + topic_count: + one: "اس زمرے میں %{count} موضوع" + other: "اس زمرے میں %{count} موضوعات" plus_subcategories_title: one: "%{name} اور ایک ذیلی زمرہ" other: "%{name} اور %{count} ذیلی زمرہ جات" @@ -1601,11 +1834,14 @@ ur: one: "+ %{count} ذیلی زمرہ" other: "+ %{count} ذیلی زمرہ جات" select_kit: - delete_item: "%{name} حذف کریں" + delete_item: "%{name} مٹائیں" filter_by: "فلٹر بذریعہ: %{name}" select_to_filter: "فلٹر کرنے کے لئے ایک قدر منتخب کریں" default_header_text: منتخب کریں... no_content: کوئی میل نہیں ملے + results_count: + one: "%{count} نتیجہ" + other: "%{count} نتائیج" filter_placeholder: تلاش کریں... filter_placeholder_with_any: تلاش کریں یا تخلیق کریں... create: "'%{content}' بنائیں" @@ -1666,16 +1902,39 @@ ur: similar_topics: "آپ کا ٹاپک ملتا ہے..." drafts_offline: "ڈرافٹس آف لائن" edit_conflict: "ترامیم میں تصادم" + group_mentioned_limit: + one: "انتباہ! آپ نے %{group}کا تذکرہ کیا، تاہم اس گروپ میں ایڈمنسٹریٹر کی تشکیل کردہ ذکر کی حد %{count} صارف سے زیادہ ہے۔ کسی کو اطلاع نہیں دی جائے گی۔" + other: "انتباہ! آپ نے %{group}کا تذکرہ کیا، تاہم اس گروپ میں ایڈمنسٹریٹر کی تشکیل کردہ ذکر کی حد %{count} صارفین سے زیادہ ہے۔ کسی کو اطلاع نہیں دی جائے گی۔" group_mentioned: one: "%{group} کا ذکر کر کہ، آپ %{count} شخص کو مطلع کرنے لگے ہیں - کیا آپ واقعی یہ کرنا چاہتے ہیں؟" other: "%{group} کا ذکر کر کہ، آپ %{count} لوگوں کو مطلع کرنے لگے ہیں - کیا آپ واقعی یہ کرنا چاہتے ہیں؟" + cannot_see_mention: + category: "آپ نے @%{username} کا ذکر کیا ہے لیکن انہیں مطلع نہیں کیا جائے گا کیونکہ انہیں اس زمرے تک رسائی نہیں ہے۔ آپ کو انہیں ایسے گروپ میں شامل کرنے کی ضرورت ہوگی جسے اس زمرے تک رسائی حاصل ہو۔" + private: "آپ نے @%{username} کا ذکر کیا ہے لیکن انہیں مطلع نہیں کیا جائے گا کیونکہ وہ اس ذاتی پیغام کو دیکھنے سے قاصر ہیں۔ آپ کو انہیں اس ذاتی پیغام میں مدعو کرنے کی ضرورت ہوگی۔" + muted_topic: "آپ نے @%{username} کا ذکر کیا لیکن انہیں مطلع نہیں کیا جائے گا کیونکہ انہوں نے اس موضوع کو خاموش کر دیا ہے۔" + not_allowed: "آپ نے @%{username} کا ذکر کیا لیکن انہیں مطلع نہیں کیا جائے گا کیونکہ انہیں اس موضوع پر مدعو نہیں کیا گیا تھا۔" + here_mention: + one: "@%{here}کا ذکر کرکے، آپ %{count} صارف کو مطلع کرنے والے ہیں - کیا آپ کو یقین ہے؟" + other: "@%{here}کا ذکر کرکے، آپ %{count} صارفین کو مطلع کرنے والے ہیں - کیا آپ کو یقین ہے؟" duplicate_link: "لگتا ہے کہ آپ کا %{domain} لنک @%{username} نے %{ago} کو ٹاپک میں پہلے سے ہی ایک جواب میں پوسٹ کر دیا تھا - کیا آپ واقعی یہ دوبارہ پوسٹ کرنا چاہتے ہیں؟" reference_topic_title: "RE: %{title}" error: title_missing: "عنوان درکار ہے" + title_too_short: + one: "عنوان کم از کم %{count} حرف کا ہونا چاہیے۔" + other: "عنوان کم از کم %{count} حروف کا ہونا چاہیے۔" + title_too_long: + one: "عنوان %{count} حروف سے زیادہ نہیں ہو سکتا" + other: "عنوان %{count} حروف سے زیادہ نہیں ہو سکتا" post_missing: "پوسٹ خالی نہیں ہو سکتی" + post_length: + one: "پوسٹ کم از کم %{count} حرف کی ہونی چاہیے" + other: "پوسٹ کم از کم %{count} حروف کی ہونی چاہیے۔" try_like: "کیا آپ نے %{heart} بٹن اِستعمال کیا ہے؟" category_missing: "ایک زمرہ کا انتخاب کرنا ضروری ہے" + tags_missing: + one: "آپ کو کم از کم %{count} ٹیگ کا انتخاب کرنا چاہیے" + other: "آپ کو کم از کم %{count} ٹیگز کا انتخاب کرنا چاہیے" topic_template_not_modified: "براہ مہربانی ٹاپک ٹَیمپلیٹ میں ترمیم کرکے اپنے ٹاپک میں تفصیلات اور مخصوصیات شامل کریں۔" save_edit: "ترمیم محفوظ کریں" overwrite_edit: "دوسری ترمیم کے اوپر لکھ ڈالیں" @@ -1688,6 +1947,7 @@ ur: create_whisper: "سرگوشی" create_shared_draft: "مشترکہ ڈرافٹ بنائیں" edit_shared_draft: "مشترکہ ڈرافٹ ترمیم کریں" + title: "یا %{modifier} دبائیں۔" users_placeholder: "ایک صارف شامل کریں" title_placeholder: "ایک مختصر جملہ میں بتائیے کہ یہ بحث کس چیز کے بارے میں ہے؟" title_or_link_placeholder: "عنوان ٹائپ کریں، یا ایک لنک یہاں پیسٹ کریں" @@ -1757,6 +2017,7 @@ ur: confirm: آپ کے پاس ایک نیا عنوان ڈرافٹ محفوظ ہوا ہے ، اگر آپ کوئی لنکڈ ٹاپک تیار کرتے ہیں تو اسے اوور رائٹ کردیا جائے گا۔ reply_as_new_group_message: label: نئے گروپ پیغام کے بطور جواب دیں + desc: ایک ہی وصول کنندگان سے شروع ہونے والا نیا پیغام بنائیں reply_as_private_message: label: نیا پیغام desc: نیا ذاتی پیغام بنائیں @@ -1776,6 +2037,8 @@ ur: desc: "تازہ ترین جواب کی تاریخ تبدیل کیے بغیر جواب دیں" reload: "دوبارہ لوڈ" ignore: "نظر انداز کریں" + image_alt_text: + aria_label: تصویر کے لیے متبادل متن notifications: tooltip: regular: @@ -1811,7 +2074,7 @@ ur: private_message: "%{username} %{description}" invited_to_private_message: "

    %{username} %{description}" invited_to_topic: "%{username} %{description}" - invitee_accepted: "%{username}نے آپ کی دعوت قبول کرلی " + invitee_accepted: "%{username}نے آپ کی دعوت قبول کرلی" moved_post: "%{username}نے %{description} منتقل کر دیا" linked: "%{username} %{description}" granted_badge: "'%{description}' حاصل کیا" @@ -1823,7 +2086,11 @@ ur: other: "%{group_name}' کے %{count} کھلی رکنیت کی درخواستیں" reaction: "%{username} %{description}" reaction_2: "%{username}, %{username2} %{description}" + votes_released: "%{description} - مکمل" dismiss_confirmation: + body: + one: "کیا تمہیں یقین ہے؟ آپ کیلیے %{count} اہم اطلاع ہے۔" + other: "کیا تمہیں یقین ہے؟ آپ کیلیے %{count} اہم اطلاعات ہیں۔" dismiss: "بر خاست کریں" cancel: "منسوخ" group_message_summary: @@ -1853,6 +2120,8 @@ ur: posted: "نئی پوسٹ" moved_post: "پوسٹ منتقل کر دی گئی" linked: "لنک کردہ" + bookmark_reminder: "بک مارک یاد دہانی" + bookmark_reminder_with_name: "بک مارک یاد دہانی - %{name}" granted_badge: "بَیج عطا کیا گیا" invited_to_topic: "ٹاپک کیلئے دعوت دی گئی" group_mentioned: "گروپ کا ذکر کیا گیا" @@ -1861,8 +2130,12 @@ ur: topic_reminder: "ٹاپک یاد دہانی" liked_consolidated: "نئے لائیکس" post_approved: "منظورشدہ پوسٹ" + membership_request_consolidated: "نئی رکنیت کی درخواستیں" + reaction: "نیا ردعمل" + votes_released: "ووٹ جاری کر دیا گیا" upload_selector: uploading: "اَپ لوڈ کیا جا رہا ہے" + processing: "پروسیسنگ اپ لوڈ" select_file: "فائل منتخب کریں" default_image_alt_text: تصویر search: @@ -1875,6 +2148,9 @@ ur: select_all: "تمام منتخب کریں" clear_all: "تمام کو صاف کریں" too_short: "آپ کا سَرچ ٹَرم بہت مختصر ہے۔" + open_advanced: "ایڈوانس تلاش کھولیں۔" + clear_search: "تلاش صاف کریں" + sort_or_bulk_actions: "ترتیب دیں یا بڑی تعداد میں نتائج منتخب کریں" result_count: one: "%{term}کیلئے %{count} نتیجہ" other: "%{term}کیلئے %{count}%{plus} نتائج" @@ -1891,19 +2167,42 @@ ur: search_google: "اس کے بجائے گُوگَل کے ساتھ تلاش کرنے کی کوشش کریں:" search_google_button: "گُوگَل" search_button: "تلاش کریں" + search_term_label: "تلاش کا مطلوبہ لفظ درج کریں" categories: "زُمرَہ جات" tags: "ٹیگز" + in: "میں" + in_this_topic: "اس موضوع میں" + in_this_topic_tooltip: "تمام عنوانات کی تلاش پر جائیں" + in_topics_posts: "تمام موضوعات اور پوسٹس میں" + enter_hint: "یا انٹر دبائیں" + in_posts_by: "%{username}کی طرف سے پوسٹس میں" + browser_tip: "%{modifier} + f" + browser_tip_description: "مقامی براؤزر کی تلاش کو دوبارہ استعمال کرنے کے لیے" + recent: "حالیہ تلاشیں" + clear_recent: "حالیہ تلاشیں صاف کریں" type: + default: "عنوانات/ پوسٹس" users: "صارفین" categories: "زُمرَہ جات" + categories_and_tags: "زمرہ جات/ٹیگز" context: user: "@%{username} کے حساب سے پوسٹس تلاش کریں" category: "#%{category} زُمرَہ میں تلاش کریں" + tag: "#%{tag} ٹیگ تلاش کریں" topic: "اِس ٹاپک میں تلاش کریں" private_messages: "پیغامات میں تلاش کریں" + tips: + category_tag: "زمرہ یا ٹیگ کے لحاظ سے فلٹرز" + author: "پوسٹ مصنف کے ذریعہ فلٹرز" + in: "میٹا ڈیٹا کےذریعہ سے فلٹرز (مثلاً ان: عنوان، ان:ذاتی، ان:پن کیاہوا)" + status: "موضوع کی حیثیت کے لحاظ سے فلٹرز" + full_search: "مکمل صفحہ کی تلاش کا آغاز" + full_search_key: "%{modifier} + درج کریں" advanced: + title: ایڈوانس فلٹرز posted_by: label: کی طرف سے پوسٹ کیا گیا + aria_label: پوسٹ مصنف کے لحاظ سے فلٹر in_category: label: زمرہ میں ڈالا گیا in_group: @@ -1912,11 +2211,13 @@ ur: label: بَیج کے ساتھ with_tags: label: ٹیگ ہواوا + aria_label: ٹیگز کا استعمال کرتے ہوئے فلٹر filters: label: صرف وہ ٹاپک/پوسٹس دکھائیں جو... title: صرف عنوان سے ملتے ہوئے likes: جو میں نے لائیک کیے posted: جن میں میں نے پوسٹ کیا + created: میں نے بنایا watching: جو میں دیکھ رہا ہوں tracking: جو میں ٹریک کر رہا ہوں private: میرے پیغامات میں ہوں @@ -1926,28 +2227,46 @@ ur: seen: جو میں نے پڑھ لیے ہوں unseen: جو میں نے نہ پڑھا ہو wiki: جو وِیکی ہو - images: تصاویر شامل کریں + images: تصویر(یں) شامل کریں all_tags: تمام درجِ بالا ٹیگز statuses: label: جہاں ٹاپک open: کھلے ہوں closed: بند ہوں + public: عوامی ہیں archived: آر کائیو کیے ہوں noreplies: کے صفر جوابات ہوں single_user: صرف ایک صارف پر مشتمل ہوں post: count: label: پوسٹ + min: + placeholder: کم از کم + aria_label: پوسٹس کی کم از کم تعداد کے حساب سے فلٹر + max: + placeholder: زیادہ سے زیادہ + aria_label: پوسٹس کی زیادہ سے زیادہ تعداد کے حساب سے فلٹر time: label: پوسٹ کیا + aria_label: پوسٹ کی گئی تاریخ کے حساب سے فلٹر before: سے پہلے after: کے بعد views: label: وِیوز + min_views: + placeholder: کم از کم + aria_label: کم از کم آراء سے فلٹر + max_views: + placeholder: زیادہ سے زیادہ + aria_label: زیادہ سے زیادہ آراء سے فلٹر + additional_options: + label: "پوسٹ کی گنتی اور موضوع کے ملاحظات کے لحاظ سے فلٹر" + hamburger_menu: "مینیو" new_item: "نیا" go_back: "واپس جائیں" not_logged_in_user: "موجودہ سرگرمی کے خلاصہ اور ترجیحات کے ساتھ صفحہِ صارف" current_user: "اپنے صفحہِ صارف پر جائیں" + view_all: "تمام %{tab}دیکھیں" topics: new_messages_marker: "آخری وزٹ" bulk: @@ -1956,18 +2275,30 @@ ur: unlist_topics: "ٹاپکس کو فہرست سے ہٹائیں" relist_topics: "ٹاپک دوبارہ فہرست کریں" defer: "ملتوی کریں" - delete: "ٹاپکس حذف کریں" + delete: "ٹاپکس مٹائیں" dismiss: "بر خاست کریں" dismiss_read: "تمام نہ پڑھے گئے کو بر خاست کریں" + dismiss_read_with_selected: + one: "%{count} بغیر پڑھے ہوئے کو مسترد کریں" + other: "%{count} بغیر پڑھے ہوئے کو مسترد کریں" dismiss_button: "بر خاست کریں..." + dismiss_button_with_selected: + one: "مستردکریں (%{count})…" + other: "مستردکریں (%{count})…" dismiss_tooltip: "صرف نئی پوسٹس برخاست کریں یا ٹاپکس کو ٹریک کرنے سے رک جائیں" also_dismiss_topics: "ان ٹاپکس کو ٹریک کرنے سے رک جائیں تاکہ وہ کبھی دوبارہ میرے لیے \"نہ پڑھے گئے\"میں نظر نہ آئیں" dismiss_new: "نیا برخاست کریں" + dismiss_new_with_selected: + one: "نیا (%{count}) مسترد کریں" + other: "نیا (%{count}) مسترد کریں" toggle: "ٹاپکس کے بَلک انتخاب کو ٹَوگل کریں" actions: "بَلک عمل" + change_category: "زمرہ مقرر کریں..." close_topics: "ٹاپکس بند کریں" archive_topics: "ٹاپکس آر کائیو کریں" move_messages_to_inbox: "اِنباکس میں منتقل کریں" + notification_level: "اطلاعات..." + change_notification_level: "نوٹیفیکیشن لیول تبدیل کریں" choose_new_category: "ٹاپکس کیلئے نئے زمرہ کا انتخاب کریں:" selected: one: "آپ نے %{count} ٹاپک منتخب کیا ہے۔" @@ -1977,20 +2308,33 @@ ur: choose_new_tags: "ان ٹاپکس کیلئے نئے ٹیگز کا انتخاب کریں:" choose_append_tags: "ان ٹاپکس پر اضافہ کرنے کیلئے نئے ٹیگز کا انتخاب کریں:" changed_tags: "ان ٹاپکس کے ٹیگز تبدیل کر دیے گئے تھے۔" + remove_tags: "تمام ٹیگز ہٹا دیں" + confirm_remove_tags: + one: "اس موضوع سے تمام ٹیگز ہٹا دیے جائیں گے۔ کیا آپ کو یقین ہے؟" + other: "تمام ٹیگز کو %{count} عنوانات سے ہٹا دیا جائے گا۔ کیا آپ کو یقین ہے؟" + progress: + one: "پیش رفت: %{count} موضوع" + other: "پیش رفت: %{count} موضوعات" none: unread: "آپ کے پاس کوئی بغیر پڑھے ٹاپک موجود نہیں۔" + unseen: "آپ کے لیے کوئی ان دیکھے موضوعات نہیں ہیں۔" new: "آپ کے پاس کوئی نئے ٹاپک موجود نہیں۔" read: "ابھی تک آپ نے کوئی ٹاپکس نہیں پڑھے۔" posted: "ابھی تک آپ نے کسی ٹاپک میں پوسٹ نہیں کیا۔" + latest: "آپ مکمل طور پر آگاہ ہیں" bookmarks: "ابھی تک آپ کے بُک مارک کیے ہوے کوئی ٹاپک نہیں ہیں۔" category: "کوئی %{category} کے ٹاپک موجود نہیں ہیں۔" top: "کوئی ٹاپ ٹاپک موجود نہیں ہیں۔" + educate: + new: '

    آپ کے نئے عنوانات یہاں ظاہر ہوں گے۔ پہلے سے طے شدہ طور پر، عنوانات کو نئے تصور کیا جاتا ہے اور اگر وہ پچھلے 2 دنوں میں بنائے گئے تھے تو وہ دکھائے گا۔

    اسے تبدیل کرنے کے لیے اپنی ترجیحات دیکھیں۔

    ' + unread: "

    آپ کے بغیر پڑھے ہوئے موضوعات یہاں ظاہر ہوتے ہیں۔

    پہلے سے طے شدہ طور پر، عنوانات کو بغیر پڑھے ہوئے سمجھا جاتا ہے اور یہ بغیر پڑھے ہوئے شماروں کو دکھائے گا اگر آپ:

    موضوع بنایا
    • موضوع کا جواب دیا
    • موضوع

    نے واضح طور پر ہر عنوان میں \U0001F514 کے ذریعے موضوع کو ٹریک یا دیکھا گیا پر سیٹ کریں۔

    اسے تبدیل کرنے کے لیے اپنی ترجیحات دیکھیں۔

    " bottom: latest: "کوئی مزید تازہ ترین ٹاپک موجود نہیں ہیں۔" posted: "کوئی مزید پوسٹ کیے گئے ٹاپک موجود نہیں ہیں۔" read: "کوئی مزید پڑھ لیے گئے ٹاپک موجود نہیں ہیں۔" new: "کوئی مزید نئے ٹاپک موجود نہیں ہیں۔" unread: "کوئی مزید بغیر پڑھے ٹاپک موجود نہیں ہیں۔" + unseen: "مزید ان دیکھے موضوعات نہیں ہیں۔" category: "مزید کوئی %{category} کے ٹاپک موجود نہیں۔" tag: "مزید کوئی %{tag} کے ٹاپک موجود نہیں۔" top: "مزید کوئی ٹاپ ٹاپک موجود نہیں۔" @@ -2044,18 +2388,30 @@ ur: back_to_list: "واپس ٹاپک فہرست پر" options: "ٹاپک اختیارات" show_links: "اِس ٹاپک کے اندر موجود لنکس دکھایں" + collapse_details: "موضوع کی تفصیلات کوسکیڑیں" + expand_details: "موضوع کی تفصیلات کو پھیلائیں" read_more_in_category: "مزید پڑھنا چاہتے ہیں؟ %{catLink} اور %{latestLink} میں دوسرے ٹاپک دیکھیں۔" read_more: "مزید پڑھنا چاہتے ہیں؟ %{catLink} یا %{latestLink}۔" unread_indicator: "کسی ممبر نے ابھی تک اِس ٹاپک کی آخری پوسٹ نہیں پڑھی ہے۔" + bumped_at_title_MF: "{FIRST_POST}: {CREATED_AT}\n{LAST_POST}: {BUMPED_AT}" browse_all_categories: تمام زمرہ جات براؤز کریں + browse_all_tags: تمام ٹیگز براؤز کریں view_latest_topics: تازہ ترین ٹاپک دیکھیے + suggest_create_topic: نئی گفتگو شروع کرنے کے لیے تیار ہیں؟ jump_reply_up: اِس سے پرانے جواب پر جائیں jump_reply_down: اِس سے نئے جواب پر جائیں - deleted: "ٹاپک حذف کردیا گیا ہے" + deleted: "ٹاپک مٹادیا گیا ہے" slow_mode_update: + title: "سست موڈ" + select: "صارفین اس موضوع میں پوسٹ کر سکتے ہیں اس عرصہ میں صرف ایک بار :" + description: "تیزی سے چلنے والی یا متنازعہ بحثوں میں سوچ بچار کو فروغ دینے کے لیے، صارفین کو اس موضوع پر دوبارہ پوسٹ کرنے سے پہلے انتظار کرنا چاہیے۔" enable: "فعال کریں" + update: "اپ ڈیٹ" + enabled_until: "اس وقت تک فعال:" remove: "غیر فعال کریں" hours: "گھنٹے:" + minutes: "منٹ:" + seconds: "سیکنڈ:" durations: 10_minutes: "10 منٹ" 15_minutes: "15 منٹ" @@ -2104,6 +2460,8 @@ ur: title: "اشاعت شیڈول کریں" temp_open: title: "عارضی طور پر کھولیں" + auto_reopen: + title: "خودکار کھلا موضوع" temp_close: title: "عارضی طور پر بند کریں" auto_close: @@ -2114,22 +2472,22 @@ ur: auto_close_after_last_post: title: "آخری پوسٹ کے بعد موضوع از خود بند ہونے والا ہے" auto_delete: - title: "خود کار طریقے سے ٹاپک حذف کریں" + title: "ٹاپک خودبخود مٹائیں" auto_bump: title: "خود کار طریقے سے ٹاپک بَمپ کریں" reminder: title: "مجھے یاد دہانی کرائیں" auto_delete_replies: - title: "جوابات خودبخود حذف کریں" + title: "جوابات خودبخود مٹائیں" status_update_notice: auto_open: "یہ ٹاپک خود کار طریقے سے کھول دیا جائے گا %{timeLeft}۔" auto_close: "یہ ٹاپک خود کار طریقے سے بند کر دیا جائے گا %{timeLeft}۔" auto_publish_to_category: "یہ ٹاپک #%{categoryName} میں شائع کر دیا جائے گا %{timeLeft}۔" auto_close_after_last_post: "یہ ٹاپک آخری جواب کے %{duration} بعد بند ہو جائے گا۔" - auto_delete: "یہ ٹاپک خود کار طریقے سے حذف کر دیا جائے گا %{timeLeft}۔" + auto_delete: "یہ ٹاپک خودبخود مٹادیا جائے گا %{timeLeft}۔" auto_bump: "یہ ٹاپک خود کار طریقے سے بَمپ کر دیا جائے گا %{timeLeft}۔" auto_reminder: "آپ کو اس ٹاپک کے بارے میں یاد دہانی کرائی جائے گا %{timeLeft}۔" - auto_delete_replies: "اس موضوع پر جوابات %{duration}کے بعد خود بخود حذف ہوجاتے ہیں۔" + auto_delete_replies: "اس موضوع پر جوابات %{duration}کے بعد خود بخودمٹادیےجاتے ہیں۔" auto_close_title: "خود کار طریقے سے بند کر نے کی سیٹِنگ" auto_close_immediate: one: "ٹاپک کی آخری پوسٹ ابھی سے %{count} گھنٹا پرانی ہے، اِسلیئے ٹاپک فوری طور پر بند کر دیا جائے گا۔" @@ -2140,13 +2498,16 @@ ur: timeline: back: "واپس" back_description: "آپنی آخری بغیر پڑھی پوسٹ پر واپس جائیں" - replies_short: "%{current}/ %{total} " + replies_short: "%{current}/ %{total}" progress: title: ٹاپک پیش رَفت jump_prompt: "پر جائیں..." + jump_prompt_of: + one: "کا %{count} پوسٹ" + other: "کا %{count} پوسٹس" jump_prompt_long: "پر جائیں..." jump_prompt_to_date: "اِس تاریخ تک" - jump_prompt_or: "یا " + jump_prompt_or: "یا" notifications: title: آپ کو اس ٹاپک کے بارے میں کتنی بار مطلع کیا جاتا ہے، تبدیل کریں reasons: @@ -2196,10 +2557,11 @@ ur: actions: title: "عوامل" recover: "ٹاپک غیر حذف کریں" - delete: "ٹاپک حذف کریں" + delete: "ٹاپک مٹائیں" open: "ٹاپک کھولیں" close: "ٹاپک بند کریں" multi_select: "پوسٹس منتخب کریں..." + slow_mode: "سلو موڈ سیٹ کریں..." timed_update: "ٹاپک ٹائمر مقرر کریں..." pin: "ٹاپک پِن کریں..." unpin: "ٹاپک سے پِن ہٹائیں..." @@ -2208,21 +2570,27 @@ ur: invisible: "غیر مندرج بنائیں" visible: "مندرج بنائیں" reset_read: "\"پڑھ لیا گیا\" کا ڈیٹا رِی سَیٹ کریں" + make_public: "عوامی موضوع بنائیں..." make_private: "ذاتی پیغام بنائیں" reset_bump_date: "بَمپ تاریخ رِی سَیٹ کریں" feature: pin: "ٹاپک پِن کریں" unpin: "ٹاپک سے پِن ہٹائیں" pin_globally: "عالمی سطح پر ٹاپک پِن کریں" + make_banner: "بینر کا موضوع بنائیں" remove_banner: "بینر ٹاپک ہٹائیں" reply: title: "جواب" help: "اس ٹاپک کا جواب لکھنا شروع کریں" share: + title: "موضوع کا اشتراک کریں" extended_title: "لِنک شیئر کریں" help: "اِس ٹاپک کا لنک شئیر کریں" instructions: "اس عنوان سے ایک لنک شیئر کریں:" copied: "عنوان کا لنک کاپی کیا گیا۔" + restricted_groups: + one: "صرف گروپ کے ممبران کونظر آتا ہے: %{groupNames}" + other: "صرف گروپس کے ممبران کو نظر آتا ہے: %{groupNames}" invite_users: "دعوت دیں" print: title: "پرنٹ" @@ -2246,6 +2614,9 @@ ur: one: "فی الحال %{categoryLink} میں پِن ہوے ٹاپک: %{count}" other: "فی الحال %{categoryLink} میں پِن ہوے ٹاپک: %{count}" pin_globally: "اِس ٹاپک کو تمام ٹاپکس کی فہرست کے سب سے اُپر ظاہر کریں جب تک کہ" + confirm_pin_globally: + one: "آپ کے پاس پہلے سے ہی عالمی سطح پر %{count} پن کیا ہوا موضوع ہے۔ نئے اور گمنام صارفین کے لیے بہت زیادہ پن کیے ہوئے عنوانات ایک بوجھ ثابت ہو سکتے ہیں۔ کیا آپ واقعی عالمی سطح پر کسی اور موضوع کو پن کرنا چاہتے ہیں؟" + other: "آپ کے پاس پہلے سے ہی عالمی سطح پر پن کیے ہوئے %{count} عنوانات ہیں۔ نئے اور گمنام صارفین کے لیے بہت زیادہ پن کیے ہوئے عنوانات ایک بوجھ ثابت ہو سکتے ہیں۔ کیا آپ واقعی عالمی سطح پر کسی اور موضوع کو پن کرنا چاہتے ہیں؟" unpin_globally: "اِس ٹاپک کو تمام ٹاپکس کی فہرست کے سب سے اُپر ظاہر کرنے سے ہٹائیں" unpin_globally_until: "اِس ٹاپک کو تمام ٹاپکس کی فہرست کے سب سے اُپر ظاہر کرنے سے ہٹائیں یا %{until} تک اِنتظار کریں۔" global_pin_note: "صارفین خود سے انفرادی طور پر ٹاپک سے پن ہٹا سکتے ہیں۔" @@ -2381,26 +2752,32 @@ ur: select_below: label: "منتخب+ذیل میں" title: "منتخب کیے ہوے میں پوسٹ اور اِس کے بعد تمام شامل کریں" - delete: منتخب کیے ہوں کو حذف کریں + delete: منتخب کیے ہوے کو مٹائیں cancel: انتخاب کرنا منسوخ کریں select_all: تمام منتخب کریں deselect_all: تمام غیر منتخب کریں description: one: آپ نے %{count} پوسٹ منتخب کی ہے۔ other: "آپ نے %{count} پوسٹس منتخب کی ہیں۔" + deleted_by_author_simple: "(مضمون مصنف نے مٹایا ہے)" post: quote_reply: "اقتباس کریں" + quote_reply_shortcut: "یا q دبائیں" quote_edit: "ترمیم" + quote_edit_shortcut: "یا e دبائیں" quote_share: "شیئر" - edit_reason: "وجہ:" + edit_reason: "وجہ: " post_number: "پوسٹ %{number}" ignored: "نظر انداز کردہ مواد" + wiki_last_edited_on: "%{dateTime}پر ویکی میں آخری ترمیم" + last_edited_on: "پوسٹ آخری بار %{dateTime}کو ترمیم کی گئی" reply_as_new_topic: "منسلک ٹاپک کے طور پر جواب دیں" reply_as_new_private_message: "اُنہی وصول کنندگان کو نئے پیغام کے طور پر جواب دیں" continue_discussion: "%{postLink} سے بحث جاری:" follow_quote: "اقتباس کی گئی پوسٹ پر جائیں" show_full: "مکمل پوسٹ دکھائیں" show_hidden: "نظر انداز کردہ مواد دکھایں۔" + deleted_by_author_simple: "(پوسٹ مصنف نے مٹایا ہے)" collapse: "بند کریں" expand_collapse: "کھولیں/بند کریں" locked: "ایک اسٹاف کے رکن نے اِس پوسٹ کو ترمیم ہونے سے روک دیا ہے" @@ -2414,25 +2791,43 @@ ur: has_replies: one: "%{count} جواب" other: "%{count} جوابات" + has_replies_count: "%{count}" + unknown_user: "(نامعلوم / حذف شدہ صارف)" has_likes_title: one: "%{count} شخص نے اِس پوسٹ کو لائیک کیا" other: "%{count} لوگوں نے اِس پوسٹ کو لائیک کیا" - has_likes_title_only_you: " آپ نے اِس پوسٹ کو لائیک کیا" + has_likes_title_only_you: "آپ کو یہ پوسٹ پسند آئی" has_likes_title_you: one: "آپ اور %{count} دوسرے شخص نے اِس پوسٹ کو لائیک کیا" other: "آپ اور %{count} دوسرے لوگوں نے اِس پوسٹ کو لائیک کیا" + filtered_replies_hint: + one: "یہ پوسٹ اور اس کا جواب دیکھیں" + other: "یہ پوسٹ اور اس کے %{count} جوابات دیکھیں" + filtered_replies_viewing: + one: "%{count} جواب دیکھیں" + other: "%{count} جوابات دیکھیں" + in_reply_to: "پیرنٹ پوسٹ لوڈ کریں۔" + view_all_posts: "تمام پوسٹس دیکھیں" errors: create: "معذرت، آپ کی پوسٹ بنانے میں ایک خرابی کا سامنا کرنا پڑا۔ براہ مہربانی دوبارہ کوشش کریں۔" edit: "معذرت، آپ کی پوسٹ ترمیم کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ براہ مہربانی دوبارہ کوشش کریں۔" upload: "معذرت، یہ فائل اَپ لوڈ کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ براہ مہربانی دوبارہ کوشش کریں۔" file_too_large: "معذرت، یہ فائل بہت بڑی ہے (زیادہ سے زیادہ سائز %{max_size_kb}kb) ہے۔ کیوں نہ آپ اپنی بڑی فائل ایک کلاؤڈ شیئرنگ سروس پر اَپ لوڈ کریں اور اس کا لنک پَیسٹ کریں؟" + file_size_zero: "معذرت، ایسا لگتا ہے کہ کچھ غلط ہو گیا ہے، آپ جس فائل کو اپ لوڈ کرنے کی کوشش کر رہے ہیں وہ 0 بائٹس ہے۔ دوبارہ کوشش کریں." + file_too_large_humanized: "معذرت، وہ فائل بہت بڑی ہے (زیادہ سے زیادہ سائز ہے %{max_size})۔ کیوں نہ اپنی بڑی فائل کو کلاؤڈ شیئرنگ سروس پر اپ لوڈ کریں، پھر لنک پیسٹ کریں؟" too_many_uploads: "معذرت، آپ ایک وقت میں صرف ایک ہی فائل اَپ لوڈ کر سکتے ہیں۔" + too_many_dragged_and_dropped_files: + one: "معذرت، آپ ایک وقت میں صرف %{count} فائل اپ لوڈ کر سکتے ہیں۔" + other: "معذرت، آپ ایک وقت میں صرف %{count} فائلیں اپ لوڈ کر سکتے ہیں۔" upload_not_authorized: "معذرت، جو فائل آپ اَپ لوڈ کرنے کے کوشش کر رہے ہیں اُس کی اجازت نہیں ہے (اجازت یافتہ ایکسٹینشنز: %{authorized_extensions})۔" image_upload_not_allowed_for_new_user: "معذرت، نئے صارفین تصاویر اَپ لوڈ نہیں کر سکتے۔" attachment_upload_not_allowed_for_new_user: "معذرت، نئے صارفین اٹیچمنٹس اَپ لوڈ نہیں کر سکتے۔" attachment_download_requires_login: "معذرت، اٹیچمنٹس ڈائونلوڈ کرنے کیلیے آپ کا لاگ اِن ہونا ضروری ہے۔" cancel_composer: + confirm: "آپ اپنی پوسٹ کے ساتھ کیا کرنا چاہیں گے؟" discard: "منسوخ" + save_draft: "بعد کے لیے ڈرافٹ محفوظ کریں" + keep_editing: "ترمیم کرتے رہیں" via_email: "یہ پوسٹ بذریعہ اِی میل پہنچی" via_auto_generated_email: "یہ پوسٹ ایک خود کار طریقے سے تخلیق کردہ اِی میل کے ذریعے پہنچی" whisper: "یہ پوسٹ ماڈریٹرز کے لئے ایک نجی وِھسپر ہے" @@ -2441,20 +2836,20 @@ ur: few_likes_left: "محبت شیئر کرنے کے لئے شکریہ! آپ کے پاس آج کے لئے صرف چند لائیکس بچے ہیں۔" controls: reply: "اس پوسٹ کا جواب لکھنا شروع کریں" - like: " اِس پوسٹ کو لائیک کریں" - has_liked: " آپ اِس پوسٹ کو لائیک کر چکے ہیں" + like: "اس پوسٹ کو پسند کریں" + has_liked: "آپ کو یہ پوسٹ پسند آئی ہے" read_indicator: "ممبران جنہوں نے اِس پوسٹ کو پڑھ لیا" undo_like: "لائیک کالعدم کریں" - edit: " اِس پوسٹ کو ترمیم کریں" + edit: "اس پوسٹ میں ترمیم کریں" edit_action: "ترمیم" edit_anonymous: "معذرت، اِس پوسٹ کو ترمیم کرنے کیلیے آپ کا لاگ اِن ہونا ضروری ہے۔" flag: "اس پوسٹ پر توجہ کے لئے اِسے نجی طور پر فلَیگ کریں یا اس کے بارے میں ایک نجی نوٹیفکیشن بھیجیں" - delete: "اِس پوسٹ کو حذف کریں" - undelete: " اِس پوسٹ کو واپس لائیں" + delete: "اِس پوسٹ کومٹائیں" + undelete: "اس پوسٹ کو غیر حذف کریں" share: "اس پوسٹ کا لنک شیئرکریں" - more: "مزید " + more: "مزید" delete_replies: - confirm: "کیا آپ اِس پوسٹ پر جوابات بھی حذف کرنا چاہتے ہیں؟" + confirm: "کیا آپ اِس پوسٹ پر جوابات بھی مٹانا چاہتے ہیں؟" direct_replies: one: "جی ہاں، اور براہ راست %{count} جواب" other: "جی ہاں، اور براہ راست %{count} جوابات" @@ -2463,6 +2858,8 @@ ur: other: "جی ہاں، اور تمام %{count} جوابات" just_the_post: "نہیں، صرف یہ پوسٹ" admin: "پوسٹ ایڈمن کے ایکشن" + permanently_delete: "مستقل طور پر مٹائیں" + permanently_delete_confirmation: "کیا آپ واقعی اس پوسٹ کو مستقل طور پر مٹانا چاہتے ہیں؟ آپ اسے بازیافت نہیں کر پائیں گے۔" wiki: "وِیکی بنائیں" unwiki: "وِیکی ختم کریں" convert_to_moderator: "اسٹاف رنگ شامل کریں" @@ -2470,22 +2867,40 @@ ur: rebake: "HTML دوبارہ بِلڈ کریں" publish_page: "صفحہ اشاعت" unhide: "چھپانا ختم کریں" + change_owner: "ملکیت تبدیل کریں..." + grant_badge: "بیج دیں..." lock_post: "پوسٹ لاک کریں" lock_post_description: "شائع کرنے والے کو اِس پوسٹ میں ترمیم کرنے سے روک دیں" unlock_post: "پوسٹ کھول دیں" unlock_post_description: "شائع کرنے والے کو اِس پوسٹ میں ترمیم کرنے کی اجازت دیں" - delete_topic_disallowed_modal: "آپ کے پاس اِس ٹاپک کو حذف کرنے کی اجازت نہیں ہے۔ اگر آپ واقعی اِسے حذف کروانا چاہتے ہیں تو، استدلال کے ساتھ ماڈریٹر کی توجہ کیلئے فلَیگ جمع کریں۔" - delete_topic_disallowed: "آپ کے پاس اِس ٹاپک کو حذف کرنے کی اجازت نہیں ہے۔" - delete_topic: "ٹاپک حذف کریں" + delete_topic_disallowed_modal: "آپ کے پاس اِس ٹاپک کو مٹانے کی اجازت نہیں ہے۔ اگر آپ واقعی اِسے مٹانا چاہتے ہیں تو، استدلال کے ساتھ ماڈریٹر کی توجہ کیلئے نشان لگائیں۔" + delete_topic_disallowed: "آپ کے پاس اِس ٹاپک کو مٹانے کی اجازت نہیں ہے" + delete_topic_confirm_modal: + one: "اس موضوع کو فی الحال %{count} سے زیادہ دیکھا گیا ہے اور یہ ایک مقبول موضوع ہو سکتا ہے۔ کیا آپ واقعی اس موضوع کو بہتر بنانے کے لیے اس میں ترمیم کرنے کے بجائے اسے مکمل طور پر مٹانا چاہتے ہیں؟" + other: "اس موضوع پر فی الحال %{count} سے زیادہ آراء ہیں اور یہ ایک مقبول موضوع ہو سکتا ہے۔ کیا آپ واقعی اس موضوع کو بہتر بنانے کے لیے اس میں ترمیم کرنے کے بجائے اسے مکمل طور پر مٹانا چاہتے ہیں؟" + delete_topic_confirm_modal_yes: "ہاں، اس موضوع کو مٹائیں" + delete_topic_confirm_modal_no: "نہیں، اس موضوع کو رکھیں" + delete_topic_error: "اس موضوع کو مٹاتے وقت ایک خرابی پیش آگئی" + delete_topic: "ٹاپک مٹائیں" + add_post_notice: "اسٹاف نوٹس شامل کریں..." + change_post_notice: "اسٹاف نوٹس تبدیل کریں..." + delete_post_notice: "عملے کی آگاہی کو مٹائیں" remove_timer: "ٹائمر ہٹائیں" + edit_timer: "ٹائمر میں ترمیم کریں" actions: people: like: one: "اِس کو لائیک کیا" other: "اِس کو لائیک کیا" + read: + one: "یہ پڑھیں" + other: "یہ پڑھیں" like_capped: one: "اور %{count} دوسرے شخص نے اِس کو لائیک کیا" other: "اور %{count} دوسرے لوگوں نے اِس کو لائیک کیا" + read_capped: + one: "اور %{count} دیگر نے اسے پڑھا" + other: "اور %{count} دیگر نے اسے پڑھا" by_you: off_topic: "آپ نے اِس کو موضوع سے ہٹ کر ہونے کے طور پر فلَیگ کیا" spam: "آپ نے اِس کو سپَیم ہونے کے طور پر فلَیگ کیا" @@ -2494,8 +2909,8 @@ ur: notify_user: "آپ نے اِس صارف کو پیغام بھیجا" delete: confirm: - one: "کیا آپ واقعی اُس پوسٹ کو حذف کرنا چاہتے ہیں؟" - other: "کیا آپ واقعی ان %{count} پوسٹس کو حذف کرنا چاہتے ہیں؟" + one: "کیا آپ واقعی اُس پوسٹ کو مٹانا چاہتے ہیں؟" + other: "کیا آپ واقعی ان %{count} پوسٹس کو مٹانا چاہتے ہیں؟" merge: confirm: one: "کیا آپ واقعی ان پوسٹس کو ضم کرنا چاہتے ہیں؟" @@ -2508,6 +2923,7 @@ ur: last: "آخری رَوِیژن" hide: "رَوِیژن چھپائیں" show: "رَوِیژن دکھائیں" + revert: "نظر ثانی %{revision}پر واپس جائیں" edit_wiki: "وِیکی میں ترمیم کریں" edit_post: "پوسٹ میں ترمیم کریں" comparing_previous_to_current_out_of_total: "%{previous} %{icon} %{current} / %{total}" @@ -2533,9 +2949,41 @@ ur: title: "ای میل کے متن کا html حصہ دکھائیں" button: "HTML" bookmarks: + create: "بک مارک بنائیں" + create_for_topic: "موضوع کے لیے بُک مارک بنائیں" + edit: "بک مارک میں ترمیم" + edit_for_topic: "موضوع کے لیے بک مارک میں ترمیم" created: "بنایا گیا" + updated: "اپ ڈیٹ کیا" name: "نام" + name_placeholder: "یہ بک مارک کس لیے ہے؟" + set_reminder: "مجھے یاد دلانا" options: "اختیارات" + actions: + delete_bookmark: + name: "بک مارک مٹائیں" + description: "آپ کی پروفائل سے بک مارک ہٹاتا ہے اور بک مارک کے لئے تمام یاددہانی کو روک دیتا ہے" + edit_bookmark: + name: "بک مارک میں ترمیم" + description: "بک مارک کے نام میں ترمیم کریں یا یاد دہانی کی تاریخ اور وقت تبدیل کریں" + clear_bookmark_reminder: + name: "یاد دہانی مٹائیں" + description: "یاد دہانی کی تاریخ اور وقت مٹائیں" + pin_bookmark: + name: "بک مارک پن کریں" + description: "بک مارک پن کریں۔ یہ اسے آپ کے بُک مارکس کی فہرست میں سب سے اوپر ظاہر کرے گا۔" + unpin_bookmark: + name: "بُک مارک اَن پن کریں" + description: "بُک مارک کو اَن پن کریں۔ یہ اب آپ کے بُک مارکس کی فہرست کے اوپری حصے میں ظاہر نہیں ہوگا۔" + filtered_replies: + viewing_posts_by: "%{post_count} پوسٹس دیکھ رہے ہیں بذریعہ" + viewing_subset: "کچھ جوابات سمٹ گئے" + viewing_summary: "اس موضوع کا خلاصہ دیکھنا" + post_number: "%{username}، پوسٹ نمبر%{post_number}" + show_all: "تمام دکھائیں" + share: + title: "پوسٹ نمبر%{post_number}شیئر کریں۔" + instructions: "اس پوسٹ کا لنک شیئر کریں:" category: none: "(کوئی زمرہ نہیں)" all: "تمام زُمرَہ جات" @@ -2543,6 +2991,7 @@ ur: edit: "ترمیم" edit_dialog_title: "ترمیم: %{categoryName}" view: "زمرہ میں ٹاپکس دیکھیے" + back: "واپس زمرے میں" general: "عام" settings: "سیٹِنگ" topic_template: "ٹاپک ٹَیمپلیٹ" @@ -2550,10 +2999,14 @@ ur: tags_allowed_tags: "اس زُمرہ میں اِن ٹیگز کو محدود کریں:" tags_allowed_tag_groups: "اس زُمرہ میں اِن ٹیگ گروپس کو محدود کریں:" tags_placeholder: "(اختیاری) اجازت والے ٹیگز کی فہرست" + tags_tab_description: "اوپر بیان کردہ ٹیگز اور ٹیگ گروپس صرف اس زمرے اور دیگر زمروں میں دستیاب ہوں گے جو ان کی وضاحت کرتے ہیں۔ وہ دیگر زمروں میں استعمال کے لیے دستیاب نہیں ہوں گے۔" tag_groups_placeholder: "(اختیاری) اجازت والے ٹیگ گروپس کی فہرست" + manage_tag_groups_link: "ٹیگ گروپوں کا انتظام" allow_global_tags_label: "دوسرے ٹیگز کی بھی اجازت دیں" + required_tag_group: + delete: "مٹائیں" topic_featured_link_allowed: "اس زمرہ میں نمایاں لنکس کو اجازت دیں" - delete: "زمرہ حذف کریں" + delete: "زمرہ مٹائیں" create: "نیا زمرہ" create_long: "نیا زمرہ بنائیں" save: "زمرہ محفوظ کریں" @@ -2570,18 +3023,24 @@ ur: foreground_color: "پیش منظر رنگ" name_placeholder: "زیادہ سے زیادہ ایک یا دو الفاظ" color_placeholder: "کوئی بھی ویب رنگ" - delete_confirm: "کیا آپ واقعی اِس زمرہ کو حذف کرنا چاہتے ہیں؟" - delete_error: "اِس زمرہ کو حذف کرنے میں ایک خرابی کا سامنا کرنا پڑا۔" + delete_confirm: "کیا آپ واقعی اِس زمرہ کو مٹانا چاہتے ہیں؟" + delete_error: "اِس زمرہ کو مٹانے میں ایک خرابی کا سامنا کرنا پڑا۔" list: "زمرہ جات کی فہرست دکھائیں" no_description: "براہ مہربانی اس زمرہ کی تفصیل شامل کریں۔" change_in_category_topic: "تفصیل ترمیم کریں" already_used: "یہ رنگ دوسرے زمرہ کیلیے استعمال ہو چکا ہے۔" security: "سیکورٹی" + security_add_group: "ایک گروپ شامل کریں" permissions: group: "گروپ" see: "ملاحظہ کریں" reply: "جواب" create: "بنائیں" + no_groups_selected: "کسی گروپ کو رسائی نہیں دی گئی ہے؛ یہ زمرہ صرف عملے کو نظر آئے گا." + everyone_has_access: 'یہ زمرہ عوامی ہے، ہر کوئی دیکھ سکتا ہے، جواب دے سکتا ہے اور پوسٹس بنا سکتا ہے۔ اجازتوں کو محدود کرنے کے لیے، "ہر ایک" گروپ کو دی گئی اجازتوں میں سے ایک یا زیادہ کو ہٹا دیں۔' + toggle_reply: "جواب کی اجازت ٹوگل کریں" + toggle_full: "تخلیق کی اجازت کو ٹوگل کریں" + inherited: 'یہ اجازت "ہر ایک" سے وراثت میں ملی ہے' special_warning: "انتباہ: یہ زمرہ ایک پہلے سے بنایا ہوا زمرہ ہے اور اِس کی سیکورٹی سیٹِنگ میں ترمیم نہیں کی جا سکتی۔ اگر آپ اِس زمرہ کو استعمال کرنے کی خواہش نہیں رکھتے، تو اِسے کسی اور حوالے سے استعمال کرنے کی بجائے، حذف کردیں۔" uncategorized_security_warning: "یہ زمرہ خاص ہے۔ اِس کا مقصد بغیر زمرہ والے ٹاپکس کیلئے جگہ فراہم کرنا ہے؛ اِس میں سیکورٹی کی ترتیبات نہیں ہو سکتیں۔" uncategorized_general_warning: 'یہ زمرہ خاص ہے۔ یہ نئے ٹاپکس جن کیلئے کسی زمرہ کا انتخاب نہ ہوا ہو، اُن کیلئے پہلے سے طے شدہ زمرہ کے طور پر استعمال ہوتا ہے۔ اگر آپ اِس رویے کو روکنے اور کسی زمرہ کے انتخاب کو لازمی کرنا چاہتے ہیں، تو برائے مہربانی یہاں ترتیب کو غیر فعال کریں۔ اگر آپ نام یا تفصیل تبدیل کرنا چاہتے ہیں، تو مرضی کے مطابق / ٹَیکسٹ متن پر جائیں۔' @@ -2589,44 +3048,53 @@ ur: images: "تصاویر" email_in: "اپنی مرضی کا اِنکَمِنگ اِیمیل ایڈریس:" email_in_allow_strangers: "اکاؤنٹس نہ رکھنے والے گمنام صارفین کی طرف سے اِیمیلز کو قبول کریں" - email_in_disabled: "ویب سائٹ کی سیٹِنگ میں اِیمیل کے ذریعے نئے ٹاپک پوسٹ کرنا غیر فعال کیا ہوا ہے۔ اِیمیل کے ذریعے نئے ٹاپک شائع کرنے کو چالو کرنے کے لئے،" + email_in_disabled: "ای میل کے ذریعے نئے عنوانات پوسٹ کرنا سائٹ کی ترتیبات میں غیر فعال ہے۔ ای میل کے ذریعے نئے عنوانات کی اشاعت کو فعال کرنے کے لیے، " email_in_disabled_click: 'سیٹِنگ میں "اِیمیل اِن" فعال کریں۔' mailinglist_mirror: "زُمرہ میلنگ فہرست کا عکس ہے" show_subcategory_list: "اس زمرہ میں ذیلی زمرہ جات کی فہرست ٹاپکس سے مندرجہ بالا دکھائیں۔" + read_only_banner: "بینر ٹیکسٹ جب کوئی صارف اس زمرے میں موضوع نہیں بنا سکتا:" num_featured_topics: "زمرہ کے صفحے پر دکھائے گئے ٹاپکس کی تعداد:" subcategory_num_featured_topics: "بالائی زمرہ کے صفحے پر دکھائے گئے نمایاں ٹاپکس کی تعداد:" all_topics_wiki: "نئے ٹاپکس کو پہلے ہی سے وِیکی بنائیں" + allow_unlimited_owner_edits_on_first_post: "پہلی پوسٹ پر مالک کی لامحدود ترامیم کی اجازت دیں" subcategory_list_style: "ذیلی زمرہ جات فہرست کا سٹائل:" sort_order: "پہلے سے طے شدہ ترتیب:" default_view: "پہلے سے طے شدہ ویو:" default_top_period: "پہلے سے طے شدہ ٹاپ کی مدت:" + default_list_filter: "ڈیفالٹ فہرست فلٹر:" allow_badges_label: "اس زُمرہ میں بیجز دینے کی اجازت دیں" edit_permissions: "ترمیم کرنے کی اجازتیں" + reviewable_by_group: "عملے کے علاوہ، اس زمرے کے مواد کا جائزہ بھی لیا جا سکتا ہے:" review_group_name: "گروپ نام" require_topic_approval: "تمام نئے ٹاپکس کیلئے ماڈریٹر کی منظوری لازمی کریں" require_reply_approval: "تمام نئے جوابات کیلئے ماڈریٹر کی منظوری لازمی کریں" this_year: "اِس سال" position: "زمرہ جات کے صفحے پر پوزیشن:" default_position: "پہلے سے طے شدہ پوزیشن" - position_disabled: "ایکٹیوٹی کے ہساب سے زُمرہ جات کی ترتیب دکھائی جائے گی۔ فہرستوں میں زُمرہ جات کی ترتیب کو کنٹرول کرنے کے لئے،" + position_disabled: "زمرہ جات کو سرگرمی کی ترتیب میں دکھایا جائے گا۔ فہرستوں میں زمروں کی ترتیب کو کنٹرول کرنے کے لیے، " position_disabled_click: '"مقررہ زمرہ جات پوزیشنوں" کی سیٹِنگ فعال کریں۔' minimum_required_tags: "ایک ٹاپک میں ضروری ٹیگز کی کم از کم تعداد:" + default_slow_mode: 'اس زمرے میں نئے عنوانات کے لیے "سلو موڈ" کو فعال کریں۔' parent: "بالائی زمرہ" num_auto_bump_daily: "روزانہ خود کار طریقے سے بَمپ کرنے کیلئے کھلے ٹاپکس کی تعداد:" navigate_to_first_post_after_read: "ٹاپک پڑھنے کے بعد پہلی پوسٹ پر جائیں" notifications: + title: "اس زمرے کے لیے اطلاع کی سطح کو تبدیل کریں" watching: title: "نظر رکھی ہوئی ہے" + description: "آپ خود بخود اس زمرے کے تمام عنوانات دیکھیں گے۔ آپ کو ہر موضوع میں ہر نئی پوسٹ کے بارے میں مطلع کیا جائے گا، اور نئے جوابات کی گنتی دکھائی جائے گی۔" watching_first_post: title: "پہلی پوسٹ پر نظر رکھی ہوئی ہے" description: "آپ کو اِس زمرہ میں نئے ٹاپکس کے بارے میں مطلع کیا جائے گا لیکن ٹاپکس کے جوابات پر نہیں۔" tracking: title: "ٹریک کیا جا رہا" + description: "آپ خود بخود اس زمرے کے تمام عنوانات کو ٹریک کریں گے۔ اگر کوئی آپ کے @نام کا ذکر کرتا ہے یا آپ کو جواب دیتا ہے تو آپ کو مطلع کیا جائے گا، اور نئے جوابات کی تعداد دکھائی جائے گی۔" regular: title: "عمومی" description: "اگر کوئی آپ کا @نام زکر کرتا ہے یا کوئی جواب دیتا ہے تو آپ کو مطلع کر دیا جائے گا۔" muted: title: "خاموش کِیا ہوا" + description: "آپ کو اس زمرے میں نئے عنوانات کے بارے میں کبھی بھی مطلع نہیں کیا جائے گا، اور وہ تازہ ترین میں ظاہر نہیں ہوں گے۔" search_priority: label: "سرچ ترجیحات" options: @@ -2658,21 +3126,30 @@ ur: moderation: "ماڈریٹر کا کام" appearance: "ظاہری شکل" email: "اِی میل" + list_filters: + all: "تمام موضوعات" + none: "کوئی ذیلی زمرہ جات نہیں" + colors_disabled: "آپ رنگوں کا انتخاب نہیں کر سکتے کیونکہ آپ کے پاس کوئی بھی انداز نہیں ہے۔" flagging: title: "ہماری کمیونٹی کو مہذب رکھنے کے لئے مدد کا شکریہ!" action: "پوسٹ فلَیگ کریں" + take_action: "کارروائی کریں..." take_action_options: default: title: "کارروائی کریں" details: "بجائے مزید کمیونٹی کی طرف سے فلَیگز کا انتظارکرنے کے، فوری طور پرفلَیگز کی حد تک پہنچ جائیں" suspend: title: "صارف معطل کریں" + details: "نشان کی حد تک پہنچیں، اور صارف کو معطل کریں" silence: title: "صارف خاموش کریں" + details: "نشان کی حد تک پہنچیں، اور صارف کو خاموش کر دیں" notify_action: "پیغام" official_warning: "آفیشل انتباہ" - delete_spammer: "سپیم کرنے والے کو حذف کریں" - yes_delete_spammer: "جی ہاں، سپیم کرنے والے کو حذف کریں" + delete_spammer: "اسپامر کو مٹائیں" + flag_for_review: "جائزہ کے لیے قطار" + delete_confirm_MF: "آپ مٹانے والے ہیں {POSTS, plural, one {# پوسٹ} other {# پوسٹس}} اور {TOPICS, plural, one {# موضوع} other {# موضوعات}} اس صارف سے، ان کا اکاؤنٹ مٹائیں، ان کے IP ایڈریس {ip_address}سے سائن اپ بلاک کریں، اور ان کا ای میل ایڈریس {email} مستقل بلاک لسٹ میں شامل کریں۔ کیا آپ کو یقین ہے کہ یہ صارف واقعی اسپامر ہے؟" + yes_delete_spammer: "ہاں، اسپامر کو مٹائیں" ip_address_missing: "(N/A)" hidden_email_address: "(مخفی)" submit_tooltip: "نجی فلَیگ جمع کریں" @@ -2711,7 +3188,7 @@ ur: about: "اِس پوسٹ کے لئے مزید لنکس دکھائیں" title: one: "%{count} مزید" - other: "%{count}مزید " + other: "%{count} مزید" topic_statuses: warning: help: "یہ ایک آفیشل انتباہ ہے۔" @@ -2741,17 +3218,28 @@ ur: pending_posts: label: "زیرِاِلتوَاء" label_with_count: "زیرِاِلتوَاء (%{count})" + posts_likes_MF: | + یہ موضوع ہے {count, plural, one {#جواب} other {# جوابات}} {ratio, select, + low {ایک اعلی پوسٹ کرنے کے تناسب کے ساتھ} + med {پوسٹ کرنے کا تناسب بہت زیادہ ہے} + high {پوسٹ کرنے کا تناسب انتہائی اعلی کے ساتھ} + other {}} original_post: "اصل پوسٹ" views: "وِیوز" + sr_views: "خیالات کے لحاظ سے ترتیب دیں" views_lowercase: one: "وِیو" other: "وِیوز" replies: "جوابات" + sr_replies: "جوابات کے لحاظ سے ترتیب دیں" views_long: one: "اِس ٹاپک کو %{count} دفعہ دیکھا جا چکا ہے" other: "اِس ٹاپک کو %{number} دفعہ دیکھا جا چکا ہے" activity: "سرگرمی" + sr_activity: "سرگرمی کے لحاظ سے ترتیب دیں" likes: "لائیکس" + sr_likes: "پسند کی طرف سے ترتیب دیں" + sr_op_likes: "اصل پوسٹ کی پسند کی طرف سے ترتیب دیں" likes_lowercase: one: "لائیک" other: "لائیکس" @@ -2760,6 +3248,8 @@ ur: one: "صارف" other: "صارفین" category_title: "زمرہ" + history_capped_revisions: "تاریخی، آخری 100 ترمیم" + history: "تاریخی" changed_by: "%{author} کی طرف سے" raw_email: title: "آنے والی اِیمیل" @@ -2786,10 +3276,14 @@ ur: title_with_count: one: "بغیر پڑھا (%{count})" other: "بغیر پڑھے (%{count})" - help: " ٹاپک جن پر فی الحال آپ نے نظر رکھی ہوئی ہے یا بغیر پڑھی پوسٹس کے ساتھ ٹریک کر رہے ہیں" + help: "وہ موضوعات جنہیں آپ فی الحال بغیر پڑھی ہوئی پوسٹس کے ساتھ دیکھ رہے ہیں یا ٹریک کر رہے ہیں" lower_title_with_count: one: "%{count} بغیر پڑھا ہوا" other: "%{count} بغیر پڑھے ہوئے" + unseen: + title: "ان دیکھا" + lower_title: "ان دیکھا" + help: "نئے عنوانات اور عنوانات جنہیں آپ فی الحال دیکھ رہے ہیں یا بغیر پڑھی ہوئی پوسٹس کے ساتھ ٹریک کر رہے ہیں۔" new: lower_title_with_count: one: "%{count} نیا" @@ -2826,27 +3320,31 @@ ur: weekly: title: "ہفتہ وار" daily: - title: "روزانہ " + title: "روزانہ" all_time: "تمام اوقات" this_year: "سال" this_quarter: "سہ ماہ" this_month: "مہینہ" this_week: "ہفتہ" today: "آج" + other_periods: "اوپر دیکھیں:" + browser_update: 'بدقسمتی سے، آپ کا براؤزر اس سائٹ پر کام کرنے کے لیے بہت پرانا ہے۔ براہ کرم اپنے براؤزر کو اپ گریڈ کریں بھرپور مواد دیکھنے کے لیے، لاگ ان کریں اور جواب دیں۔' permission_types: full: "بنائیں / جواب دیں / ملاحظہ کریں" create_post: "جواب دیں / ملاحظہ کریں" readonly: "ملاحظہ کریں" lightbox: download: "ڈاؤن لوڈ" + open: "اصل تصویر" previous: "پچھلا (بائیں تیر کلید)" next: "اگلا (بائیں تیر کلید)" counter: "%curr% of %total%" close: "بند (Esc)" content_load_error: 'مواد لوڈ نہیں کیا جا سکا۔' image_load_error: 'تصویر لوڈ نہیں کی جا سکی۔' + cannot_render_video: اس ویڈیو کو رینڈر نہیں کیا جا سکتا کیونکہ آپ کا براؤزر کوڈیک کو سپورٹ نہیں کرتا ہے۔ keyboard_shortcuts_help: - shortcut_key_delimiter_comma: "،" + shortcut_key_delimiter_comma: "، " shortcut_key_delimiter_plus: "+" shortcut_delimiter_or: "%{shortcut1} یا %{shortcut2}" shortcut_delimiter_slash: "%{shortcut1}/%{shortcut2}" @@ -2864,6 +3362,8 @@ ur: profile: "%{shortcut} پروفائل" messages: "%{shortcut} پیغامات" drafts: "%{shortcut} ڈرافٹس" + next: "%{shortcut} اگلا موضوع" + previous: "%{shortcut} پچھلا موضوع" navigation: title: "نیویگیشن" jump: "%{shortcut} پوسٹ # پر جائیں" @@ -2878,15 +3378,29 @@ ur: notifications: "%{shortcut} اطلاعات کھولیں" hamburger_menu: "%{shortcut} ہیمبرگر مینو کھولیں" user_profile_menu: "%{shortcut} صارف مینو کھولیں" - show_incoming_updated_topics: "اَپ ڈیٹ ہوے ٹاپک دکھائیں" - search: "%{shortcut}سرچ " + show_incoming_updated_topics: "%{shortcut} اپ ڈیٹ کردہ عنوانات دکھائیں" + search: "%{shortcut} سرچ" help: "%{shortcut} کیبورڈ مدد کھولیں" + dismiss_new: "%{shortcut} نئے کو برخاست" dismiss_topics: "%{shortcut} ٹاپک برخاست کریں" log_out: "%{shortcut} لاگ آوٹ" composing: title: "کمپوز کرنا" return: "%{shortcut} کمپوزر پر واپس جائیں" fullscreen: "%{shortcut} پوری اسکرین کمپوزر" + bookmarks: + title: "بک مارکنگ" + enter: "%{shortcut} محفوظ اور بند کریں۔" + later_today: "%{shortcut} آج بعد میں" + later_this_week: "%{shortcut} اس ہفتے کے آخر میں" + tomorrow: "%{shortcut} کل" + next_week: "%{shortcut} اگلے ہفتے" + next_month: "%{shortcut} اگلے مہینے" + next_business_week: "%{shortcut} اگلے ہفتے کا آغاز" + next_business_day: "%{shortcut} اگلے کاروباری دن" + custom: "%{shortcut} مرضی کے مطابق تاریخ اور وقت" + none: "%{shortcut} کوئی یاد دہانی نہیں" + delete: "%{shortcut} بک مارک مٹائیں" actions: title: "عمل" bookmark_topic: "%{shortcut} بُک مارک ٹاپک ٹوگل کریں" @@ -2901,13 +3415,19 @@ ur: flag: "%{shortcut} پوسٹ فلَیگ کریں" bookmark: "%{shortcut} پوسٹ بُک مارک کریں" edit: "%{shortcut} پوسٹ ترمیم کریں" - delete: "%{shortcut} پوسٹ حذف کریں" + delete: "%{shortcut} پوسٹ مٹائیں" mark_muted: "%{shortcut} ٹاپک خاموش کریں" mark_regular: "%{shortcut} معمول کا (ڈیفالٹ) ٹاپک" mark_tracking: "%{shortcut} ٹاپک ٹریک کریں" mark_watching: "%{shortcut} ٹاپک پر نظر رکھیں" print: "%{shortcut} ٹاپک پرِنٹ کریں" defer: "%{shortcut} ٹاپک ملتوی کریں" + topic_admin_actions: "%{shortcut} عنوان ایڈمن ایکشنز کھولیں" + search_menu: + title: "تلاش کا مینیو" + prev_next: "%{shortcut} انتخاب کو اوپر اور نیچے منتقل کریں" + insert_url: "%{shortcut} کھلے کمپوزر میں انتخاب داخل کریں" + full_page_search: "%{shortcut} پورے صفحہ کی تلاش" badges: earned_n_times: one: "یہ بَیج %{count} دفعہ حاصل کیا" @@ -2940,8 +3460,21 @@ ur: name: دیگر posting: name: پوسٹ کرنا + favorite_max_reached: "آپ مزید بیجز کو پسند نہیں کر سکتے۔" + favorite_max_not_reached: "اس بیج کو پسندیدہ کے بطور نشان زد کریں" + favorite_count: "%{max} بیجز کو پسندیدہ کے طور پر %{count}نشان زد کیا گیا" download_calendar: + title: "کیلنڈر ڈاؤن لوڈ کریں" + save_ics: ".ics فائل ڈاؤن لوڈ کریں" + save_google: "گوگل کیلنڈر میں شامل کریں" + remember: "مجھے دوبارہ مت پوچھیں" + remember_explanation: "(آپ اس ترجیح کو اپنے صارف کی ترجیحات میں تبدیل کر سکتے ہیں)" download: "ڈاؤن لوڈ" + default_calendar: "ڈیفالٹ کیلنڈر" + default_calendar_instruction: "تعین کریں کہ تاریخیں محفوظ ہونے پر کون سا کیلنڈر استعمال کیا جائے" + add_to_calendar: "کیلنڈر میں شامل کریں" + google: "گوگل کیلنڈر" + ics: "ICS" tagging: all_tags: "تمام ٹیگز" other_tags: "دیگر ٹیگز" @@ -2950,12 +3483,40 @@ ur: changed: "تبدیل کیے گئے ٹیگ:" tags: "ٹیگز" choose_for_topic: "اختیاری ٹیگز" + choose_for_topic_required: + one: "کم از کم %{count} ٹیگ منتخب کریں..." + other: "کم از کم %{count} ٹیگز منتخب کریں..." + info: "معلومات" + default_info: "یہ ٹیگ کسی بھی زمرے تک محدود نہیں ہے، اور اس کا کوئی مترادف نہیں ہے۔" + staff_info: "پابندیاں شامل کرنے کے لیے، اس ٹیگ کو ٹیگ گروپمیں ڈالیں۔" + category_restricted: "یہ ٹیگ ان زمروں تک محدود ہے جن تک آپ کو رسائی کی اجازت نہیں ہے۔" + synonyms: "مترادفات" + synonyms_description: "جب مندرجہ ذیل ٹیگز استعمال کیے جائیں گے، تو وہ %{base_tag_name}سے بدل جائیں گے۔" + save: "ٹیگ کا نام اور تفصیل محفوظ کریں۔" + tag_groups_info: + one: 'یہ ٹیگ گروپ "%{tag_groups}" سے تعلق رکھتا ہے۔' + other: "یہ ٹیگ ان گروپوں سے تعلق رکھتا ہے: %{tag_groups}۔" + category_restrictions: + one: "یہ صرف اس زمرے میں استعمال کیا جا سکتا ہے:" + other: "یہ صرف ان زمروں میں استعمال کیا جا سکتا ہے:" + edit_synonyms: "مترادفات میں ترمیم" + add_synonyms_label: "مترادفات شامل کریں:" add_synonyms: "شامل کریں" - delete_tag: "ٹیگ حذف کریں" + add_synonyms_explanation: + one: "کوئی بھی جگہ جو فی الحال اس ٹیگ کو استعمال کرتی ہے اس کی بجائے %{tag_name} استعمال کرنے کے لیے تبدیل کر دی جائے گی۔ کیا آپ واقعی یہ تبدیلی کرنا چاہتے ہیں؟" + other: "کوئی بھی جگہ جو فی الحال ان ٹیگز کو استعمال کرتی ہے اس کی بجائے %{tag_name} استعمال کرنے کے لیے تبدیل کر دی جائے گی۔ کیا آپ واقعی یہ تبدیلی کرنا چاہتے ہیں؟" + add_synonyms_failed: "درج ذیل ٹیگز کو مترادفات کے طور پر شامل نہیں کیا جا سکا: %{tag_names}۔ یقینی بنائیں کہ ان کے مترادفات نہیں ہیں اور یہ کسی دوسرے ٹیگ کے مترادف نہیں ہیں۔" + remove_synonym: "مترادف ہٹا دیں" + delete_synonym_confirm: 'کیا آپ واقعی مترادف "%{tag_name}" کومٹانا چاہتے ہیں؟' + delete_tag: "ٹیگ مٹائیں" delete_confirm: one: "کیا آپ واقعی اس ٹیگ کو حذف اور %{count} ٹاپک، جس کو یہ آسائین ہواوا ہے، سے ہٹا دینا چاہتے ہیں؟" other: "کیا آپ واقعی اس ٹیگ کو حذف اور %{count} ٹاپکس، جن کو یہ آسائین ہواوا ہے، سے ہٹا دینا چاہتے ہیں؟" - delete_confirm_no_topics: "کیا آپ واقعی اِس ٹَیگ کو حذف کرنا چاہتے ہیں؟" + delete_confirm_no_topics: "کیا آپ واقعی اِس ٹَیگ کو مٹانا چاہتے ہیں؟" + delete_confirm_synonyms: + one: "اس کا مترادف بھی مٹادیا جائے گا۔" + other: "اس کے %{count} مترادفات بھی مٹا دیے جائیں گے۔" + edit_tag: "ٹیگ نام اور تفصیل میں ترمیم کریں" description: "تفصیل" sort_by: "ترتیب بہ:" sort_by_count: "شمار" @@ -2967,13 +3528,15 @@ ur: upload_instructions: "فی سطر ایک، اختیاری طور پر ایک ٹیگ گروپ کے ساتھ 'tag_name,tag_group' کے فارمیٹ میں۔" upload_successful: "ٹیگز کامیابی سے اپ لوڈ ہو گئے" delete_unused_confirmation: - one: "%{count} ٹیگ کو حذف کردیا جائے گا: %{tags}" - other: "%{count} ٹیگز کو حذف کردیا جائے گا: %{tags}" + one: "%{count} ٹیگ کومٹادیا جائے گا: %{tags}" + other: "%{count} ٹیگز کومٹادیا جائے گا: %{tags}" delete_unused_confirmation_more_tags: one: "%{tags} اور %{count} مزید" other: "%{tags} اور %{count} مزید" - delete_unused: "غیر استعمال شدہ ٹیگز حذف کریں" - delete_unused_description: "تمام اُن ٹیگز کو حذف کریں جو کسی بھی ٹاپک یا ذاتی پیغام سے منسلک نہیں ہیں" + delete_no_unused_tags: "کوئی غیر استعمال شدہ ٹیگ نہیں ہیں۔" + tag_list_joiner: "، " + delete_unused: "غیر استعمال شدہ ٹیگز مٹائیں" + delete_unused_description: "تمام اُن ٹیگز کو مٹائیں جو کسی بھی ٹاپک یا ذاتی پیغام سے منسلک نہیں ہیں" cancel_delete_unused: "منسوخ" filters: without_category: "%{filter} %{tag} ٹاپکس" @@ -2998,19 +3561,33 @@ ur: description: "آپ کو اِس ٹیگ کے ساتھ موجود نئے ٹاپکس کی کسی بھی چیز کے بارے میں مطلع نہیں کیا جائے گا، اور یہ تازہ ترین میں بھی نظر نہیں آئیں گے۔" groups: title: "ٹیگ گروپس" + about_heading: "ایک ٹیگ گروپ منتخب کریں یا ایک نیا بنائیں" + about_heading_empty: "شروع کرنے کے لیے ایک نیا ٹیگ گروپ بنائیں" + about_description: "ٹیگ گروپس آپ کو ایک جگہ پر بہت سے ٹیگز کی اجازتوں کا انتظام کرنے میں مدد کرتے ہیں۔" new: "نیا گروپ" + new_title: "نیا گروپ بنائیں" + edit_title: "ٹیگ گروپ میں ترمیم کریں" tags_label: "اِس گروپ میں ٹیگز" parent_tag_label: "بالائی ٹیگ" + parent_tag_description: "اس گروپ کے ٹیگز صرف اس صورت میں استعمال کیے جاسکتے ہیں جب پیرنٹ ٹیگ موجود ہو۔" one_per_topic_label: "اِس گروپ سے ایک ٹیگ فی ٹاپک محدود کریں" new_name: "نیا ٹیگ گروپ" name_placeholder: "نام" save: "محفوظ کریں" - delete: "حذف کریں" - confirm_delete: "کیا آپ واقعی اِس ٹیگ گروپ کو حذف کرنا چاہتے ہیں؟" + delete: "مٹائیں" + confirm_delete: "کیا آپ واقعی اِس ٹیگ گروپ کو مٹانا چاہتے ہیں؟" everyone_can_use: "ٹیگز ہر کوئی استعمال کر سکتا ہے" + usable_only_by_groups: "ٹیگز ہر کسی کو نظر آتے ہیں، لیکن صرف درج ذیل گروپ ہی انہیں استعمال کر سکتے ہیں۔" + visible_only_to_groups: "ٹیگز صرف مندرجہ ذیل گروپوں کو نظر آتے ہیں" + cannot_save: "ٹیگ گروپ کو محفوظ نہیں کیا جا سکتا۔ یقینی بنائیں کہ کم از کم ایک ٹیگ موجود ہے، ٹیگ گروپ کا نام خالی نہیں ہے، اور ٹیگز کی اجازت کے لیے ایک گروپ کا انتخاب کیا گیا ہے۔" + tags_placeholder: "ٹیگز تلاش کریں یا بنائیں" + parent_tag_placeholder: "اختیاری" + select_groups_placeholder: "گروپس منتخب کریں..." + disabled: "ٹیگنگ غیر فعال ہے۔ " topics: none: unread: "آپ کے پاس کوئی بغیر پڑھے ٹاپک موجود نہیں۔" + unseen: "آپ کے لیے کوئی ان دیکھے موضوعات نہیں ہیں۔" new: "آپ کے پاس کوئی نئے ٹاپک موجود نہیں۔" read: "ابھی تک آپ نے کوئی ٹاپکس نہیں پڑھے۔" posted: "ابھی تک آپ نے کسی ٹاپک میں پوسٹ نہیں کیا۔" @@ -3020,22 +3597,30 @@ ur: invite: custom_message: "اپنی مرضی کا پیغام لکھ کر، اپنے دعوت نامہ کو ذرا سا مذید ذاتی بنائیں۔" custom_message_placeholder: "اپنی مرضی کے پیغام درج کریں" + approval_not_required: "جیسے ہی صارف اس دعوت کو قبول کرے گا اسے خودکار طور پر منظور کر لیا جائے گا۔" custom_message_template_forum: "ارے، آپ کو اس فورم میں شامل ہونا چاہیے!" custom_message_template_topic: "ارے، میں نے سوچا کہ آپ اِس ٹاپک سے لطف اندوز ہوں گے!" forced_anonymous: "انتہائی بوجھ کی وجہ سے، یہ عارضی طور پر سب کو ایسے دکھایا جا رہا ہے جیسے لاگ آؤٹ صارف اِسے دیکھتے ہیں۔" + forced_anonymous_login_required: "سائٹ بہت زیادہ بوجھ میں ہے اور اس وقت لوڈ نہیں کی جا سکتی، چند منٹوں میں دوبارہ کوشش کریں۔" footer_nav: back: "واپس" + forward: "آگے" share: "شیئر" dismiss: "بر خاست کریں" safe_mode: enabled: "سیف موڈ فعال ہے، سیف موڈ سے باہر نکلنے کے لئے اِس براؤزر وِنڈو کو بند کریں" + image_removed: "(تصویر ہٹا دی گئی)" do_not_disturb: + title: "کے لئے پریشان مت کرو..." + label: "پریشان مت کرو" remaining: "%{remaining} باقی رہتا ہے..." options: half_hour: "30 منٹ" one_hour: "1 گھنٹہ" two_hours: "2 گھنٹے" + tomorrow: "کل تک" custom: "اپنی مرضی کا" + set_schedule: "اطلاع کا شیڈول مرتب کریں" trust_levels: names: newuser: "نیا صارف" @@ -3043,10 +3628,31 @@ ur: member: "ممبر" regular: "رَیگولر" leader: "لِیڈر" + detailed_name: "%{level}: %{name}" + pick_files_button: + unsupported_file_picked: "آپ نے غیر معاونت شدہ فائل منتخب کی ہے۔ معاون فائل کی اقسام — %{types}." user_activity: + no_activity_title: "ابھی تک کوئی سرگرمی نہیں" no_activity_others: "کوئی سرگرمی نہیں۔" + no_replies_title: "آپ نے ابھی تک کسی بھی عنوان کا جواب نہیں دیا ہے" no_replies_others: "کوئی جوابات نہیں۔" + no_drafts_title: "آپ نے کوئی ڈرافٹ شروع نہیں کیا ہے" + no_drafts_body: "پوسٹ کرنے کے لیے بالکل تیار نہیں؟ جب بھی آپ کوئی موضوع، جواب، یا ذاتی پیغام لکھنا شروع کریں گے تو ہم خود بخود ایک نیا مسودہ محفوظ کریں گے اور اسے یہاں درج کریں گے۔ رد کرنے کے لیے کینسل بٹن کو منتخب کریں یا بعد میں جاری رکھنے کے لیے اپنے ڈرافٹ کو محفوظ کریں۔" + no_likes_title: "آپ نے ابھی تک کوئی عنوان پسند نہیں کیا ہے" + no_likes_body: "کودنے اور تعاون شروع کرنے کا ایک بہترین طریقہ یہ ہے کہ پہلے سے ہو چکی گفتگو کو پڑھنا شروع کریں، اور اپنی پسند کی پوسٹس پر %{heartIcon} کو منتخب کریں!" no_likes_others: "کوئی لائیک کردہ پوسٹس نہیں۔" + no_topics_title: "آپ نے ابھی تک کوئی عنوان شروع نہیں کیا ہے" + no_topics_title_others: "%{username} نے ابھی تک کوئی عنوان شروع نہیں کیا ہے" + no_read_topics_title: "آپ نے ابھی تک کوئی عنوان نہیں پڑھا ہے" + no_read_topics_body: "ایک بار جب آپ بحثیں پڑھنا شروع کر دیں، تو آپ کو یہاں ایک فہرست نظر آئے گی۔ پڑھنا شروع کرنے کے لیے، ایسے عنوانات تلاش کریں جو آپ کی دلچسپی ٹاپ یا زمرہ جات یا کلیدی لفظ %{searchIcon}کے ذریعے تلاش کریں۔" + no_group_messages_title: "کوئی گروپ پیغامات نہیں ملے" + topic_entrance: + sr_jump_top_button: "پہلی پوسٹ پر جائیں" + sr_jump_bottom_button: "آخری پوسٹ پر جائیں" + fullscreen_table: + expand_btn: "ٹیبل کو پھیلائیں" + second_factor_auth: + redirect_after_success: "دو فیکٹر کی توثیق کامیاب ہے۔ پچھلے صفحہ…پر ری ڈائریکٹ کیا جا رہا ہے۔" admin_js: type_to_filter: "فِلٹر کرنے کے لئے ٹائپ کریں..." admin: @@ -3056,7 +3662,7 @@ ur: remove_muted_tags_from_latest: always: "ہمیشہ" only_muted: "جب اکیلے یا دوسرے خاموش کردہ ٹیگز کے ساتھ استعمال ہو" - never: "کبھی نہیں " + never: "کبھی نہیں" reports: title: "دستیاب رپورٹوں کی فہرست" dashboard: @@ -3075,13 +3681,15 @@ ur: latest_version: "تازہ ترین" problems_found: "آپ کی موجودہ سائٹ ترتیبات کے مطابق کچھ مشورے" new_features: + title: "\U0001F381 نئی خصوصیات" dismiss: "بر خاست کریں" + learn_more: "مزید جانیں" last_checked: "آخری دفعہ چیک کیا گیا" refresh_problems: "رِیفریش" no_problems: "کوئی مسائل نہیں پائے گئے۔" moderators: "ماڈریٹرز:" admins: "ایڈمن:" - silenced: "خاموش کیو ہوئے: " + silenced: "خاموش:" suspended: "معطل کردہ:" private_messages_short: "پغم" private_messages_title: "پیغامات" @@ -3131,6 +3739,7 @@ ur: daily: روزانہ monthly: ماہانہ weekly: ہفتہ وار + dates: "تاریخیں (UTC)" groups: "تمام گروپس" disabled: "یہ رپورٹ غیر فعال ہے" totals_for_sample: "نمونہ کیلئے کل" @@ -3140,6 +3749,7 @@ ur: trending_search: more: 'تلاشی کے لاگز' disabled: 'سرچ رجحان کی رپورٹ غیر فعال ہے۔ اعداد و شمار جمع کرنے کیلئے لاگ سرچ قُوَیریز کو فعال کریں۔' + average_chart_label: اوسط filters: file_extension: label: فائل ایکسٹینشن @@ -3147,6 +3757,8 @@ ur: label: گروپ category: label: زمرہ + include_subcategories: + label: "ذیلی زمرہ جات شامل کریں۔" groups: new: title: "نیا گروپ" @@ -3168,16 +3780,24 @@ ur: title: "کون اِس گروپ کو دیکھ سکتا ہے؟" public: "ہر کوئی" logged_on_users: "لاگ شدہ صارفین" + members: "گروپ کے مالکان، ممبران اور ماڈریٹرز" + staff: "گروپ کے مالکان اور ناظمین" owners: "گروپ مالکان" description: "ایڈمن تمام گروپس دیکھ سکتے ہیں۔" members_visibility_levels: - title: "کون اِس گروپ کے ممبران کو دیکھ سکتا ہے؟" + title: "اس گروپ کے اراکین کو کون دیکھ سکتا ہے؟" + description: "ایڈمنز تمام گروپس کے ممبران کو دیکھ سکتے ہیں۔ Flair تمام صارفین کو نظر آتا ہے۔" publish_read_state: "گروپ پیغامات پر گروپ رِیڈ اسٹیٹ شائع کریں۔" membership: automatic: خود کار طریقے سے trust_levels_title: "ممبران کو شامل کرتے وقت خود کار طریقے سے دیا جانے والا ٹرسٹ لَیول:" + effects: اثرات trust_levels_none: "کوئی نہیں" automatic_membership_email_domains: "جو صارفین ایک ایسے اِیمیل ڈومین کے ساتھ رجسٹر کرتے ہیں جو اِس فہرست میں سے کسی ایک سے پورا ملتا ہے، تو اُن کو خود کار طریقے سے اِس گروپ میں شامل کر دیا جائے گا:" + automatic_membership_user_count: + one: "%{count} صارف کے پاس نئے ای میل ڈومینز ہیں اور اسے گروپ میں شامل کیا جائے گا۔" + other: "%{count} صارفین کے پاس نئے ای میل ڈومینز ہیں اور انہیں گروپ میں شامل کیا جائے گا۔" + automatic_membership_associated_groups: "جو صارفین یہاں درج سروس پر گروپ کے ممبر ہیں وہ سروس کے ساتھ لاگ ان ہونے پر خود بخود اس گروپ میں شامل ہو جائیں گے۔" primary_group: "خود کار طریقے سے پرائمری گروپ کے طور پر مقرر کریں" name_placeholder: "گروپ کا نام، کوئی خالی جگہ نہیں، نامِ صارفین کے اصولوں کے مطابق" primary: "پرائمری گروپ" @@ -3187,9 +3807,13 @@ ur: refresh: "رِیفریش" about: "یہاں اپنے گروپ کی رکنیت کو اور ناموں میں ترمیم کریں" group_members: "گروپ کے اراکین" - delete: "حذف کریں" - delete_confirm: "اِس گروپ کو حذف کریں؟" - delete_failed: "گروپ کو حذف کرنے میں ناکام۔ اگر یہ ایک خودکار گروپ ہے تو اسے ختم نہیں کیا جا سکتا۔" + delete: "مٹائیں" + delete_confirm: "اِس گروپ کو مٹائیں؟" + delete_with_messages_confirm: + one: "اس گروپ کومٹانے سے %{count} میسج یتیم ہو جائیں گے، گروپ ممبرز کو مزید اس تک رسائی حاصل نہیں ہو گی۔

    کیا آپ کو یقین ہے؟" + other: "اس گروپ کومٹانے سے %{count} پیغامات یتیم ہو جائیں گے، گروپ کے ممبران ان تک مزید رسائی حاصل نہیں کر سکیں گے۔

    کیا آپ کو یقین ہے؟" + delete_failed: "گروپ کو مٹانے میں ناکامی۔ اگر یہ ایک خودکار گروپ ہے تو اسے ختم نہیں کیا جا سکتا۔" + delete_automatic_group: یہ ایک خودکار گروپ ہے اور اسے حذف نہیں کیا جا سکتا۔ delete_owner_confirm: "'%{username}' کے لیے مالک کے استحقاق کو ہٹائیں؟" add: "شامل کریں" custom: "اپنی مرضی کا" @@ -3205,16 +3829,84 @@ ur: none: "کوئی فعال API کیز اِس وقت موجود نہیں ہیں۔" user: "صارف" title: "API" + key: "کی" created: بنایا گیا + updated: اپ ڈیٹ + last_used: آخری بار استعمال + never_used: (کبھی نہیں) generate: "تخلیق کریں" + undo_revoke: "کالعدم کریں۔" revoke: "منسوخ کریں" all_users: "تمام صارفین" + active_keys: "ایکٹو API کیز" + manage_keys: چابیاں منظم کریں show_details: تفصیلات description: تفصیل + no_description: (کوئی وضاحت نہیں) + all_api_keys: تمام API کی چابیاں + user_mode: صارف کی سطح + scope_mode: دائرہ کار + impersonate_all_users: کسی بھی صارف کی نقالی + single_user: "واحد صارف" + user_placeholder: صارف نام درج کریں۔ + description_placeholder: اس چابی کو کس کام کے لیے استعمال کیا جائے گا؟ save: محفوظ کریں + new_key: نئی API چابی + revoked: منسوخ + delete: مستقل طور پر مٹائیں + not_shown_again: یہ چابی دوبارہ نہیں دکھائی جائے گی۔ یقینی بنائیں کہ آپ جاری رکھنے سے پہلے ایک کاپی کرلیں۔ continue: جاری رکھی scopes: + description: | + اسکوپس استعمال کرتے وقت، آپ API چابی کو اختتامی پوائنٹس کے مخصوص سیٹ تک محدود کر سکتے ہیں۔ + آپ یہ بھی وضاحت کر سکتے ہیں کہ کن پیرامیٹرز کی اجازت ہوگی۔ متعدد اقدار کو الگ کرنے کے لیے کوما استعمال کریں۔ + title: اسکوپس + granular: دانے دار + read_only: صرف پڑھنے کیلے + global: عالمی + global_description: API چابی پر کوئی پابندی نہیں ہے اور تمام اینڈ پوائنٹس قابل رسائی ہیں۔ + resource: وسائل action: عمل + allowed_parameters: اجازت یافتہ پیرامیٹر + optional_allowed_parameters: اجازت شدہ پیرامیٹرز (اختیاری) + any_parameter: (کوئی بھی پیرامیٹر) + allowed_urls: اجازت یافتہ URLs + descriptions: + global: + read: API کی کو صرف پڑھنے تک محدود کریں۔ + topics: + read: اس میں کوئی موضوع یا کوئی مخصوص پوسٹ پڑھیں۔ آر ایس ایس کی بھی حمایت حاصل ہے۔ + write: ایک نیا موضوع بنائیں یا موجودہ موضوع پر پوسٹ کریں۔ + update: ایک موضوع کو اپ ڈیٹ کریں۔ عنوان، زمرہ، ٹیگز وغیرہ تبدیل کریں۔ + read_lists: ٹاپک، نیا، تازہ ترین، وغیرہ جیسے عنوانات کی فہرستیں پڑھیں۔ RSS بھی تعاون یافتہ ہے۔ + wordpress: ورڈپریس wp-discourse پلگ ان کے کام کرنے کے لیے ضروری ہے۔ + posts: + edit: کسی بھی پوسٹ یا مخصوص میں ترمیم کریں۔ + categories: + list: زمروں کی فہرست حاصل کریں۔ + show: ID کے لحاظ سے ایک زمرہ حاصل کریں۔ + uploads: + create: ایک نئی فائل اپ لوڈ کریں یا بیرونی اسٹوریج پر سنگل یا ملٹی پارٹ ڈائریکٹ اپ لوڈز شروع کریں۔ + users: + bookmarks: صارف کے بُک مارکس کی فہرست بنائیں۔ یہ ICS فارمیٹ استعمال کرتے وقت بُک مارک ریمائنڈرز لوٹاتا ہے۔ + sync_sso: DiscourseConnect کا استعمال کرتے ہوئے صارف کو سنکرونائز کریں۔ + show: صارف کے بارے میں معلومات حاصل کریں۔ + check_emails: صارف ای میلز کی فہرست. + update: صارف کی پروفائل کی معلومات کو اپ ڈیٹ کریں۔ + log_out: صارف کے لیے تمام سیشن لاگ آؤٹ کریں۔ + anonymize: صارف اکاؤنٹس کو گمنام بنائیں۔ + delete: صارف کے اکاؤنٹس کومٹائیں۔ + list: صارفین کی فہرست حاصل کریں۔ + email: + receive_emails: آنے والی ای میلز پر کارروائی کرنے کے لیے اس دائرہ کار کو میل وصول کنندہ کے ساتھ جوڑیں۔ + badges: + create: ایک نیا بیج بنائیں۔ + show: بیج کے بارے میں معلومات حاصل کریں۔ + update: ایک بیج کو اپ ڈیٹ کریں۔ + delete: ایک بیج مٹائیں۔ + list_user_badges: صارف کے بیجز کی فہرست + assign_badge_to_user: کسی صارف کو بیج تفویض کریں۔ + revoke_badge_from_user: کسی صارف سے بیج منسوخ کریں۔ web_hooks: title: "وَیب ھُوک٘س٘" none: "اِس وقت کوئی وَیب ھُوک٘س٘ نہیں ہیں۔" @@ -3229,6 +3921,7 @@ ur: go_back: "واپس فہرست پر" payload_url: "پَیلوڈ یو.آر.ایل." payload_url_placeholder: "https://example.com/postreceive" + warn_local_payload_url: "ایسا لگتا ہے کہ آپ ویب ہک کو مقامی یو آر ایل پر سیٹ کرنے کی کوشش کر رہے ہیں۔ مقامی پتے پر پہنچایا گیا واقعہ ضمنی اثرات یا غیر متوقع طرز عمل کا سبب بن سکتا ہے۔ جاری رہے؟" secret_invalid: "سیکرٹ میں کوئی بھی خالی حروف نہ ہونا ضروری ہے۔" secret_too_short: "سیکرٹس کا کم از کم 12 حروف پر مشتمل ہونا ضروری ہے۔" secret_placeholder: "ایک اختیاری سٹرنگ، سِگنَیچر تخلیق کرنے کیلئے استعمال کیا جاتا ہے" @@ -3247,7 +3940,7 @@ ur: tags_filter: "متحرک ٹیگز" groups_filter_instructions: "متعلقہ وَیب ھُوک٘س٘ صرف اُس صورت میں متحرک کیے جائیں گے اگر واقعہ مخصوص گروپوں کے ساتھ تعلق رکھتا ہے۔ تمام گروپوں کے لئے وَیب ھُوک٘س٘ متحرک کرنے کیلئے خالی چھوڑ دیں۔" groups_filter: "متحرک کیے گئے گروپ" - delete_confirm: "اِس وَیب ھُوک٘س٘گروپ کو حذف کریں؟" + delete_confirm: "اِس وَیب ھُوک کومٹائیں؟" topic_event: name: "ٹاپک واقعہ" details: "جب ایک نیا ٹاپک، رِیوائز، تبدیل یا حذف کیا جائے۔" @@ -3256,6 +3949,7 @@ ur: details: "جب ایک نیا جواب، ترمیم، حذف یا رِِیکور کیا جائے۔" user_event: name: "صارف واقعہ" + details: "جب کوئی صارف لاگ ان ہوتا ہے، لاگ آؤٹ ہوتا ہے، اپنے ای میل کی تصدیق کرتا ہے، تخلیق، منظور یا اپ ڈیٹ ہوتا ہے۔" group_event: name: "گروپ واقعہ" details: "جب ایک گروپ بنایا، اَپ ڈیٹ یا ختم کیا جاتا ہے۔" @@ -3271,6 +3965,18 @@ ur: notification_event: name: "اطلاع واقعہ" details: "جب صارف کو اپنی فِیڈ میں اطلاع موصول ہوتی ہے۔" + user_promoted_event: + name: "یوزر پروموٹڈ ایونٹ" + details: "جب کسی صارف کو ایک اعتماد کی سطح سے دوسری سطح پر ترقی دی جاتی ہے۔" + user_badge_event: + name: "بیج گرانٹ ایونٹ" + details: "جب صارف کو بیج ملتا ہے۔" + group_user_event: + name: "گروپ یوزر ایونٹ" + details: "جب کسی صارف کو گروپ میں شامل یا ہٹا دیا جاتا ہے۔" + like_event: + name: "ایونٹ کی طرح" + details: "جب کوئی صارف کسی پوسٹ کو پسند کرتا ہے۔" delivery_status: title: "موصول ہونے کی آگاہی" inactive: "غیر فعال" @@ -3396,10 +4102,11 @@ ur: new: "نیا" new_style: "نیا سٹائل" install: "انسٹال" - delete: "حذف کریں" - delete_confirm: 'کیا آپ واقعی "%{theme_name}" حذف کرنا چاہتے ہیں؟' + delete: "مٹائیں" + delete_confirm: 'کیا آپ واقعی "%{theme_name}" مٹانا چاہتے ہیں؟' color: "رنگ" opacity: "دھندلاپن" + copy: "نقل" copy_to_clipboard: "کلِپ بورڈ میں کاپی کریں" copied_to_clipboard: "کلِپ بورڈ میں کاپی کر لیا گیا" copy_to_clipboard_error: "کلِپ بورڈ میں ڈیٹا کاپی کرنے پر خرابی کا سامنا کر نا پرا" @@ -3415,9 +4122,11 @@ ur: theme: "تھیم" component: "جزو" components: "اجزاء" + filter_placeholder: "کو فلٹر کرنے کے لیے ٹائپ کریں…" theme_name: "تھیم نام" component_name: "جزو نام" themes_intro: " شروعات کیلئے، ایک موجودہ تھیم منتخب کریں یا ایک نئی انسٹال کریں" + themes_intro_emoji: "خاتون آرٹسٹ ایموجی" beginners_guide_title: "ڈسکورس تِھیمز کا استعمال کرنے کیلئے ابتدائی رہنمائی" developers_guide_title: "ڈسکورس تِھیمز کیلئے ڈویلپر رہنمائی" browse_themes: "کمیونٹی تِھیمز دیکھیں" @@ -3437,20 +4146,29 @@ ur: settings: "ترتیبات" translations: "ترجمہ" extra_scss: "اضافی SCSS" + extra_files: "اضافی فائلیں" + extra_files_upload: "ان فائلوں کو دیکھنے کے لیے تھیم برآمد کریں۔" + extra_files_remote: "ان فائلوں کو دیکھنے کے لیے تھیم ایکسپورٹ کریں یا گٹ ریپوزٹری کو چیک کریں۔" preview: "پیشگی دیکھیں" show_advanced: "اَیڈوانسڈ شعبہ دکھائیں" hide_advanced: "اَیڈوانسڈ شعبہ چھپائیں" hide_unused_fields: "اَیڈوانسڈ غیر استعمال شدہ چھپائیں" is_default: "تِھیم ڈیفالٹ کے طور پر فعال ہے" user_selectable: "صارفین تِھیم کو منتخب کرسکتے ہیں" + color_scheme_user_selectable: "رنگ کی سکیم کو صارفین کی طرف سے منتخب کیا جا سکتا ہے" + auto_update: "ڈسکورس اپ ڈیٹ ہونے پر آٹو اپ ڈیٹ" color_scheme: "رنگ پیلیٹ" + default_light_scheme: "ہلکا (ڈیفالٹ)" color_scheme_select: "تِھیم میں استعمال ہونے والے رنگ منتخب کریں" custom_sections: "اپنی مرضی کے حصے:" theme_components: "تِھیم کے اجزاء" + add_all_themes: "تمام تھیمز شامل کریں" convert: "تبدیل کریں" convert_component_alert: "کیا آپ واقعی اِس کمپنَینٹ کو تِھیم میں تبدیل کر دینا چاہتے ہیں؟ یہ %{relatives} سے کمپنَینٹ کے طور پر ہٹا دیا جائے گا۔" convert_component_tooltip: "اِس کمپنَینٹ کو تِھیم میں تبدیل کریں" + convert_component_alert_generic: "کیا آپ واقعی اس جزو کو تھیم میں تبدیل کرنا چاہتے ہیں؟" convert_theme_alert: "کیا آپ واقعی اِس تِھیم کو کمپنَینٹ میں تبدیل کر دینا چاہتے ہیں؟ یہ %{relatives} سے بالائی چیز کے طور پر ہٹا دیا جائے گا۔" + convert_theme_alert_generic: "کیا آپ واقعی اس تھیم کو جزو میں تبدیل کرنا چاہتے ہیں؟" convert_theme_tooltip: "اِس تِھیم کو کمپنَینٹ میں تبدیل کریں" inactive_themes: "غیر فعال تِھیمز:" inactive_components: "غیر استعمال شدہ کمپنَینٹ:" @@ -3473,28 +4191,36 @@ ur: upload: "اَپ لوڈ" select_component: "ایک کمپنَینٹ منتخب کریں..." unsaved_changes_alert: "آپ نے ابھی تک اپنی تبدیلیوں کو محفوظ نہیں کیا ہے، کیا آپ ان کو رد کر کہ آگے جانا چاہتے ہیں؟" + unsaved_parent_themes: "آپ نے تھیمز کے لیے جزو تفویض نہیں کیا ہے، کیا آپ آگے بڑھنا چاہتے ہیں؟" discard: "منسوخ" stay: "ادھر رہیں" css_html: "اپنی مرضی کے مطابق CSS/HTML" edit_css_html: "CSS/HTML ترمیم کریں" edit_css_html_help: "آپ نے کوئی بھی CSS یا HTML ترمیم نہیں کیا ہے" - delete_upload_confirm: "اِس اَپ لوڈ کو حذف کریں؟ (تِھیم CSS کام کرنا بند کر سکتی ہے!)" + delete_upload_confirm: "اِس اَپ لوڈ کومٹائیں؟ (تِھیم CSS کام کرنا بند کر سکتی ہے!)" + component_on_themes: "ان تھیمز پر جزو شامل کریں" + included_components: "شامل اجزاء" + add_all: "سب شامل کریں" import_web_tip: "رِیپَوزِٹَری میں موجود تِھیم" + direct_install_tip: "کیا آپ واقعی ذیل میں درج ریپوزٹری سے %{name} انسٹال کرنا چاہتے ہیں؟" import_web_advanced: "اَیڈوانسڈ..." import_file_tip: ".tar.gz، .zip، یا .dcstyle.json فائل میں موجود تِھیم" is_private: "تھِیم ایک ذاتی گِٹ رِیپَوزِٹَری میں ہے" remote_branch: "برانچ کا نام (اختیاری)" public_key: "رِیپَوزِٹَری کو درج ذیل پبلک کلید ایکسَیس فراہم کریں:" + public_key_note: "اوپر ایک درست پرائیویٹ ریپوزٹری یو آر ایل داخل کرنے کے بعد، ایک SSH کی یہاں تیار اور ڈسپلے کی جائے گی۔" install: "انسٹال" installed: "انسٹال کیا ہوا" install_popular: "مقبول" install_upload: "آپ کی ڈیوائس سے" install_git_repo: "گِٹ رِیپَوزِٹَری میں سے" install_create: "نیا بنائیں" + duplicate_remote_theme: "تھیم کا جزو \"%{name}\" پہلے ہی انسٹال ہے، کیا آپ واقعی ایک اور کاپی انسٹال کرنا چاہتے ہیں؟" about_theme: "بارے میں" license: "لائسنس" version: "ورژن:" authors: "مصنف:" + creator: "بنائی گئی:" source_url: "ذریعہ" enable: "فعال کریں" disable: "غیر فعال کریں" @@ -3509,6 +4235,7 @@ ur: check_for_updates: "اپ ڈیٹ کے لیے چیک کریں" updating: "اَپ ڈَیٹ کیا جا رہا ہے..." up_to_date: "تِھیم اَپ ڈَیٹ ہے، آخری دفعہ چَیک کیا:" + has_overwritten_history: "موجودہ تھیم ورژن اب موجود نہیں ہے کیونکہ گٹ ہسٹری کو زبردستی دھکا لگا کر اوور رائٹ کر دیا گیا ہے۔" add: "شامل کریں" theme_settings: "تھیم کی ترتیبات" no_settings: "اِس تھیم کی کوئی ترتیبات نہیں ہیں۔" @@ -3518,7 +4245,9 @@ ur: one: "تِھیم %{count} کَمِٹ پیچھے ہے!" other: "تِھیم %{count} کَمِٹس پیچھے ہے!" compare_commits: "(نئے کَمِٹس دیکھیں)" + remote_theme_edits: "اگر آپ اس تھیم میں ترمیم کرنا چاہتے ہیں، تو آپ کو اس کے ریپوزٹری پرتبدیلی درج کروانا ہوگی" repo_unreachable: "اِس تِھیم کی گِٹ رِیپَوزِٹَری سے رابطہ نہیں کیا جا سکا۔ تکنیکی خرابی کا پیغام:" + imported_from_archive: "یہ تھیم زپ فائل سے درآمد کی گئی تھی۔" scss: text: "CSS" title: "اپنی مرضی کے مطابق CSS درج کریں، ہم تمام درست CSS اور SCSS سٹائلز قبول کرتے ہیں" @@ -3534,11 +4263,29 @@ ur: embedded_scss: text: "اَیمبَیڈ کیا ہوا CSS" title: "تبصرے کے اَیمبَیڈ شدہ ورژن کے ساتھ فراہم کیے جانے والا اپنی مرضی کا CSS درج کریں" + color_definitions: + text: "رنگین تعریفیں" + title: "اپنی مرضی کے مطابق رنگ تعریف درج کریں (صرف اعلی درجے کی صارفین)" + placeholder: |2- + + اس اسٹائل شیٹ کو سی ایس ایس کسٹم پراپرٹیز کی فہرست میں حسب ضرورت رنگ شامل کرنے کے لیے استعمال کریں۔ + + مثال: + + %{example} + + پلگ انز اور/یا کور کے ساتھ تنازعات سے بچنے کے لیے پراپرٹی کے ناموں کا سابقہ لگانے کی انتہائی سفارش کی جاتی ہے۔ + head_tag: + text: "ہیڈ" + title: "HTML جو ہیڈ ٹیگ سے پہلے داخل کیا جائے گا۔" body_tag: text: "متن" + title: "HTML جو باڈی ٹیگ سے پہلے داخل کیا جائے گا۔" yaml: text: "YAML" title: "تھیم ترتیبات کی وضاحت YAML فارمَیٹ میں کریں" + scss_color_variables_warning: 'تھیمز میں بنیادی SCSS کلر متغیرات کا استعمال فرسودہ ہے۔ براہ کرم اس کے بجائے CSS حسب ضرورت خصوصیات استعمال کریں۔ مزید تفصیلات کے لیے یہ گائیڈ دیکھیں۔' + scss_warning_inline: "تھیمز میں بنیادی SCSS کلر متغیرات کا استعمال فرسودہ ہے۔" colors: select_base: title: "بَیس رنگ پیلیٹ منتخب کریں" @@ -3549,7 +4296,7 @@ ur: about: "اپنی تِھیمز کے استعمال کردہ رنگوں میں ترمیم کریں۔ شروع کرنے کے لئے ایک نئی پیلیٹ بنائیں۔" new_name: "نئی رنگ پیلیٹ" copy_name_prefix: "نقل از" - delete_confirm: "اِس رنگ پیلیٹ کو حذف کریں؟" + delete_confirm: "اِس رنگ کی تختی کومٹائیں؟" undo: "کالعدم کریں" undo_title: "اِس رنگ کو آخری دفعہ محفوظ کیے جانے کے وقت سے لے کر اَب تک کی اپنی تمام تبدیلیوں کو کالعدم کریں۔" revert: "رِیوَرٹ" @@ -3557,6 +4304,10 @@ ur: primary: name: "بنیادی" description: "زیادہ تر ٹَیکسٹ، آئیکنز اور بارڈرز" + primary-medium: + name: "پرائمری میڈیم" + primary-low-mid: + name: "بنیادی-کم-وسط" secondary: name: "ثانوی" description: "مَین پسِ منظر کا رنگ، اور کچھ بٹنوں کے ٹَیکسٹ کا رنگ۔" @@ -3575,6 +4326,12 @@ ur: highlight: name: "اجاگر کریں" description: "اُجاگر کیے گئے عناصر، جیسے کہ پوسٹس اور ٹاپک، کے پسِ منظر کا رنگ." + highlight-high: + name: "ہائی لائٹ-اعلیٰ" + highlight-medium: + name: "ہائی لائٹ -میڈیم" + highlight-low: + name: "ہائی لائٹ-کم" danger: name: "خطرہ" description: "اِعمال، جیسے کہ پوسٹس اور ٹاپکس کو حذف کرنا، کو نمایاں کرنے کا رنگ۔" @@ -3634,6 +4391,7 @@ ur: format: "فارمیٹ" html: "html" text: "ٹَیکسٹ" + html_preview: "ای میل مواد کا پیش نظارہ" last_seen_user: "صارف کو آخری دفعہ دیکھا:" no_result: "ڈائجسٹ کیلئے کوئی نتائج نہیں پائے گئے۔" reply_key: "جواب کِی" @@ -3670,11 +4428,11 @@ ur: performed_by: "کی طرف سے عمل کیا کیا" no_results: "کوئی ماڈرَیشَن ہسٹری موجود نہیں ہے۔" actions: - delete_user: "صارف حذف کر دیا گیا" + delete_user: "صارف مٹادیا گیا" suspend_user: "صارف معطل کر دیا گیا" silence_user: "صارف خاموش کر دیا گیا" - delete_post: "پوسٹ حذف کر دی گئی" - delete_topic: "ٹاپک حذف کر دیا گیا" + delete_post: "پوسٹ مٹادی گئی" + delete_topic: "موضوع مٹادیا گیا۔" post_approved: "منظورشدہ پوسٹ" logs: title: "لاگز" @@ -3686,7 +4444,7 @@ ur: topic_id: "ٹاپک ID" post_id: "پوسٹ ID" category_id: "زمرہ ID" - delete: "حذف کریں" + delete: "مٹائیں" edit: "ترمیم کریں" save: "محفوظ کریں" screened_actions: @@ -3708,14 +4466,14 @@ ur: show: "دکھائیں" modal_title: "تفصیلات" no_previous: "کوئی پچھلی وَیلیو نہیں ہے۔" - deleted: "کوئی نئی وَیلیو نہیں۔ ریکارڈ حذف کر دیا گیا تھا۔" + deleted: "کوئی نئی قدر نہیں۔ اِنْدِراج مٹادیا گیا۔" actions: - delete_user: "صارف کو حذف کریں" + delete_user: "صارف کو مٹائیں" change_trust_level: "ٹرسٹ لَیول تبدیل کریں" change_username: "صارف نام تبدیل کریں" change_site_setting: "ویب سائٹ کی سیٹِنگ تبدیل کریں" change_theme: "تِھیم تبدیل کریں" - delete_theme: "تِھیم حذف کریں" + delete_theme: "تِھیم مٹائیں" change_site_text: "سائٹ ٹَیکسٹ تبدیل کریں" suspend_user: "صارف معطل کریں" unsuspend_user: "صارف کی معطلی ختم کریں" @@ -3724,14 +4482,14 @@ ur: grant_badge: "بَیج دیں" revoke_badge: "بَیج منسوخ کریں" check_email: "اِیمیل چیک کریں" - delete_topic: "ٹاپک حذف کریں" + delete_topic: "ٹاپک مٹائیں" recover_topic: "ٹاپک غیر حذف کریں" - delete_post: "پوسٹ حذف کریں" + delete_post: "پوسٹ مٹائیں" impersonate: "نقالی" anonymize_user: "صارف گمنام بنائیں" roll_up: "IP بلاکس رول اَپ کریں" change_category_settings: "زمرہ جات کی سیٹِنگ تبدیل کریں" - delete_category: "زمرہ حذف کریں" + delete_category: "زمرہ مٹائیں" create_category: "زمرہ بنائیں" silence_user: "صارف خاموش کریں" unsilence_user: "صارف کی خاموشی ختم کریں" @@ -3742,7 +4500,7 @@ ur: grant_moderation: "ماڈرَیشَن عطا کریں" revoke_moderation: "ماڈرَیشَن منسوخ کریں" backup_create: "بیک اَپ بنائیں" - deleted_tag: "حذف کیا گیا ٹیگ" + deleted_tag: "مٹایا گیا ٹیگ" deleted_unused_tags: "حذف کردہ غیر استعمال شدہ ٹیگز" renamed_tag: "نام تبدیل کیا گیا ٹیگ" revoke_email: "اِیمیل منسوخ کریں" @@ -3759,6 +4517,7 @@ ur: post_edit: "پوسٹ ترمیم" post_unlocked: "پوسٹ کھول دی گئی" check_personal_message: "ذاتی پیغام چیک کریں" + disabled_second_factor: "دو فیکٹر توثیق غیر فعال کریں" topic_published: "ٹاپک شائع کر دیا گیا" post_approved: "منظورشدہ پوسٹ" post_rejected: "مسترد کردہ پوسٹ" @@ -3780,6 +4539,26 @@ ur: change_theme_setting: "تِھیم کی سیٹِنگ تبدیل کریں" disable_theme_component: "تِھیم کمپنَینٹ غیر فعال کریں" enable_theme_component: "تِھیم کمپنَینٹ فعال کریں" + revoke_title: "عنوان کو منسوخ کریں" + change_title: "عنوان تبدیل کریں" + api_key_create: "api کی بنائیں" + api_key_update: "api کی اپ ڈیٹ" + api_key_destroy: "api کی مٹائیں" + override_upload_secure_status: "اپ لوڈ کی محفوظ حیثیت کو اوور رائیڈ کریں" + page_published: "صفحہ کی اشاعت" + page_unpublished: "صفحہ غیر مطبوعہ" + add_email: "ای میل شامل کریں" + update_email: "ای میل کو اپ ڈیٹ کریں۔" + destroy_email: "ای میل کو مٹائیں" + topic_closed: "موضوع بند" + topic_opened: "موضوع کھول دیا" + topic_archived: "موضوع محفوظ شدہ" + topic_unarchived: "موضوع کو غیر محفوظ کر دیا" + post_staff_note_create: "عملے کا نوٹ شامل کریں" + post_staff_note_destroy: "عملے کے نوٹ کو مٹائیں" + delete_group: "گروپ مٹائیں" + watched_word_create: "دیکھا ہوا لفظ شامل کریں" + watched_word_destroy: "دیکھا ہوا لفظ مٹائیں" screened_emails: title: "سکرین کی گئی اِیمیلز" description: "جب کوئی نیا اکاؤنٹ بنانے کی کوشش کرے گا، تو مندرجہ ذیل اِیمیل ایڈریسوں کو چیک کیا جائے گا اور رجسٹریشن بلاک، یا کوئی اور کارروائی کی جائے گی۔" @@ -3793,6 +4572,7 @@ ur: domain: "ڈومین" screened_ips: title: "سکرین کیے گئے IP" + description: 'IP پتے جو دیکھے جا رہے ہیں۔ اجازت دینے والے IP پتوں کی فہرست میں "اجازت دیں" کا استعمال کریں۔' delete_confirm: "کیا آپ واقعی %{ip_address} کیلئے اصول کو ہٹانا چاہتے ہیں؟" actions: block: "بلاک کریں" @@ -3823,22 +4603,40 @@ ur: title: "نظر رکھے ہوئے الفاظ" search: "سرچ" clear_filter: "صاف کریں" + show_words: + one: "%{count} لفظ دکھائیں۔" + other: "%{count} الفاظ دکھائیں" download: ڈاؤن لوڈ clear_all: تمام کو صاف کریں + clear_all_confirm: "کیا آپ واقعی %{action} ایکشن کے لیے دیکھے گئے تمام الفاظ کو صاف کرنا چاہتے ہیں؟" + invalid_regex: 'دیکھا ہوا لفظ "%{word}" ایک غلط ریگولر ایکسپریشن ہے۔' + regex_warning: 'دیکھے گئے الفاظ ریگولر ایکسپریشنز ہیں اور وہ خود بخود الفاظ کی حدود کو شامل نہیں کرتے ہیں۔ اگر آپ چاہتے ہیں کہ ریگولر ایکسپریشن پورے الفاظ سے مماثل ہو، تو اپنے ریگولر ایکسپریشن کے شروع اور آخر میں \b شامل کریں۔' actions: block: "بلاک کریں" censor: "سَینسَر" require_approval: "منظوری کی ضرورت ہے" flag: "فلَیگ" replace: "بدلیں" + tag: "ٹیگ" silence: "خاموش کریں" + link: "لنک" action_descriptions: block: "اِن الفاظ پر مشتمل پوسٹس شائع ہونے سے روکیں۔ جب صارف اپنی پوسٹ شائع کرنے کی کوشش کرے گا تو اُسے ایک خرابی کا پیغام دکھایا جائے گا۔" censor: "اِن الفاظ پر مشتمل پوسٹس کوشائع ہونے دیں، لیکن سینسر کیے گئے الفاظ کو چھپانے والے حروف کے ساتھ تبدیل کریں۔" require_approval: "اِن الفاظ پر مشتمل پوسٹس کے نظر آنے سے پہلے اسٹاف کی طرف سے منظوری کی ضرورت ہوگی۔" flag: "اِن الفاظ پر مشتمل پوسٹس کوشائع ہونے دیں، لیکن ان کو غیر مناسب کے طور پر فلَیگ کریں تاکہ منتظمین ان کا جائزہ لے سکیں۔" + replace: "پوسٹس میں الفاظ کو دوسرے الفاظ سے بدلیں" + tag: "پہلی پوسٹ کی بنیاد پر موضوعات کو خودکار طور پر ٹیگ کریں" + silence: "ان الفاظ پر مشتمل صارفین کی پہلی پوسٹس کو دیکھنے سے پہلے عملے کی منظوری درکار ہوگی اور صارف خود بخود خاموش ہو جائے گا۔" + link: "پوسٹس میں الفاظ کو لنکس سے بدلیں" form: + label: "لفظ یا جملہ ہے۔" + placeholder: "لفظ یا جملہ درج کریں (* سب کیلیے )" placeholder_regexp: "رَیگولر اَیکسپرَیشَن" + replace_label: "متبادل" + replace_placeholder: "مثال" + tag_label: "ٹیگ" + link_label: "لنک" link_placeholder: "https://example.com" add: "شامل کریں" success: "کامیابی" @@ -3847,6 +4645,7 @@ ur: upload_successful: "اَپ لوڈ کامیاب ہوا۔ الفاظ شامل کردیے گئے ہیں۔" test: button_label: "ٹیسٹ" + modal_title: "%{action}: دیکھے گئے الفاظ کو جانچیں" description: "نظر رکھے ہوئے الفاظ کے ساتھ مَیچ چَیک کرنے کیلئے نیچے ٹَیکسٹ درج کریں۔" found_matches: "ملے مَیچ:" no_matches: "کوئی میل نہیں ملے" @@ -3892,6 +4691,7 @@ ur: title: "اِس صارف کا اِیمیل ایڈریس ظاہر کریں" text: "دکھائیں" check_sso: + title: "SSO پے لوڈ کو ظاہر کریں" text: "دکھائیں" user: suspend_failed: "اِس صارف کو معطل کرتے ہوے کچھ غلط ہو گیا %{error}" @@ -3901,6 +4701,13 @@ ur: suspend_reason_hidden_label: "آپ کیوں معطل کر رہے ہیں؟ صارف جب لاگ اِن کرنے کی کوشش کرے گا تو اُسے یہ ٹیکسٹ دکھایا جائے گا۔ اِسے مختصر رکھیں۔" suspend_reason: "وجہ" suspend_reason_title: "وجہ معطلی" + suspend_reasons: + not_listening_to_staff: "عملے کی رائے نہیں سنیں گے" + consuming_staff_time: "عملے کے وقت کی غیر متناسب مقدار استعمال کی" + combative: "بہت جنگجو" + in_wrong_place: "غلط جگہ پر" + no_constructive_purpose: "کمیونٹی میں اختلاف پیدا کرنے کے علاوہ ان کے اعمال کا کوئی تعمیری مقصد نہیں" + custom: "ترمیم..." suspend_message: "اِی مَیل پیغام" suspend_message_placeholder: "اختیاریطور پر معطلی کے بارے میں مزید معلومات فراہم کریں اور یہ صارف کو ای میل کر دی جائیں گی۔" suspended_by: "کی طرف سے معطل کیا گیا" @@ -3914,17 +4721,20 @@ ur: silence_message_placeholder: "(ڈیفالٹ پیغام بھیجنے کیلئے خالی چھوڑ دیں)" suspended_until: "(%{until} تک)" cant_suspend: "یہ صارف معطل نہیں کیا جا سکتا۔" - delete_posts_failed: "پوسٹس حذف کرنے میں ایک مسئلہ پیش آیا۔" + delete_posts_failed: "پوسٹس مٹانے میں ایک مسئلہ پیش آیا۔" post_edits: "پوسٹ ترامیم" + view_edits: "ترامیم دیکھیں" penalty_post_actions: "آپ اِس مُنسلِک پوسٹ کے ساتھ کیا کرنا چاہیں گے؟" - penalty_post_delete: "پوسٹ حذف کریں" - penalty_post_delete_replies: "پوسٹ حذف کریں + جوابات کو بھی" - penalty_post_edit: " پوسٹ ترمیم کریں" + penalty_post_delete: "پوسٹ مٹائیں" + penalty_post_delete_replies: "پوسٹ مٹائیں + جوابات کو بھی" + penalty_post_edit: "پوسٹ میں ترمیم کریں" penalty_post_none: "کچھ نہ کریں" penalty_count: "سزا شمار" + penalty_history: "سزا کی تاریخ" clear_penalty_history: title: "سزا ہِسٹری صاف کریں" description: "سزاوں والے صارفین TL3 تک نہیں پہنچ سکتے" + delete_all_posts_confirm_MF: "آپ مٹانے والے ہیں {POSTS, plural, one {#پوسٹ} other {# پوسٹس}} اور {TOPICS, plural, one {# موضوع} other {# موضوعات}}. کیا آپ پر \n یقین ہیں؟" silence: "خاموش کریں" unsilence: "خاموشی ختم کریں" silenced: "خاموش کیا ہوا؟" @@ -3941,13 +4751,14 @@ ur: logged_out: "صارف کو تمام ڈِیوائیسِز سے لاگ آؤٹ کر دیا گیا" revoke_admin: "اَیڈمِن منسوخ کریں" grant_admin: "اَیڈمِن عطا کریں" + grant_admin_success: "نئے منتظم کی تصدیق ہوئی۔" grant_admin_confirm: "نئے ایڈمِنِسٹریٹر کی توثیق کیلئے ہم نے آپ کو ایک ای میل بھیجی ہے۔ براہ مہربانی اسے کھولیں اور ہدایات پر عمل کریں۔" revoke_moderation: "ماڈرَیشَن منسوخ کریں" grant_moderation: "ماڈرَیشَن عطا کریں" unsuspend: "معطلی ختم کریں" suspend: "معطل کریں" show_flags_received: "ملے فلَیگز دکھائیں" - flags_received_by: "%{username} کو ملے فلَیگز " + flags_received_by: "%{username} کو ملے نشانات" flags_received_none: "اِس صارف کو کوئی فلَیگز نہیں ملے۔" reputation: ساکھ permissions: اجازتیں @@ -3957,10 +4768,13 @@ ur: private_topics_count: نِجی ٹاپک posts_read_count: پڑھی گئیں پوسٹس post_count: بنائی گئیں پوسٹس + second_factor_enabled: دو فیکٹر کی توثیق فعال ہے topics_entered: دیکھ لیے گئے ٹاپک flags_given_count: فلَیگز دیے گئے flags_received_count: فلَیگز ملے warnings_received_count: انتباہات ملیں + warnings_list_warning: | + ایک ناظم کے طور پر، ہو سکتا ہے آپ ان تمام موضوعات کو نہ دیکھ سکیں۔ اگر ضروری ہو تو، ایڈمن یا جاری کرنے والے ماڈریٹر سے @moderators کو پیغام تک رسائی دینے کو کہیں۔ flags_given_received_count: "فلَیگز دیے / ملے" approve: "منظور" approved_by: "کی طرف سے منظور کیا گیا" @@ -3972,32 +4786,63 @@ ur: anonymize_confirm: "کیا آپ واقعی اِس اکاؤنٹ کو گمنام بنانا چاہتے ہیں؟ یہ صارف نام اور اِیمیل تبدیل، اور تمام پروفائل معلومات رِی سَیٹ کر دے گا۔" anonymize_yes: "جی ہاں، اِس اکاؤنٹ کو گمنام بنائیں" anonymize_failed: "اِس اکاؤنٹ کو گمنام بنانے میں ایک مسئلہ پیش آیا۔" - delete: "صارف حذف کریں" + delete: "صارف مٹائیں" delete_posts: - button: "تمام پوسٹس حذف کریں" + button: "تمام پوسٹس مٹائیں" progress: - description: "پوسٹس حذف کی جا رہی ہیں..." + title: "پوسٹس کو مٹانے کی پیشرفت" + description: "پوسٹس مٹائی جا رہی ہیں..." confirmation: + title: "@%{username}کی تمام پوسٹس مٹائیں" + description: | +

    کیا آپ واقعی @%{username}کی %{post_count} پوسٹس کو مٹانا چاہیں گے؟ + +

    اسے کالعدم نہیں کیا جا سکتا!

    + +

    جاری رکھنے کے لیے ٹائپ کریں: %{text}

    + text: "@%{username}کی پوسٹس مٹائیں" + delete: "@%{username}کی پوسٹس مٹائیں" cancel: "منسوخ" merge: + button: "ضم" prompt: + title: "منتقلی اور حذف کریں @%{username}" + description: | +

    براہ کرم @%{username}کے مواد کے لیے ایک نیا مالک منتخب کریں۔

    + +

    @%{username} کے ذریعہ بنائے گئے تمام عنوانات، پوسٹس، پیغامات اور دیگر مواد کو منتقل کر دیا جائے گا۔

    + target_username_placeholder: "نئے مالک کا صارف نام" + transfer_and_delete: "منتقلی اور مٹائیں @%{username}" cancel: "منسوخ" + progress: + title: "پیشرفت کو ضم کریں" confirmation: + title: "منتقلی اور حذف کریں @%{username}" + description: | +

    تمام @%{username}کے مواد کو منتقل کیا جائے گا اور @%{targetUsername}سے منسوب کیا جائے گا۔ مواد کی منتقلی کے بعد، @%{username}کا اکاؤنٹ حذف کر دیا جائے گا۔

    + +

    اسے کالعدم نہیں کیا جا سکتا!

    + +

    جاری رکھنے کے لیے ٹائپ کریں: %{text}

    + text: "@%{username} سے @%{targetUsername}منتقل کریں" + transfer_and_delete: "منتقلی اور مٹائیں @%{username}" cancel: "منسوخ" + merging_user: "صارف کو ضم کیا جا رہا ہے..." + merge_failed: "صارفین کو ضم کرتے وقت ایک خامی تھی۔" delete_forbidden_because_staff: "اَیڈمن اور ماڈریٹرز حذف نہیں کیے جا سکتے۔" delete_posts_forbidden_because_staff: "اَیڈمن اور ماڈریٹرز کی تمام پوسٹس حذف نہیں کی جا سکتیں۔" delete_forbidden: - one: "اگر صارفین کی پوسٹس موجود ہوں تو اُنہیں حذف نہیں کیا جا سکتا۔ ایک صارف کو حذف کرنے کی کوشش سے پہلے تمام پوسٹس کو حذف کریں۔ (%{count} دن سے زیادہ پرانی پوسٹس کوحذف نہیں کیا جا سکتا۔)" - other: "اگر صارفین کی پوسٹس موجود ہوں تو اُنہیں حذف نہیں کیا جا سکتا۔ ایک صارف کو حذف کرنے کی کوشش سے پہلے تمام پوسٹس کو حذف کریں۔ (%{count} دن سے زیادہ پرانی پوسٹس کوحذف نہیں کیا جا سکتا۔)" + one: "اگر صارفین کے پاس پوسٹس ہیں تو انہیں حذف نہیں کیا جا سکتا۔ کسی صارف کو مٹانے کی کوشش کرنے سے پہلے تمام پوسٹس کومٹائیں۔ ( %{count} دن سے زیادہ پرانی پوسٹس کو حذف نہیں کیا جا سکتا۔)" + other: "اگر صارفین کے پاس پوسٹس ہیں تو انہیں حذف نہیں کیا جا سکتا۔ کسی صارف کو مٹانے کی کوشش کرنے سے پہلے تمام پوسٹس کومٹائیں۔ ( %{count} دن سے زیادہ پرانی پوسٹس کو حذف نہیں کیا جا سکتا۔)" cant_delete_all_posts: one: "تمام پوسٹس کو حذف نہیں کیا جاسکتا۔ کچھ پوسٹس %{count} دن پرانی سے زیادہ پرانی ہیں۔(delete_user_max_post_age سیٹِنگ۔)" - other: "تمام پوسٹس کو حذف نہیں کیا جاسکتا۔ کچھ پوسٹس %{count} دن پرانی سے زیادہ پرانی ہیں۔(delete_user_max_post_age سیٹِنگ۔)" + other: "تمام پوسٹس کو حذف نہیں کیا جاسکتا۔ کچھ پوسٹس %{count} دن سے زیادہ پرانی ہیں۔(delete_user_max_post_age سیٹِنگ۔)" cant_delete_all_too_many_posts: one: "تمام پوسٹس کو حذف نہیں کیا جاسکتا کیونکہ صارف کی %{count} سے زیادہ پوسٹس ہیں۔ (delete_all_posts_max)" other: "تمام پوسٹس کو حذف نہیں کیا جاسکتا کیونکہ صارف کی %{count} سے زیادہ پوسٹس ہیں۔ (delete_all_posts_max)" delete_confirm: "صارفین کو حذف کرنے کے بجائے اُن کو نام نہاد کر دینے کو ترجیح دی جاتی ہے، تاکہ موجودہ بات چیتوں سے مواد ہٹانے سے بچا جا سکے۔

    کیا آپ واقعی اِس صارف کو حذف کرنا چاہتے ہیں؟ یہ مُستقِل عمل ہے!" delete_and_block: "اِس اِیمیل اور IP ایڈریس کو حذف اور بلاک کریں" - delete_dont_block: "صرف حذف کریں" + delete_dont_block: "صرف مٹائیں" deleting_user: "صارف حذف کیا جا رہا ہے..." deleted: "صارف حذف کر دیا گیا تھا۔" delete_failed: "اُس صارف کو حذف کرنے میں ایک مسئلہ پیش آیا۔ صارف کو حذف کرنے سے پہلے اس بات کا یقین کرلیں کہ تمام پوسٹس حذف کر دی گئی ہیں۔" @@ -4027,6 +4872,7 @@ ur: threshold_reached: "اس اِیمیل سے بہت زیادہ باؤنسِز موصول ہوئے۔" trust_level_change_failed: "صارف کا ٹرسٹ لَیول تبدیل کرنے میں ایک مسئلہ پیش آیا۔" suspend_modal_title: "صارف معطل کریں" + confirm_cancel_penalty: "کیا آپ واقعی جرمانہ رد کرنا چاہتے ہیں؟" trust_level_2_users: "ٹرسٹ لَیول 2 کے صارفین" trust_level_3_requirements: "ٹرسٹ لَیول 3 کے تقاضے" trust_level_locked_tip: "ٹرسٹ لَیول لاک ہے، سِسٹم صارف کا لَیول زیادہ یا کم نہیں کرے گا" @@ -4035,6 +4881,7 @@ ur: unlock_trust_level: "ٹرسٹ لَیول کا لاک ختم کریں" silenced_count: "خاموش کیو ہوئے" suspended_count: "معطل کردہ" + last_six_months: "گزشتہ 6 ماہ" tl3_requirements: title: "ٹرسٹ لَیول 3 کے تقاضے" table_title: @@ -4045,7 +4892,7 @@ ur: visits: "زائرین کی تعداد" days: "دن" topics_replied_to: "جن ٹاپکس کا جواب دیا گیا" - topics_viewed: " دیکھ لیے گئے ٹاپک" + topics_viewed: "دیکھے گئے موضوعات" topics_viewed_all_time: "دیکھ لیے گئے ٹاپک (تمام وقت)" posts_read: "پڑھی گئیں پوسٹس" posts_read_all_time: "پڑھی گئیں پوسٹس (تمام وقت)" @@ -4065,11 +4912,15 @@ ur: locked_will_not_be_promoted: "ٹرسٹ کی سطح مقفل۔ کبھی بھی ترقی نہیں دی جائے گی۔" locked_will_not_be_demoted: "ٹرسٹ کی سطح مقفل۔ کبھی بھی تنزلی نہیں کی جائے گی۔" discourse_connect: + title: "DiscourseConnect اکیلا سائن آن" external_id: "بیرونی آئی ڈی" external_username: "صارف کا نام" external_name: "نام" external_email: "اِی میل" external_avatar_url: "پروفائل کی تصویر کا یوآرایل" + last_payload: "آخری پے لوڈ" + delete_sso_record: "ایس ایس او ریکارڈ کو حذف کریں" + confirm_delete: "کیا آپ واقعی اس DiscourseConnect ریکارڈ کو حذف کرنا چاہیں گے؟" user_fields: title: "صارف کے لیے دیئے گئے خانے" help: "خانے شامل کریں جو آپ کے صارفین پُر کر سکیں۔" @@ -4100,10 +4951,15 @@ ur: title: "صارف کارڈ پر دکھائیں؟" enabled: "صارف کارڈ پر دکھایا گیا" disabled: "صارف کارڈ پر نہیں دکھایا گیا" + searchable: + title: "قابل تلاش؟" + enabled: "قابل تلاش؟" + disabled: "قابل تلاش نہیں" field_types: text: "ٹَیکسٹ کی جگہ" confirm: "تصدیق" dropdown: "ڈراپ ڈاؤن" + multiselect: "کثیر الانتخاب" site_text: description: "آپ اپنے فورم پر کسی بھی ٹَیکسٹ کو اپنی مرضی کے مطابق بدل سکتے ہیں۔ براہ مہربانی شروع کرنے کیلئے ذیل میں سرچ کریں:" search: "آپ جس ٹَیکسٹ میں ترمیم کرنا چاہتے اُسے سرچ کریں" @@ -4114,12 +4970,19 @@ ur: go_back: "واپس سَرچ پر" recommended: "اَپنی ضروریات کے مطابق کرنے کے لئے ہم مندرجہ ذیل ٹَیکسٹ کو اپنی مرضی کے حساب سے تبدیل کرنے کی تجویز دیں گے:" show_overriden: "صرف اووَر رائیڈ ہوئے دکھائیں" + locale: "زبان:" + fallback_locale_warning: "آپ %{fallback}کی بنیاد پر ایک زبان میں ترمیم کر رہے ہیں۔ وہ صارفین جو %{fallback} کو اپنی انٹرفیس زبان کے طور پر منتخب کرتے ہیں وہ آپ کی تبدیلیاں نہیں دیکھ پائیں گے۔" more_than_50_results: "50 سے زائد نتائج ہیں۔ برائے مہربانی اپنی تلاش کو تنگ کریں۔" settings: show_overriden: "صرف اووَر رائیڈ ہوئے دکھائیں" + history: "تبدیلی کی تاریخ دیکھیں" reset: "رِی سَیٹ" none: "کوئی نہیں" site_settings: + emoji_list: + invalid_input: "ایموجی لسٹ میں صرف درست ایموجی نام ہونے چاہئیں، جیسے: گلے ملنا" + add_emoji_button: + label: "ایموجی شامل کریں" title: "سیٹِنگ" no_results: "کوئی نتائج نہیں پائے گئے۔" more_than_30_results: "30 سے زائد نتائج ہیں۔ برائے مہربانی اپنی تلاش کو تنگ کریں یا ایک زمرہ منتخب کریں۔" @@ -4167,7 +5030,14 @@ ur: secret_list: invalid_input: "اِن پُٹ فیلڈز خالی یا عمودی بار کے حرف پر مشتمل نہیں ہو سکتیں۔" default_categories: + modal_description: "کیا آپ اس تبدیلی کو تاریخی طور پر لاگو کرنا چاہیں گے؟ اس سے %{count} موجودہ صارفین کی ترجیحات بدل جائیں گی۔" modal_yes: "جی ہاں" + modal_no: "نہیں، صرف آگے بڑھتے ہوئے تبدیلی کا اطلاق کریں" + simple_list: + add_item: "آئٹم شامل کریں..." + json_schema: + edit: ایڈیٹر لانچ کریں + modal_title: "%{name}میں ترمیم" badges: title: بَیج new_badge: نیا بَیج @@ -4204,7 +5074,12 @@ ur: enabled: بَیج فعال کریں icon: آئیکن image: تصویر + graphic: گرافک icon_help: "ایک فونٹ اَوسم آئیکن کا نام درج کریں (عام آئیکنز کیلئے 'far-' سابقہ اور برانڈ آئیکنز کیلئے 'fab-' سابقہ استعمال کریں)" + image_help: "تصویر اپ لوڈ کرنا آئیکن فیلڈ کو اوور رائیڈ کرتا ہےاگر دونوں سیٹ ہیں تو ۔" + select_an_icon: "ایک آئکن منتخب کریں" + upload_an_image: "ایک تصویر اپ لوڈ کریں" + read_only_setting_help: "متن میں ترمیم" query: بَیج قُوَیری (SQL) target_posts: ھدف پوسٹس کو قُوَیری کریں auto_revoke: روزانہ رَیوَوکَیشَن قُوَیری چلائیں @@ -4237,22 +5112,40 @@ ur: with_time: %{username} %{time} پر badge_intro: title: "شروعات کیلئے، ایک موجودہ بَیج منتخب کریں یا ایک نیا بنائیں" + emoji: "خاتون طالب علم ایموجی" what_are_badges_title: "بَیج کیا ہیں؟" badge_query_examples_title: "بَیج قُوَیری کی مثالیں" + mass_award: + title: اجتمائی ایوارڈ + description: ایک ہی وقت میں بہت سے صارفین کو ایک ہی بیج سے نوازیں۔ + no_badge_selected: شروع کرنے کے لیے براہ کرم ایک بیج منتخب کریں۔ + perform: "صارفین کو ایک ہی بیج سے نوازیں" + upload_csv: صارف کے ای میلز یا صارف ناموں کے ساتھ CSV اپ لوڈ کریں + aborted: براہ کرم ایک CSV اپ لوڈ کریں جس میں صارف کے ای میلز یا صارف نام ہوں + success: آپ کا CSV موصول ہو گیا ہے اور %{count} صارفین کو جلد ہی ان کا بیج موصول ہو جائے گا۔ + csv_has_unmatched_users: "درج ذیل اندراجات CSV فائل میں ہیں لیکن وہ موجودہ صارفین سے مماثل نہیں ہو سکیں، اور اس وجہ سے بیج وصول نہیں کریں گے:" + csv_has_unmatched_users_truncated_list: "CSV فائل میں %{count} اندراجات تھے جو موجودہ صارفین سے مماثل نہیں ہوسکے، اور اس وجہ سے بیج وصول نہیں کریں گے۔ بے مثال اندراجات کی بڑی تعداد کی وجہ سے، صرف پہلی 100 دکھائی گئی ہیں:" + replace_owners: پچھلے مالکان سے بیج ہٹا دیں + grant_existing_holders: موجودہ بیج ہولڈرز کو اضافی بیجز دیں emoji: title: "اِیمَوجی" + help: "نئے ایموجی شامل کریں جو سب کے لیے دستیاب ہوں گے۔ متعدد فائلوں کو ان کے فائل کے ناموں کا استعمال کرتے ہوئے ایموجیز بنانے کے لیے نام درج کیے بغیر ایک ساتھ گھسیٹیں اور چھوڑیں۔ منتخب کردہ گروپ کو ان تمام فائلوں کے لیے استعمال کیا جائے گا جو ایک ہی وقت میں شامل کی گئی ہیں۔ آپ فائل چننے والے کو کھولنے کے لیے 'نیا ایموجی شامل کریں' پر بھی کلک کر سکتے ہیں۔" add: "نئی اِیمَوجی شامل کریں" + choose_files: "فائلوں کا انتخاب" uploading: "اَپ لوڈ کیا جا رہا ہے..." name: "نام" group: "گروپ" image: "تصویر" + alt: "حسب ضرورت ایموجی پیش نظارہ" delete_confirm: "کیا آپ واقعی :%{name}: اِیمَوجی حذف کرنا چاہتے ہیں؟" embedding: get_started: "اگر آپ ڈِسکورس کو ایک اور ویب سائٹ پر اَیمبَیڈ کرنا چاہتے ہیں، تو ہوسٹ شامل کر کے شروع کریں۔" confirm_delete: "کیا آپ واقعی اس ہَوسٹ کو حذف کرنا چاہتے ہیں؟" + sample: "گفتگو کے عنوانات کو تخلیق اور شامل کرنے کے لیے درج ذیل HTML کوڈ کو اپنی سائٹ میں چسپاں کریں۔ REPLACE_ME کو اس صفحہ کے کینونیکل URL سے بدل دیں جس پر آپ اسے شامل کر رہے ہیں۔" title: "اَیمبَیڈ کرنا" host: "اجازت یافتہ ہَوسٹ" class_name: "کلاس کا نام" + allowed_paths: "پاتھ کی اجازت کی فہرست" edit: "ترمیم" category: "زمرہ میں پوسٹ کریں" add_host: "ہَوسٹ شامل کریں" @@ -4263,12 +5156,14 @@ ur: embed_post_limit: "پعسٹس اَیمبَیڈ کرنے کی زیادہ سے زیادہ تعداد۔" embed_title_scrubber: "پوسٹس کے عنوان صاف کرنے کیلئے استعمال ہونے والا رَیگولَر اَیکسپرَیشن" embed_truncate: "اَیمبَیڈ کی گئی پوسٹس کو تراشیں" + embed_unlisted: "جب تک کوئی جواب نہیں آتا درآمد شدہ عنوانات کو غیر فہرست میں رکھا جائے گا۔" allowed_embed_selectors: "عناصر کیلئے CSS سلیکٹر جن کی اَیمبَیڈ میں اجازت ہے" blocked_embed_selectors: "عناصر کیلئے CSS سلیکٹر جن کو اَیمبَیڈ سے ہٹایا گیا ہے" allowed_embed_classnames: "اجازت یافتہ CSS کلاسوں کے نام" save: "اَیمبَیڈ کرنے کی سیٹِنگ محفوظ کریں" permalink: title: "دائمی لِنکس" + description: "URLs جو فورم کو معلوم نہیں, کے لیے دوبارہ ہدایات لاگو کریں۔" url: "URL" topic_id: "ٹاپک ID" topic_title: "ٹاپک" @@ -4276,6 +5171,10 @@ ur: post_title: "پوسٹ" category_id: "زمرہ ID" category_title: "زمرہ" + tag_name: "ٹیگ کا نام" + external_url: "بیرونی یا متعلقہ URL" + destination: "منزل" + copy_to_clipboard: "پرمالنک کو کلپ بورڈ میں کاپی کریں" delete_confirm: کیا آپ واقعی اِس دائمی لِنک کو حذف کرنا چاہتے ہیں؟ form: label: "نیا:" @@ -4313,5 +5212,8 @@ ur: moderator: "ماڈریٹر" regular: "رَیگولر صارف" previews: + topic_title: "ایک بحث کےموضوع کی سرخی" share_button: "شیئر" reply_button: "جواب" + topic_preview: "موضوع دکھائیں" + homepage_preview: "ہوم پیج دکھائیں" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index a36722d01a..66f2bfc274 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -96,7 +96,6 @@ vi: to_placeholder: "đến nay" share: topic_html: 'Chủ đề:%{topicTitle}' - post: "đăng #%{postNumber}" close: "đóng" twitter: "Chia sẻ trên Twitter" facebook: "Chia sẻ trên Facebook" @@ -1600,22 +1599,16 @@ vi: not_approved: "Tài khoản của bạn chưa được kiểm duyệt. Bạn sẽ nhận được email thông báo khi bạn được phép đăng nhập." google_oauth2: name: "Goole" - title: "với Google" twitter: name: "Twitter" - title: "với Twitter" instagram: name: "Instagram" - title: "với Instagram" facebook: name: "Facebook" - title: "với Facebook" github: name: "GitHub" - title: "với GitHub" discord: name: "Discord" - title: "với Discord" second_factor_toggle: totp: "Sử dụng ứng dụng xác thực thay thế" backup_code: "Sử dụng mã dự phòng để thay thế" @@ -1630,7 +1623,6 @@ vi: success: "Tài khoản của bạn đã được tạo và bây giờ bạn đã đăng nhập." name_label: "T" password_label: "Mật khẩu" - optional_description: "(tùy chọn)" password_reset: continue: "Tiếp tục truy cập %{site_name}" emoji_set: @@ -2656,10 +2648,8 @@ vi: tag_groups_placeholder: "(Tùy chọn) danh sách các nhóm thẻ được phép" manage_tag_groups_link: "Quản lý nhóm thẻ" allow_global_tags_label: "Đồng thời cho phép các thẻ khác" - tag_group_selector_placeholder: "(Tùy chọn) Nhóm thẻ" - required_tag_group_description: "Yêu cầu các chủ đề mới có thẻ từ nhóm thẻ:" - min_tags_from_required_group_label: "Số thẻ:" - required_tag_group_label: "Nhóm thẻ:" + required_tag_group: + delete: "Xóa" topic_featured_link_allowed: "Cho phép các liên kết nổi bật trong danh mục này" delete: "Xóa chuyên mục" create: "Chuyên mục mới" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index fddd46e255..9657bee3b0 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -98,7 +98,7 @@ zh_CN: to_placeholder: "截至日期" share: topic_html: '话题:%{topicTitle}' - post: "帖子 #%{postNumber}" + post: "%{username} 发布帖子 %{postNumber}" close: "关闭" twitter: "分享到 Twitter" facebook: "分享到 Facebook" @@ -261,6 +261,8 @@ zh_CN: unbookmark_with_reminder: "点击以移除此话题中的所有书签和提醒。" bookmarks: created: "您已将此帖子加入书签。%{name}" + create_for: "为 %{type} 创建书签" + edit_for: "为 %{type} 编辑书签" not_bookmarked: "将此帖子加入书签" remove_reminder_keep_bookmark: "删除提醒并保留收藏" created_with_reminder: "您已将此帖子加入书签,并于 %{date}设置了一个提醒。%{name}" @@ -321,6 +323,7 @@ zh_CN: saved: "已保存!" upload: "上传" uploading: "正在上传…" + processing: "正在处理..." uploading_filename: "正在上传:%{filename}…" processing_filename: "正在处理:%{filename}…" clipboard: "剪贴板" @@ -1101,6 +1104,8 @@ zh_CN: warnings: "官方警告" read_more_in_group: "想阅读更多?浏览%{groupLink}或中的其他话题。" read_more: "想阅读更多内容? 请在 个人私信中浏览其他消息。" + read_more_group_pm_MF: "还有{ UNREAD, plural, =0 {} other { # 个未读} }{ NEW, plural, =0 {} other {{BOTH, select, true{和} false {} other{}} # 个新消息} },或者浏览{groupLink}的其他消息" + read_more_personal_pm_MF: "还有{ UNREAD, plural, =0 {} other { # 个未读} }{ NEW, plural, =0 {} other {{BOTH, select, true{和} false {} other{}} # 个新消息} },或者浏览个人消息" preferences_nav: account: "帐户" security: "安全性" @@ -1710,27 +1715,23 @@ zh_CN: not_approved: "您的帐户尚未获得批准。一旦可以登录,您会收到电子邮件通知。" google_oauth2: name: "Google" - title: "通过 Google" - sr_title: "使用谷歌登录" twitter: name: "Twitter" - title: "通过 Twitter" - sr_title: "使用推特登录" instagram: name: "Instagram" - title: "通过 Instagram" + title: "使用 Instagram 登录" sr_title: "使用 Instagram 登录" facebook: name: "Facebook" - title: "通过 Facebook" + title: "用脸书登入" sr_title: "用脸书登入" github: name: "GitHub" - title: "通过 GitHub" + title: "使用 GitHub 登录" sr_title: "使用 GitHub 登录" discord: name: "Discord" - title: "通过 Discord" + title: "使用 Discord 登录" sr_title: "使用 Discord 登录" second_factor_toggle: totp: "改用身份验证器应用" @@ -1747,7 +1748,6 @@ zh_CN: success: "您的帐户已创建,您现在已登录。" name_label: "姓名" password_label: "密码" - optional_description: "(可选)" password_reset: continue: "继续访问 %{site_name}" emoji_set: @@ -2680,7 +2680,12 @@ zh_CN: show_hidden: "查看已忽略的内容。" deleted_by_author_simple: "(帖子已被作者删除)" collapse: "收起" + sr_collapse_replies: "折叠嵌入回复" + sr_expand_replies: + other: "这个帖子有 %{count} 个回复。点击展开" expand_collapse: "展开/收起" + sr_below_embedded_posts_description: "帖子#%{post_number} 回复" + sr_embedded_reply_description: "@%{username} 回复帖子 #%{post_number}" locked: "一名管理人员已将此帖子锁定为无法编辑" gap: other: "查看 %{count} 个隐藏回复" @@ -2697,6 +2702,10 @@ zh_CN: has_likes_title_only_you: "您赞了此帖子" has_likes_title_you: other: "您和其他 %{count} 人赞了此帖子" + sr_post_like_count_button: + other: "%{count} 人点赞这个帖子。点击查看" + sr_post_read_count_button: + other: "%{count} 人阅读过这个帖子。点击查看" filtered_replies_hint: other: "查看此帖子及其 %{count} 个回复" filtered_replies_viewing: @@ -2788,6 +2797,8 @@ zh_CN: other: "还有其他 %{count} 人赞了此帖子" read_capped: other: "还有其他 %{count} 人阅读了此帖子" + sr_post_likers_list_description: "喜欢这个帖子的用户" + sr_post_readers_list_description: "阅读此帖的用户" by_you: off_topic: "您将此帖子举报为偏离话题" spam: "您将此帖子举报为垃圾信息" @@ -2888,10 +2899,11 @@ zh_CN: tag_groups_placeholder: "(可选)允许的标签组列表" manage_tag_groups_link: "管理标签组" allow_global_tags_label: "也允许其他标签" - tag_group_selector_placeholder: "(可选)标签组" - required_tag_group_description: "要求新话题包含标签组中的标签:" - min_tags_from_required_group_label: "标签数量:" - required_tag_group_label: "标签组:" + required_tag_group: + description: "新主题要求必须有来自标签组的标签。" + delete: "删除" + add: "添加必需的标签组" + placeholder: "选择标签组..." topic_featured_link_allowed: "在此类别中允许精选链接" delete: "删除类别" create: "新建类别" @@ -3108,13 +3120,18 @@ zh_CN: other {}} original_post: "原始帖子" views: "浏览量" + sr_views: "按浏览量排序" views_lowercase: other: "浏览量" replies: "回复" + sr_replies: "按回复排序" views_long: other: "此话题已被浏览 %{number} 次" activity: "活动" + sr_activity: "按活动排序" likes: "赞" + sr_likes: "按赞排序" + sr_op_likes: "按原帖点赞排序" likes_lowercase: other: "赞" users: "用户" @@ -3488,7 +3505,7 @@ zh_CN: unsupported_file_picked: "您选择了不受支持的文件。支持的文件类型 – %{types}。" user_activity: no_activity_title: "尚无活动" - no_activity_body: "欢迎来到我们的社区!您是新来的,还没有参与讨论。作为第一步,请访问 置顶内容类别 并开始阅读!在您喜欢或想了解更多信息的帖子上选择 %{heartIcon} 用户选项添加图片和个人简介,以帮助其他人了解您。" + no_activity_body: "欢迎来到我们的社区!作为新人,你还未参与过讨论。第一步,请访问 置顶内容 或 分类 并开始阅读!在你喜欢或想了解更多的帖子上选择 %{heartIcon} 。当你参与时,你的活动将被列在这里。" no_activity_others: "无活动。" no_replies_title: "您还没有回复任何话题" no_replies_others: "无回复。" @@ -3498,9 +3515,13 @@ zh_CN: no_likes_body: "加入并开始贡献的一个好方法是阅读已有的主题,并在您喜欢的帖子上点击 %{heartIcon}" no_likes_others: "没有赞过的帖子。" no_topics_title: "您还没有开始任何话题" + no_topics_title_others: "%{username} 还没有开始任何话题" no_read_topics_title: "您还没有阅读任何话题。" no_read_topics_body: "开始阅读讨论后,您将在此处看到一个列表。要开始阅读,请在 置顶类别 或按关键字搜索 %{searchIcon}" no_group_messages_title: "未找到群组消息" + topic_entrance: + sr_jump_top_button: "跳到第一个帖子" + sr_jump_bottom_button: "跳到最后一个帖子" fullscreen_table: expand_btn: "展开表格" second_factor_auth: @@ -3725,6 +3746,7 @@ zh_CN: topics: read: 阅读一个话题或其中的一个帖子。也支持 RSS。 write: 创建一个新话题或发布到现有话题。 + update: 更新一个话题。更改标题、类别、标签等。 read_lists: 阅读诸如热门、新、最新等话题列表。也支持 RSS。 wordpress: WordPress wp-discourse 插件正常工作所必需。 posts: @@ -3951,6 +3973,7 @@ zh_CN: delete_confirm: '确定要删除“%{theme_name}”吗?' color: "颜色" opacity: "不透明度" + copy: "复制" copy_to_clipboard: "复制到剪贴板" copied_to_clipboard: "已复制到剪贴板" copy_to_clipboard_error: "复制到剪贴板时出错" @@ -4221,6 +4244,7 @@ zh_CN: time: "时间" user: "用户" email_type: "电子邮件类型" + details_title: "显示电子邮件详情" to_address: "目标地址" test_email_address: "要测试的电子邮件地址" send_test: "发送测试电子邮件" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 4f4266a9ed..fa370c9f98 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -93,7 +93,6 @@ zh_TW: to_placeholder: "至今" share: topic_html: '話題:%{topicTitle}' - post: "貼文 #%{postNumber} " close: "關閉" twitter: "分享到 Twitter" facebook: "分享到 Facebook" @@ -1408,19 +1407,14 @@ zh_TW: not_approved: "您的帳號尚未通過審核。一旦您的帳號通過審核,就會傳送電子郵件通知您。" google_oauth2: name: "Google" - title: "使用 Google 帳號" twitter: name: "Twitter" - title: "使用 Twitter" instagram: name: "Instagram" - title: "用 Instagram 登入" facebook: name: "Facebook" - title: "使用 Facebook" github: name: "GitHub" - title: "使用 GitHub" second_factor_toggle: totp: "請改用身份驗證應用程式" backup_code: "請改用備用碼" @@ -1434,7 +1428,6 @@ zh_TW: success: "你的帳號已被建立,且您已經登入了。" name_label: "姓名" password_label: "密碼" - optional_description: "(選擇性)" password_reset: continue: "繼續連接至 %{site_name}" emoji_set: @@ -2284,6 +2277,8 @@ zh_TW: tags_placeholder: "(可選)允許使用的標籤列表" tag_groups_placeholder: "(可選)允許使用的標籤組列表" allow_global_tags_label: "也允許其他標籤(Tags)" + required_tag_group: + delete: "刪除" topic_featured_link_allowed: "允許在該分類中發布精選的連結標題" delete: "刪除分類" create: "新分類" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 00d90ddb08..7489a6a619 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -472,7 +472,6 @@ ar: too_many: "عذرًا، لا يمكنك إضافة أكثر من %{limit} إشارة مرجعية، انتقل إلى %{user_bookmarks_url} لإزالة بعضها." cannot_set_past_reminder: "لا يمكنك ضبط تذكير بالإشارة المرجعية في الماضي." cannot_set_reminder_in_distant_future: "لا يمكنك ضبط تذكير بالإشارة المرجعية بعد أكثر من 10 أيام في المستقبل." - time_must_be_provided: "يجب إدخال الوقت لجميع التذكيرات" reminders: at_desktop: "في المرة القادمة التي أستخدم فيها كمبيوتر سطح المكتب" later_today: "لاحقًا اليوم" @@ -1657,10 +1656,8 @@ ar: summary_percent_filter: "يظهر أعلى % من المنشورات عندما يضغط المستخدم على \"تلخيص هذا الموضوع\"" summary_max_results: "الحد الأقصى لعدد المنشورات التي تم إرجاعها بواسطة \"تلخيص هذا الموضوع\"" enable_system_message_replies: "يسمح للمستخدمين بالرد على رسائل النظام، حتى إذا كانت الرسائل الشخصية متوقفة." - enable_long_polling: "يمكن لناقل الرسائل المُستخدَم في الإشعارات استخدام الاستقصاء الطويل" enable_chunked_encoding: "تفعيل استجابات الترميز المقسَّمة بواسطة الخادم. تعمل هذه الميزة على معظم الإعدادات، ولكن قد يتم تخزين بعض الخوادم الوكيلة مؤقتًا، مما يتسبب في تأخير الاستجابات" long_polling_base_url: "عنوان URL الأساسي المُستخدَم في الاستقصاء الطويل (عندما تخدم شبكة توصيل المحتوى (CDN) محتوًى ديناميكيًا، احرص على ضبط هذا على سحب الموارد من المصدر) eg: http://origin.site.com" - long_polling_interval: "المدة الزمنية التي سينتظرها الخادم قبل الرد على العملاء عند عدم وجود أي بيانات لإرسالها (للأعضاء الذين سجَّلوا الدخول فقط)" polling_interval: "المدة الزمنية المفترض أن يظل العملاء الذين سجَّلوا الدخول متصلين خلالها بالمللي ثانية عندما لا يكون الاستقصاء الطويل مستخدمًا" anon_polling_interval: "المدة الزمنية المفترض استقصاء العملاء المجهولين خلالها بالمللي ثانية" background_polling_interval: "المدة الزمنية المفترض استقصاء العملاء المجهولين خلالها بالمللي ثانية (عندما تكون النافذة في الخلفية)" diff --git a/config/locales/server.be.yml b/config/locales/server.be.yml index dc9f895ace..9a6c0f5fde 100644 --- a/config/locales/server.be.yml +++ b/config/locales/server.be.yml @@ -938,9 +938,7 @@ be: summary_percent_filter: "Калі карыстальнік націскае «Сумаваць гэтую тэму», паказваюць верхнюю% паведамленняў" summary_max_results: "Максімум паведамлення якiя вяртаюцца «Абагульніць Гэтую тэму»" enable_system_message_replies: "Дазваляе карыстальнікам адказваць на сістэмныя паведамленні, нават калі асабістыя паведамленні адключаныя" - enable_long_polling: "Шына паведамленняў выкарыстоўваюцца для апавяшчэння можа выкарыстоўваць доўгі апытанне" long_polling_base_url: "Базавы URL выкарыстоўваецца для апытання (калі CDN абслугоўвае дынамічны кантэнт, не забудзьцеся ўсталяваць гэта паходжанне цягнуць), напрыклад: HTTP:" - long_polling_interval: "Колькасць часу, сервер павінен чакаць, перш чым адказваць на запыты кліентаў, калі няма дадзеных для адпраўкі (зарэгістраваных карыстальнікаў толькі)" polling_interval: "Калі не долго апытання, як часта павінны ўвайсці ў сістэму кліентаў апытання ў мілісекундах" anon_polling_interval: "Як часта павінны ананімныя кліенты апытання ў мілісекундах" background_polling_interval: "Як часта павінны кліенты апытання ў мілісекундах (калі акно ў фонавым рэжыме)" diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml index e0a512846e..836ad448a1 100644 --- a/config/locales/server.bg.yml +++ b/config/locales/server.bg.yml @@ -692,9 +692,7 @@ bg: email_custom_headers: "Разделен със знака \"|\" списък от персонализирани имейл хедъри. " summary_score_threshold: "Минимална оценка необходима за една публикация, за да бъде включена в \"Обобщи тази тема\"" summary_percent_filter: "Когато потребителят кликне върху \"Обобщи тази тема\", се показват % от най-добрите публикации " - enable_long_polling: "Системата за обработка на известия, да използва long polling - заявки с дълго изчакване / дълъг интервал " long_polling_base_url: "Основен URL използван за long polling - проверка на дълги интервали (когато се използва CDN за динамично съдръжание, уверете се, че тази стойност е настроена към основното съдържание) Например: http://origin.site.com" - long_polling_interval: "Времето, през което сървърът трябва да изчака преди да отговори на клиентите, когато няма данни за изпращане (влезли потребители само)" polling_interval: "Когато няма \"long polling\", колко често логнатите потребители трябва да правят заявка към сървъра в милисекунди" anon_polling_interval: "Колко често трябва анонимни клиенти да правят заявка към сървъра в милисекунди" background_polling_interval: "Колко често трябва клиентите да правят заявка към сървъра в милисекунди (когато прозорецът е на заден план)" diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index c21c87c470..7b794ff363 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -635,8 +635,6 @@ bs_BA: email_custom_headers: "A pipe-delimited list of custom email headers" summary_score_threshold: "The minimum score required for a post to be included in 'Summarize This Topic'" summary_percent_filter: "When a user clicks 'Summarize This Topic', show the top % of posts" - enable_long_polling: "Message bus used for notification can use long polling" - long_polling_interval: "Interval before a new long poll is issued in milliseconds " polling_interval: "How often should logged in user clients poll in milliseconds" anon_polling_interval: "How often should anonymous clients poll in milliseconds" cooldown_minutes_after_hiding_posts: "Number of minutes a user must wait before they can edit a post hidden via community flagging" diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index 70972c9da9..128a922e68 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -1254,9 +1254,7 @@ ca: summary_percent_filter: "Quan un usuari fa clic en 'Resumeix aquest tema', mostra el % superior de les publicacions" summary_max_results: "Nombre màxim de publicacions retornades per \"Resumeix aquest tema\"" enable_system_message_replies: "Permet als usuaris respondre als missatges del sistema, fins i tot si els missatges personals estan desactivats" - enable_long_polling: "El bus de missatges emprat per a notificar pot utilitzar 'long polling'" long_polling_base_url: "URL base emprat per a 'long polling' (quan una xarxa de CDN serveix contingut dinàmic, assegureu-vos que ho heu configurat com a 'origin pull'), p. ex. http://origin.site.com" - long_polling_interval: "Quantitat de temps que el servidor hauria d'esperar abans de respondre a clients quan no hi ha dades per a enviar (només usuaris que han iniciat sessió)" polling_interval: "Si no hi ha 'long polling', amb quina freqüència haurien de mostrejar els clients que han iniciat sessió en mil·lisegons." anon_polling_interval: "Amb quina freqüència en mil·lisegons haurien de mostrejar els clients anònims" background_polling_interval: "Amb quina freqüència en mil·lisegons haurien de mostrejar els clients (quan la finestra és en segon pla)" diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index 3ef7831e35..5b33efc232 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -834,7 +834,6 @@ cs: unique_posts_mins: "Kolik minut musí uplynout, než může uživatel zaslat příspěvek se stejným obsahem" manifest_screenshots: "Snímky obrazovky, které na stránce s výzvou k instalaci předvádějí funkce vaší instance a funkce. Všechny obrázky by měly být lokálně nahrávány a mít stejné rozměry." email_custom_headers: "Seznam vlastních hlaviček emailů, oddělený svislítkem" - enable_long_polling: "'Message bus' smí používat dlouhé výzvy" anon_polling_interval: "Jak často se mají zasílat výzvy anonymním uživatelům v milisekundách" post_menu: "Určuje, které položky se zobrazí v menu u příspěvku a v jakém pořadí. Příklad: like|edit|flag|delete|share|bookmark|reply" send_welcome_message: "Poslat všem novým uživatelům uvítací zprávu s rychlým návodem jak začít." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index da3c9700ea..760ed69077 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -101,6 +101,7 @@ da: maximum_staged_user_per_email_reached: "Maksimalt antal af brugere oprettet pr. email." no_subject: "(intet emne)" no_body: "(intet meddelelsesindhold)" + missing_attachment: "(Vedhæftning %{filename} mangler)" errors: empty_email_error: "Dette sker når den rå email der modtages er tom." no_message_id_error: "Dette sker når emailen ikke har nogen 'Message-Id' header." @@ -220,6 +221,8 @@ da:

    Ellers bedes du Nulstille adgangskode.

    user_exists: "Der er ingen grund til at invitere %{email}, personen har allerede en profil!" + invite_exists: "Du har allerede inviteret %{email}." + invalid_email: "%{email} er ikke en gyldig e-mailadresse." rate_limit: one: "Du har allerede sendt %{count} invitation den seneste dag, vent venligst %{time_left}, før du prøver igen." other: "Du har allerede sendt %{count} invitationer den seneste dag, vent venligst %{time_left}, før du prøver igen." @@ -228,6 +231,8 @@ da: disabled_errors: discourse_connect_enabled: "Invitationer er deaktiveret, fordi DiscourseConnect er aktiveret." invalid_access: "Du har ikke tilladelse til at se den ønskede ressource." + requires_groups: "Invitationen blev ikke gemt, fordi det angivne emne er utilgængeligt. Tilføj en af følgende grupper: %{groups}." + domain_not_allowed: "Din e-mail kan ikke bruges til at indløse denne invitation." bulk_invite: file_should_be_csv: "Den overførte fil skal være i csv format." max_rows: "De første %{max_bulk_invites} invitationer er blevet sendt. Prøv at opdele filen i mindre dele." @@ -263,6 +268,7 @@ da: invalid_whisper_access: "Enten er hvisken ikke aktiveret, eller du har ikke adgang til at oprette hviskende indlæg" not_in_group: title_topic: "Du skal anmode om medlemskab af gruppen '%{group}' for at se dette emne." + title_category: "Du skal anmode om medlemskab til gruppen %{group}, for at se denne kategori." request_membership: "Anmod om medlemskab" join_group: "Bliv medlem af gruppen" deleted_topic: "Ups! Dette emne er blevet slettet og er ikke længere tilgængeligt." @@ -378,7 +384,6 @@ da: already_bookmarked_post: "Du kan ikke bogmærke det samme indlæg to gange." cannot_set_past_reminder: "Du kan ikke oprette en bogmærke-påmindelse i fortiden." cannot_set_reminder_in_distant_future: "Du kan ikke oprette en bogmærke-påmindelse længere end 10 år ud i fremtiden" - time_must_be_provided: "Der skal angives tid for alle påmindelser" reminders: at_desktop: "Næste gang jeg er på mit skrivebord" later_today: "Senere i dag" @@ -466,6 +471,8 @@ da: Du kan redigere dit forrige svar for at tilføje et citat ved at markere teksten og vælge citér svar-knappen som dukker op. Det er nemmere for alle at læse emner som har færre detaljerede svar i stedet for mange mindre, individuelle svar. + dominating_topic: Du har skrevet mere end %{percent}% af svarene her, er der andre, som du gerne vil høre fra? + get_a_room: Du har svaret @%{reply_username} %{count} gange, vidste du, at du kunne sende dem en personlig besked i stedet? too_many_replies: | ### Du kan ikke skrive flere indlæg i dette emne @@ -553,6 +560,8 @@ da: attributes: word: too_many: "For mange ord til den handling" + base: + invalid_url: "Erstatnings URL er ugyldig" <<: *errors uncategorized_category_name: "Ukategoriseret" vip_category_name: "Lounge" @@ -616,6 +625,7 @@ da: permission_conflict: "Enhver gruppe, der har tilladelse til at få adgang til en underkategori, skal også have adgang til den overordnede kategori. Følgende grupper har adgang til en af underkategorierne, men ingen adgang til overordnet kategori: %{group_names}." disallowed_topic_tags: "Dette emne har mærker der ikke er tilladt af denne kategori: '%{tags}“" disallowed_tags_generic: "Dette emne har ikke tilladte mærker." + slug_contains_non_ascii_chars: "indeholder ikke-ascii- tegn" cannot_delete: uncategorized: "Denne kategori er speciel. Den er beregnet som et opholdsområde for emner, der ikke har nogen kategori; den kan ikke slettes." has_subcategories: "Kan ikke slette denne kategori, fordi den har under kategorier." @@ -638,6 +648,7 @@ da: slow_down: "Du har udført denne handling for mange gange, prøv venligst igen senere." too_many_requests: "Du har udført denne handling for mange gange. Vent venligst %{time_left} før du prøver igen." by_type: + first_day_replies_per_day: "Vi sætter pris på din entusiasme, bliv ved med det! Når det er sagt, så har du af hensyn til vores fællesskab nået det maksimale antal svar, som en ny bruger kan oprette på sin første dag. Vent venligst %{time_left} og du vil kunne oprette flere svar." create_like: "Wow! Du har delt en masse kærlighed! Du har nået det maksimale daglige 'Synes godt om' for i dag, men efterhånden som du stiger i tillid niveauer, vil du optjene flere daglige 'Synes godt om' Du vil kunne 'Synes godt om' indlæg igen om %{time_left}." hours: one: "%{count} time" @@ -1334,7 +1345,7 @@ da: allow_uncategorized_topics: "Tillad at emner kan oprettes uden et kategori. ADVARSEL: Hvis der er nogen ikke-kategoriserede emner, skal du først kategorisere dem inden du deaktiverer dette." allow_duplicate_topic_titles: "Tillad emner med identiske titler ." unique_posts_mins: "Hvor mange minutter der skal gå før en bruger kan skrive et indlæg med det samme indhold igen." - educate_until_posts: "When the user starts typing their first (n) new posts, show the pop-up new user education panel in the composer." + educate_until_posts: "Når brugeren begynder at skrive deres første (n) nye indlæg, skal pop op panel med ny bruger undervisning vises i editoren." title: "Sitets navn, som anvendt i tittel." site_description: "Beskriv denne side i én sætning, som anvendt i meta beskrivelsen." short_site_description: "Kort beskrivelse, som bruges i titelmærket på websiden." @@ -1375,9 +1386,7 @@ da: summary_percent_filter: "Når en bruger kllikker 'Opsummer dette Emne' vises top % af indlæg" summary_max_results: "Maksimum antal indlæg returneret af 'Opsummer dette emne'" enable_system_message_replies: "Tillader brugere at svare på systemmeddelelser, også selvom personlige meddelelser er deaktiveret" - enable_long_polling: "Message bus til underretninger kan bruge long polling" long_polling_base_url: "URL anvendt for afsteminger (når CDN leverer dynamisk indhold, så sæt dette til oprindelig / orginal) f.eks: http://origin.site.com" - long_polling_interval: "Mængde tid før serveren bør vente før den svarer klienter når der ikke er ny data at sende (kun for brugere der er logget ind)" polling_interval: "Når der ikke hentes data langsomt, hvor tit skal en klient der er logget på hente ny data i millisekunder" anon_polling_interval: "Hvor ofte skal anonyme brugere polle, i millisekunder." background_polling_interval: "Hvor tit skal klienter hente data i millisekunder (når vinduet er i baggrunden)" diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 5b4e4e4ce5..e7fed8fcf4 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -63,6 +63,7 @@ de: unrecognized_extension: "Unbekannte Dateiendung: %{extension}" import_error: generic: Beim Importieren dieses Themes ist ein Fehler aufgetreten + upload: "Fehler beim Erstellen des Upload-Assets: %{name}. %{errors}" about_json: "Importfehler: about.json existiert nicht oder ist ungültig. Bist du dir sicher, dass dies ein Discourse-Theme ist?" about_json_values: "about.json enthält ungültige Werte: %{errors}" modifier_values: "about.json-Modifikatoren enthalten ungültige Werte: %{errors}" @@ -394,11 +395,13 @@ de: bookmarks: errors: already_bookmarked_post: "Du kannst denselben Beitrag nicht zweimal zu deinen Lesezeichen hinzufügen." + already_bookmarked: "Du kannst denselben %{type} nicht zweimal mit einem Lesezeichen versehen." too_many: "Entschuldige, leider kannst du nicht mehr als %{limit} Lesezeichen hinzufügen. Besuche %{user_bookmarks_url}, um welche zu entfernen." cannot_set_past_reminder: "Du kannst keine Lesezeichen-Erinnerung in der Vergangenheit setzen." cannot_set_reminder_in_distant_future: "Du kannst keine Lesezeichen-Erinnerung einstellen, die mehr als 10 Jahre in der Zukunft liegt." - time_must_be_provided: "die Zeit muss für alle Erinnerungen angegeben werden" + time_must_be_provided: "Die Zeit muss für alle Erinnerungen angegeben werden" for_topic_must_use_first_post: "Du kannst nur den ersten Beitrag verwenden, um das Thema mit einem Lesezeichen zu versehen." + bookmarkable_id_type_required: "Der Name und Typ des Datensatzes, für den ein Lesezeichen erstellt werden soll, ist erforderlich." reminders: at_desktop: "Nächstes Mal, wenn ich an meinem PC bin" later_today: "Im Laufe des Tages" @@ -1483,10 +1486,8 @@ de: summary_timeline_button: "Schaltfläche \"Zusammenfassen\" in der Zeitleiste anzeigen" enable_personal_messages: "Erlaube Benutzern mit der Vertrauensstufe 1 (konfigurierbar über die minimale Vertrauensstufe zum Senden von Nachrichten), Nachrichten zu erstellen und auf Nachrichten zu antworten. Beachte, dass das Team immer Nachrichten senden kann." enable_system_message_replies: "Erlaube Benutzern, auf Systemnachrichten zu antworten, auch wenn persönliche Nachrichten deaktiviert sind" - enable_long_polling: "Nachrichtenbus für Benachrichtigungen kann Long Polling nutzen." enable_chunked_encoding: "Aktiviere „chunked encoding“-Antworten durch den Server. Diese Funktion ist mit den meisten Set-ups kompatibel, aber einige Proxys können puffern, wodurch die Antworten verzögert werden" long_polling_base_url: "Basis-URL für Long Polling (wenn zum Ausliefern von dynamischen Inhalten ein CDN verwendet wird, setze dies auf Origin-Pull), z. B. http://origin.site.com" - long_polling_interval: "Wartezeit, bevor der Server auf Clients reagiert, wenn keine Daten gesendet werden müssen (nur für angemeldete Benutzer)" polling_interval: "Polling-Intervall in Millisekunden für angemeldete Clients, wenn Long Polling nicht verwendet wird." anon_polling_interval: "Polling-Intervall in Millisekunden für anonyme Clients." background_polling_interval: "Polling-Intervall in Millisekunden für Clients, wenn sich das Browser-Fenster im Hintergrund befindet." @@ -4369,6 +4370,12 @@ de: other: '„%{tag_name}“ ist beschränkt auf folgende Kategorien: %{category_names}' synonym: 'Synonyme sind nicht erlaubt. Verwende stattdessen „%{tag_name}“.' has_synonyms: 'Schlagwort „%{tag_name}“ kann nicht verwendet werden, da es Synonyme hat.' + restricted_tags_cannot_be_used_in_category: + one: 'Das Tag "%{tags}" kann nicht in der Kategorie "%{category}" verwendet werden. Bitte entferne es.' + other: 'Die folgenden Schlagwörter können nicht in der Kategorie "%{category}" verwendet werden: %{tags}. Bitte entferne sie.' + category_does_not_allow_tags: + one: 'In der Kategorie "%{category}" ist das Tag "%{tags}" nicht zulässig. Bitte entferne es.' + other: 'In der Kategorie "%{category}" sind die folgenden Schlagwörter nicht zulässig: "%{tags}“. Bitte entferne sie.' required_tags_from_group: one: "Du musst mindestens %{count} „%{tag_group_name}“-Schlagwort hinzufügen. Die Schlagwörter in dieser Gruppe sind: %{tags}." other: "Du musst mindestens %{count} „%{tag_group_name}“-Schlagwörter hinzufügen. Die Schlagwörter in dieser Gruppe sind: %{tags}." @@ -4677,6 +4684,7 @@ de: ignore_error: "Entschuldige, du kannst diesen Benutzer nicht ignorieren." mute_error: "Entschuldige, du kannst diesen Benutzer nicht stummschalten." error: "Leider kannst Du die Benachrichtigungsebene für diesen Benutzer nicht ändern." + invalid_value: '„%{value}“ ist keine gültige Benachrichtigungsstufe.' discord: not_in_allowed_guild: "Authentifizierung fehlgeschlagen. Du bist kein Mitglied einer erlaubten Discord-Gilde." old_keys_reminder: diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index b7e621ec8f..f70ed8cc75 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -956,9 +956,7 @@ el: force_https: "Αναγκάζει το site σας να χρησιμοποιεί μόνο HTTPS. ΠΡΟΣΟΧΗ: Μην το ενεργοποιήσετε, μέχρι να επαληθεύσετε ότι το HTTPS είναι πλήρως εγκατεστημένο και λειτουργεί απολύτως παντού! Ελέγξατε το CDN σας, όλες τις κοινωνικές συνδέσεις και τα εξωτερικά σας λογότυπα / εξαρτήσεις για να βεβαιωθείτε ότι είναι όλα συμβατά με HTTPS;" summary_score_threshold: "Η ελάχιστη βαθμολογία που απαιτείται από μια ανάρτηση για να συμπεριληφθεί στο «Συνοψίστε αυτό το θέμα»" summary_percent_filter: "Όταν ο χρήστης επιλέξει «Συνοψίστε αυτό το θέμα», δείξε το κορυφαίο % των αναρτήσεων" - enable_long_polling: "Η αρτηρία μηνυμάτων που χρησιμοποιείτε για ειδοποιήσεις μπορεί να χρησιμοποιήσει μακρυά μέθοδο εξέτασης." long_polling_base_url: "Base URL που χρησιμοποιείτε για μακρύ ψήφισμα (όταν ένα CDN εξυπηρετεί δυναμικό περιεχόμενο, βεβαιωθείτε ότι το ορίσατε σε έλξη προέλευσης ) π.χ.: http://origin.site.com" - long_polling_interval: "Χρονικό διάστημα που ο διακομιστής θα πρέπει να περιμένει πριν απαντήσει στους πελάτες όταν δεν υπάρχουν δεδομένα για την αποστολή (μόνο συνδεδεμένοι χρήστες )" polling_interval: "Όταν δεν χρησιμοποιείται μακρυά μέθοδος εξέτασης, πόσο συχνά θα πρέπει οι συνδεδεμένοι χρήστες να ελέγχονται σε χιλιοστά του δευτερολέπτου" anon_polling_interval: "Πόσο συχνά θα πρέπει οι ανώνυμοι χρήστες να ελέγχονται σε χιλιοστά του δευτερολέπτου" background_polling_interval: "Πόσο συχνά θα πρέπει οι συνδεδεμένοι πελάτες να ελέγχονται σε χιλιοστά του δευτερολέπτου (όταν το παράθυρο είναι στο παρασκήνιο)" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 7530935ff5..92b0f58b58 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -72,6 +72,7 @@ en: unrecognized_extension: "Unrecognized file extension: %{extension}" import_error: generic: An error occurred while importing that theme + upload: "Error creating upload asset: %{name}. %{errors}" about_json: "Import Error: about.json does not exist, or is invalid. Are you sure this is a Discourse Theme?" about_json_values: "about.json contains invalid values: %{errors}" modifier_values: "about.json modifiers contain invalid values: %{errors}" @@ -429,11 +430,13 @@ en: bookmarks: errors: already_bookmarked_post: "You cannot bookmark the same post twice." + already_bookmarked: "You cannot bookmark the same %{type} twice." too_many: "Sorry, you cannot add more than %{limit} bookmarks, visit %{user_bookmarks_url} to remove some." cannot_set_past_reminder: "You cannot set a bookmark reminder in the past." cannot_set_reminder_in_distant_future: "You cannot set a bookmark reminder more than 10 years in the future." - time_must_be_provided: "time must be provided for all reminders" + time_must_be_provided: "Time must be provided for all reminders" for_topic_must_use_first_post: "You can only use the first post to bookmark the topic." + bookmarkable_id_type_required: "The name and type of the record to bookmark is required." reminders: at_desktop: "Next time I'm at my desktop" @@ -1572,10 +1575,8 @@ en: enable_personal_messages: "Allow trust level 1 (configurable via min trust to send messages) users to create messages and reply to messages. Note that staff can always send messages no matter what." enable_system_message_replies: "Allows users to reply to system messages, even if personal messages are disabled" - enable_long_polling: "Message bus used for notification can use long polling" enable_chunked_encoding: "Enable chunked encoding responses by the server. This feature works on most setups however some proxies may buffer, causing responses to be delayed" long_polling_base_url: "Base URL used for long polling (when a CDN is serving dynamic content, be sure to set this to origin pull) eg: http://origin.site.com" - long_polling_interval: "Amount of time the server should wait before responding to clients when there is no data to send (logged on users only)" polling_interval: "When not long polling, how often should logged on clients poll in milliseconds" anon_polling_interval: "How often should anonymous clients poll in milliseconds" background_polling_interval: "How often should the clients poll in milliseconds (when the window is in the background)" @@ -2334,6 +2335,9 @@ en: base_font: "Base font to use for most text on the site. Themes can override via the `--font-family` CSS custom property." heading_font: "Font to use for headings on the site. Themes can override via the `--heading-font-family` CSS custom property." + enable_sitemap: "Generate a sitemap for your site and include it in the robots.txt file." + sitemap_page_size: "Number of URLs to include in each sitemap page. Max 50.000" + short_title: "The short title will be used on the user's home screen, launcher, or other places where space may be limited. It should be limited to 12 characters." dashboard_hidden_reports: "Allow to hide the specified reports from the dashboard." @@ -2586,6 +2590,8 @@ en: actions: grant_admin: description: "For additional security measures, you need to confirm your 2FA before %{username} is granted admin access." + discourse_connect_provider: + description: "%{hostname} has requested that you confirm your 2FA. You'll be redirected back to the site once you confirm your 2FA." admin: email: sent_test: "sent!" @@ -4810,6 +4816,12 @@ en: other: '"%{tag_name}" is restricted to the following categories: %{category_names}' synonym: 'Synonyms are not allowed. Use "%{tag_name}" instead.' has_synonyms: '"%{tag_name}" cannot be used because it has synonyms.' + restricted_tags_cannot_be_used_in_category: + one: 'The "%{tags}" tag cannot be used in the "%{category}" category. Please remove it.' + other: 'The following tags cannot be used in the "%{category}" category: %{tags}. Please remove them.' + category_does_not_allow_tags: + one: 'The "%{category}" category does not allow the "%{tags}" tag. Please remove it.' + other: 'The "%{category}" category does not allow the following tags: "%{tags}". Please remove them.' required_tags_from_group: one: "You must include at least %{count} %{tag_group_name} tag. The tags in this group are: %{tags}." other: "You must include at least %{count} %{tag_group_name} tags. The tags in this group are: %{tags}." @@ -5154,6 +5166,7 @@ en: ignore_error: "Sorry, you can't ignore that user." mute_error: "Sorry, you can't mute that user." error: "Sorry, you cannot change the notification level for that user." + invalid_value: '"%{value}" is not a valid notification level.' discord: not_in_allowed_guild: "Authentication failed. You are not a member of a permitted Discord guild." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 0442fc8d4b..71d00e9e54 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -394,11 +394,13 @@ es: bookmarks: errors: already_bookmarked_post: "No puedes guardar la misma publicación en marcadores dos veces." + already_bookmarked: "No puedes añadir a marcadores el mismo %{type} dos veces." too_many: "Lo sentimos, pero no puedes añadir más de %{limit} publicaciones a marcadores. Visita %{user_bookmarks_url} para quitar algunos." cannot_set_past_reminder: "No puedes establecer un recordatorio de marcador en el pasado." cannot_set_reminder_in_distant_future: "No puedes establecer un recordatorio de marcador más de 10 años en el futuro." - time_must_be_provided: "se debe indicar el tiempo para todos los recordatorios" + time_must_be_provided: "Hay que poner un tiempo para todos recordatorios" for_topic_must_use_first_post: "Solo puedes usar la primera publicación para añadir el tema a marcadores." + bookmarkable_id_type_required: "El nombre y el tipo del elemento a añadir a marcadores es obligatorio." reminders: at_desktop: "La próxima vez que esté en mi ordenador" later_today: "Más tarde durante el día de hoy" @@ -1483,10 +1485,8 @@ es: summary_timeline_button: "Mostrar un botón para «Resumir» en la línea de tiempo" enable_personal_messages: "Permitir que los usuarios con nivel de confianza 1 (configurable a través del ajuste min trust to send messages) puedan crear y responder mensajes. Ten en cuenta que el personal siempre puede mandar mensajes, sin importar los ajustes." enable_system_message_replies: "Permite a los usuarios responder a los mensajes del sistema, incluso si los mensajes personales están desactivados." - enable_long_polling: "Los mensajes usados para notificaciones pueden usar el long polling" enable_chunked_encoding: "Activar respuestas en lotes del servidor. Esta funcionalidad debería funcionar en casi todos los entornos, pero algunos proxies pueden causar que las respuestas tarden" long_polling_base_url: "URL base usada para el long polling (cuando un CDN esta sirviendo contenido dinámico, asegúrate de ajustar esto al pull de origen) ejemplo: http://origin.site.com" - long_polling_interval: "Cantidad de tiempo que el servidor debe de esperar antes de responder a los clientes que no hay datos para enviar (solamente usuarios con sesión iniciada)." polling_interval: "Cuando no este en long polling, frecuencia con la que los clientes con sesión iniciada hacen poll en milisegundos" anon_polling_interval: "Frecuencia en milisegundos con la que los clientes anónimos hacen poll" background_polling_interval: "Frecuencia en milisegundos con la que los clientes con sesión iniciada hacen poll (mientras que la ventana está en segundo plano)" @@ -4370,6 +4370,12 @@ es: other: '«%{tag_name}» está restringida a las siguientes categorías: %{category_names}' synonym: 'Los sinónimos no están permitidos. Utiliza "%{tag_name}" en su lugar.' has_synonyms: '"%{tag_name}" no se puede utilizar porque tiene sinónimos.' + restricted_tags_cannot_be_used_in_category: + one: 'La etiqueta «%{tags}» no puede usarse en la categoría «%{category}». Por favor, quítala.' + other: 'Las siguientes etiquetas no pueden usarse en la categoría «%{category}»: %{tags}. Por favor, quítalas.' + category_does_not_allow_tags: + one: 'La categoría «%{category}» no permite la etiqueta «%{tags}». Por favor, quítala.' + other: 'La categoría «%{category}» no permite las siguientes etiquetas: «%{tags}». Por favor, quítalas.' required_tags_from_group: one: "Debes incluir al menos %{count} etiqueta de %{tag_group_name}. Las etiquetas en este grupo son: %{tags}." other: "Debes incluir al menos %{count} etiquetas de %{tag_group_name}. Las etiquetas en este grupo son: %{tags}." @@ -4678,6 +4684,7 @@ es: ignore_error: "Lo sentimos, pero no puedes ignorar a este usuario." mute_error: "Lo sentimos, pero no puedes silenciar a este usuario." error: "Lo siento, no puedes cambiar el nivel de notificación de ese usuario." + invalid_value: '«%{value}» no es un nivel de notificaciones válido.' discord: not_in_allowed_guild: "La autenticación falló. No eres miembro de ningún clan de Discord permitido." old_keys_reminder: diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index d36cd02fd7..732bc9ece3 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -915,9 +915,7 @@ fa_IR: force_https: "اجبار به استفاده از HTTPS ، تا زمانی که HTTPS را کامل تنظیم نکرده‌اید این گزینه را فعال نکنید. آیا تمام CDN ها و شبکه‌های اجتماعی جهت ورود را بررسی کردید و تمام لوگو‌ها و وابستگی‌ها بدون مشکل با HTTPS کار می‌کنند؟" summary_score_threshold: "حداقل امتیاز برای یک نوشته که بتواند شامل \" خلاصه این موضوع\" شود" summary_percent_filter: "وقتی کاربر بر روی ' خلاصه این موضوع' کلیک کرد٬‌ % بهترین نوشته‌ها را نشان بده" - enable_long_polling: "message bus استفاده شده برای آگاه سازی می تواند برای رای گیری طولانی استفاده شود. " long_polling_base_url: " URL پایه استفاده شده برای رای گیری طولانی (وقتی CDN خدمت محتوای پویا می دهد٬ از تنظیم بودن منشا این کشش مطمئن شوید) برای نمونه : http://origin.site.com" - long_polling_interval: "مدت زمانی که سرور قبل پاسخ دادن به مشتری‌ها باید صبر کند، وقتی در آن‌جا داده ای برای ارسال نیست (فقط کاربران وارد شده)" polling_interval: "وقتی رای گیری طولانی نیست، هر چند مدت باید وارد سیستم شود برای نظرسنجی مشتری‌ها در هر میلی‌ثانیه." anon_polling_interval: "هر چند مدت باید مشتری‌های ناشناس نظرسنجی بشوند در هر میلی ثانیه" background_polling_interval: "هر چند وقت یکبار مشتری‌ها باید نظرسنجی بشوند در هر ثانیه (وقتی پنجره در پس زمینه است)" diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index ba5535aa0f..8844e61c32 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -387,7 +387,6 @@ fi: too_many: "Et voi lisätä enempää kuin %{limit} kirjanmerkkiä. Voit poistaa kirjanmerkkejä kohdassa %{user_bookmarks_url}." cannot_set_past_reminder: "Kirjanmerkkimuistutusta ei voi asettaa menneisyyteen." cannot_set_reminder_in_distant_future: "Kirjanmerkkimuistutusta ei voi asettaa yli 10 vuoden päähän tulevaisuuteen." - time_must_be_provided: "kaikkiin muistutuksiin on asetettava kellonaika" reminders: at_desktop: "Ensi kerralla kun olen työpöytälaitteellani" later_today: "Myöhemmin tänään" @@ -1444,10 +1443,8 @@ fi: summary_percent_filter: "Kun käyttäjä klikkaa 'Näytä ketjun tiivistelmä', näytä paras % viesteistä" summary_max_results: "Maksimimäärä viestejä, jotka näytetään ketjun tiivistelmässä" enable_system_message_replies: "Sallii käyttäjien vastata järjestelmän viesteihin, vaikka yksityisviestit eivät olisikaan käytössä" - enable_long_polling: "Ilmoitusten käyttämä viestiväylä voi käyttää long pollingia" enable_chunked_encoding: "Ota käyttöön palvelimen joukkokoodausvastaukset. Tämä ominaisuus toimii useimmissa määrityksissä, mutta jotkin välityspalvelimet saattavat puskuroida, mikä aiheuttaa vastausten viivästymisen" long_polling_base_url: "Perus-URL, jota käytetään long pollingissa (kun CDN tarjoaa dynaamista sisältöä, varmista että tämä on asetettu noudoksi lähteestä) esim: http://lähde.sivusto.com" - long_polling_interval: "Kuinka kauan palvelimen pitäisi odottaa ennen vastaamista asiakkaalle, kun lähetettävää dataa ei ole (vain kirjautuneille käyttäjille)" polling_interval: "Kun long polling ei ole käytössä, kuinka usein kirjautuneet käyttäjät pollaavat millisekunneissa." anon_polling_interval: "Kuinka usein anonyymit käyttäjät pollaavat millisekunneissa" background_polling_interval: "Kuinka usein asiakkaat pollaavat, millisekunneissa (kun ikkuna ei ole aktiivisena)" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 715c86e909..2d6caa3155 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -16,6 +16,7 @@ fr: date_only: "%B %-d, %Y" long: "%B %-d, %Y, %l:%M%P" no_day: "%B %Y" + calendar_ics: "%Y%m%dT%H%M%SZ" date: month_names: - null @@ -62,6 +63,7 @@ fr: unrecognized_extension: "Extension de fichier non reconnue : %{extension}" import_error: generic: Une erreur s'est produite lors de l'importation de ce thème + upload: "Erreur lors de la création de l'élément de téléchargement : %{name}. %{errors}" about_json: "Erreur d'importation : about.json n'existe pas ou est invalide. Êtes-vous sûr(e) qu'il s'agit d'un thème Discourse ?" about_json_values: "about.json contient des valeurs invalides : %{errors}" modifier_values: "les modificateurs du fichier about.json contiennent des valeurs invalides : %{errors}" @@ -205,6 +207,8 @@ fr: local_login_cannot_be_disabled_if_second_factor_enforced: "Vous ne pouvez pas désactiver les connexions locales lorsque l'authentification à deux facteurs est obligatoire. Veuillez désactiver l'obligation d'authentification à deux facteurs avant de désactiver les connexions locales." cannot_enable_s3_uploads_when_s3_enabled_globally: "Il n'est pas possible d'activer l'envoi de fichiers sur S3 car il est déjà activé globalement, et l'activer au niveau du site pourrait provoquer des problèmes majeurs avec les fichiers envoyés." cors_origins_should_not_have_trailing_slash: "Vous ne devez pas ajouter la barre oblique finale (/) aux origines CORS." + slow_down_crawler_user_agent_must_be_at_least_3_characters: "Les champs \"User-agent\" spécifiés ne peuvent être inférieurs à 3 caractères, afin d'éviter tout ralentissement accidentel des utilisateurs légitimes du site." + slow_down_crawler_user_agent_cannot_be_popular_browsers: "Pour ce réglage, vous ne pouvez pas utiliser les valeurs suivantes : %{values}." conflicting_google_user_id: 'Le Google Account ID a changé pour ce compte ; un responsable doit intervenir pour des raisons de sécurité. Merci de contacter les responsables et les renvoyer vers
    https://meta.discourse.org/t/76575' onebox: invalid_address: "Nous sommes désolés, nous n'avons pas pu générer d'aperçu pour cette page Web car le serveur « %{hostname} » n'a pas pu être trouvé. Au lieu d'un aperçu, seul un lien apparaîtra dans votre message. :cry:" @@ -225,6 +229,8 @@ fr:

    Si vous vous souvenez de votre mot de passe, vous pouvez vous connecter.

    Sinon, veuillez réinitialiser votre mot de passe.

    + not_found_template_link: | +

    La date d'expiration de cette invitation à %{site_name} est dépassée. Nous vous invitons à demander à la personne qui vous a invité(e) de vous faire parvenir une nouvelle invitation.

    user_exists: "Inutile d'inviter %{email}, cette personne dispose déjà d'un compte !" invite_exists: "Vous avez déjà invité %{email}." invalid_email: "%{email} n'est pas une adresse de courriel valable." @@ -236,6 +242,8 @@ fr: disabled_errors: discourse_connect_enabled: "Les invitations sont désactivées car DiscourseConnect est activé." invalid_access: "Vous n'êtes pas autorisé(e) à afficher la ressource demandée." + requires_groups: "L'invitation n'a pas été enregistrée car le sujet indiqué n'est pas accessible. Veuillez ajouter un des groupes suivants : %{groups}." + domain_not_allowed: "Votre adresse de courriel ne peut pas servir à utiliser cette invitation." bulk_invite: file_should_be_csv: "Le fichier envoyé doit être au format CSV." max_rows: "Les premières %{max_bulk_invites} invitations ont été envoyées. Essayez de diviser le fichier en parties plus petites." @@ -261,6 +269,7 @@ fr: not_found: "L'URL ou la ressource demandée n'a pas été retrouvée." invalid_access: "L'accès à la ressource demandée n'est pas autorisé." authenticator_not_found: "La méthode d'authentification n'existe pas ou a été désactivée." + authenticator_no_connect: "Ce fournisseur d'authentification ne permet pas d'établir une connexion avec un compte utilisateur existant sur le forum." invalid_api_credentials: "Vous n'êtes pas autorisé(e) à voir cette ressource. Le nom d’utilisateur ou la clé API n'est pas valide." provider_not_enabled: "Vous n'êtes pas autorisé(e) à voir cette ressource. Le fournisseur d'authentification n'est pas activé." provider_not_found: "Vous n'êtes pas autorisé(e) à voir cette ressource. Le fournisseur d'authentification n'existe pas." @@ -330,6 +339,8 @@ fr: too_many_links: one: "Nous sommes désolés, les nouveaux utilisateurs ne peuvent insérer qu'un seul lien par message." other: "Nous sommes désolés, les nouveaux utilisateurs ne peuvent insérer que %{count} liens par message." + contains_blocked_word: "Navré, il vous est impossible d'utiliser le mot « %{word} » car il est interdit." + contains_blocked_words: "Navré, il vous est impossible de publier ce message car les mots suivants sont interdits : « %{words} »." spamming_host: "Désolé, vous ne pouvez pas insérer de lien vers ce domaine." user_is_suspended: "Les utilisateurs suspendus ne sont pas autorisés à publier des messages." topic_not_found: "Une erreur est survenue. Peut-être que ce sujet a été fermé ou supprimé pendant que vous le regardiez ?" @@ -384,10 +395,13 @@ fr: bookmarks: errors: already_bookmarked_post: "Il n'est pas possible de mettre plus d'un signet sur un même message." + already_bookmarked: "Vous ne pouvez pas mettre en signet le même %{type} deux fois." too_many: "Nous sommes désolés, vous ne pouvez pas créer plus de %{limit} signets. Pour en supprimer certains, rendez-vous sur %{user_bookmarks_url} ." cannot_set_past_reminder: "Vous ne pouvez pas définir de rappel de signet à une date passée." cannot_set_reminder_in_distant_future: "Vous ne pouvez pas définir de rappel de signet plus de 10 ans dans le futur." - time_must_be_provided: "l'heure doit être réglée pour tous les rappels" + time_must_be_provided: "Un horaire doit être indiqué pour chaque rappel" + for_topic_must_use_first_post: "Un signet peut seulement être associé au premier message d'un sujet." + bookmarkable_id_type_required: "Le nom et le type de l'enregistrement à mettre en signet sont requis." reminders: at_desktop: "La prochaine fois que je suis à mon bureau" later_today: "Plus tard dans la journée" @@ -475,6 +489,8 @@ fr: Vous pouvez modifier votre dernière réponse et ajouter une citation en sélectionnant le texte à citer et en cliquant sur le bouton Citer. Il est plus facile de lire des discussions comprenant des réponses approfondies plutôt que de nombreuses réponses individuelles brèves. + dominating_topic: Vous êtes l'auteur de %{percent} % des réponses actuelles à ce sujet ; pourriez-vous laisser l'occasion à d'autres personnes de s'exprimer ? + get_a_room: Nous remarquons que vous avez déjà envoyé un certain nombre de réponses (%{count}) à @%{reply_username} ; saviez-vous qu'il est également possible d'échanger des messages directs avec cette personne sur notre site ? too_many_replies: | ### Vous avez atteint le nombre maximal de réponses dans ce sujet @@ -625,7 +641,7 @@ fr: errors: not_found: "Catégorie non trouvée !" uncategorized_parent: "La catégorie « Sans catégorie » ne peut pas avoir de parent" - self_parent: "La catégorie parente d'une sous-catégorie ne peut pas se référencer elle-même" + self_parent: "Le parent d'une sous-catégorie ne peut pas être elle-même" depth: "Vous ne pouvez pas imbriquer une sous-catégorie sous une autre" invalid_email_in: "L'adresse « %{email} » n'est pas une adresse courriel valide." email_already_used_in_group: "L'adresse « %{email} » est déjà utilisée par le groupe « %{group_name} »." @@ -634,6 +650,7 @@ fr: permission_conflict: "Tout groupe autorisé à accéder à une sous-catégorie doit également avoir accès à la catégorie parente. Les groupes suivants ont accès à une des sous-catégories, mais n'ont pas accès à la catégorie parente : %{group_names}." disallowed_topic_tags: "Ce sujet contient des étiquettes qui ne sont pas autorisées dans cette catégorie : %{tags}" disallowed_tags_generic: "Ce sujet contient des étiquettes interdites." + slug_contains_non_ascii_chars: "contient des caractères non-ascii" cannot_delete: uncategorized: "Cette catégorie est spéciale. Elle sert à rassembler les sujets qui n'ont pas de catégorie ; elle ne peut pas être supprimée." has_subcategories: "Vous ne pouvez pas supprimer cette catégorie car elle comprend des sous-catégories." @@ -649,9 +666,13 @@ fr: post: image_placeholder: broken: "Cette image ne fonctionne pas" + hidden_bidi_character: "Les caractères de contrôle bidirectionnels permettent de modifier l'ordre dans lequel les éléments d'un texte seront affichés. Ceci pourrait servir à dissimuler du code informatique malveillant." has_likes: one: "%{count} « J'aime »" other: "%{count} « J'aime »" + cannot_permanently_delete: + many_posts: "Vous ne pouvez pas supprimer définitivement ce sujet car il contient un trop grand nombre de réponses." + wait_or_different_admin: "Un délai de %{time_left} doit d'abord s'écouler avant que vous ne puissiez supprimer définitivement ce message. Alternativement, un autre adminstrateur peut réaliser cette suppression immédiatement." rate_limiter: slow_down: "Vous avez réalisé cette action un trop grand nombre de fois, veuillez réessayer plus tard." too_many_requests: "Vous avez effectué cette action un trop grand nombre de fois. Veuillez attendre %{time_left} avant de réessayer." @@ -875,6 +896,8 @@ fr: draft_backup: pm_title: "Brouillons relatifs à des sujets en cours de discussion." pm_body: "Sujet contenant des brouillons enregistrés" + user_activity: + no_log_search_queries: "La journalisation des recherches effectuées par les utilisateurs est désactivée. (Les administrateurs ont la possibilité de les activer dans les paramètres du site.)" email_settings: pop3_authentication_error: "L'authentification auprès du serveur POP3 a échoué. Vérifiez le nom d'utilisateur et le mot de passe avant de réessayer." imap_authentication_error: "L'authentification auprès du serveur IMAP a échoué. Vérifiez le nom d'utilisateur et le mot de passe avant de réessayer." @@ -935,6 +958,7 @@ fr: remove: "Ce sujet n'est plus à la une. Il ne sera plus affiché en haut de chaque page." unsubscribed: title: "Préférences de courriel enregistrées !" + description: "Les préférences d'envoi de courriel à %{email} ont été mises à jour. Pour modifier vos paramètres d'envoi de courriel, rendez-vous dans la rubrique Préférences de votre profil utilsiateur." topic_description: "Pour vous réabonner à %{link}, utilisez le contrôle des notifications en bas ou à droite du sujet." private_topic_description: "Pour vous réabonner, utilisez le contrôle des notifications en bas ou à droite du sujet." uploads: @@ -1314,19 +1338,26 @@ fr: mutes_count: Mis en sourdine description: "Les utilisateurs qui ont été mis en sourdine ou ignorés par de nombreux autres utilisateurs." top_users_by_likes_received: + title: "Utilisateurs ayant reçu le plus de J'aime" labels: user: Utilisateur qtt_like: '« J''aime » reçus' description: "Top 10 des utilisateurs qui ont reçu des likes." top_users_by_likes_received_from_inferior_trust_level: + title: "Utilisateurs ayant reçu le plus de J'aime venant d'utilisateurs d'un niveau de confiance inférieur" labels: user: Utilisateur + trust_level: Niveau de confiance qtt_like: '« J''aime » reçus' + description: "Liste des 10 utilisateurs ayant reçu le plus de J'aime de la part d'utilisateurs d'un niveau de confiance inférieur au leur." top_users_by_likes_received_from_a_variety_of_people: + title: "Utilisateurs ayant reçu le plus de J'aime d'une grande diversité de personnes" labels: user: Utilisateur qtt_like: '« J''aime » reçus' + description: "Liste des 10 utilisateurs ayant reçu le plus de J'aime de la part d'une grande diversité de personnes différentes." dashboard: + group_email_credentials_warning: 'L''authentification auprès du serveur de courriel a échoué avec les identifiants spécifiés pour le groupe %{group_full_name}. Les courriels liés à ce groupe ne pourront pas être expédiés avant que ce problème ne soit résolu. %{error}' rails_env_warning: "Votre serveur fonctionne dans l'environnement de %{env}." host_names_warning: "Votre fichier config/database.yml utilise le nom d'hôte par défaut. Veuillez renseigner votre nom d'hôte." sidekiq_warning: 'Sidekiq n''est pas en cours d''exécution. De nombreuses tâches, comme l''envoi de courriels, sont exécutées de manière asynchrone par Sidekiq. Assurez-vous d''avoir au moins un processus Sidekiq en exécution. En savoir plus sur Sidekiq.' @@ -1352,7 +1383,9 @@ fr: force_https_warning: "Votre site Web utilise SSL. Mais l'option « force_https » n'est pas encore activée dans les paramètres du site." out_of_date_themes: "Des mises à jour sont disponibles pour les thèmes suivants :" unreachable_themes: "Nous n'avons pas pu vérifier les mises à jour pour les thèmes suivants :" + watched_word_regexp_error: "L'expression régulière pour détecter des mots surveillés et déclencher l'action '%{action}' n'est pas valide. Veuillez vérifier la configuration des mots surveillés ou désactiver le paramètre 'watched words regular expressions'." site_settings: + allow_bulk_invite: "Autoriser l'envoi d'invitations multiples via l'importation d'un fichier CSV" disabled: "désactivé" display_local_time_in_user_card: "Afficher l'heure locale en fonction du fuseau horaire de l'utilisateur quand sa carte est ouverte." censored_words: "Mots qui seront automatiquement remplacés par ■■■■" @@ -1373,6 +1406,8 @@ fr: min_personal_message_title_length: "Longueur minimale pour un titre de message en nombre de caractères" max_emojis_in_title: "Nombre maximal d'émojis permis dans le titre d'un sujet" min_search_term_length: "Longueur minimale en caractères du terme à rechercher" + search_tokenize_chinese: "Forcer le système de recherche à segmenter en symboles le chinois, y compris sur des sites non-chinois" + search_tokenize_japanese: "Forcer le système de recherche à segmenter en symboles le japonais, y compris sur des sites non-japonais" search_prefer_recent_posts: "Si les recherches dans votre forum sont lentes, cette option effectue l'indexation des messages les plus récents en premier" search_recent_posts_size: "Combien de messages récents à garder dans l'index" log_search_queries: "Archiver les requêtes de recherche des utilisateurs" @@ -1383,6 +1418,7 @@ fr: category_search_priority_high_weight: "Pondération appliquée au classement des résultats de recherche pour les catégories à priorité élevée." allow_uncategorized_topics: "Autoriser la création de sujets sans catégorie. ATTENTION : s'il existe des sujets sans catégorie, vous devez les classer avant de désactiver ce paramètre." allow_duplicate_topic_titles: "Autoriser la création de sujet avec le même titre." + allow_duplicate_topic_titles_category: "Permettre l'existence de plusieurs sujets au titre identique s'ils appartiennent à des catégories différentes. Le paramètre 'allow_duplicate_topic_titles' doit, lui, être désactivé." unique_posts_mins: "Combien de temps avant qu'un utilisateur puisse publier le même contenu à nouveau" educate_until_posts: "Lors de la rédaction des (n) premiers nouveaux sujets de l'utilisateur, afficher le panneau d'aide à la saisie." title: "Le nom du site utilisé dans la balise title." @@ -1402,6 +1438,7 @@ fr: tl2_post_edit_time_limit: "Un auteur de niveau de confiance 2 ou supérieur peut modifier ses messages pendant (n) minutes après leur publication. Mettre à 0 pour retirer cette limite." edit_history_visible_to_public: "Autoriser tout le monde à voir les versions précédentes d'un message modifié. Quand cette option est désactivée, seuls les responsables peuvent voir l'historique." delete_removed_posts_after: "Les messages retirés par leur auteur seront automatiquement supprimés après (n) heures. Si cette valeur est à 0, les messages seront supprimés immédiatement." + notify_users_after_responses_deleted_on_flagged_post: "Lorsqu'un message fait l'objet d'un signalement puis d'une suppression, envoyer une notification à tous les utilisateurs qui avaient répondu à ce message." max_image_width: "Largeur maximale des images dans un message" max_image_height: "Hauteur maximale des images dans un message" responsive_post_image_sizes: "Adapter la taille des visualisations d'images aux écrans à haut DPI avec les ratios suivants. Retirez toutes les valeurs pour désactiver l'adaptation dynamique des images." @@ -1414,6 +1451,7 @@ fr: show_pinned_excerpt_mobile: "Afficher les extraits des sujets épinglés en vue mobile." show_pinned_excerpt_desktop: "Afficher les extraits des sujets épinglés en vue bureau." post_onebox_maxlength: "Longueur maximale en nombre de caractères d'un message Discourse intégré comme Onebox." + blocked_onebox_domains: "Liste de domaines qui ne seront jamais intégrés dans une Onebox. (Ex : wikipedia.org.) (Les symboles joker '*' et '?' ne sont pas pris en charge.)" allowed_inline_onebox_domains: "Une liste de domaines qui seront transformés en Onebox s'ils ont été insérés sans titre" enable_inline_onebox_on_all_domains: "Ignorer le paramètre « inline_onebox_domain_allowlist » et permettre des Onebox en ligne pour tous les domaines." force_custom_user_agent_hosts: "Domaines pour lesquels utiliser un agent utilisateur personnalisé pour les requêtes de Onebox. (Cela est utile pour les domaines limitant les accès par agent utilisateur.)" @@ -1439,16 +1477,17 @@ fr: detailed_404: "Donne plus d'informations aux utilisateurs concernant la raison pour laquelle ils ne peuvent pas accéder à ce sujet en particulier. Note : cela est moins sécurisé car les utilisateurs sauront si une URL pointe vers un sujet valide ou non." enforce_second_factor: "Force les utilisateurs à activer l'authentification à deux facteurs. Sélectionner « tous » pour l'appliquer à tous les utilisateurs. Sélectionner « responsables » pour l'appliquer aux responsables uniquement." force_https: "Forcer votre site en HTTPS uniquement. ATTENTION : n'activez PAS cette fonction tant que vous n'avez pas vérifié que le HTTPS est complètement configuré et fonctionne absolument partout ! Avez-vous vérifié que vos CDN, vos connexions de réseaux sociaux et tous les logos/dépendances tiers sont eux aussi compatibles avec HTTPS ?" + same_site_cookies: "Émettre des cookies avec l'attribut SameSite. Cela permet d'éliminer tous les vecteurs de requêtes illégitimes par rebond (attaques de type « CSRF ») dans les navigateurs qui le prennent en charge (en mode « Lax » ou « Strict »). Attention : le mode « Strict » ne fonctionne que sur des sites qui obligent à s'authentifier et qui utilisent un système d'authentification externe." summary_score_threshold: "Le score minimal pour qu'un message soit inclus dans le résultat de « Résumer ce sujet »" summary_posts_required: "Nombre minimal de messages dans un sujet avant que la fonctionnalité « Résumer ce sujet » soit activée. Les modifications de ce paramètre sont appliquées rétroactivement sur une semaine." summary_likes_required: "Nombre minimal de « J'aime » dans un sujet avant que la fonctionnalité « Résumer ce sujet » soit activée. Les modifications de ce paramètre sont appliquées rétroactivement sur une semaine." summary_percent_filter: "Quand un utilisateur clique sur « Résumer ce sujet », montrer le top % des messages" summary_max_results: "Nombre maximal de messages inclus dans le résultat de « Résumer ce sujet »" + summary_timeline_button: "Afficher un bouton \"Résumer\" dans la timeline de chaque sujet" + enable_personal_messages: "Autoriser les utilisateurs de niveau de confiance n (défini par le paramètre 'min trust to send messages') à envoyer des messages directs et à y répondre. Veuillez noter que les responsables peuvent envoyer des messages directs dans tous les cas." enable_system_message_replies: "Permettre aux utilisateurs de répondre aux messages système, même si les messages directs sont désactivés." - enable_long_polling: "Utiliser les requêtes longues pour le flux de notifications." enable_chunked_encoding: "Activer les réponses d'encodage par bloc par le serveur. Cette fonctionnalité fonctionne dans la plupart des configurations, mais certains proxys peuvent mettre les réponses en mémoire tampon et risquent donc de les retarder" long_polling_base_url: "Racine de l'URL utilisée pour les requêtes longues (dans le cas de l'utilisation d'un CDN pour fournir du contenu dynamique, pensez à le configurer en mode « origin pull »). Par exemple : http://origin.site.com" - long_polling_interval: "Délai d'attente du serveur avant de répondre aux clients lorsqu'il n'y a pas de données à envoyer\n(réservé aux utilisateurs connectés)" polling_interval: "À quelle fréquence les clients connectés devraient-ils interroger le serveur, en millisecondes (sans utiliser les requêtes longues)" anon_polling_interval: "À quelle fréquence les clients anonymes doivent-ils envoyer une requête au serveur, en millisecondes" background_polling_interval: "À quelle fréquence les clients doivent-ils envoyer une requête au serveur, en millisecondes (lorsque la fenêtre est en arrière-plan)" @@ -1464,6 +1503,9 @@ fr: tl2_additional_edits_per_day_multiplier: "Augmenter la limite quotidienne de modifications de messages pour les utilisateurs de niveau 2 (membres) en la multipliant par ce nombre" tl3_additional_edits_per_day_multiplier: "Augmenter la limite quotidienne de modifications de messages pour les utilisateurs de niveau 3 (habitués) en la multipliant par ce nombre" tl4_additional_edits_per_day_multiplier: "Augmenter la limite quotidienne de modifications de messages pour les utilisateurs de niveau 4 (meneurs) en la multipliant par ce nombre" + tl2_additional_flags_per_day_multiplier: "Augmenter la limite maximale de signalements quotidiens pour les utilisateurs de niveau de confiance 2 (membres) en la multipliant par ce nombre." + tl3_additional_flags_per_day_multiplier: "Augmenter la limite maximale de signalements quotidiens pour les utilisateurs de niveau de confiance 3 (habitués) en la multipliant par ce nombre." + tl4_additional_flags_per_day_multiplier: "Augmenter la limite maximale de signalements quotidiens pour les utilisateurs de niveau de confiance 4 (meneurs) en la multipliant par ce nombre." num_users_to_silence_new_user: "Si les messages d'un nouvel utilisateur obtiennent num_spam_flags_to_silence_new_user signalements de la part de ce nombre d'utilisateurs différents, masquer tous ses messages et l'empêcher de publier à l'avenir. 0 désactive cette fonctionnalité." num_tl3_flags_to_silence_new_user: "Si les messages d'un nouvel utilisateur obtiennent ce nombre de signalements de la part de num_tl3_users_to_silence_new_user utilisateurs différents de niveau de confiance 3, masquer tous ses messages et l'empêcher de publier à l'avenir. 0 désactive cette fonctionnalité." num_tl3_users_to_silence_new_user: "Si les messages d'un nouvel utilisateur obtiennent num_tl3_flags_to_silence_new_user signalements de la part de plusieurs utilisateurs différents de niveau de confiance 3, masquer tous ses messages et l'empêcher de publier à l'avenir. 0 désactive cette fonctionnalité." @@ -1489,6 +1531,7 @@ fr: gtm_container_id: "Identifiant de conteneur Google Tag Manager, par exemple : GTM-ABCDEF.
    Note : les scripts tiers chargés par GTM devront peut-être être ajoutés à la liste d'autorisation « content security policy script src »." enable_escaped_fragments: "Utiliser l'API Ajax-Crawling de Google si aucun robot n'est détecté. Voir https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" moderators_manage_categories_and_groups: "Autoriser les modérateurs à gérer les catégories et les groupes" + moderators_change_post_ownership: "Permettre aux modérateurs de modifier l'utilisateur propriétaire d'un message." cors_origins: "Requêtes cross-origin (CORS) autorisées. Chaque origine doit inclure http:// ou https://. La variable d'environnement DISCOURSE_ENABLE_CORS doit être renseignée sur la valeur « true » pour activer CORS." use_admin_ip_allowlist: "Les administrateurs ne peuvent se connecter que s'ils sont sur une adresse IP définie dans la liste des IP sous surveillance (Administration > Journaux > IP sous surveillance)." blocked_ip_blocks: "Une liste de blocs IP privés qui ne doivent jamais être indexés par Discourse" @@ -1497,6 +1540,7 @@ fr: allowed_iframes: "Une liste des préfixes du domaine src iframe que Discourse peut autoriser en toute sécurité dans les messages" allowed_crawler_user_agents: "Les agents utilisateurs des robots d'indexation qui sont autorisés à accéder au site. ATTENTION : CE PARAMÈTRE BLOQUERA TOUS LES ROBOTS D'INDEXATION NON LISTÉS ICI !" blocked_crawler_user_agents: "Mot unique insensible à la casse dans la chaîne de caractères de l'agent utilisateur identifiant les robots d'exploration Web qui ne devraient pas être autorisés à accéder au site. Ne s'applique pas si la liste d'autorisations est définie." + slow_down_crawler_user_agents: 'Champs "User-agent" des robots d''indexation dont les requêtes seront plafonnées en vitesse, conformément au paramètre ''slow down crawler rate''. Chaque entrée doit contenir un minimum de 3 caractères.' slow_down_crawler_rate: "Si slow_down_crawler_user_agents est défini, ce taux s'appliquera à tous les robots d'indexation (nombre de secondes d'attente entre requêtes)" content_security_policy: "Activer Content-Security-Policy" content_security_policy_report_only: "Activer Content-Security-Policy-Report-Only" @@ -1533,7 +1577,9 @@ fr: allow_index_in_robots_txt: "Indiquer dans le fichier robots.txt que ce site peut être indexé par les moteurs de recherche Web. Dans des cas exceptionnels, vous pouvez remplacer définitivement le fichier robots.txt." blocked_email_domains: "Une liste de domaines de courriels, séparés par des barres verticales (caractère « | ») que les utilisateurs ne sont pas autorisés à utiliser pour s'inscrire. Exemple : mailinator.com|trashmail.net" allowed_email_domains: "Une liste de domaines de courriels, séparés par des barres verticales (caractère « | ») que les utilisateurs DOIVENT utiliser pour s'inscrire. ATTENTION : les utilisateurs avec une adresse de courriel d'un domaine non repris dans la liste ne seront pas autorisés à s'inscrire !" + normalize_emails: "Vérifier que l'adresse de courriel normalisée n'est pas déjà utilisée. L'adresse normalisée s'obtient en ôtant tous les '.', et en ôtant tous les caractères éventuellement situés entre les symboles '+' et '@' de l'adrese indiquée par l'utilisateur." auto_approve_email_domains: "Les utilisateurs dont le domaine de l'adresse courriel appartient à cette liste seront automatiquement approuvés." + hide_email_address_taken: "Ne pas indiquer aux utilisateurs qu'un compte associé à une adresse de courriel donnée existe sur le serveur, au moment de l'inscription ou lors de la récupération d'un mot de passe perdu. Exiger une adresse de courriel complète lors de la récupération d'un mot de passe perdu." log_out_strict: "Lors de la déconnexion, déconnecter TOUTES les sessions de l'utilisateur sur tous les appareils" version_checks: "Vérifier périodiquement les serveurs Discourse pour obtenir des informations de mises à jour et les afficher sur le tableau de bord /admin" new_version_emails: "Envoyer un courriel à contact_email quand une nouvelle version de Discourse est disponible." @@ -1562,6 +1608,11 @@ fr: auth_overrides_email: "Écrase l'adresse de courriel définie localement en la remplaçant par l'adresse de courriel définie sur le site externe à chaque connexion, et interdit de la modifier localement. S'applique à tous les fournisseurs de service d'authentification. (ATTENTION : des divergences peuvent se produire en raison d'un algorithme de normalisation des adresses de courriel locales)" auth_overrides_username: "Écrase le nom d'utilisateur défini localement en le remplaçant par le nom d'utilisateur défini sur le site externe à chaque connexion, et interdit de le modifier localement. S'applique à tous les fournisseurs de service d'authentification. (ATTENTION : des divergences peuvent se produire en raison de contraintes de longueur ou de complexité des noms d'utilisateur)" auth_overrides_name: "Écrase le nom complet défini localement en le remplaçant par le nom complet défini sur le site externe à chaque connexion, et interdit de le modifier localement. S'applique à tous les fournisseurs de service d'authentification." + discourse_connect_overrides_avatar: "Écrase l'image de profil en la remplaçant par une image de profil définie par un site externe, par le biais de données transmises avec le protocole DiscourseConnect. Lorsque ce paramètre est activé, les utilisateurs ne sont pas autorisés à envoyer et utiliser leurs propres images de profil dans Discourse." + discourse_connect_overrides_location: "Écrase le champ utilisateur \"Localisation\" en le remplaçant par une valeur définie par un site externe, par le biais de données transmises avec le protocole DiscourseConnect. Lorsque ce paramètre est activé, les utilisateurs ne sont pas autorisés à modifier ce champ." + discourse_connect_overrides_website: "Écrase le champ utilisateur \"Site internet\" en le remplaçant par une valeur définie par un site externe, par le biais de données transmises avec le protocole DiscourseConnect. Lorsque ce paramètre est activé, les utilisateurs ne sont pas autorisés à modifier ce champ." + discourse_connect_overrides_profile_background: "Écrase l'arrière-plan du profil en le remplaçant par une valeur définie par un site externe, par le biais de données transmises avec le protocole DiscourseConnect." + discourse_connect_overrides_card_background: "Écrase l'arrière-plan de la carte utilisateur en le remplaçant par une valeur définie par un site externe, par le biais de données transmises avec le protocole DiscourseConnect." discourse_connect_not_approved_url: "Rediriger les comptes DiscourseConnect non validés vers cette URL" discourse_connect_allows_all_return_paths: "Ne pas restreindre le nom de domaine du champ return_paths indiqué par DiscourseConnect (si ce paramètres est désactivé, l'emplacement indiqué par « return path » doit se trouver sur le site actuel)" enable_local_logins: "Activer l'authentification locale avec nom d'utilisateur et mot de passe. ATTENTION : si ce paramètre est désactivé, il vous sera peut-être impossible de vous connecter si vous n'avez pas configuré au moins une méthode d'authentification alternative." @@ -1573,6 +1624,7 @@ fr: google_oauth2_client_secret: "Clé secrète du client de votre application Google." google_oauth2_prompt: "Liste facultative de valeurs de chaînes de caractères délimitées par des espaces, qui spécifie si le serveur d'autorisation invite l'utilisateur à s'authentifier à nouveau et à donner son consentement. Voir https://developers.google.com/identity/protocols/OpenIDConnect#prompt pour connaître les valeurs possibles." google_oauth2_hd: "Un domaine optionnel Google Apps Hosted auquel la connexion sera limitée. Voir https://developers.google.com/identity/protocols/OpenIDConnect#hd-param pour obtenir plus de détails." + google_oauth2_hd_groups: "(réglage expérimental) Pour chaque utilisateur, récupérer les groupes Google existant sur le domaine hébergé au moment de l'authentification. Les groupes Google ainsi récupérés peuvent être utilisés pour délencher automatiquement l'appartenance à un groupe Discourse. (Voir la rubrique 'Groupes' dans les paramètres.)" enable_twitter_logins: "Activer l'authentification Twitter, nécessite twitter_consumer_key et twitter_consumer_secret. Voir Configurer la connexion Twitter (avec rich embeds) pour Discourse." twitter_consumer_key: "Clé consommateur pour l'authentification Twitter, enregistrée sur https://developer.twitter.com/apps" twitter_consumer_secret: "Secret consommateur pour l'authentification Twitter, enregistrée sur https://developer.twitter.com/apps" @@ -1587,6 +1639,7 @@ fr: discord_secret: "Clé secrète Discord" discord_trusted_guilds: 'Seuls les membres de ces guildes Discord peuvent se connecter via Discord. Utilisez les identifiants numériques des guildes. Pour plus d''informations, suivez les instructions ici. Laissez vide pour autoriser toutes les guildes.' enable_backups: "Autoriser les administrateurs à créer des sauvegardes du forum" + allow_restore: "Autoriser la restauration de données, ce qui peut écraser TOUTES les données du site ! Laissez ce paramètre désactivé, sauf si vous envisagez de restaurer une sauvegarde." maximum_backups: "Nombre maximal de sauvegardes à conserver sur le disque. Les anciennes sauvegardes seront automatiquement supprimées" automatic_backups_enabled: "Créer automatiquement des sauvegardes suivant la fréquence définie dans le paramètre « backup frequency »" backup_frequency: "Nombre de jours entre chaque sauvegarde" @@ -1649,6 +1702,8 @@ fr: external_emoji_url: "URL du service externe de fourniture d'images d'émojis. Laisser ce champ vierge pour désactiver cette fonctionnalité." use_site_small_logo_as_system_avatar: "Utiliser le logo de petit format du site à la place de l'avatar de l'utilisateur 'system'. Nécessite que ce logo soit défini." restrict_letter_avatar_colors: "Une liste de couleurs au format hexadécimal à 6 chiffres utilisées pour l'arrière-plan des avatars en forme de lettres." + enable_listing_suspended_users_on_search: "Permettre aux utilisateurs habitués de trouver des utilisateurs suspendus lors d'une recherche." + selectable_avatars_mode: "Restreindre l'utilisation d'avatars personnalisés aux niveaux de confiance spécifiés, et n'autoriser les autres utilisateurs qu'à choisir un avatar dans la liste 'selectable_avatars'." selectable_avatars: "Liste d'avatars que les utilisateurs peuvent sélectionner." allow_all_attachments_for_group_messages: "Autoriser toutes les pièces jointes pour les messages de groupes." png_to_jpg_quality: "Qualité du fichier JPEG converti (1 représente la qualité la plus faible, 99 la meilleure qualité, 100 sert à désactiver)." @@ -1713,6 +1768,9 @@ fr: max_mentions_per_post: "Nombre maximal de noms d'utilisateurs que les utilisateurs peuvent mentionner dans un message." max_users_notified_per_group_mention: "Nombre maximal d'utilisateurs qui peuvent recevoir une notification si un groupe est mentionné (si le seuil est atteint, aucune notification ne sera envoyée)" enable_mentions: "Permettre aux utilisateurs de mentionner d'autres utilisateurs." + here_mention: "Nom d'une @mention permettant aux utilisateurs privilégiés d'envoyer une notification à chaque participant à un sujet, avec une limite de 'max_here_mentioned' notifications envoyées. Doit être distinct de tout nom d'utilisateur existant." + max_here_mentioned: "Nombre maximum de personnes recevant une notification lorsque @here est utilisée." + min_trust_level_for_here_mention: "Niveau de confiance minimal requis pour utiliser la mention @here." create_thumbnails: "Créer un aperçu et une visionneuse pour les images qui sont trop grandes pour le message." email_time_window_mins: "Attendre (n) minutes avant l'envoi des courriels de notification, afin de laisser une chance aux utilisateurs de modifier ou de finaliser leurs messages." personal_email_time_window_seconds: "Attendre (n) secondes avant d'envoyer des courriels de notification directs, afin de donner aux utilisateurs la possibilité de modifier et de finaliser leurs messages." @@ -1763,6 +1821,7 @@ fr: topic_view_duration_hours: "Compter la vue d'un sujet une seule fois par IP ou par utilisateur toutes les N heures" user_profile_view_duration_hours: "Compter la vue d'un profil d'utilisateur une seule fois par IP ou par utilisateur qui visite toutes les N heures" levenshtein_distance_spammer_emails: "Une adresse courriel sera attribuée à un spammeur connu même si elle diffère par ce nombre de caractères." + max_new_accounts_per_registration_ip: "S'il y a déjà (n) comptes avec un niveau de confiance de 0 utilisant cette adresse IP (et si aucun n'est un responsable ni un utilisateur avec un niveau de confiance de 2 ou plus), ne plus accepter de nouvelles inscriptions en provenance de cette adresse IP. Régler cette valeur à 0 désactive cette limite." min_ban_entries_for_roll_up: "En cliquant sur le bouton Consolider, une liste d'au moins (N) adresses interdites sera remplacée par une plage de sous réseau." max_age_unmatched_emails: "Effacer les adresses courriel sous surveillance sans correspondance après (N) jours" max_age_unmatched_ips: "Effacer les adresses IP sous surveillance sans correspondance après (N) jours" @@ -1898,6 +1957,8 @@ fr: permalink_normalizations: "Appliquer l'expression régulière suivante avant de détecter les permaliens, par exemple /(\\/topic.*)\\?.*/\\1 supprimera les chaînes de requête des chemins de sujet. Le format est regex+string, utilisez \\1 etc. pour capturer des séquences" global_notice: "Afficher une bannière de notification d'URGENCE globale qui ne peut pas être ignorée par les visiteurs (HTML autorisé). Laissez le champ vide pour masquer la bannière." disable_system_edit_notifications: "Désactiver les notifications de modifications par l'utilisateur système lorsque l'option « download_remote_images_to_local » est activée." + disable_category_edit_notifications: "Ne pas déclencher de notifications lorsque la catégorie d'un sujet est modifiée." + disable_tags_edit_notifications: "Ne pas déclencher de notifications lorsque les étiquettes d'un sujet sont modifiées." notification_consolidation_threshold: "Nombre de notifications de « J'aime » ou de demandes d'adhésion à partir duquel les notifications sont regroupées en une seule. Mettre 0 pour désactiver." likes_notification_consolidation_window_mins: "Durée en minutes après laquelle les notifications de « J'aime » sont consolidées en une seule notification une fois que le seuil est atteint. Le seuil peut être configuré via « SiteSetting.Notification_Consolidation_Threshold »." automatically_unpin_topics: "Désépingler automatiquement les sujets lorsque l'utilisateur atteint la fin de la liste." @@ -1909,6 +1970,7 @@ fr: app_association_ios: "Contenu de apple-app-site-association, utilisé pour créer des Universal Links entre ce site et des applications iOS." share_anonymized_statistics: "Partager les statistiques d'utilisation anonymes." auto_handle_queued_age: "Après tant de jours écoulés, traiter automatiquement les éléments en attente d'examen. Les signalements seront ignorés, les messages et les utilisateurs en attente d'examen seront rejetés. Réglez cette valeur à 0 pour désactiver cette fonction." + penalty_step_hours: "Durées par défaut de mise en sourdine ou de suspension des utilisateurs, en heures. La première valeur correspond à la première infraction d'un compte, la deuxième valeur à sa deuxième infraction, etc." svg_icon_subset: "Ajouter des icônes supplémentaires FontAwesome 5 que vous souhaitez inclure dans vos médias. Utilisez le préfixe « fa- » pour les icônes solides, « far- » pour les icônes normales et « fab- » pour les icônes de marques." max_prints_per_hour_per_user: "Nombre maximal d'accès à la page /print (mettre à 0 pour désactiver)" full_name_required: "Le nom complet est requis dans le profil utilisateur." @@ -1920,6 +1982,7 @@ fr: warn_reviving_old_topic_age: "Lorsque quelqu'un commence à répondre à un sujet dont la dernière réponse remonte à plusieurs jours, un avertissement sera affiché. Désactivez la fonctionnalité en indiquant 0." autohighlight_all_code: "Forcer la coloration syntaxique dans tous les blocs de codes, même si aucun langage de programmation n'est défini explicitement." highlighted_languages: "Règles de coloration syntaxique supportées. (Attention : supporter trop de langages peut impacter les performances.) Voir : https://highlightjs.org/static/demo/ pour une démonstration." + show_copy_button_on_codeblocks: "Afficher un bouton dans les blocs de mise en forme de type 'code' permettant de copier le contenu du bloc vers le presse-papier de l'utilisateur." embed_any_origin: "Autoriser le contenu intégré, quelle que soit son origine. Cela est nécessaire pour les applications mobiles utilisant du HTML statique." embed_topics_list: "Permettre l'intégration HTML des listes de sujets" embed_set_canonical_url: "Configurer l'URL canonique des sujets intégrés à l'URL du contenu intégré." @@ -1957,6 +2020,7 @@ fr: max_allowed_message_recipients: "Nombre maximal de destinataires autorisés dans un message." watched_words_regular_expressions: "Les mots surveillés sont des expressions régulières." enable_diffhtml_preview: "Fonctionnalité expérimentale utilisant diffHTML pour synchroniser la prévisualisation plutôt que de redéclencher un traitement d'affichage entier." + enable_fast_edit: "Permet de sélectionner une courte portion d'un message pour la modifier directement." old_post_notice_days: "Jours avant que la remarque de publication devienne obsolète" new_user_notice_tl: "Niveau de confiance minimal requis pour voir les nouveaux avis de publication d'utilisateur." returning_user_notice_tl: "Niveau de confiance minimum requis pour voir les avis postés par les utilisateurs de retour." @@ -2039,9 +2103,13 @@ fr: share_quote_buttons: "Choisissez les éléments qui apparaissent dans le bouton de partage de citation, ainsi que leur ordre." share_quote_visibility: "Choisissez quand afficher le bouton de partage de citation : jamais, uniquement aux utilisateurs anonymes ou à tous les utilisateurs. " create_revision_on_bulk_topic_moves: "Créer une révision pour les premiers messages lorsque les sujets sont déplacés en masse dans une nouvelle catégorie." + allow_changing_staged_user_tracking: "Permettre aux administrateurs de modifier les préférences de notification relatives aux catégories et aux tags, pour les comptes d'utilisateurs distants." + use_email_for_username_and_name_suggestions: "Utiliser la première moitié des adresses de courriel pour suggérer des noms d'utilisateur et des pseudos. Veuillez noter que cela permet à chacun de facilement deviner les adresses de courriel entières des utilisateurs (car une grande part de ceux-ci sont inscrits à des services communément utilisés tels que GMail)." errors: + invalid_css_color: "Couleur non valide. Indiquez un nom de couleur ou une valeur hexadécimale." invalid_email: "Adresse courriel invalide." invalid_username: "Il n'y a aucun utilisateur ayant ce nom d'utilisateur." + valid_username: "Ce nom d'utilisateur est déjà pris." invalid_group: "Il n'y a aucun groupe ayant ce nom." invalid_integer_min_max: "La valeur doit être comprise entre %{min} et %{max}." invalid_integer_min: "La valeur doit être de %{min} ou plus." @@ -2056,6 +2124,7 @@ fr: invalid_json: "Données JSON non valides." invalid_reply_by_email_address: "La valeur doit contenir « %{reply_key} » et être différente du courriel de notification." invalid_alternative_reply_by_email_addresses: "Toutes les valeurs doivent contenir « %{reply_key} » et être différentes du courriel de notification." + invalid_domain_hostname: "Ne peut inclure les caractères '*' ou '?'." pop3_polling_host_is_empty: "Vous devez définir « pop3 polling host » avant d'activer le relevé via POP3." pop3_polling_username_is_empty: "Vous devez définir « pop3 polling username » avant d'activer le relevé via POP3." pop3_polling_password_is_empty: "Vous devez définir « pop3 polling password » avant d'activer le relevé via POP3." @@ -2084,6 +2153,9 @@ fr: leading_trailing_slash: "L'expression régulière ne doit pas commencer et se terminer par une barre oblique." unicode_usernames_avatars: "Les avatars du système n'acceptent pas les noms d'utilisateurs contenant des caractères Unicode." list_value_count: "La liste doit contenir exactement %{count} valeurs." + google_oauth2_hd_groups: "Pour activer ce paramètre, vous devez d'abord configurer 'google oauth2 hd'." + search_tokenize_chinese_enabled: "Pour activer ce paramètre, vous devez d'abord désactiver 'search_tokenize_chinese'." + search_tokenize_japanese_enabled: "Pour activer ce paramètre, vous devez d'abord désactiver 'search_tokenize_japanese'." placeholder: discourse_connect_provider_secrets: key: "www.example.com" @@ -2212,6 +2284,7 @@ fr: reset_not_allowed_from_ip_address: "Vous ne pouvez pas réinitialiser le mot de passe depuis cette adresse IP." suspended: "Vous ne pouvez pas vous connecter jusqu'au %{date}." suspended_with_reason: "Compte suspendu jusqu'au %{date} : %{reason}" + suspended_with_reason_forever: "Compte supsendu : %{reason}" errors: "%{errors}" not_available: "Indisponible. Essayer %{suggestion} ?" something_already_taken: "Un problème est survenu. Votre nom d'utilisateur ou votre adresse courriel est peut-être déjà enregistré(e). Essayez le lien d'oubli du mot de passe." @@ -2244,6 +2317,13 @@ fr: second_factor_toggle: totp: "Utiliser une application d'authentification ou une clé de sécurité" backup_code: "Utiliser un code de secours à la place" + second_factor_auth: + challenge_not_found: "Un défi d'authentification par 2FA n'a pas pu être trouvé dans le contexte de votre session actuelle." + challenge_expired: "Le délai d'expiration de l'authentification par 2FA s'est écoulé. Veuillez réessayer." + challenge_not_completed: "Vous n'avez pas effectué l'authentification par 2FA requise pour accomplir cette tâche. Veuillez vous authentifier par 2FA et réessayer." + actions: + grant_admin: + description: "Par mesure de sécurité, vous devez vous authentifier par 2FA avant que l'accès administrateur ne soit accordé à %{username}." admin: email: sent_test: "envoyé !" @@ -2424,17 +2504,63 @@ fr: test_mailer: title: "Envoyer un courriel de test" subject_template: "[%{email_prefix}] Test de délivrabilité du courriel" + text_body_template: | + Ceci est un courriel de test expédié par + + [**%{base_url}**][0] + + Nous espérons que ce test de remise du courriel vous sera parvenu correctement ! + + Voici une [liste de points à vérifier pour assurer la bonne remise de vos courriels][1]. + + Bonne chance, + + Vos amis développeurs de [Discourse](https://www.discourse.org) + + [0] : %{base_url} + [1] : https://meta.discourse.org/t/email-delivery-configuration-checklist/209839 new_version_mailer: title: "Nouvelle version" subject_template: "[%{email_prefix}] Nouvelle version de Discourse, mise à jour disponible" + text_body_template: | + Hourra, une nouvelle version de [Discourse](https://www.discourse.org) est disponible ! + + Votre version : %{installed_version} + Nouvelle version : **%{new_version}** + + - Effectuez facilement la mise à jour depuis votre navigateur en utilisant la **[mise à jour en 1 clic](%{base_url}/admin/upgrade)** + + - Découvrez les nouveautés en consultant les [notes de version](https://meta.discourse.org/tags/release-notes) ou l'[historique GitHub](https://github.com/discourse/discourse/commits/master) + + - Rendez vous sur [meta.discourse.org](https://meta.discourse.org) pour consulter des actualités, des discussions et obtenir de l'aide concernant Discourse new_version_mailer_with_notes: title: "Nouvelle version avec notes de modifications" subject_template: "[%{email_prefix}] Mise à jour disponible" + text_body_template: | + Hourra, une nouvelle version de [Discourse](https://www.discourse.org) est disponible ! + + Votre version : %{installed_version} + Nouvelle version : **%{new_version}** + + - Effectuez facilement la mise à jour depuis votre navigateur en utilisant la **[mise à jour en 1 clic](%{base_url}/admin/upgrade)** + + - Découvrez les nouveautés en consultant les [notes de version](https://meta.discourse.org/tags/release-notes) ou l'[historique GitHub](https://github.com/discourse/discourse/commits/master) + + - Rendez vous sur [meta.discourse.org](https://meta.discourse.org) pour consulter des actualités, des discussions et obtenir de l'aide concernant Discourse + + ### Notes de version + + %{notes} flag_reasons: off_topic: "Votre message a été signalé comme étant **hors sujet** : la communauté considère qu'il ne correspond pas au sujet en question qui est défini par son titre et son premier message." inappropriate: "Votre message a été signalé comme étant **répréhensible** : la communauté a estimé que son contenu était offensant, injurieux, violent ou qu'il enfreignait notre [charte communautaire](%{base_path}/guidelines)." spam: "Votre message a été signalé comme étant du **spam** : la communauté considère qu'il s'agit d'une publicité ou du contenu trop promotionnel au lieu d'être utile et pertinent au regard du sujet." notify_moderators: "Votre message a été **signalé aux modérateurs** : la communauté considère qu'il nécessite l'intervention d'un responsable." + responder: + off_topic: "Ce message a été signalé comme étant **hors sujet** : la communauté considère qu'il ne correspond pas au sujet en question tel que défini par son titre et son premier message." + inappropriate: "Ce message a été signalé comme étant **répréhensible** : la communauté a estimé que son contenu était offensant, injurieux, violent ou qu'il enfreignait notre [charte communautaire](%{base_path}/guidelines)." + spam: "Ce message a été signalé comme étant du **spam** : la communauté considère qu'il s'agit d'une publicité, c'est-à-dire d'un message nature trop promotionnelle au lieu d'être utile et pertinent au regard du sujet." + notify_moderators: "Ce message a été **signalé aux modérateurs** : la communauté considère qu'il nécessite l'intervention d'un responsable." flags_dispositions: agreed: "Merci de nous en informer. Nous sommes en accord avec votre signalement et nous travaillons à sa résolution." agreed_and_deleted: "Merci de nous en informer. Nous sommes en accord avec votre signalement et avons supprimé le message." @@ -2445,6 +2571,8 @@ fr: one: "Ce sujet est fermé pour au moins %{count} heure suite à plusieurs signalements de la communauté." other: "Ce sujet est fermé pour au moins %{count} heures suite à plusieurs signalements de la communauté." system_messages: + reviewables_reminder: + subject_template: "Des éléments sont à examiner dans la file des messages en attente d'examen" private_topic_title: "Sujet #%{id}" contents_hidden: "Veuillez visiter le message pour voir son contenu." post_hidden: @@ -2525,6 +2653,8 @@ fr: Pour plus d'informations, nous vous invitons à consulter notre [charte communautaire](%{base_url}/guidelines). flags_agreed_and_post_deleted_for_responders: + title: "Cette réponse à un message signalé a été supprimée par un responsable." + subject_template: "Cette réponse à un message signalé a été supprimée par un responsable." text_body_template: | Bonjour, @@ -2605,15 +2735,41 @@ fr: backup_succeeded: title: "Sauvegarde effectuée" subject_template: "Sauvegarde terminée avec succès" + text_body_template: | + La sauvegarde s'est terminée avec succès. + + Pour la télécharger, rendez-vous dans la section [Administration > Sauvegardes](%{base_url}/admin/backups) du site. + + Voici les détails journalisés : + + %{logs} backup_failed: title: "Sauvegarde échouée" subject_template: "Échec de la sauvegarde" + text_body_template: | + La sauvegarde a échoué. + + Voici les détails journalisés : + + %{logs} restore_succeeded: title: "Restauration réussie" subject_template: "Restauration terminée avec succès" + text_body_template: | + La restauration de données s'est terminée avec succès. + + Voici les détails journalisés : + + %{logs} restore_failed: title: "Restauration échouée" subject_template: "Échec de la restauration" + text_body_template: | + La restauration de données a échoué. + + Voici les détails journalisés : + + %{logs} bulk_invite_succeeded: title: "Invitation en masse réussie" subject_template: "Envoi des invitations en masse réussi" @@ -2992,6 +3148,7 @@ fr: subject_re: "Re : " subject_pm: "[MD] " email_from: "%{user_name} via %{site_name}" + email_from_without_site: "%{user_name}" user_notifications: previous_discussion: "Réponses précédentes" reached_limit: @@ -3234,6 +3391,10 @@ fr: account_suspended_forever: title: "Compte suspendu" subject_template: "[%{email_prefix}] Votre compte a été suspendu" + text_body_template: | + Votre compte sur le forum a été suspendu. + + Motif : %{reason} account_silenced: title: "Compte mis en sourdine" subject_template: "[%{email_prefix}] Votre compte a été mis en sourdine" @@ -3244,6 +3405,10 @@ fr: account_silenced_forever: title: "Compte mis en sourdine" subject_template: "[%{email_prefix}] Votre compte a été mis en sourdine" + text_body_template: | + Votre compte sur le forum a été mis en sourdine. + + Motif : %{reason} account_exists: title: "Le compte existe déjà" subject_template: "[%{email_prefix}] Le compte existe déjà" @@ -3491,16 +3656,29 @@ fr: store_failure: "Erreur lors du stockage du fichier numéro %{upload_id} pour l'utilisateur %{user_id}." file_missing: "Nous sommes désolés, vous devez fournir un fichier à envoyer." empty: "Nous sommes désolés, mais le fichier que vous avez fourni est vide." + failed: "Navré, l'envoi de fichier a échoué. Nous vous prions de réessayer." png_to_jpg_conversion_failure_message: "Une erreur est survenue lors de la conversion du format PNG en JPG." optimize_failure_message: "Une erreur est survenue lors de l'optimisation de l'image envoyée." + download_failure: "Le téléchargement du fichier depuis la source externe a échoué." + size_mismatch_failure: "La taille du fichier envoyé vers S3 ne correspondait pas à la taille attendue. %{additional_detail}" + create_multipart_failure: "L'initiation du chargement partionné dans l'espace de stockage externe a échoué." + abort_multipart_failure: "L'annulation du chargement partitionné dans l'espace de stockage externe a échoué." + complete_multipart_failure: "Le chargement partionné dans l'espace de stockage externe a échoué." + external_upload_not_found: "L'objet ou le fichier n'a pas été trouvé dans l'espace de stockage externe. %{additional_detail}" + checksum_mismatch_failure: "La vérification d'intégrité du fichier que vous avez envoyé a échoué. Le contenu du fichier a possiblement été modifié pendant son envoi. Nous vous prions de réessayer." + cannot_promote_failure: "L'envoi n'a pas pu être effectué. Il avait peut-être déjà été effectué avec succès une première fois, ou avait peut-être déjà échoué une première fois." + size_zero_failure: "Navré, un problème est survenu : le fichier reçu semble être vide. Nous vous prions de réessayer l'envoi." attachments: too_large: "Nous sommes désolés, le fichier que vous essayez d'envoyer est trop volumineux (la taille maximale autorisée est de %{max_size_kb} Ko)." + too_large_humanized: "Navré, le fichier que vous souhaitez joindre est trop lourd. (La taille maximale permise est de %{max_size}.)" images: too_large: "Nous sommes désolés, l'image que vous essayez d'envoyer est trop grande (la taille maximale autorisée est de %{max_size_kb} Ko). Veuillez la redimensionner puis réessayez." + too_large_humanized: "Navré, les dimensions de l'image que vous souhaitez joindre sont trop grandes. (Les dimensions maximales permises sont de %{max_size}.) Nous vous prions de redimensionner cette image avant de réessayer." larger_than_x_megapixels: "Nous sommes désolés, l'image que vous essayez d'envoyer est trop grande (la dimension maximale est de %{max_image_megapixels} mégapixels). Veuillez la redimensionner puis réessayez." size_not_found: "Nous sommes désolés, mais nous n'avons pas pu déterminer la taille de votre image. Peut-être est-elle corrompue ?" placeholders: too_large: "(image plus grande que %{max_size_kb} Ko)" + too_large_humanized: "(image supérieure à %{max_size})" avatar: missing: "Nous sommes désolés, nous ne parvenons pas à trouver un avatar associé à cette adresse courriel. Pouvez-vous essayer de l'envoyer à nouveau ?" flag_reason: @@ -3925,7 +4103,9 @@ fr: mass_award: errors: invalid_csv: Une erreur s'est produite à la ligne %{line_number}. Veuillez confirmer que le fichier CSV contient une adresse courriel par ligne. + too_many_csv_entries: Trop de lignes dans le fichier CSV. Veuillez fournir un fichier CSV contenant un maximum de %{count} lignes. badge_disabled: Veuillez préalablement activer le badge %{badge_name}. + cant_grant_multiple_times: Impossible d'attribuer plusieurs fois à un seul utilisateur le badge %{badge_name}. editor: name: Éditeur description: Première modification d'un message @@ -4195,6 +4375,12 @@ fr: other: 'L''étiquette « %{tag_name} » est réservée aux catégories suivantes : %{category_names}' synonym: 'Les synonymes ne sont pas autorisés. Utilisez « %{tag_name} ».' has_synonyms: 'L''étiquette « %{tag_name} » ne peut pas être utilisée car elle a des synonymes.' + restricted_tags_cannot_be_used_in_category: + one: 'La balise "%{tags}" ne peut pas être utilisée dans la catégorie "%{category}". Veuillez la supprimer.' + other: 'Les balises suivantes ne peuvent pas être utilisées dans la catégorie "%{category}" : %{tags}. Veuillez les supprimer.' + category_does_not_allow_tags: + one: 'La catégorie "%{category}" n''autorise pas la balise "%{tags}". Veuillez la supprimer.' + other: 'La catégorie "%{category}" n''autorise pas les balises suivantes : "%{tags}". Veuillez les supprimer.' required_tags_from_group: one: "Vous devez inclure au moins %{count} étiquette %{tag_group_name}. Les étiquettes associées à ce groupe sont : %{tags}" other: "Vous devez inclure au moins %{count} étiquettes %{tag_group_name}. Les étiquettes associées à ce groupe sont : %{tags}" @@ -4238,8 +4424,10 @@ fr: label: "Nom de votre communauté" placeholder: "Le repaire de Jeanne" site_description: + label: "Décrivez votre communauté en une courte phrase (s'affichera dans les moteurs de recherche et les réseaux sociaux)" placeholder: "Un endroit de discussion pour Jeanne et ses amis" short_site_description: + label: "Décrivez votre communauté en quelques mots (servira de titre à la page d'accueil)" placeholder: "Meilleure communauté de tous les temps" introduction: title: "Introduction" @@ -4299,7 +4487,10 @@ fr: label: "Ville pour les litiges" placeholder: "San Francisco, Californie" styling: + title: "Choix du style" fields: + color_scheme: + label: "Jeu de couleurs" body_font: label: "Police de contenu" heading_font: @@ -4307,6 +4498,7 @@ fr: styling_preview: label: "Prévisualiser" homepage_style: + label: "Style de la page d'accueil" choices: latest: label: "Sujets récents" @@ -4395,7 +4587,19 @@ fr: missing_version: "Vous devez fournir un paramètre de version" conflict: "Un conflit de mise à jour vous empêche de faire cette action." reasons: + post_count: "Les premiers messages de chaque utilisateur doivent être approuvés par un responsable. Voir %{link}." + trust_level: "Les réponses des utilisateurs dont le niveau de confiance est bas doivent être approuvées par un responsable. Voir %{link}." + new_topics_unless_trust_level: "Les sujets créés par des utilisateurs dont le niveau de confiance est bas doivent être approuvés par un responsable. Voir %{link}." + fast_typer: "Le nouvel utilisateur a écrit son premier message avec une rapidité suspecte, comportement typique des bots et des spammeurs. Voir%{link}." + auto_silence_regex: "Nouvel utilisateur dont le premier message contient le motif textuel défini par le paramètre %{link}." + watched_word: "Ce message contient un mot surveillé. Voir votre %{link}." + category: "Les messages de cette catégorie doivent être approuvés manuellement par un responsable. Voir %{link}." + must_approve_users: "Tous les nouveaux utilisateurs doivent être approuvés par un responsable. Voir %{link}." + invite_only: "Tous les nouveaux utilisateurs doivent être invités. Voir %{link}." email_auth_res_enqueue: "Ce courriel n'a pas passé une vérification DMARC, il est fort probable qu'il ne provienne pas de l'expéditeur annoncé. Vérifiez les en-têtes du courriel brut pour plus d'informations." + email_spam: "Ce courriel a été identifié comme un spam d'après l'en-tête défini dans %{link}." + suspect_user: "Ce nouvel utilisateur a renseigné son profil sans lire de sujets ni de messages, comportement typique d'un spammeur. Voir %{link}." + contains_media: "Ce message contient un ou plusieurs médias audio ou vidéo. Voir %{link}." queued_by_staff: "Un responsable a estimé que ce message devait être examiné avant sa publication. Il restera masqué jusqu'à son approbation par un modérateur." actions: agree: @@ -4481,6 +4685,7 @@ fr: ignore_error: "Nous sommes désolés, vous ne pouvez pas ignorer cet utilisateur." mute_error: "Nous sommes désolés, vous ne pouvez pas mettre cet utilisateur en sourdine." error: "Désolé, vous ne pouvez pas modifier le niveau de notification pour cet utilisateur." + invalid_value: '"%{value}" n''est pas un niveau de notification valide.' discord: not_in_allowed_guild: "L'authentification a échoué. Vous n'êtes membre d'aucune guilde Discord autorisée." old_keys_reminder: diff --git a/config/locales/server.gl.yml b/config/locales/server.gl.yml index d22103cf07..b9bd9381d2 100644 --- a/config/locales/server.gl.yml +++ b/config/locales/server.gl.yml @@ -372,7 +372,6 @@ gl: already_bookmarked_post: "Non pode gardar a mesma publicación nos marcadores dúas veces." cannot_set_past_reminder: "Non pode estabelecer un recordatorio de marcador no pasado." cannot_set_reminder_in_distant_future: "Non pode estabelecer un recordatorio de marcador máis de 10 anos cara ao futuro." - time_must_be_provided: "débese facilitar o período para todos os recordatorios" reminders: at_desktop: "A vindeira vez que estea no meu ordenador" later_today: "Hoxe, máis tarde" @@ -1400,10 +1399,8 @@ gl: summary_percent_filter: "Cando un usuario preme en «Resumir este tema» mostrar a porcentaxe de publicacións destacadas" summary_max_results: "Número máximo de publicacións que devolve a opción «Resumir este tema»" enable_system_message_replies: "Permitirlles aos usuarios responder mensaxes do sistema, mesmo aínda que as mensaxes persoais estean desactivadas" - enable_long_polling: "O bus de mensaxes utilizado para a notificación pode utilizar «long polling»" enable_chunked_encoding: "Active as respostas mediante codificación fragmentaria do servidor. Esta funcionalidade serve na maioría das configuracións, pero algúns servidores intermedios poden retelas, o que provocaría o atraso nas respostas" long_polling_base_url: "URL base empregado para o «long polling» (cando unha rede de distribución de contido serve contido dinámico, asegúrese de axustalo como «origin pull»); por exemplo: http://origin.site.com" - long_polling_interval: "Tempo que o servidor debe de esperar antes de responderlles aos clientes que non hai datos para enviar (soamente usuarios con sesión iniciada)." polling_interval: "Se non se utilizar «long polling», con que frecuencia -en milisegundos- deben iniciar sesión os clientes" anon_polling_interval: "Con que frecuencia -en milisegundos- deberían recoller mostras os clientes anónimos" background_polling_interval: "Con que frecuencia -en milisegundos- deberían recoller mostras os clientes (se a xanela está en segundo plano)" diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 77bf9221fb..5dbd496b8f 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -63,6 +63,7 @@ he: unrecognized_extension: "סיומת הקובץ אינה מוכרת: %{extension}" import_error: generic: אירעה שגיאה בעת ייבוא ערכת העיצוב הזו + upload: "שגיאה ביצירת משאב להעלאה: %{name}. %{errors}" about_json: "שגיאת ייבוא: about.json לא קיים או שהוא שגוי. האם אכן מדובר בערכת עיצוב של Discourse?" about_json_values: "about.json מכיל ערכים שגויים: %{errors}" modifier_values: "המחליפים ב־about.json מכילים ערכים שגויים: %{errors}" @@ -436,11 +437,13 @@ he: bookmarks: errors: already_bookmarked_post: "אי אפשר לסמן את אותו הפוסט פעמיים" + already_bookmarked: "אי אפשר לסמן את אותו %{type} פעמיים." too_many: "לא ניתן להוסיף למעלה מ־%{limit} סימניות, נא לבקר ב%{user_bookmarks_url} כדי להסיר כמה מהנוכחיות." cannot_set_past_reminder: "אי אפשר להגדיר תזכורת סימנייה למועד שעבר." cannot_set_reminder_in_distant_future: "לא ניתן להגדיר תזכורת סימנייה למעבר ל־10 שנים בעתיד." - time_must_be_provided: "יש לספק זמן לכל התזכורות" + time_must_be_provided: "יש לציין זמן לכל התזכורות" for_topic_must_use_first_post: "מותר להשתמש רק בפוסט הראשון כדי להוסיף את הנושא לסימניות." + bookmarkable_id_type_required: "דרושים שם וסוג הרשומה לסימון." reminders: at_desktop: "בשימוש הבא דרך שולחן העבודה" later_today: "בהמשך היום" @@ -1566,10 +1569,8 @@ he: summary_timeline_button: "הצגת כפתור ‚סיכום’ בציר הזמן" enable_personal_messages: "לאפשר למשתמשים בדרגת אמון 1 (ניתן להגדרה דרך אמון מזערי לשליחת הודעות) ליצור הודעות ולענות להודעות. נא לשים לב שהסגל תמיד יכול לשלוח הודעות, ללא קשר." enable_system_message_replies: "מאפשר למשתמשים להגיב להודעות מערכת אפילו כשהודעות אישיות מושבתות" - enable_long_polling: "באס הודעות שמשמש להתראות יכול להשתמש בתשאול ארוך (long polling)" enable_chunked_encoding: "הפעלת תגובות קידוד מחולקות מצד השרת. תכונה זו עובדת ברוב התצורות אך חלק מהמתווכים עשויים לכלוא כצעד ביניים, מה שעלול לגרום להאטה" long_polling_base_url: "כתובת הבסיס שמשמשת לתשאול ארוך (כאשר CDN מגיש תוכן דינמי, יש להגדיר זאת למשיכה המקורית) למשל: http://origin.site.com" - long_polling_interval: "כמות הזמן שהשרת צריך לחכות לפני שעונה ללקוחות, כאשר אין מידע לשליחה (משתמשים רשומים מחוברים למערכת בלבד)" polling_interval: "כאשר לא מבצעים תשאול ארוך (long polling), כל כמה זמן לקוחות מחוברים למערכת יבצעו poll, במילי-שניות" anon_polling_interval: "באיזו תכיפות לקוחות אלמוניים יתשאלו, במילישניות" background_polling_interval: "באיזו תכיפות צריכים לקוחות לבצע תשאול (poll) במילישניות (כאשר החלון נמצא ברקע)" @@ -4509,6 +4510,16 @@ he: other: '„%{tag_name}” מוגבלת לקטגוריות הבאות: %{category_names}' synonym: 'אסור להשתמש במילים נרדפות. יש להשתמש ב־„%{tag_name}” במקום.' has_synonyms: 'לא ניתן להשתמש ב־„%{tag_name}” כיוון שיש לו מילים נרדפות.' + restricted_tags_cannot_be_used_in_category: + one: 'לא ניתן להשתמש בתגית „%{tags}” בקטגוריה „%{category}”. נא להסיר אותה.' + two: 'לא ניתן להשתמש בתגיות הבאות בקטגוריה „%{category}”: %{tags}. נא להסיר אותן.' + many: 'לא ניתן להשתמש בתגיות הבאות בקטגוריה „%{category}”: %{tags}. נא להסיר אותן.' + other: 'לא ניתן להשתמש בתגיות הבאות בקטגוריה „%{category}”: %{tags}. נא להסיר אותן.' + category_does_not_allow_tags: + one: 'בקטגוריה „%{category}” אסור להשתמש בתגית „%{tags}”. נא להסיר אותה.' + two: 'בקטגוריה „%{category}” אסור להשתמש בתגיות הבאות: „%{tags}”. נא להסיר אותן.' + many: 'בקטגוריה „%{category}” אסור להשתמש בתגיות הבאות: „%{tags}”. נא להסיר אותן.' + other: 'בקטגוריה „%{category}” אסור להשתמש בתגיות הבאות: „%{tags}”. נא להסיר אותן.' required_tags_from_group: one: "עליך לכלול לפחות תגית %{tag_group_name} %{count}. התגיות בקבוצה הזאת הן: %{tags}." two: "עליך לכלול לפחות %{count} תגיות %{tag_group_name}. התגיות בקבוצה הזאת הן: %{tags}." @@ -4821,6 +4832,7 @@ he: ignore_error: "אין אפשרות להתעלם מהמשתמש הזה, עמך הסליחה." mute_error: "אין אפשרות להשתיק את המשתמש הזה, עמך הסליחה." error: "אין לך אפשרות להחליף את רמת ההתראות למשתמש הזה, עמך הסליחה." + invalid_value: '„%{value}” אינה רמת התראות חוקית.' discord: not_in_allowed_guild: "האימות נכשל. הגילדה ב־Discord אליה הצטרפת אינה מורשית לגשת." old_keys_reminder: diff --git a/config/locales/server.hu.yml b/config/locales/server.hu.yml index 4086949e0c..e723de26dd 100644 --- a/config/locales/server.hu.yml +++ b/config/locales/server.hu.yml @@ -2145,6 +2145,7 @@ hu: title: "Elutasítás" notification_level: ignore_error: "Sajnáljuk, de nem tilthatod le ezt a felhasználót" + invalid_value: 'A(z) „%{value}” nem érvényes értesítési szint.' old_keys_reminder: body: | Szia! Ez egy szokásos éves biztonsági emlékeztető a Discourse példányától. diff --git a/config/locales/server.hy.yml b/config/locales/server.hy.yml index cf8db808fb..a3b1bfee71 100644 --- a/config/locales/server.hy.yml +++ b/config/locales/server.hy.yml @@ -1119,9 +1119,7 @@ hy: summary_percent_filter: "Երբ օգտատերը սեղմում է 'Ամփոփել Այս Թեման' , ցուցադրել գրառումների թոփ %-ը" summary_max_results: "'Ամփոփել Այս Թեման' -ի կողմից վերադարձված առավելագույն գրառումները" enable_system_message_replies: "Թույլատրում է օգտատերերին պատասխանել համակարգային հաղորդագրությունների, անգամ եթե անձնական հաղորդագրություններն անջատված են" - enable_long_polling: "Ծանուցման համար օգտագործվող նամակների ավտոբուսը (Message bus) կարող է օգտագործել long polling:" long_polling_base_url: "long polling-ի համար օգտագործվող հիմնական URL (երբ CDN-ը տալիս է դինամիկ բովանդակություն, համոզվեք, որ սա սահմանված է origin pull) , օրինակ՝ http://origin.site.com" - long_polling_interval: "Ժամանակի քանակը, որ սերվերը պետք է սպասի մինչև հաճախորդին արձագանքելը, երբ ուղարկելու ենթակա տվյալներ չկան (միայն մուտքագրված օգտատերերի համար)" polling_interval: "When not long polling, how often should logged on clients poll in milliseconds" anon_polling_interval: "How often should anonymous clients poll in milliseconds" background_polling_interval: "How often should the clients poll in milliseconds (when the window is in the background)" diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml index fec91c5ad6..9d64d0b812 100644 --- a/config/locales/server.id.yml +++ b/config/locales/server.id.yml @@ -16,6 +16,7 @@ id: date_only: "%B %-d, %Y" long: "%B %-d, %Y, %l:%M%P" no_day: "%Y %B" + calendar_ics: "%Y%m%dT%H%M%SZ" date: month_names: - null @@ -40,6 +41,7 @@ id: topics: "Topik" posts: "post" loading: "Memuat" + powered_by_html: 'Didukung oleh Discourse, paling baik dilihat dengan JavaScript diaktifkan' sign_up: "Mendaftar" log_in: "Log In" submit: "Kirimkan" @@ -47,31 +49,44 @@ id: disable_remote_images_download_reason: "Unduh gambar jarak jauh dimatikan karena ruang penyimpanan di server tidak cukup." anonymous: "Anonim" remove_posts_deleted_by_author: "Dihapus oleh penulis" + redirect_warning: "Kami tidak dapat memverifikasi bahwa tautan yang Anda pilih benar-benar diposkan ke forum. Jika Anda tetap ingin melanjutkan, pilih tautan di bawah ini." on_another_topic: "Di topik lain" + inline_oneboxer: + topic_page_title_post_number: "#%{post_number}" + topic_page_title_post_number_by_user: "#%{post_number} oleh %{username}" themes: + bad_color_scheme: "Tidak dapat memperbarui tema, palet warna tidak valid" other_error: "Terjadi masalah saat memperbarui tema" compile_error: unrecognized_extension: "Ekstensi file tidak dikenal: %{extension}" import_error: + generic: Terjadi kesalahan saat mengimpor tema itu unpack_failed: "Gagal membongkar berkas" file_too_big: "Berkas yang tidak dikompresi terlalu besar." settings_errors: + invalid_yaml: "YAML yang diberikan tidak valid." number_value_not_valid_min_max: "Itu harus antara %{min} dan %{max}." number_value_not_valid_min: "Itu harus lebih besar dari atau sama dengan %{min}." number_value_not_valid_max: "Itu harus lebih kecil dari atau sama dengan %{max}." string_value_not_valid: "Panjang nilai baru tidak berada dalam rentang yang diizinkan." string_value_not_valid_min_max: "Panjangnya harus antara %{min} dan %{max} karakter." string_value_not_valid_min: "Panjangnya minimal %{min} karakter." + locale_errors: + invalid_yaml: "Terjemahan YAML tidak valid" emails: incoming: default_subject: "Topik ini membutuhkan judul" show_trimmed_content: "Tampilkan konten yang dipersingkat" + no_subject: "(tidak ada subjek)" errors: empty_email_error: "Terjadi bila surel mentah yang kami terima tidak berisi apapun (kosong)." no_message_id_error: "Terjadi bila surel tidak memiliki header 'Message-Id'." auto_generated_email_error: "Terjadi bila 'precedence' header ditentukan sebagai: list, junk, bulk atau auto_reply, atau ketika header lainnya mengandung: kumpulkan otomatis, balas otomatis, atau buat otomatis." inactive_user_error: "Terjadi bila pengirim sedang tidak aktif" silenced_user_error: "Terjadi bila pengirim telah dibungkam" + screened_email_error: "Terjadi ketika alamat email pengirim sudah disaring." + unsubscribe_not_allowed: "Terjadi ketika berhenti berlangganan melalui email tidak diperbolehkan untuk pengguna ini." + email_not_allowed: "Terjadi ketika alamat email tidak ada dalam daftar yang diizinkan atau ada di daftar blokir." unrecognized_error: "Kesalahan Tidak Dikenali" errors: &errors messages: diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 4b61d3df04..85eef94794 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -63,6 +63,7 @@ it: unrecognized_extension: "Estensione del file non riconosciuta: %{extension}" import_error: generic: Errore durante l'importazione del tema + upload: "Errore nella creazione della risorsa di caricamento: %{name}. %{errors}" about_json: "Errore di importazione: about.json non esiste o non è valido. Sei sicuro che questo sia un tema di Discourse?" about_json_values: "about.json contiene valori non validi: %{errors}" modifier_values: "I modificatori about.json contengono valori non validi: %{errors}" @@ -393,11 +394,13 @@ it: bookmarks: errors: already_bookmarked_post: "Non puoi aggiungere due volte lo stesso messaggio nei segnalibri." + already_bookmarked: "Non puoi aggiungere due volte lo stesso %{type} ai segnalibri." too_many: "Siamo spiacenti, non puoi aggiungere più di %{limit} segnalibri, visita %{user_bookmarks_url} per rimuoverne alcuni." cannot_set_past_reminder: "Non è possibile impostare un promemoria per un segnalibro nel passato." cannot_set_reminder_in_distant_future: "Non è possibile impostare un promemoria per un segnalibro per più di 10 anni nel futuro." - time_must_be_provided: "l'orario deve essere fornito per tutti i promemoria" + time_must_be_provided: "L'orario deve essere fornito per tutti i promemoria" for_topic_must_use_first_post: "Puoi usare solo il primo messaggio per aggiungere un segnalibro all'argomento." + bookmarkable_id_type_required: "Sono richiesti sia il nome sia il tipo di segnalibro." reminders: at_desktop: "La prossima volta che sarò al mio desktop" later_today: "Più tardi oggi" @@ -1474,10 +1477,8 @@ it: summary_max_results: "Messaggi massimi restituiti da 'Riassumi questo argomento'." enable_personal_messages: "Autorizza gli utenti con livello di attendibilità 1 (configurabile attraverso \"min livello di attendibilità per l'invio di messaggi\") a creare e rispondere ai messaggi. Nota che lo staff può inviare messaggi in ogni caso." enable_system_message_replies: "Consente agli utenti di rispondere ai messaggi di sistema, anche se i messaggi personali sono disabilitati" - enable_long_polling: "Il message bus per le notifiche può usare il long polling" enable_chunked_encoding: "Abilita le risposte di codifica in blocchi dal server. Questa funzionalità va bene sulla maggior parte delle configurazioni, tuttavia alcuni proxy possono eseguire la bufferizzazione, provocando un ritardo nelle risposte" long_polling_base_url: "URL di base usato per il long polling (quando una CDN serve contenuto dinamico, bisogna impostarlo come origin pull) es. http://origin.site.com" - long_polling_interval: "Tempo di attesa prima che il server risponda ai client che non ci sono dati da trasmettere (solo per utenti autenticati)" polling_interval: "Se non si esegue il long polling, quanto spesso i client autenticati devono fare poll in millisecondi" anon_polling_interval: "Frequenza in millisecondi con cui client anonimi effettuano il poll" background_polling_interval: "Quanto spesso i client devono fare poll in millisecondi (quando la finestra è in background)" @@ -3870,6 +3871,9 @@ it: other: '"%{tag_name}" è limitato alle seguenti categorie: %{category_names}' synonym: 'I sinonimi non sono ammessi. Utilizza invece "%{tag_name}".' has_synonyms: '"%{tag_name}" non può essere utilizzato perché ha dei sinonimi.' + category_does_not_allow_tags: + one: 'La categoria "%{category}" non consente l''etichetta "%{tags}". Si prega di rimuoverla.' + other: 'La categoria "%{category}" non consente le seguenti etichette: "%{tags}". Si prega di rimuoverle.' required_tags_from_group: one: "Devi includere almeno %{count} etichetta %{tag_group_name}. Le etichette in questo gruppo sono: %{tags}." other: "Devi includere almeno %{count} etichette %{tag_group_name}. Le etichette in questo gruppo sono: %{tags}." @@ -4165,6 +4169,7 @@ it: ignore_error: "Spiacenti, non puoi ignorare questo utente." mute_error: "Spiacenti, non puoi silenziate questo utente." error: "Non si può modificare il livello di notifica per questo utente." + invalid_value: '"%{value}" non è un livello di notifica valido.' discord: not_in_allowed_guild: "Autenticazione non riuscita. Non sei un membro di una gilda Discord consentita." old_keys_reminder: diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index 922ce7ec0d..5cddf38be8 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -366,7 +366,6 @@ ja: too_many: "ブックマークを %{limit} 件以上追加できません。%{user_bookmarks_url}にアクセスして、いくつか削除してください。" cannot_set_past_reminder: "ブックマークリマインダーを過去の時間に設定できません。" cannot_set_reminder_in_distant_future: "ブックマークリマインダーは 10 年以上先の日時に設定できません。" - time_must_be_provided: "すべてのリマインダーには時間の指定が必要です。" reminders: at_desktop: "次にデスクトップに戻ったとき" later_today: "今日の後程" @@ -1394,10 +1393,8 @@ ja: summary_percent_filter: "「このトピックを要約」をクリックしたとき表示される上位投稿の割合%" summary_max_results: "'このトピックを要約' が返す最大投稿数" enable_system_message_replies: "個人メッセージが無効である場合でも、ユーザーによるシステムメッセージへの返信を許可する" - enable_long_polling: "通知用のメッセージバスによるロングポーリングの利用を許可する" enable_chunked_encoding: "サーバーによるチャンク形式エンコーディング応答を有効にする。この機能はほとんどのセットアップで動作しますが、一部のプロキシではバッファリングが生じ、応答が遅延する可能性があります。" long_polling_base_url: "ロングポーリングのベース URL (CDN が動的コンテンツを配信している場合、これを origin pull に指定してください) 例: http://origin.site.com" - long_polling_interval: "送信するデータが存在しない場合に、サーバーがクライアントに応答するまで待機する時間 (ログインユーザーのみ)" polling_interval: "ロングポーリングではないときの、ログイン済みクライアントのポーリング間隔 (ミリ秒)" anon_polling_interval: "匿名クライアントのポーリング間隔 (ミリ秒)" background_polling_interval: "ウィンドウがバックグラウンド時のクライアントのポーリング間隔 (ミリ秒)" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 2ce8c59b5c..2bba1c1442 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -369,7 +369,6 @@ ko: too_many: "죄송합니다. 북마크를 %{limit}개 이상 추가 할 수 없습니다. %{user_bookmarks_url} 을 방문하여 일부를 제거하십시오." cannot_set_past_reminder: "과거에는 북마크 미리 알림을 설정할 수 없습니다." cannot_set_reminder_in_distant_future: "앞으로 10 년 이상 책갈피 알림을 설정할 수 없습니다." - time_must_be_provided: "모든 알림은 시간이 설정되어야합니다." reminders: at_desktop: "다음에 저는 데스크탑에 있습니다" later_today: "오늘 늦게" @@ -1417,10 +1416,8 @@ ko: summary_timeline_button: "타임라인에 '요약' 버튼 표시" enable_personal_messages: "회원레벨1 사용자가 메시지를 작성하고 메시지에 답장할 수 있도록 허용합니다. 관리자는 언제든지 메시지를 보낼 수 있습니다." enable_system_message_replies: "개인 메시지가 비활성화 된 경우에도 사용자가 시스템 메시지에 회신 할 수 있습니다" - enable_long_polling: "Message bus used for notification can use long polling" enable_chunked_encoding: "서버에서 청크 분할 인코딩 응답을 활성화합니다. 이 기능은 대부분의 설정에서 작동하지만 일부 프록시는 버퍼링되어 응답이 지연될 수 있습니다." long_polling_base_url: "long polling에 사용 될 Base URL (CDN이 동적 콘텐트를 제공할 시에는 origin pull로 설정) eg: http://origin.site.com" - long_polling_interval: "보낼 데이터가 없을 때 응답 전에 서버가 기다려야하는 시간 (로그인된 유저 전용)" polling_interval: "long polling을 안 쓸 때 로그인된 클라이언트가 몇 밀리초마다 poll해야 하는 지" anon_polling_interval: "How often should anonymous clients poll in milliseconds" background_polling_interval: "(페이지를 안 보고 있을 때) 클라이언트가 몇 밀리초마다 poll해야 하는 지" diff --git a/config/locales/server.lt.yml b/config/locales/server.lt.yml index 2119fe9522..200e47000e 100644 --- a/config/locales/server.lt.yml +++ b/config/locales/server.lt.yml @@ -323,7 +323,6 @@ lt: already_bookmarked_post: "Negalite du kartus pažymėti to paties įrašo." cannot_set_past_reminder: "Negalite nustatyti žymių priminimo praeityje." cannot_set_reminder_in_distant_future: "Negalite nustatyti žymės priminimo daugiau nei 10 metų ateityje." - time_must_be_provided: "turi būti numatytas laikas visiems priminimams" for_topic_must_use_first_post: "Norėdami pažymėti temą, galite naudoti tik pirmąjį įrašą." reminders: later_today: "Šiandien vėliau" @@ -1283,7 +1282,6 @@ lt: summary_timeline_button: "Rodyti mygtuką “Apibendrinti” laiko juostoje" enable_personal_messages: "Leisti 1 patikimumo lygio (konfigūruojamas naudojant minimalų patikimumą, kad būtų išsiųstas pranešimas) vartotojams kurti pranešimus ir atsakyti į pranešimus. Atminkite, kad darbuotojai visada gali siųsti žinutes." enable_system_message_replies: "Leidžia vartotojams atsakyti į sistemos pranešimus, net jei asmeniniai pranešimai yra išjungti" - long_polling_interval: "Kiek laiko serveris turi palaukti prieš atsakydamas klientams, kai nėra duomenų, kuriuos reikia siųsti (tik prisijungę vartotojai)" polling_interval: "Kai apklausa trunka neilgai, kaip dažnai reikia prisijungti prie klientų apklausos milisekundėmis" hide_post_sensitivity: "Tikimybė, kad pažymėtas įrašas bus paslėptas" silence_new_user_sensitivity: "Tikimybė, kad naujas vartotojas bus nutildytas remiantis šlamšto vėliavomis" diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 08e6399784..eb7c199ff7 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -359,7 +359,6 @@ nl: already_bookmarked_post: "U kunt geen twee bladwijzers voor hetzelfde bericht maken." cannot_set_past_reminder: "U kunt geen bladwijzerherinnering in het verleden instellen." cannot_set_reminder_in_distant_future: "U kunt geen bladwijzerherinnering later dan over 10 jaar instellen." - time_must_be_provided: "tijd moet voor alle herinneringen worden opgegeven" reminders: at_desktop: "De volgende keer vanaf mijn computer" later_today: "Later vandaag" @@ -1350,10 +1349,8 @@ nl: summary_percent_filter: "Wanneer een gebruiker op 'Dit topic samenvatten' klikt, de top % van berichten tonen" summary_max_results: "Maximale aantal weergegeven berichten door 'Dit topic samenvatten'" enable_system_message_replies: "Toestaan dat gebruikers op systeemberichten kunnen antwoorden, zelfs als persoonlijke berichten zijn uitgeschakeld" - enable_long_polling: "Gebruikte 'message bus' voor melding kan 'long polling' gebruiken" enable_chunked_encoding: "Gesegmenteerde coderingsantwoorden van de server inschakelen. Deze functie werkt in de meeste configuraties, hoewel sommige proxy's kunnen bufferen, waardoor reacties worden vertraagd" long_polling_base_url: "Basis-URL voor 'long polling' (als een CDN dynamische inhoud aanbiedt, zorg er dan voor dat dit op 'origin pull' is ingesteld), zoals http://origin.site.com" - long_polling_interval: "De hoeveelheid tijd die de server hoort te wachten voordat op clients wordt gereageerd als er geen gegevens te verzenden zijn (alleen aangemelde gebruikers)" polling_interval: "Als geen 'long polling' wordt toegepast, hoe vaak aangemelde clients moeten pollen in milliseconden" anon_polling_interval: "Hoe vaak anonieme clients moeten pollen in milliseconden" background_polling_interval: "Hoe vaak clients moeten pollen in milliseconden (als het venster in de achtergrond staat)" diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index ca227a8516..b917eab040 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -438,7 +438,6 @@ pl_PL: too_many: "Przepraszamy, nie możesz dodać więcej niż %{limit} zakładek. Odwiedź stronę %{user_bookmarks_url} aby niektóre usunąć." cannot_set_past_reminder: "Nie możesz ustawić przypomnienia o zakładce w przeszłości." cannot_set_reminder_in_distant_future: "Nie możesz ustawić przypomnienia o zakładce na więcej niż 10 lat w przyszłości." - time_must_be_provided: "czas musi zostać podany dla wszystkich przypomnień" for_topic_must_use_first_post: "Możesz użyć tylko pierwszego posta, aby dodać do zakładek temat." reminders: at_desktop: "Następnym razem będę na swoim pulpicie" @@ -1582,10 +1581,8 @@ pl_PL: summary_timeline_button: "Pokaż przycisk „Podsumuj” na osi czasu" enable_personal_messages: "Zezwalaj użytkownikom na poziomie zaufania 1 (konfigurowalnym za pomocą min. zaufania do wysyłania wiadomości) na tworzenie i odpowiadanie na wiadomości. Należy pamiętać, że personel zawsze może wysyłać wiadomości bez względu na wszystko." enable_system_message_replies: "Pozwala użytkownikom odpowiadać na wiadomości systemowe, nawet jeśli wiadomości osobiste są wyłączone" - enable_long_polling: "Message bus used for notification can use long polling" enable_chunked_encoding: "Włącz odpowiedzi fragmentaryczne serwera. Ta funkcja działa w większości konfiguracji, jednak niektóre serwery proxy mogą buforować odpowiedzi, powodując opóźnienia." long_polling_base_url: "Podstawowy URL używany dla long polling (kiedy CDN dostarcza dynamicznych treści, upewnij się że ustawiłeś/łaś to w origin pull) np.: http://origin.site.com" - long_polling_interval: "Okres czasu jaki serwer powinien odczekać przed odpowiedzią klientowi kiedy nie ma danych do przesłania (tylko do zalogowanych użytkowników)" polling_interval: "Kiedy nie long polling, jak często zalogowani klienci powinni poll w milisekundach" anon_polling_interval: "How often should anonymous clients poll in milliseconds" background_polling_interval: "Jak często klienci powinni poll w milisekundach (kiedy okno jest w tle)" @@ -4500,6 +4497,11 @@ pl_PL: other: '"%{tag_name}" jest ograniczony do kategorii "%{category_names}"' synonym: 'Synonimy nie są dozwolone. Zamiast tego użyj "%{tag_name}".' has_synonyms: 'Nie można użyć "%{tag_name}", ponieważ zawiera synonimy.' + restricted_tags_cannot_be_used_in_category: + one: 'Tag "%{tags}" nie może być użyty w kategorii "%{category}". Usuń go.' + few: 'Następujące tagi nie mogą być używane w kategorii "%{category}": %{tags}. Usuń je.' + many: 'Następujące tagi nie mogą być używane w kategorii "%{category}": %{tags}. Usuń je.' + other: 'Następujące tagi nie mogą być używane w kategorii "%{category}": %{tags}. Usuń je.' required_tags_from_group: one: "Musisz dołączyć co najmniej %{count} %{tag_group_name} tag. Tagi w tej grupie to: %{tags}." few: "Musisz dołączyć co najmniej %{count} %{tag_group_name} tagi. Tagi w tej grupie to: %{tags}." diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 91d83897c6..ce2c0a202a 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -376,7 +376,6 @@ pt: too_many: "Desculpe, você não pode adicionar mais de %{limit} favoritos, visite %{user_bookmarks_url} para remover alguns." cannot_set_past_reminder: "Você não pode definir um lembrete de favoritos no passado." cannot_set_reminder_in_distant_future: "Você não pode definir um lembrete de favoritos para mais de 10 anos no futuro." - time_must_be_provided: "tempo deve ser fornecido para todos os lembretes" reminders: at_desktop: "Da próxima vez que eu estiver no meu desktop" later_today: "Hoje, mais tarde" @@ -1010,9 +1009,7 @@ pt: force_https: "Forçar o site a usar apenas HTTPS. ALERTA: NÃO active esta opção enquanto não verificar que o HTTPS está completamente configurado e funcional absolutamente em todo o lado! Verificou a sua CDN, todos os logins por rede social, e quaisquer logos ou outras dependências externas para garantir que também são compatíveis com HTTPS?" summary_score_threshold: "Pontuação mínima necessária para que uma mensagem seja incluída em 'Resumir Este Tópico'" summary_percent_filter: "Quando um utilizador clica em 'Resumir Este Tópico', mostrar as melhores % de mensagens" - enable_long_polling: "O sistema de mensagens usado para notificações pode fazer solicitações longas" long_polling_base_url: "URL base utilizado para sondar o servidor (quando um CDN serve conteúdo dinâmico, certifique-se de definir isto para a \"pull\" original) por exemplo: http://origem.site.com" - long_polling_interval: "O tempo que um servidor deverá aguardar antes de responder aos clientes quando não existirem dados para serem enviados (apenas utilizadores autenticados)" polling_interval: "Quando não está a ocorrer uma solicitação ao servidor, com que frequência devem os clientes ligados requerer uma atualização, em milissegundos" anon_polling_interval: "Com que frequência os clientes anónimos podem sondar o servidor em milisegundos" background_polling_interval: "Com que frequência deverão os clientes solicitar o servidor, em milissegundos (quando a janela está em plano de fundo)" diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index d3af401948..f9cc074e8b 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -394,7 +394,6 @@ pt_BR: too_many: "Desculpe, não é possível adicionar mais de %{limit} favoritos, acesse %{user_bookmarks_url} para remover alguns." cannot_set_past_reminder: "Você não pode definir um lembrete de favorito em uma data passada." cannot_set_reminder_in_distant_future: "Você não pode definir um lembrete de favorito mais de dez anos posteriormente." - time_must_be_provided: "a hora deve ser informada para todos os lembretes" for_topic_must_use_first_post: "Você só pode usar a primeira postagem para adicionar o tópico aos favoritos." reminders: at_desktop: "Da próxima vez que estiver em meu desktop" @@ -1473,10 +1472,8 @@ pt_BR: summary_timeline_button: "Exibir um botão \"Resumir\" na linha do tempo" enable_personal_messages: "Permita que usuários(as) do nível de confiança 1 (configurável por meio do nível mínimo para enviar mensagens) criem e respondam a mensagens. Nove que a equipe sempre pode enviar mensagens." enable_system_message_replies: "Permite que os(as) usuários(as) respondam às mensagens do sistema, mesmo se as mensagens pessoais estiverem desativadas" - enable_long_polling: "O sistema de mensagens das notificações pode fazer sondagens longas." enable_chunked_encoding: "Ative respostas de codificação em bloco no servidor. Esse recurso funciona na maioria das configurações, mas alguns proxies podem ser armazenados em buffer, atrasando as respostas" long_polling_base_url: "URL de base utilizada para \"sondagem longa\" (auando um CDN for configurado, verifique se essa configuração é a padrão). Por exemplo: http://origin.site.com" - long_polling_interval: "Tempo que o servidor deve aguardar antes de responder aos clientes quando não houver dados para enviar (apenas usuários(as) que entraram com a conta)" polling_interval: "Frequência com que clientes devem entrar com a conta, em milisegundos, quando não fizerem sondagem longa" anon_polling_interval: "Frequência com que clientes anônimos(as) podem sondar o servidor em milisegundos" background_polling_interval: "Frequência com que clientes devem sondar o servidor em milisegundos (com a janela em segundo plano)" diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 7c03bd170d..3f831a19d4 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -840,9 +840,7 @@ ro: force_https: "Forțează site-ul să folosească exclusiv HTTPS. ATENȚIE: NU activa această opțiune până nu ai verificat dacă HTTPS este configurat în întregime și funcționează absolut peste tot. Ai verificat, de asemenea, dacă și CND-ul, toate autentificările cu cont pe rețele de socializaer și toate logo-urile și dependențele externe sunt compatibile cu HTTPS?" summary_score_threshold: "Scorul minim necesar pentru ca o postare să fie inclusă în 'Rezumă acest subiect'" summary_percent_filter: "Când un utilizator face click pe 'Rezumatul acestui subiect', arată primele % de postări" - enable_long_polling: "Bus-ul de mesaje folosit pentru notificări poate utiliza long polling." long_polling_base_url: "URL de bază folosit pentru long polling (atunci când un CDN servește conținut dinamic, asigură-te că setezi asta pe origin pull) ex: http://origin.site.com" - long_polling_interval: "Durata cât serverul va trebui să aștepte înainte de a răspunde clienților atunci când nu există date de trimis (exclusiv utilizatori autentificați)" polling_interval: "Atunci când nu se face long polling, cât de des ar trebui clienții autentificați să facă interogări, în milisecunde." anon_polling_interval: "Cât de frecvent să interogheze clienții anonimi, în milisecunde" background_polling_interval: "Cât de des să interogheze clienții, în milisecunde (când fereastra este fundal)" diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index d60961e243..8b18a9200d 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -63,6 +63,7 @@ ru: unrecognized_extension: "Нераспознанное расширение файла: %{extension}" import_error: generic: При импорте темы произошла ошибка + upload: "Ошибка при создании ресурса загрузки: %{name}. %{errors}" about_json: "Ошибка импорта: файл about.json не существует или имеет неверный формат. Вы действительно импортируете тему Discourse?" about_json_values: "Файл about.json содержит недопустимые значения:%{errors}" modifier_values: "модификаторы about.json содержат недопустимые значения: %{errors}" @@ -436,11 +437,13 @@ ru: bookmarks: errors: already_bookmarked_post: "Вы не можете дважды добавить сообщение в закладки" + already_bookmarked: "Вы не можете дважды добавить %{type} в закладки" too_many: "К сожалению, вы не можете добавить больше %{limit} закладок, посетите страницу %{user_bookmarks_url}, чтобы удалить некоторые из них." 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: "Необходимо указать имя и тип записи для добавления в закладки." reminders: at_desktop: "При следующем посещении форума" later_today: "Сегодня, но позже" @@ -1589,10 +1592,8 @@ ru: summary_timeline_button: "Отображать кнопку 'Сводка' рядом со шкалой времени" enable_personal_messages: "Разрешать пользователям уровня доверия 1 (настраивается через минимальный уровень доверия для отправки сообщений) создавать сообщения и отвечать на них. Обратите внимание, что сотрудники всегда могут отправлять сообщения, несмотря на настроенный уровень доверия." enable_system_message_replies: "Позволять пользователям отвечать на системные сообщения, даже если личные сообщения отключены" - enable_long_polling: "Использовать механизм long polling для уведомлений о событиях" enable_chunked_encoding: "Разрешить фрагментацию сообщений (chunked encoding) на сервере. Эта функция работает в большинстве случаев, однако некоторые прокси могут буферизировать контент, вызывая задержку ответов" long_polling_base_url: "Базовый URL, используемый для long polling (при использовании CDN для раздачи динамического контента установите в этом параметре адрес origin pull), например: http://origin.site.com" - long_polling_interval: "Время, которое ожидает сервер до ответа клиентам, когда нет данных для отправки (только для пользователей, вошедших на сайт)" polling_interval: "Если не используется long polling, как часто следует вошедшим клиентам опрашивать сервер, в миллисекундах" anon_polling_interval: "Как часто следует анонимным клиентам опрашивать сервер, в миллисекундах" background_polling_interval: "Как часто следует клиентам опрашивать сервер, в миллисекундах (когда окно находится в фоновом режиме)" @@ -2146,7 +2147,7 @@ ru: default_email_in_reply_to: "Включать по умолчанию выдержку из ответов на сообщения в электронные письма." default_other_new_topic_duration_minutes: "Глобальное условие по умолчанию, по которому темы считаются новыми." default_other_auto_track_topics_after_msecs: "Глобальное время по умолчанию перед автоматическим отслеживанием темы." - default_other_notification_level_when_replying: "Глобальный уровень уведомлений по умолчанию, когда пользователь отвечает на тему." + default_other_notification_level_when_replying: "Глобальный уровень уведомлений по умолчанию при ответе пользователя в теме." default_other_external_links_in_new_tab: "По умолчанию открывать внешние ссылки в новой вкладке." default_other_enable_quoting: "Включить по умолчанию ответ с цитированием для выделенного текста." default_other_enable_defer: "Включить отложенную тему по умолчанию." @@ -4428,6 +4429,16 @@ ru: other: 'Тег "%{tag_name}" может быть использован только в следующих разделах: "%{category_names}"' synonym: 'Синонимы не допускаются. Вместо них используйте тег "%{tag_name}".' has_synonyms: 'Тег "%{tag_name}" не может быть использован, поскольку у него есть синонимы.' + restricted_tags_cannot_be_used_in_category: + one: 'Тег "%{tags}" нельзя использовать в разделе "%{category}". Пожалуйста, удалите его.' + few: 'Следующие теги нельзя использовать в разделе "%{category}": %{tags}. Пожалуйста, удалите их.' + many: 'Следующие теги нельзя использовать в разделе "%{category}": %{tags}. Пожалуйста, удалите их.' + other: 'Следующие теги нельзя использовать в разделе "%{category}": %{tags}. Пожалуйста, удалите их.' + category_does_not_allow_tags: + one: 'В разделе "%{category}" не разрешается использовать тег "%{tags}". Пожалуйста, удалите его.' + few: 'В разделе "%{category}" не разрешается использовать следующие теги: %{tags}. Пожалуйста, удалите их.' + many: 'В разделе "%{category}" не разрешается использовать следующие теги: %{tags}. Пожалуйста, удалите их.' + other: 'В разделе "%{category}" не разрешается использовать следующие теги: %{tags}. Пожалуйста, удалите их.' required_tags_from_group: one: "Вы должны указать как минимум %{count} тег из группы %{tag_group_name}. Группа содержит следующие теги: %{tags}" few: "Вы должны указать как минимум %{count} тега из группы %{tag_group_name}. Группа содержит следующие теги: %{tags}" @@ -4740,6 +4751,7 @@ ru: ignore_error: "Извините, но вы не можете игнорировать этого пользователя." mute_error: "Извините, но вы не можете отключить уведомления у этого пользователя." error: "К сожалению, вы не можете изменить уровень уведомлений для этого пользователя." + invalid_value: 'Значение "%{value}" не является допустимым уровнем уведомления.' discord: not_in_allowed_guild: "Ошибка аутентификации. Вы не являетесь членом разрешённой гильдии Discord." old_keys_reminder: diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 9bb5ea21e3..a188b17a75 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -812,9 +812,7 @@ sk: email_custom_headers: "Zoznam voliteľných emailových hlavičiek oddelených |" summary_score_threshold: "Minimálne požadované skóre príspevku na to aby bol zahrnutý do 'Sumarizuj túto tému'" summary_percent_filter: "Ak používateľ klikne na 'Sumarizuj túto tému', zobraz najlepších % príspevkov" - enable_long_polling: "Zbernica správ pre upozornenia môže používať techniku long-polling" long_polling_base_url: "Base URL used for long polling (when a CDN is serving dynamic content, be sure to set this to origin pull) eg: http://origin.site.com" - long_polling_interval: "Ako dlho má server čakať pred odpovedaním klientom ak nie sú na zaslanie žiadne dáta (iba pre prihlásených používateľov)" polling_interval: "Ak sa nepoužíva technika long-polling, ako často majú byť klienti dotazovaný v milisekundách" anon_polling_interval: "Ako často majú byť dotazovaný anonymný klienti v milisekundách" background_polling_interval: "Ako často majú byť dotazovaný klienti v milisekundách (ak je okno v pozadí)" diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index e5543c1d8a..a600087455 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -638,9 +638,7 @@ sq: email_custom_headers: "A pipe-delimited list of custom email headers" summary_score_threshold: "The minimum score required for a post to be included in 'Summarize This Topic'" summary_percent_filter: "When a user clicks 'Summarize This Topic', show the top % of posts" - enable_long_polling: "Message bus used for notification can use long polling" long_polling_base_url: "Base URL used for long polling (when a CDN is serving dynamic content, be sure to set this to origin pull) eg: http://origin.site.com" - long_polling_interval: "Amount of time the server should wait before responding to clients when there is no data to send (logged on users only)" polling_interval: "When not long polling, how often should logged on clients poll in milliseconds" anon_polling_interval: "How often should anonymous clients poll in milliseconds" background_polling_interval: "How often should the clients poll in milliseconds (when the window is in the background)" diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 023b683014..0b2f40529f 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -63,6 +63,7 @@ sv: unrecognized_extension: "Okänt filnamnstillägg: %{extension}" import_error: generic: Det uppstod ett fel vid import av det temat + upload: "Det uppstod ett fel under skapandet av uppladdningstillgång: %{name}. %{errors}" about_json: "Importfel: about.json finns inte eller är ogiltig. Är du säker på att detta är ett Discourse-tema?" about_json_values: "about.json innehåller ogiltiga värden: %{errors}" modifier_values: "about.json-modifierare innehåller ogiltiga värden: %{errors}" @@ -394,11 +395,13 @@ sv: bookmarks: errors: already_bookmarked_post: "Du kan inte bokmärka samma inlägg två gånger." + already_bookmarked: "Du kan inte bokmärka samma %{type} två gånger." too_many: "Tyvärr kan du inte lägga till fler än %{limit} bokmärken, gå till %{user_bookmarks_url} för att ta bort några." cannot_set_past_reminder: "Du kan inte sätta en påminnelse för bokmärke i det förflutna." cannot_set_reminder_in_distant_future: "Du kan inte sätta en påminnelse för bokmärke mer än 10 år framåt i tiden." - time_must_be_provided: "tid måste anges för alla påminnelser" + time_must_be_provided: "Tid måste anges för alla påminnelser" for_topic_must_use_first_post: "Du kan bara använda det första inlägget för att bokmärka ämnet." + bookmarkable_id_type_required: "Namnet och typen av post som ska bokmärkas krävs." reminders: at_desktop: "Nästa gång jag är vid datorn" later_today: "Senare idag" @@ -1463,10 +1466,8 @@ sv: summary_timeline_button: "Visa en \"Sammanfatta\"-knapp på tidslinjen" enable_personal_messages: "Tillåt användare med förtroendenivå 1 (konfigurera genom minsta förtroendenivå för att skicka meddelanden) att skapa meddelanden och svara på meddelanden. Notera att personalen alltid kan skicka meddelanden oavsett." enable_system_message_replies: "Tillåter användare att svara på systemmeddelanden, även om personliga meddelanden har inaktiverats" - enable_long_polling: "Meddelande-buss som används för avisering kan använda long polling" enable_chunked_encoding: "Aktivera packade kodningssvar (chunked) från servern. Den här funktionen fungerar på de flesta inställningar, men vissa proxyservrar kan buffra, vilket gör att svaren fördröjs" long_polling_base_url: "URL som används för long polling (när en CDN levererar dynamiskt innehåll, kontrollera att det här är inställt till origin pull) se: http://origin.site.com" - long_polling_interval: "Tid som servern bör vänta innan den svarar på klienter när det inte finns någon data att skicka (endast loggad på användare)" polling_interval: "När long polling inte har aktiverats, hur ofta bör inloggade klienter polla i millisekunder" anon_polling_interval: "Hur ofta bör anonyma klienter polla i millisekunder" background_polling_interval: "Hur ofta bör klienter polla i millisekunder (när fönstret är i bakgrunden)" @@ -3993,6 +3994,12 @@ sv: other: '"%{tag_name}" är begränsad till följande kategorier: %{category_names}' synonym: 'Synonymer tillåts inte. Använd "%{tag_name}" istället.' has_synonyms: '"%{tag_name}" kan inte användas eftersom den har synonymer.' + restricted_tags_cannot_be_used_in_category: + one: 'Taggen "%{tags}" kan inte användas i kategorin "%{category}". Ta bort den.' + other: 'Följande taggar kan inte användas i kategorin "%{category}": %{tags}. Ta bort dem.' + category_does_not_allow_tags: + one: 'Kategorin "%{category}" tillåter inte taggen "%{tags}". Ta bort den.' + other: 'Kategorin "%{category}" tillåter inte följande taggar: "%{tags}". Ta bort dem.' required_tags_from_group: one: "Du måste omfatta minst %{count} %{tag_group_name}-tagg. Taggarna i den här gruppen är: %{tags}." other: "Du måste omfatta minst %{count} %{tag_group_name}-taggar. Taggarna i den här gruppen är: %{tags}." @@ -4301,6 +4308,7 @@ sv: ignore_error: "Tyvärr kan du inte ignorera den användaren." mute_error: "Tyvärr kan du inte tysta den användaren." error: "Tyvärr kan du inte ändra aviseringsnivån för den användaren." + invalid_value: '"%{value}" är inte en giltig aviseringsnivå.' discord: not_in_allowed_guild: "Autentisering misslyckades. Du är inte medlem i ett tillåtet Discord-sällskap." old_keys_reminder: diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 63f250b2b1..ad096bd4e4 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -63,6 +63,7 @@ tr_TR: unrecognized_extension: "Tanınmayan dosya uzantısı: %{extension}" import_error: generic: Bu tema içe aktarılırken bir hata oluştu + upload: "Yükleme öğesi oluşturulurken hata oluştu: %{name}. %{errors}" about_json: "İçe Aktarma Hatası: about.json mevcut değil veya geçersiz. Bunun bir Discourse Teması olduğundan emin misiniz?" about_json_values: "about.json geçersiz değerler içeriyor: %{errors}" modifier_values: "about.json değiştiricileri geçersiz değerler içeriyor: %{errors}" @@ -391,11 +392,13 @@ tr_TR: bookmarks: errors: already_bookmarked_post: "Aynı gönderiyi iki kez işaretleyemezsiniz." + already_bookmarked: "Aynı %{type} iki kez işaretlenemez." too_many: "Ne yazık ki %{limit} adetten fazla yer imi ekleyemezsiniz; var olanlardan bazılarını kaldırmak için %{user_bookmarks_url} adresini ziyaret edin." cannot_set_past_reminder: "Geçmişte bir yer imi anımsatıcısı ayarlayamazsınız." cannot_set_reminder_in_distant_future: "10 yıldan uzun bir gelecek için bir yer imi hatırlatıcısı ayarlayamazsınız." - time_must_be_provided: "tüm hatırlatıcılar için zaman belirtilmelidir" + time_must_be_provided: "Tüm hatırlatıcılar için zaman belirtilmelidir" for_topic_must_use_first_post: "Konuyu işaretlemek için yalnızca ilk gönderiyi kullanabilirsiniz." + bookmarkable_id_type_required: "İşaretlenecek kaydın adı ve türü zorunludur." reminders: at_desktop: "Bir dahaki sefere masaüstümdeyim" later_today: "Bugün ilerleyen saatlerde" @@ -1470,10 +1473,8 @@ tr_TR: summary_timeline_button: "Zaman çizelgesinde 'Özetle' düğmesi göster" enable_personal_messages: "Güven düzeyi 1 (ileti göndermek için minimum güven düzeyi ile yapılandırılabilir) kullanıcıların ileti oluşturmasına ve iletileri yanıtlamasına izin verin. Personelin ne olursa olsun her zaman ileti gönderebileceğini unutmayın." enable_system_message_replies: "Kişisel iletiler devre dışı bırakılsa bile kullanıcıların sistem iletilerini yanıtlamasına izin verir" - enable_long_polling: "Bildiri için kullanılan ileti yolu uzun sorgular yapabilir" enable_chunked_encoding: "Sunucu tarafından yığınlanmış kodlama yanıtlarını etkinleştirin. Bu özellik çoğu kurulumda çalışır, ancak bazı proxy'ler arabelleğe alabilir ve yanıtların gecikmesine neden olabilir" long_polling_base_url: "Uzun sorgular için kullanılan baz URL (CDN dinamik içerik sunuyorsa bunu origin olarak ayarladığınıza emin olun) örn: http://origin.site.com." - long_polling_interval: "Gönderilecek bilgi olmadığı zaman sunucunun kullanıcılara geri dönmeden önce beklemesi gereken zaman (sadece giriş yapmış kullanıcın için)" polling_interval: "Uzun sorgular yapılmadığı zaman, kaç mili saniyede bir giriş yapmış kullanıcılar poll yapmalı" anon_polling_interval: "Kaç mili saniyede bir anonim kullanıcılar sorgu yapmalı" background_polling_interval: "(Pencere arkaplanda olduğu zaman) kaç mili saniyede bir kullanıcılan sorgu yapmalı" @@ -4093,6 +4094,7 @@ tr_TR: notification_level: ignore_error: "Üzgünüz, o kullanıcıyı görmezden gelemezsiniz." mute_error: "Üzgünüz, o kullanıcıyı sessize alamazsınız." + invalid_value: '"%{value}" geçerli bir bildirim düzeyi değil.' discord: not_in_allowed_guild: "Kimlik doğrulama başarısız oldu. İzin verilen bir Discord guildinin üyesi değilsiniz." old_keys_reminder: diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index f73c99d208..9de49c1b49 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -438,7 +438,6 @@ uk: too_many: "На жаль, ви не можете додати більше %{limit} закладок, відвідайте %{user_bookmarks_url} щоб видалити деякі з них." cannot_set_past_reminder: "Ви не можете встановити нагадування в минулому." cannot_set_reminder_in_distant_future: "Ви не можете встановити нагадування більше як за 10 років у майбутньому." - time_must_be_provided: "потрібно вказати час для всіх нагадувань" for_topic_must_use_first_post: "Ви можете використати тільки перше повідомлення, щоб додати в закладки тему." reminders: at_desktop: "Наступного разу на мій робочий стіл" @@ -1575,10 +1574,8 @@ uk: summary_timeline_button: "Відображати кнопку «Підсумок» на шкалі часу" enable_personal_messages: "Дозволити користувачам рівня довіри 1 (який можна налаштувати через мінімальний рівень довіри для надсилання повідомлень) створювати повідомлення та відповідати на повідомлення. Зверніть увагу, що співробітники завжди можуть надсилати повідомлення, незалежно від того." enable_system_message_replies: "Дозволяє користувачам відповідати на системні повідомлення, навіть якщо приватні повідомлення відключені" - enable_long_polling: "Використовувати механізм long polling для повідомлень про події" enable_chunked_encoding: "Увімкнути дозвіл на фрагментацію повідомлень (chunked encoding) на сервері. Ця функція працює на більшості випадків, однак деякі проксі-сервери можуть буферизувати контент, що спричиняє затримку відповідей" long_polling_base_url: "Базовий URL, використовуваний для long polling (при використанні CDN для роздачі динамічного контенту встановіть в цьому параметрі адресу origin pull), наприклад: http://origin.site.com" - long_polling_interval: "Час, який очікує сервер до відповіді клієнтам, коли немає даних для відправки (тільки для користувачів, які увійшли на сайт)" polling_interval: "Якщо не використовується long polling, як часто слід авторизованим клієнтам опитувати сервер, в мілісекундах" anon_polling_interval: "Як часто слід анонімним клієнтам опитувати сервер, в мілісекундах" background_polling_interval: "Як часто слід клієнтам опитувати сервер, в мілісекундах (коли вікно знаходиться в фоновому режимі)" diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml index 41e02ee943..79fdce73fd 100644 --- a/config/locales/server.ur.yml +++ b/config/locales/server.ur.yml @@ -16,6 +16,7 @@ ur: date_only: "%B %-d, %Y" long: "%B %-d, %Y, %l:%M%P" no_day: "%B %Y" + calendar_ics: "%Y%m%dT%H%M%SZ" date: month_names: - null @@ -52,24 +53,34 @@ ur: redirect_warning: "ہم تصدیق نہ کر سکے کہ آپ کا منتخب کردہ لِنک اصل میں فورم پر شائع کیا گیا تھا کہ نہیں۔ اگر آپ پھر بھی آگے بڑھنا چاہتے ہیں، تو نیچے دیئے گئے لِنک کو منتخب کریں۔" on_another_topic: "دوسرے ٹاپک پر" inline_oneboxer: + topic_page_title_post_number: "#%{post_number}" topic_page_title_post_number_by_user: "%{username} کی طرف سے #%{post_number}" themes: bad_color_scheme: "تِھیم اَپ ڈَیٹ نہیں ہو سکتی، غلط رنگ پیلیٹ" other_error: "تِھیم اَپ ڈَیٹ کرتے ہوے کچھ غلط ہو گیا" + ember_selector_error: "معذرت – #ember یا .ember-view CSS سلیکٹرز استعمال کرنے کی اجازت نہیں ہے، کیونکہ یہ نام متحرک طور پر رن ٹائم پر بنائے جاتے ہیں اور وقت کے ساتھ ساتھ تبدیل ہوتے جائیں گے، جس کے نتیجے میں CSS ٹوٹ جاتا ہے۔ کوئی مختلف سلیکٹر آزمائیں۔" compile_error: unrecognized_extension: "نامعلوم فائل ایکسٹینشن: %{extension}" import_error: + generic: اس تھیم کو درآمد کرتے وقت ایک خرابی پیش آگئی + about_json: "درآمد کی خرابی: about.json موجود نہیں ہے، یا غلط ہے۔ کیا آپ کو یقین ہے کہ یہ ایک ڈسکورس تھیم ہے؟" about_json_values: "about.json غلط اقدار پر مشتمل ہے: %{errors}" + modifier_values: "about.json موڈیفائر میں غلط اقدار ہیں: %{errors}" git: "گِٹ رِیپَوزِٹَری کلَون کرنے میں خرابی، رسائی کی اجازت نہیں یا رِیپَوزِٹَری موجود نہیں ہے" + git_ref_not_found: "چیک آؤٹ کرنے سے قاصر گِٹ حوالہ: %{ref}" unpack_failed: "فائل کھولنے میں ناکام" + file_too_big: "غیر کمپریسڈ فائل بہت بڑی ہے۔" unknown_file_type: "آپ کی اَپ لوڈ کردہ فائل ایک درست ڈسکورس تِھیم نہیں لگ رہی۔" + not_allowed_theme: "`%{repo}` اجازت یافتہ تھیمز کی فہرست میں نہیں ہے ('allowed_theme_repos' عالمی ترتیب کو چیک کریں)۔" errors: component_no_user_selectable: "تِھیم کے کمپنَینٹ صارف کے قابلِ انتخاب نہیں ہو سکتے" component_no_default: "تِھیم کے کمپنَینٹ ڈیفالٹ تِھیم نہیں ہوسکتے" component_no_color_scheme: "تِھیم کے کمپنَینٹ کے رنگ پیلیٹ نہیں ہوسکتے" no_multilevels_components: "ماتحت تِھیمز والی تِھیمز خود ماتحت تِھیمز نہیں ہو سکتیں" + optimized_link: آپٹمائزڈ تصویری لنکس عارضی ہیں اور تھیم سورس کوڈ میں شامل نہیں ہونے چاہئیں۔ settings_errors: invalid_yaml: "فراہم کردہ YAML غلط ہے۔" + data_type_not_a_number: "`%{name}` قسم کی ترتیب غیر تعاون یافتہ ہے۔ تائید شدہ اقسام ہیں `انٹیجر`، `بول`، `لسٹ`، `اینم` اور `اپ لوڈ`" name_too_long: "بہت طویل نام کے ساتھ ایک ترتیب موجود ہے۔ زیادہ سے زیادہ لمبائی 255 ہے" default_value_missing: "ترتیب `%{name}` کی کوئی ڈِیفالٹ قدر نہیں ہے" default_not_match_type: "ترتیب `%{name}` کی ڈِیفالٹ قدر کی قِسم، ترتیب قِسم سے مَیچ نہیں کرتی۔" @@ -82,7 +93,7 @@ ur: string_value_not_valid: "نئی قدر کی لمبائی اجازت یافتہ رَینج کے اندر نہیں ہے۔" string_value_not_valid_min_max: "اِس کا %{min} اور %{max} حروف لمبائی کے درمیان ہونا لازمی ہے۔" string_value_not_valid_min: "کم از کم %{min} حروف لمبا ہونا لازمی ہے۔" - string_value_not_valid_max: "زیادہ سے زیادہ %{min} حروف لمبا ہونا لازمی ہے۔" + string_value_not_valid_max: "یہ زیادہ سے زیادہ %{max} حروف کا ہونا چاہیے۔" locale_errors: top_level_locale: "ایک لوکیل فائل میں سب سے اوپر کی سطح کی کِی کا نام لوکیل نام سے ملنا چاہیے" invalid_yaml: "ترجمہ YAML غلط" @@ -93,6 +104,10 @@ ur: maximum_staged_user_per_email_reached: "فی اِی میل بنائے گئے سٹَیجڈ صارفین کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔" no_subject: "(کوئی موضوع نہیں)" no_body: "(کوئی مواد نہیں)" + missing_attachment: "(ملحقہ %{filename} غائب ہے)" + continuing_old_discussion: + one: "[%{title}](%{url}) سے بحث کو جاری رکھنا، کیونکہ اسے %{count} دن سے زیادہ پہلے بنایا گیا تھا۔" + other: "[%{title}](%{url}) سے بحث کو جاری رکھنا، کیونکہ اسے %{count} دن سے زیادہ پہلے بنایا گیا تھا۔" errors: empty_email_error: "ہو جاتا ہے جب موصول ہونے والی راء میل خالی ہوتی ہے۔" no_message_id_error: "ہو جاتا ہے جب میل کا 'Message-Id' ہیڈر نہ ہو۔" @@ -102,6 +117,7 @@ ur: from_reply_by_address_error: "ہو جاتا ہے جب ہم فرام ہیڈر جواب بذریعہ ای میل ایڈریس سے ملتا ہے۔" inactive_user_error: "ہو جاتا ہے جب بھیجنے والا سرگرم نہ ہو۔" silenced_user_error: "ہو جاتا ہے جب بھیجنے والا خاموش کیا گیا ہو۔" + bad_destination_address: "تب ہوتا ہے جب To/Cc فیلڈز میں سے کوئی بھی ای میل ایڈریس کنفیگر کردہ آنے والے ای میل ایڈریس سے مماثل نہیں ہوتا ہے۔" strangers_not_allowed_error: "ہو جاتا ہے جب کسی صارف نے ایک ایسے زُمرہ میں نیا ٹاپک بنانے کی کوشش کی ہو جس کا وہ رکن نہ ہوں۔" insufficient_trust_level_error: "ہو جاتا ہے جب کسی صارف نے ایک ایسے زُمرہ میں نیا ٹاپک بنانے کی کوشش کی ہو جس پر اُن کے پاس مطلوبہ ٹرسٹ لَیول نہ ہو۔" reply_user_not_matching_error: "ہو جاتا ہے جب جواب ایک ایسے ای میل ایڈریس سے آیا ہو جو اطلاع بھیجے جانے والے ایڈریس سے مختلف ہو۔" @@ -110,7 +126,10 @@ ur: bounced_email_error: "یہ ای میل ایک لوٹا دیئے جانے والی ای میل کی رپورٹ ہے۔" screened_email_error: "ہو جاتا ہے جب بھیجنے والے کا ای میل ایڈریس پہلے سے ہی سکرین ہوا ہو۔" unsubscribe_not_allowed: "ہو جاتا ہے جب اِس صارف کیلئے ای میل کے ذریعے سبسکرِپشن ختم کرنے کی اجازت نہ ہو۔" + email_not_allowed: "ایسا ہوتا ہے جب ای میل ایڈریس اجازت یافتہ فہرست میں نہ ہو یا بلاک لسٹ میں ہو۔" unrecognized_error: "نامعلوم خرابی" + secure_media_placeholder: "ریڈیکٹڈ: اس سائٹ پر محفوظ میڈیا فعال ہے، موضوع ملاحظہ کریں یا منسلک میڈیا کو دیکھنے کے لیے میڈیا دیکھیں پر کلک کریں۔" + view_redacted_media: "میڈیا دیکھیں" errors: &errors format: ! "%{attribute} %{message}" format_with_full_message: "%{attribute}: %{message}" @@ -121,20 +140,24 @@ ur: accepted: منظور کرنا لازمی ہے blank: خالی نہیں ہو سکتا present: خالی ہونا لازمی ہے - confirmation: ! "%{attribute}کے ساتھ مماثلت نہیں رکھتا " + confirmation: ! "%{attribute}سے مماثل نہیں۔" empty: خالی نہیں ہو سکتا equal_to: '%{count} کے برابر ہونا لازمی ہے' even: غیر طاق ہونا لازمی ہے exclusion: مخصوص ہے - greater_than: '%{count}سے زیادہ ہونا لازمی ہے ' - greater_than_or_equal_to: '%{count}سے زیادہ یا اُس کے برابر ہونا لازمی ہے ' + greater_than: '%{count}سے زیادہ ہونا چاہیے۔' + greater_than_or_equal_to: '%{count}سے بڑا یا اس کے برابر ہونا چاہیے' has_already_been_used: "پہلے سے ہی استعمال کیا جا چکا ہے" inclusion: فہرست میں شامل نہیں ہے invalid: غلط ہے is_invalid: "واضح نہیں لگتا، کیا یہ ایک مکمل جملہ ہے؟" + is_invalid_meaningful: "غیر واضح لگتا ہے، زیادہ تر الفاظ ایک ہی حروف پر مشتمل ہیں؟" + is_invalid_unpretentious: "غیر واضح لگتا ہے، ایک یا زیادہ الفاظ بہت طویل ہیں؟" + is_invalid_quiet: "غیر واضح لگتا ہے، کیا آپ اسے تمام CAPS میں درج کرنا چاہتے تھے؟" + invalid_timezone: "'%{tz}' ایک درست ٹائم زون نہیں ہے" contains_censored_words: "درج ذیل سَینسَر کیے گئے الفاظ پر مشتمل ہے: %{censored_words}" - less_than: '%{count}سے کم ہونا لازمی ہے ' - less_than_or_equal_to: '%{count}سے کم یا اُس کے برابر ہونا لازمی ہے ' + less_than: '%{count}سے کم ہونا چاہیے' + less_than_or_equal_to: '%{count}سے کم یا اس کے برابر ہونا چاہیے' not_a_number: نمبر نہیں ہے not_an_integer: اِنٹیجَر ہونا لازمی ہے odd: طاق ہونا لازمی ہے @@ -154,7 +177,8 @@ ur: wrong_length: one: غلط لمبائی ہے (%{count} حرف لمبا ہونا چاہئے) other: غلط لمبائی ہے (%{count} حروف لمبا ہونا چاہئے) - other_than: "%{count}کے علاوہ ہونا لازمی ہے " + other_than: "%{count}کے علاوہ ہونا چاہیے" + auth_overrides_username: "صارف نام کو توثیق فراہم کرنے والے کی طرف سے اپ ڈیٹ کرنے کی ضرورت ہے، کیونکہ `auth_overrides_username` ترتیب فعال ہے۔" template: body: ! "درج ذیل خانوں کے ساتھ مسائل تھے:" header: @@ -168,36 +192,83 @@ ur: one: "آپ نے غلط انتخاب %{name} کی وضاحت کی ہے" other: "آپ نے غلط انتخابات %{name} کی وضاحت کی ہے" default_categories_already_selected: "آپ کسی دوسری فہرست میں استعمال کردہ زُمرَہ کا انتخاب نہیں کرسکتے۔" + default_tags_already_selected: "آپ کسی دوسری فہرست میں استعمال ہونے والا ٹیگ منتخب نہیں کر سکتے۔" s3_upload_bucket_is_required: "آپ S3 پر اَپ لوڈز فعال نہیں کرسکتے جب تک کہ آپ نے 's3_upload_bucket' فراہم نہیں کیا۔" enable_s3_uploads_is_required: "آپ S3 پر انوینٹری فعال نہیں کرسکتے جب تک کہ آپ نے S3 پر اپ لوڈز کو فعال نہیں کیا ہو۔" + page_publishing_requirements: "اگر محفوظ میڈیا فعال ہو تو صفحہ کی اشاعت کو فعال نہیں کیا جا سکتا۔" s3_backup_requires_s3_settings: "آپ S3 کو بیک اپ کے طور پر استعمال نہیں کرسکتے جب تک کہ آپ نے '%{setting_name}' فراہم نہیں کیا ہو۔" s3_bucket_reused: "آپ 's3_upload_bucket' اور 's3_backup_bucket' کیلئے ایک ہی بَکِّٹ کا استعمال نہیں کرسکتے۔ ایک مختلف بَکِّٹ کا انتخاب کریں یا ہر بَکِّٹ کیلئے ایک مختلف راستہ استعمال کریں۔" + secure_media_requirements: "محفوظ میڈیا کو فعال کرنے سے پہلے S3 اپ لوڈز کا فعال ہونا ضروری ہے۔" + share_quote_facebook_requirements: "فیس بک کے لیے اِقْتِباس شیئرنگ کو فعال کرنے کے لیے آپ کو فیس بک ایپ آئی ڈی سیٹ کرنی ہوگی۔" + second_factor_cannot_enforce_with_socials: "آپ سوشل لاگ ان کے فعال ہونے کے ساتھ 2FA نافذ نہیں کر سکتے۔ آپ کو پہلے لاگ ان کو غیر فعال کرنا ہوگا بذریعہ: %{auth_provider_names}" + second_factor_cannot_be_enforced_with_disabled_local_login: "اگر مقامی لاگ ان غیر فعال ہیں تو آپ 2FA نافذ نہیں کر سکتے۔" + second_factor_cannot_be_enforced_with_discourse_connect_enabled: "اگر DiscourseConnect فعال ہو تو آپ 2FA نافذ نہیں کر سکتے۔" + local_login_cannot_be_disabled_if_second_factor_enforced: "اگر 2FA نافذ ہے تو آپ مقامی لاگ ان کو غیر فعال نہیں کر سکتے۔ مقامی لاگ ان کو غیر فعال کرنے سے پہلے نافذ شدہ 2FA کو غیر فعال کریں۔" + cannot_enable_s3_uploads_when_s3_enabled_globally: "آپ S3 اپ لوڈز کو فعال نہیں کر سکتے کیونکہ S3 اپ لوڈز پہلے سے ہی عالمی سطح پر فعال ہیں، اور اس سائٹ کی سطح کو فعال کرنے سے اپ لوڈز میں اہم مسائل پیدا ہو سکتے ہیں۔" + cors_origins_should_not_have_trailing_slash: "آپ کو ذَیل سلیش (/) کو CORS کےآغاز میں شامل نہیں کرنا چاہیے۔" + slow_down_crawler_user_agent_must_be_at_least_3_characters: "صارف کے ایجنٹوں کو کم از کم 3 حروف کا ہونا ضروری ہے تاکہ انسانی صارفین کو محدود کرنے کی غلط شرح سے بچا جا سکے۔" + slow_down_crawler_user_agent_cannot_be_popular_browsers: "آپ ترتیب میں درج ذیل میں سے کوئی بھی قدر شامل نہیں کر سکتے ہیں: %{values}۔" conflicting_google_user_id: 'اِس اکاؤنٹ کے لئے گُوگَل اکاؤنٹ ID تبدیل ہوگئی ہے؛ سیکیورٹی وجوہات کی بناہ پر اسٹاف کی مداخلت کی ضرورت ہے۔ براہ مہربانی اسٹاف سے رابطہ کریں اور اُنہیں اشارہ کریں
    https://meta.discourse.org/t/76575' + onebox: + invalid_address: "معذرت، ہم اس ویب صفحہ کو پیش کرنے سے قاصر تھے، کیونکہ سرور '%{hostname}' نہیں مل سکا۔ پیش نظارہ کے بجائے، آپ کی پوسٹ میں صرف ایک لنک ظاہر ہوگا۔ :cry:" + error_response: "معذرت، ہم اس ویب صفحہ کوپیش کرنے سے قاصر تھے، کیونکہ ویب سرور نے %{status_code}کا ایک ایرر کوڈ واپس کیا تھا۔ پیش نظارہ کے بجائے، آپ کی پوسٹ میں صرف ایک لنک ظاہر ہوگا۔ :cry:" + missing_data: + one: "معذرت، ہم اس ویب صفحہ کوپیش کرنے سے قاصر تھے، کیونکہ درج ذیل oEmbed/OpenGraph ٹیگ نہیں مل سکا: %{missing_attributes}" + other: "معذرت، ہم اس ویب صفحہ کو پیش کرنے سے قاصر تھے، کیونکہ درج ذیل oEmbed/OpenGraph ٹیگز نہیں مل سکے: %{missing_attributes}" + word_connector: + comma: ", " invite: + expired: "آپ کے دعوتی ٹوکن کی میعاد ختم ہو گئی ہے۔ براہ کرم عملے سے رابطہ کریں۔" not_found: "آپ کا دعوتی ٹوکن غلط ہے۔ براہ کرم سٹاف سے رابطہ کریں۔" not_found_json: "آپ کا دعوتی ٹوکن غلط ہے۔ براہ کرم سٹاف سے رابطہ کریں۔" + not_matching_email: "آپ کا ای میل پتہ اور دعوتی ٹوکن سے وابستہ ای میل ایڈریس مماثل نہیں ہیں۔ براہ کرم عملے سے رابطہ کریں۔" + not_found_template: | +

    آپ کی دعوت %{site_name} کا پہلے ہی فائدہ لیا گیا ہے۔

    + +

    اگر آپ کو اپنا پاس ورڈ یاد ہے تو آپ لاگ ان کر سکتے ہیں۔

    + +

    ورنہ براہ کرم پاس ورڈ ری سیٹ کریں۔

    + not_found_template_link: | +

    %{site_name} پردی گئی اس دعوت کا فائدہ اٹھالیا گیا ہے۔ براہ کرم اس شخص سے پوچھیں جس نے آپ کو مدعو کیا ہے کہ وہ آپ کو ایک نیا دعوت نامہ بھیجے۔

    user_exists: "%{email}کو مدعو کرنے کی کوئی ضرورت نہیں ہے، اُن کا پہلے سے ہی ایک اکاؤنٹ ہے!" + invite_exists: "آپ نے پہلے ہی %{email}کو مدعو کر لیا" + invalid_email: "%{email} ایک درست ای میل پتہ نہیں۔" + rate_limit: + one: "آپ پچھلے 24 گھنٹوں میں پہلے ہی %{count} دعوت نامہ بھیج چکے ہیں، براہ کرم دوبارہ کوشش کرنے سے پہلے %{time_left} انتظار کریں۔" + other: "آپ پچھلے 24 گھنٹوں میں پہلے ہی %{count} دعوت نامے بھیج چکے ہیں، براہ کرم دوبارہ کوشش کرنے سے پہلے %{time_left} انتظار کریں۔" confirm_email: "

    آپ تقریباً کام مکمل کر چکے ہیں! ہم نے آپ کے ایمیل پتہ پر ایک ایکٹیویشن میل بھیج دی ہے۔ براہ مہربانی اپنے اکاؤنٹ کو چالو کرنے کیلئے میل میں دی گئی ہدایات پر عمل کریں۔

    اگر یہ آپ کو موصول نہ ہو، تو اپنا سپَیم فولڈر چیک کریں۔

    " + cant_invite_to_group: "آپ کو صارفین کو مخصوص گروپ (گروپوں) میں مدعو کرنے کی اجازت نہیں ہے۔ اس بات کو یقینی بنائیں کہ آپ اس گروپ (گروپوں)کے مالک ہیں جس کو آپ مدعو کرنے کی کوشش کر رہے ہیں۔" disabled_errors: + discourse_connect_enabled: "دعوت نامے غیر فعال ہیں کیونکہ DiscourseConnect فعال ہے۔" invalid_access: "آپ کو درخواست کردہ ریسورس کو دیکھنے کی اجازت نہیں ہے۔" + requires_groups: "دعوت نامہ محفوظ نہیں کیا گیا تھا کیونکہ مخصوص موضوع ناقابل رسائی ہے۔ درج ذیل گروپس میں سے ایک شامل کریں: %{groups}۔" + domain_not_allowed: "آپ کا ای میل اس دعوت کا فائدہاٹھانےکے لیے استعمال نہیں کیا جا سکتا۔" bulk_invite: file_should_be_csv: "اَپ لوڈ شدہ فائل CSV فارمیٹ میں ہونی چاہئے۔" max_rows: "پہلے %{max_bulk_invites} دعوت نامے بھیجے جا سکتے ہیں۔ فائل کو چھوٹے حصوں میں تقسیم کرنے کی کوشش کریں۔" error: "یہ فائل اَپ لوڈ کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ براہ مہربانی کچھ دیر بعد دوبارہ کوشش کریں۔" + invite_link: + email_taken: "یہ ای میل پہلے سے استعمال میں ہے. اگر آپ کے پاس پہلے سے ہی اکاؤنٹ ہے تو براہ کرم لاگ ان کریں یا پاس ورڈ دوبارہ ترتیب دیں۔" + max_redemptions_limit: "2 اور %{max_limit}کے درمیان ہونا چاہیے۔" topic_invite: failed_to_invite: "صارف کو مندرجہ ذیل گروپوں میں سے کسی ایک کی رکنیت کے بغیر اِس ٹاپک میں مدعو نہیں کیا جاسکتا: %{group_names}۔" user_exists: "معذرت، وہ صارف پہلے ہی مدعو کیا جا چکا ہے۔ آپ صارف کو ایک ٹاپک پر صرف ایک ہی دفعہ مدعو کرسکتے ہیں۔" + muted_topic: "معذرت، اس صارف نے اس موضوع کو خاموش کر دیا۔" + receiver_does_not_allow_pm: "معذرت، وہ صارف آپ کو نجی پیغامات بھیجنے کی اجازت نہیں دیتا۔" + sender_does_not_allow_pm: "معذرت، آپ اس صارف کو آپ کو نجی پیغامات بھیجنے کی اجازت نہیں دیتے۔" + user_cannot_see_topic: "%{username} موضوع نہیں دیکھ سکتا۔" backup: operation_already_running: "ایک کارروائی اِس وقت چل رہی ہے۔ ابھی ایک نیا کام شروع نہیں کیا جا سکتا۔" backup_file_should_be_tar_gz: "بیک اَپ فائل کو .tar.gz آرکائیو ہونا چاہئے۔" not_enough_space_on_disk: "اس بیک اَپ کو اَپ لوڈ کرنے کیلئے ڈِسک میں کافی جگہ نہیں ہے۔" - invalid_filename: " بیک اَپ فائل کے نام میں غلط حروف شامل ہیں۔ درست حروف a-z 0-9 . - _ ہیں۔" + invalid_filename: "بیک اپ فائل کا نام غلط حروف پر مشتمل ہے۔ درست حروف az 0-9 - _. ہیں۔" file_exists: "آپ جو فائل اَپ لوڈ کرنے کی کوشش کر رہے ہیں وہ پہلے سے ہی موجود ہے۔" invalid_params: "آپ نے درخواست پر غلط پیرامیٹرز فراہم کیے ہیں: %{message}" not_logged_in: "آپ کو یہ کرنے کیلئے لاگ اِن ہونا ضروری ہے۔" not_found: "مطلوبہ URL یا ریسورس نہیں مل سکی۔" invalid_access: "آپ کو درخواست کردہ ریسورس کو دیکھنے کی اجازت نہیں ہے۔" authenticator_not_found: "توثیق کا طریقہ موجود نہیں، یا غیر فعال ہے۔" + authenticator_no_connect: "یہ تصدیق فراہم کنندہ کسی موجودہ فورم اکاؤنٹ سے کنکشن کی اجازت نہیں دیتا ہے۔" invalid_api_credentials: "آپ کو درخواست کردہ ریسورس کو دیکھنے کی اجازت نہیں ہے۔ API کا صارف نام یا کِی غلط ہے۔" provider_not_enabled: "آپ کو درخواست کردہ ریسورس کو دیکھنے کی اجازت نہیں ہے۔ تصدیق فراہم کنندہ فعال نہیں ہے۔" provider_not_found: "آپ کو درخواست کردہ ریسورس کو دیکھنے کی اجازت نہیں ہے۔ تصدیق فراہم کنندہ موجود نہیں ہے۔" @@ -206,7 +277,12 @@ ur: email_template_cant_be_modified: "اِس اِیمیل ٹَیمپلیٹ کو ترمیم نہیں کیا جاسکتا" invalid_whisper_access: "یا تو سرگوشیوں کو فعال نہیں کیا گیا ہے یا آپ کو سرگوشی پوسٹس بنانے کی اجازت نہیں ہے" not_in_group: + title_topic: "اس موضوع کو دیکھنے کے لیے آپ کو '%{group}' گروپ کی رکنیت کی درخواست کرنی ہوگی۔" + title_category: "اس زمرے کو دیکھنے کے لیے آپ کو '%{group}' گروپ کی رکنیت کی درخواست کرنی ہوگی۔" + request_membership: "رکنیت کی درخواست" join_group: "گروپ میں شامل ہوں" + deleted_topic: "افوہ! یہ موضوع مٹا دیا گیا ہے اور اب دستیاب نہیں۔" + delete_topic_failed: "اس موضوع کو مٹانے میں ایک خامی تھی۔ براہ کرم سائٹ کے منتظم سے رابطہ کریں۔" reading_time: "پڑھنے کیلئے وقت" likes: "لائیکس" too_many_replies: @@ -226,10 +302,10 @@ ur: configure: "اَیمبَیڈ کرنا ترتیب دیں" more_replies: one: "%{count} مزید جواب" - other: "%{count}مزید جوابات " + other: "%{count}مزید جوابات" loading: "بحث لَوڈ ہو رہی ہے..." permalink: "دائمی لِنک" - imported_from: "%{link}پر اصل اندراج کیلئے یہ ایک ساتھی بحث ٹاپک ہے " + imported_from: "یہ %{link}پر اصل اندراج کے لیے ایک ساتھی بحث کا موضوع ہے" in_reply_to: "◀ %{username}" replies: one: "%{count} جواب" @@ -239,6 +315,7 @@ ur: other: "%{count} لائیکس" last_reply: "آخری جواب" created: "بنایا گیا" + new_topic: "نیا موضوع بنائیں" no_mentions_allowed: "معذرت، آپ دوسرے صارفین کا ذکر نہیں کر سکتے۔" too_many_mentions: one: "معذرت، آپ ایک پوسٹ میں صرف ایک صارف کا ذکر کر سکتے ہیں۔" @@ -247,6 +324,11 @@ ur: too_many_mentions_newuser: one: "معذرت، نئے صارفین ایک پوسٹ میں صرف ایک دوسرے صارف کا ذکر کر سکتے ہیں۔" other: "معذرت، نئے صارفین ایک پوسٹ میں صرف %{count} صارفین کا ذکر کر سکتے ہیں۔" + no_embedded_media_allowed_trust: "معذرت، آپ کسی پوسٹ میں میڈیا آئٹمز کو ضم نہیں کر سکتے۔" + no_embedded_media_allowed: "معذرت، نئے صارفین پوسٹس میں میڈیا آئٹمز کو ضم نہیں کر سکتے۔" + too_many_embedded_media: + one: "معذرت، نئے صارفین پوسٹ میں صرف ایک انضمامی میڈیا آئٹم رکھ سکتے ہیں۔" + other: "معذرت، نئے صارفین پوسٹ میں صرف %{count} انضمامی میڈیا آئٹمز رکھ سکتے ہیں۔" no_attachments_allowed: "معذرت، نئے صارفین پوسٹس میں اَٹَیچمنٹس نہیں ڈال سکتے۔" too_many_attachments: one: "معذرت، نئے صارفین ایک پوسٹ میں صرف ایک اَٹَیچمنٹ ڈال سکتے ہیں۔" @@ -256,6 +338,8 @@ ur: too_many_links: one: "معذرت، نئے صارفین ایک پوسٹ میں صرف ایک لِنک ڈال سکتے ہیں۔" other: "معذرت، نئے صارفین ایک پوسٹ میں صرف %{count} لِنکس ڈال سکتے ہیں۔" + contains_blocked_word: "معذرت، آپ لفظ '%{word}' پوسٹ نہیں کر سکتے۔ اس کی اجازت نہیں ." + contains_blocked_words: "معذرت، آپ اسے پوسٹ نہیں کر سکتے۔ اجازت نہیں: %{words}۔" spamming_host: "معذرت، آپ اس ہَوسٹ کیلئے لِنک پوسٹ نہیں کر سکتے۔" user_is_suspended: "معطل صارفین کو پوسٹ کرنے کی اجازت نہیں ہے۔" topic_not_found: "کچھ غلط ہو گیا ہے۔ شاید جس دوران آپ اِسے دیکھ رہے تھے، یہ ٹاپک بند یا حذف کر دیا گیا؟" @@ -263,6 +347,10 @@ ur: max_pm_recipients: "معذرت، آپ زیادہ سے زیادہ %{recipients_limit} وصول کنندگان کو ایک پیغام بھیج سکتے ہیں۔" pm_reached_recipients_limit: "معذرت، آپ کے ایک پیغام میں %{recipients_limit} سے زیادہ وصول کنندگان نہیں ہوسکے۔" removed_direct_reply_full_quotes: "خود کار طریقے سے پوری پچھلی پوسٹ کا اقتباس ہٹا دیا گیا۔" + watched_words_auto_tag: "خودکار طور پر ٹیگ شدہ موضوع" + secure_upload_not_allowed_in_public_topic: "معذرت، درج ذیل محفوظ اپ لوڈ(ز) کو عوامی موضوع میں استعمال نہیں کیا جا سکتا: %{upload_filenames}۔" + create_pm_on_existing_topic: "معذرت، آپ موجودہ موضوع پر PM نہیں بنا سکتے۔" + slow_mode_enabled: "یہ موضوع سلو موڈ میں ہے۔" just_posted_that: "آپ نے جو حال ہی میں پوسٹ کیا ہے، یہ اُسی کی طرح کا بہت زیادہ ہے" invalid_characters: "غلط حروف شامل ہیں" is_invalid: "واضح نہیں لگتا، کیا یہ ایک مکمل جملہ ہے؟" @@ -276,6 +364,9 @@ ur: rss_num_posts: one: "%{count} پوسٹ" other: "%{count} پوسٹس" + rss_num_participants: + one: "%{count} شریک" + other: "%{count} شرکاء" read_full_topic: "مکمل ٹاپک پڑھیں" private_message_abbrev: "پغم" rss_description: @@ -289,7 +380,7 @@ ur: top_daily: "روزانہ کے ٹاپ ٹاپک" posts: "تازہ ترین پوسٹس" private_posts: "تازہ ترین ذاتی پیغامات" - group_posts: "%{group_name} کی طرف سے تازہ ترین پوسٹس " + group_posts: "%{group_name}سے تازہ ترین پوسٹس" group_mentions: "%{group_name} کی طرف سے تازہ ترین ذکر" user_posts: "@%{username} کی طرف سے تازہ ترین پوسٹس" user_topics: "@%{username} کی طرف سے تازہ ترین ٹاپک" @@ -298,10 +389,19 @@ ur: too_late_to_edit: "یہ پوسٹ بہت دیر پہلے بنائی گئی تھی۔ یہ مزید ترمیم یا حذف نہیں کی جا سکتی۔" edit_conflict: "یہ پوسٹ کسی دوسرے صارف کی طرف سے ترمیم کی گئی تھی اور آپ کی تبدیلیوں کو اب محفوظ نہیں کیا جا سکتا۔" revert_version_same: "موجودہ وَرژن وہی وَرژن ہے جسے آپ واپس لوٹانے کی کوشش کررہے ہیں۔" + cannot_edit_on_slow_mode: "یہ موضوع سلو موڈ میں ہے۔ سوچ سمجھ کر بحث کرنے کی حوصلہ افزائی کرنے کے لیے، اس موضوع میں پرانی پوسٹس میں ترمیم کرنے کی فی الحال سلو موڈ میں اجازت نہیں ہے۔" excerpt_image: "تصویر" bookmarks: + errors: + already_bookmarked_post: "آپ ایک ہی پوسٹ کو دو بار بک مارک نہیں کر سکتے۔" + too_many: "معذرت، آپ %{limit} سے زیادہ بک مارکس شامل نہیں کر سکتے، کچھ کو ہٹانے کے لیے %{user_bookmarks_url} ملاحظہ کریں۔" + cannot_set_past_reminder: "آپ ماضی میں بک مارک کی یاد دہانی سیٹ نہیں کر سکتے۔" + cannot_set_reminder_in_distant_future: "آپ مستقبل میں 10 سال سے زیادہ بک مارک ریمائنڈر سیٹ نہیں کر سکتے۔" + for_topic_must_use_first_post: "آپ موضوع کو بک مارک کرنے کے لیے صرف پہلی پوسٹ کا استعمال کر سکتے ہیں۔" reminders: + at_desktop: "اگلی بار میں اپنے ڈیسک ٹاپ پر ہوں" later_today: "آج بعد میں" + next_business_day: "اگلے کاروباری دن" tomorrow: "کَل" next_week: "اگلے ہفتے" next_month: "اگلے ماہ" @@ -318,11 +418,17 @@ ur: one: "'%{username}' پہلے سے ہی اِس گروپ کا رکن ہے۔" other: "مندرجہ ذیل صارفین پہلے سے ہی اِس گروپ کے ارکان ہیں: %{username}" invalid_domain: "'%{domain}' ایک درست ڈَومَین نہیں ہے۔" - invalid_incoming_email: "'%{domain}' ایک درست ایِ میل ایڈریس نہیں ہے۔" + invalid_incoming_email: "'%{email}' ایک درست ای میل پتہ نہیں۔" email_already_used_in_group: "'%{email}' پہلے ہی گروپ '%{group_name}' کے ذیرِاستعمال ہے۔" email_already_used_in_category: "'%{email}' پہلے ہی زُمرہ '%{category_name}' کے ذیرِاستعمال ہے۔" cant_allow_membership_requests: "آپ ایک ایسا گروپ جس کا کوئی مالک نہ ہو، اُس کیلئے رکنیت کی درخواستوں کی اجازت نہیں دے سکتے۔" already_requested_membership: "آپ نے پہلے سے ہی اِس گروپ کیلئے رکنیت کی درخواست کی ہوئی ہے۔" + adding_too_many_users: + one: "ایک ساتھ زیادہ سے زیادہ %{count} صارف کو شامل کیا جا سکتا ہے" + other: "ایک ساتھ زیادہ سے زیادہ %{count} صارفین کو شامل کیا جا سکتا ہے" + usernames_or_emails_required: "صارف نام یا ای میل موجود ہونا ضروری ہے" + no_invites_with_discourse_connect: "آپ صرف رجسٹرڈ صارفین کو مدعو کر سکتے ہیں جب DiscourseConnect فعال ہو۔" + no_invites_without_local_logins: "مقامی لاگ ان غیر فعال ہونے پر آپ صرف رجسٹرڈ صارفین کو مدعو کر سکتے ہیں" default_names: everyone: "ہر کوئی" admins: "ایڈمن" @@ -335,10 +441,34 @@ ur: trust_level_4: "ٹرسٹ_لَیول_4" request_membership_pm: title: "@%{group_name} کیلئے رکنیت کی درخواست" + request_accepted_pm: + title: "آپ کو @%{group_name}میں قبول کر لیا گیا ہے" + body: | + آپ کی @%{group_name} درج کرنے کی درخواست قبول کر لی گئی ہے اور اب آپ ممبر ہیں۔ education: until_posts: one: "%{count} پوسٹ" other: "%{count} پوسٹس" + "new-topic": | + %{site_name} — میں خوش آمدید **ایک نئی بات چیت شروع کرنے کا شکریہ!** + + - کیا عنوان دلچسپ لگتا ہےاگر آپ اسے بلند آواز سے پڑھتے ہیں؟ کیا یہ ایک اچھا خلاصہ ہے؟ + + - کون اس میں دلچسپی لے گا؟ یہ ضروری کیوں ھے؟ آپ کس قسم کے جوابات چاہتے ہیں؟ + + - اپنے عنوان میں عام طور پر استعمال ہونے والے الفاظ شامل کریں تاکہ دوسرے اسے *تلاش* کرسکیں۔ اپنے موضوع کو متعلقہ عنوانات کے ساتھ گروپ کرنے کے لیے، ایک زمرہ (یا ٹیگ) منتخب کریں۔ + + مزید کے لیے، [ہماری کمیونٹی گائیڈلائنز دیکھیں](%{base_path}/guidelines)۔ یہ پینل صرف آپ کے پہلے %{education_posts_text}کے لیے ظاہر ہوگا۔ + "new-reply": | + %{site_name} — میں خوش آمدید **تعاون کے لیے شکریہ!** + + - اپنے ساتھی کمیونٹی کے اراکین کے ساتھ مہربانی سے پیش آئیں۔ + + - کیا آپ کا جواب گفتگو کو بہتر بناتا ہے؟ + + - تعمیری تنقید خوش آئند ہے، لیکن *خیالات* پر تنقید کریں، لوگوں پر نہیں۔ + + مزید کے لیے، [ہماری کمیونٹی گائیڈلائنز دیکھیں](%{base_path}/guidelines)۔ یہ پینل صرف آپ کے پہلے %{education_posts_text}کے لیے ظاہر ہوگا۔ avatar: | ### آپ کے اکاؤنٹ کیلئے ایک تصویر کیسی لگے گی؟ @@ -355,6 +485,8 @@ ur: متن کو اُجاگر کر کہ ظاہر ہونے والے جواب اقتباس کریں بٹن منتخب کرنے سے ایک اقتباس شامل کرنے کیلئے آپ اپنے پچھلے جواب میں ترمیم کرسکتے ہیں۔ ہر ایک کیلئے اُن ٹاپکس کو پڑھ نا زیادہ آسان ہے جس میں بہت سے چھوٹے، انفرادی جوابات کے مقابلے میں تفصیلی لیکن کم جوابات ہوں۔ + dominating_topic: آپ نے یہاں %{percent}% سے زیادہ جوابات پوسٹ کیے ہیں، کیا کوئی اور ہے جس سے آپ سننا چاہیں گے؟ + get_a_room: آپ نے @%{reply_username} %{count} بار جواب دیا ہے، کیا آپ جانتے ہیں کہ اس کے بجائے آپ انہیں ذاتی پیغام بھیج سکتے ہیں؟ too_many_replies: | ### آپ اس ٹاپک پر جوابات کے نمبر کی حد تک پہنچ گئے ہیں @@ -362,11 +494,11 @@ ur: ایک اور جواب کو شامل کرنے کے بجائے، براہ کرم اپنے پچھلے جوابوں میں ترمیم، یا دیگر ٹاپکس ملاحظہ کرنے کے بارے میں سوچیں۔ reviving_old_topic: | - ### اِس ٹاپک کو بحال کریں؟ + ### اس موضوع کو بحال کریں؟ - اِس ٹاپک پر آخری جواب **** تھا۔ آپ کا جواب ٹاپک کو اپنی فہرست کے سب سے اوپر پہنچا دے گا اور گفتگو میں پہلے ملوث تمام افراد کو مطلع کردے گا۔ + اس موضوع کا آخری جواب **%{time_ago}** تھا۔ آپ کا جواب موضوع کو اس کی فہرست میں سب سے اوپر لے جائے گا اور بات چیت میں پہلے سے شامل کسی کو بھی مطلع کرے گا۔ - کیا آپ واقعی یہ پرانی گفتگو جاری رکھنا چاہتے ہیں؟ + کیا آپ واقعی اس پرانی گفتگو کو جاری رکھنا چاہتے ہیں؟ activerecord: attributes: category: @@ -389,7 +521,10 @@ ur: cant_send_pm: "معذرت، آپ اُس صارف کو ذاتی پیغام نہیں بھیج سکتے۔" no_user_selected: "آپ کا ایک درست صارف منتخب کرنا ضروری ہے۔" reply_by_email_disabled: "ای میل کے ذریعے جواب دینا غیر فعال کردیا گیا ہے۔" + send_to_email_disabled: "معذرت، آپ ای میل پر ذاتی پیغامات نہیں بھیج سکتے۔" target_user_not_found: "جن صارفین کو آپ یہ پیغام بھیج رہے ہیں اُن میں سے ایک نہ مل سکا۔" + unable_to_update: "اس موضوع کو اپ ڈیٹ کرنے میں ایک خامی تھی۔" + unable_to_tag: "موضوع کو ٹیگ کرنے میں ایک خامی تھی۔" featured_link: invalid: "غلط ہے۔ URL میں http:// یا https:// شامل ہونا چاہئے۔" user: @@ -407,6 +542,10 @@ ur: same_as_password: "آپ کے پاسورڈ جیسا ہی ہے۔" ip_address: signup_not_allowed: "اِس اکاؤنٹ سے سائن اَپ کی اجازت نہیں ہے۔" + user_profile: + attributes: + featured_topic_id: + invalid: "یہ موضوع آپ کے پروفائل پر نمایاں نہیں کیا جا سکتا۔" user_email: attributes: user_id: @@ -430,14 +569,19 @@ ur: attributes: execute_at: in_the_past: "مستقبل میں ہونا ضروری ہے۔" + duration_minutes: + cannot_be_zero: "0 سے زیادہ ہونا چاہیے۔" + exceeds_maximum: "20 سال سے زیادہ نہیں ہو سکتا۔" translation_overrides: attributes: value: - invalid_interpolation_keys: 'مندرجہ ذیل اِنٹَرپَولَیشَن کیز غلط ہیں: "%{keys}"' + invalid_interpolation_keys: 'مندرجہ ذیل اِنٹَرپَولَیشَن کی (ز) غلط ہیں: "%{keys}"' watched_word: attributes: word: too_many: "اس کارروائی کیلئے بہت زیادہ الفاظ" + base: + invalid_url: "متبادل URL غلط ہے۔" <<: *errors uncategorized_category_name: "بِلا زُمرہ والے" vip_category_name: "لاؤنج" @@ -466,25 +610,25 @@ ur: title: "لاؤنج میں خوش آمدید" body: |2 - مبارک باد! :confetti_ball: + مبارک ہو! :confetti_ball: - اگر آپ اِس ٹاپک کو دیکھ سکتے ہیں، تو آپ کو حال ہی میں **رَیگولر** (ٹرسٹ لَیول 3) پر ترقی دے دی گئی ہے۔ + اگر آپ اس موضوع کو دیکھ سکتے ہیں، تو آپ کو حال ہی میں **باقاعدہ** (ٹرسٹ لیول 3) پر ترقی دی گئی ہے۔ اب آپ … - * کسی بھی ٹاپک کے عنوان میں ترمیم کر سکتے ہیں - * کسی بھی ٹاپک کا زُمرہ تبدیل کر سکتے ہیں - * اپنے تمام لِنکس فَولَو کروا سکتے ہیں ([خود کار طریقے سےغیر فَولَو](https://en.wikipedia.org/wiki/Nofollow) is removed) ہٹا دیا جاتا ہے) - * ایک ذاتی لاؤنج زُمرہ جو صرف ٹرسٹ لَیول 3 یا اُس سے زیادہ والے صارفین کو نظر آتا ہے تک رسائی کر سکتے ہیں - * ایک ہی فلَیگ سے سپَیم چھپا سکتے ہیں + * کسی بھی عنوان کے عنوان میں ترمیم کرسکتے ہیں + * کسی بھی عنوان کی زمرہ کو تبدیل کرسکتے ہیں + * اپنے تمام لنکس کو فالو کریں ([خودکار nofollow](https://en.wikipedia.org/wiki/Nofollow) ہٹا دیا گیا ہے ) + * ایک پرائیویٹ لاؤنج زمروں تک رسائی حاصل کریں جو صرف ٹرسٹ لیول 3 اور اس سے اوپر والے صارفین کو نظر آتے ہیں + * ایک نشان کے ساتھ اسپام کو چھپائیں - یہاں [ساتھی رَیگولرز کی موجودہ فہرست](%{base_path}/badges/3/regular) ہے۔ ہیلو کہنا نہ بھولیے گا۔ + یہ ہے [ساتھی ریگولر کی موجودہ فہرست](%{base_path}/badges/3/regular)۔ ہیلو ضرور کہنا۔ - اِس کمیونٹی کا ایک اہم حصہ ہونے کا شکریہ! + اس کمیونٹی کا ایک اہم حصہ بننے کا شکریہ! - (ٹرسٹ لَیول پر مزید معلومات کیلئے، [یہ ٹاپک دیکھیے][اعتماد]۔ براہ کرم نوٹ کریں کہ صرف ایسے ممبران جو وقت کے ساتھ شرائط کو پورا کرتے رہیں گے صرف وہی رَیگولرز میں شامل رہیں گے۔) + (اعتماد کی سطحوں کے بارے میں مزید معلومات کے لیے، [یہ موضوع دیکھیں][trust]براہ کرم نوٹ کریں کہ صرف وہی ممبران جو وقت کے ساتھ ساتھ تقاضوں کو پورا کرتے رہتے ہیں ریگولر رہیں گے۔) - [اعتماد]: https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/ + [trust]: https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/ admin_quick_start_title: "مجھے سب سے پہلے پڑھیے: ایڈمن فوری آغاز گائیڈ" category: topic_prefix: "%{category} زُمرہ کے بارے میں" @@ -502,6 +646,7 @@ ur: permission_conflict: "کوئی بھی گروپ جسے کسی ذیلی زمرہ تک رسائی کی اجازت ہے، اُسے بالائی زمرہ تک رسائی بھی حاصل ہونی چائیے۔ مندرجہ زیل گروپس کو کسی ایک ذیلی زمرہ تک رسائی حاصل ہے، لیکن بالائی زمرہ تک رسائی نہیں: %{group_names}۔" disallowed_topic_tags: "اس ٹاپک میں ایسے ٹیگز ہیں جو اِس زمرہ میں ممنوع ہیں: '%{tags}'" disallowed_tags_generic: "اِس ٹاپک پر ممنوع ٹَیگز ہیں۔" + slug_contains_non_ascii_chars: "غیر ascii حروف پر مشتمل ہے" cannot_delete: uncategorized: "یہ زمرہ خاص ہے۔ اِس کا مقصد بغیر زمرہ والے ٹاپکس کیلئے جگہ فراہم کرنا ہے؛ اِسے حذف نہیں کیا جا سکتا۔" has_subcategories: "اِس زُمرہ کو حذف نہیں کیا جا سکتا کیونکہ اِس کے ذیلی زُمرہ ہیں۔" @@ -517,15 +662,31 @@ ur: post: image_placeholder: broken: "یہ تصویر ٹوٹی ہوئی ہے" + hidden_bidi_character: "دو طرفہ حروف اس ترتیب کو تبدیل کر سکتے ہیں جس میں متن پیش کیا جاتا ہے۔ یہ بدنیتی پر مبنی خطرناک کوڈ کو غیر واضح کرنے کے لیے استعمال کیا جا سکتا ہے۔" has_likes: one: "%{count} لائیک" other: "%{count} لائیکس" + cannot_permanently_delete: + many_posts: "آپ اس موضوع کو مستقل طور پر حذف نہیں کر سکتے کیونکہ دوسری پوسٹس ہیں۔" + wait_or_different_admin: "اس پوسٹ کو مستقل طور پر حذف کرنے سے پہلے آپ کو %{time_left} انتظار کرنا ہوگا یا کسی دوسرے منتظم کو یہ کرنا ہوگا۔" rate_limiter: slow_down: "آپ یہ کارروائی بہت بار کر چکے ہیں، براہ کرم بعد میں دوبارہ کوشش کیجیے۔" too_many_requests: "آپ نے یہ کارروائی بہت زیادہ دفعہ کی ہے۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" by_type: + first_day_replies_per_day: "ہم آپ کے جوش و جذبے کی تعریف کرتے ہیں، اسے جاری رکھیں! اس کے بعد، ہماری کمیونٹی کی حفاظت کے لیے، آپ جوابات کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں جو ایک نیا صارف اپنے پہلے دن کر سکتا ہے۔ براہ کرم %{time_left} انتظار کریں اور آپ مزید جوابات تخلیق کر سکیں گے۔" + first_day_topics_per_day: "ہم آپ کے جوش و جذبے کی تعریف کرتے ہیں! اس کے بعد، ہماری کمیونٹی کی حفاظت کے لیے، آپ ان موضوعات کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں جو ایک نیا صارف اپنے پہلے دن کر سکتا ہے۔ براہ کرم %{time_left} انتظار کریں اور آپ مزید نئے عنوانات بنا سکیں گے۔" + create_topic: "آپ عنوانات کو بہت زیادہ جلدی بنا رہے ہیں۔ دوبارہ کوشش کرنے سے پہلے براہ کرم %{time_left} انتظار کریں۔" + create_post: "آپ بہت زیادہ جلدی جواب دے رہے ہیں۔ دوبارہ کوشش کرنے سے پہلے براہ کرم %{time_left} انتظار کریں۔" + delete_post: "آپ پوسٹس کو بہت زیادہ جلدی ڈیلیٹ کر رہے ہیں۔ دوبارہ کوشش کرنے سے پہلے براہ کرم %{time_left} انتظار کریں۔" + public_group_membership: "آپ گروپوں میں شامل ہو رہے ہیں/چھوڑ رہے ہیں۔ دوبارہ کوشش کرنے سے پہلے براہ کرم %{time_left} انتظار کریں۔" + topics_per_day: "آپ فی دن زیادہ سے زیادہ نئے عنوانات تک پہنچ گئے ہیں۔ آپ %{time_left} میں مزید نئے عنوانات بنا سکتے ہیں۔" + pms_per_day: "آپ فی دن پیغامات کی اجازت کی زیادہ سے زیادہ حد تک پہنچ گئے ہیں۔ آپ %{time_left}میں مزید نئے پیغامات بنا سکتے ہیں۔" + create_like: "زبردست! آپ بہت پیار بانٹ رہے ہیں! آپ آج کے لیے زیادہ سے زیادہ یومیہ لائکس تک پہنچ گئے ہیں، لیکن جیسے جیسے آپ اعتماد کی سطح حاصل کرتے جائیں گے، آپ روزانہ مزید لائکس حاصل کریں گے۔ آپ پوسٹس کو دوبارہ %{time_left} میں پسند کر سکیں گے۔" + create_bookmark: "آپ روزانہ بک مارکس کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ آپ %{time_left} میں مزید بک مارکس بنا سکتے ہیں۔" + edit_post: "آپ روزانہ کی ترامیم کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ آپ %{time_left} میں مزید ترامیم جمع کرا سکتے ہیں۔" live_post_counts: "آپ بہت جلدی سے لائیو پوسٹ شمار کیلئے درخواست کر رہے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" unsubscribe_via_email: "آپ ای میل کے ذریعہ رکنیت ختم کرنے کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + topic_invitations_per_day: "آپ موضوع کی دعوتوں کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ آپ %{time_left} میں مزید دعوت نامے بھیج سکتے ہیں۔" hours: one: "%{count} گھنٹا" other: "%{count} گھنٹے" @@ -547,7 +708,7 @@ ur: other: "%{count}سیکنڈ" less_than_x_minutes: one: "< %{count}منٹ " - other: "< %{count}منٹ " + other: "< %{count}m" x_minutes: one: "%{count}منٹ" other: "%{count}منٹ " @@ -586,7 +747,7 @@ ur: other: "%{count} منٹ قبل" about_x_hours: one: "قبل از %{count} گھنٹہ" - other: " %{count} گھنٹے قبل" + other: "%{count} گھنٹے قبل" x_days: one: "قبل از %{count} دن" other: "%{count} دن قبل" @@ -595,7 +756,7 @@ ur: other: "تقریباً %{count} مہینے قبل" x_months: one: "%{count} ماہ قبل" - other: " %{count} مہینے قبل " + other: "%{count} مہینے قبل" about_x_years: one: "تقریباً %{count} سال قبل" other: "تقریباً %{count} سال قبل" @@ -629,6 +790,7 @@ ur: unknown: "نامعلوم براؤزر" device: android: "اینڈرائیڈ ڈیوائس" + chromebook: "کروم OS" ipad: "آئی پَیڈ" iphone: "آئی فون" ipod: "آئی پَوڈ" @@ -639,17 +801,34 @@ ur: unknown: "نامعلوم ڈیوائس" os: android: "اینڈرائیڈ" + chromeos: "کروم OS" ios: "آئی اَو اَیس" linux: "لینکس" macos: "مَیک اَو اَیس" windows: "مائیکروسافٹ وِنڈَوز" unknown: "نامعلوم آپریٹنگ سسٹم" change_email: + wrong_account_error: "آپ غلط اکاؤنٹ میں لاگ ان ہیں، براہ کرم لاگ آؤٹ کریں اور دوبارہ کوشش کریں۔" confirmed: "آپ کا اِی میل اپڈیٹ کر دیا کیا ہے۔" please_continue: "%{site_name} پر جاری رکھیں" error: "آپ کا اِی میل ایڈریس تبدیل کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ شاید یہ ایڈریس پہلے سے استعمال میں ہے؟" + doesnt_exist: "وہ ای میل پتہ آپ کے اکاؤنٹ سے وابستہ نہیں ہے۔" error_staged: "آپ کا اِی میل تبدیل کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ یہ ایڈریس پہلے سے ہی ایک سٹَیجڈ صارف کے استعمال میں ہے۔" already_done: "معذرت، یہ تصدیقی لِنک اب درست نہیں ہے۔ شاید آپ کا اِی میل پہلے ہی بدل چکا تھا؟" + confirm: "تصدیق کریں" + max_secondary_emails_error: "آپ ثانوی ای میلز کی زیادہ سے زیادہ اجازت شدہ حد تک پہنچ گئے ہیں۔" + authorizing_new: + title: "اپنے نئے ای میل کی تصدیق کریں" + description: "براہ کرم تصدیق کریں کہ آپ اپنا ای میل پتہ اس میں تبدیل کرنا چاہتے ہیں:" + description_add: "براہ کرم تصدیق کریں کہ آپ ایک متبادل ای میل پتہ شامل کرنا چاہتے ہیں:" + authorizing_old: + title: "اپنا ای میل ایڈریس تبدیل کریں" + description: "براہ کرم اپنے ای میل ایڈریس کی تبدیلی کی تصدیق کریں" + description_add: "براہ کرم تصدیق کریں کہ آپ ایک متبادل ای میل پتہ شامل کرنا چاہتے ہیں:" + old_email: "پرانا ای میل: %{email}" + new_email: "نیا ای میل: %{email}" + almost_done_title: "نئے ای میل ایڈریس کی تصدیق ہو رہی" + almost_done_description: "تبدیلی کی تصدیق کے لیے ہم نے آپ کے نئے ای میل پتے پر ایک ای میل بھیجی ہے!" associated_accounts: revoke_failed: "%{provider_name} کے ساتھ آپ کے اکاؤنٹ کو منسوخ کرنے میں ناکامی۔" connected: "(کنَیکٹ شدہ)" @@ -664,6 +843,7 @@ ur: activated: "معذرت، یہ اکاؤنٹ پہلے ہی سے چالو کر دیا گیا ہے۔" admin_confirm: title: "توثیق ایڈمن اکاؤنٹ" + description: "کیا آپ واقعی %{target_username} (%{target_email}) کو ایڈمنسٹریٹر بنانا چاہتے ہیں؟" grant: "عطا ایڈمن رسائی" complete: "%{target_username} اب ایک ایڈمِنِسٹریٹر ہے۔" back_to: "%{title} پر واپس جائیں" @@ -692,7 +872,7 @@ ur: email_title: 'آپ کی "%{title}" میں پوسٹ' email_body: "%{link}\n\n%{message}" notify_moderators: - title: " کچھ اور" + title: "اس کے علاوہ کچھ اور" description: "اِس پوسٹ پر اسٹاف کی توجہ درکار ہے جس کی وجہ مندرجہ بالا درج وجوہات میں شامل نہیں ہے۔" short_description: "اسٹاف کی توجہ کسی اور وجہ کی بناہ پر درکار ہے" email_title: '"%{title}" میں ایک پوسٹ پر اسٹاف کی توجہ درکار ہے' @@ -703,8 +883,44 @@ ur: short_description: "اِس پوسٹ کو بُک مارک کریں" like: title: "لائیک" - description: " اِس پوسٹ کو لائیک کریں" - short_description: " اِس پوسٹ کو لائیک کریں" + description: "اس پوسٹ کو پسند کریں" + short_description: "اس پوسٹ کو پسند کریں" + draft: + sequence_conflict_error: + title: "مسودہ کی غلطی" + description: "ڈرافٹ کو دوسری ونڈو میں ایڈٹ کیا جا رہا ہے۔ براہ کرم اِس صفحہ کو دوبارہ لوڈ کریں۔" + draft_backup: + pm_title: "جاری عنوانات سے بیک اپ ڈرافٹ" + pm_body: "بیک اپ ڈرافٹ پر مشتمل موضوع" + user_activity: + no_log_search_queries: "تلاش کے لاگ کے سوالات فی الحال غیر فعال ہیں (ایڈمنسٹریٹر انہیں سائٹ کی ترتیبات میں فعال کر سکتا ہے)۔" + email_settings: + pop3_authentication_error: "فراہم کردہ POP3 شناخت میں ایک مسئلہ تھا، صارف نام اور پاس ورڈ چیک کریں اور دوبارہ کوشش کریں۔" + imap_authentication_error: "فراہم کردہ IMAP شناخت کے ساتھ ایک مسئلہ تھا، صارف نام اور پاس ورڈ چیک کریں اور دوبارہ کوشش کریں۔" + imap_no_response_error: "IMAP سرور کے ساتھ مواصلت کرتے وقت ایک خرابی پیش آگئی۔ %{message}" + smtp_authentication_error: "فراہم کردہ SMTP شناخت میں ایک مسئلہ تھا، صارف نام اور پاس ورڈ چیک کریں اور دوبارہ کوشش کریں۔" + authentication_error_gmail_app_password: 'درخواست کے لیے مخصوص پاس ورڈ درکار ہے۔ اس گوگل ہیلپ آرٹیکل پر مزید جانیں۔' + smtp_server_busy_error: "SMTP سرور فی الحال مصروف ہے، بعد میں دوبارہ کوشش کریں۔" + smtp_unhandled_error: "SMTP سرور کے ساتھ مواصلت کرتے وقت ایک بگڑی ہوئی خرابی تھی۔ %{message}" + imap_unhandled_error: "IMAP سرور کے ساتھ مواصلت کرتے وقت ایک بگڑی ہوئی خرابی تھی۔ %{message}" + connection_error: "سرور سے منسلک ہونے میں ایک خرابی تھی، سرور کا نام اور پورٹ چیک کریں اور دوبارہ کوشش کریں۔" + timeout_error: "سرور سے منسلک ہونے کا وقت ختم ہو گیا، سرور کا نام اور پورٹ چیک کریں اور دوبارہ کوشش کریں۔" + unhandled_error: "ای میل کی ترتیبات کی جانچ کرتے وقت بگڑی ہوئی غلطی۔ %{message}" + webauthn: + validation: + invalid_type_error: "فراہم کردہ webauthn کی قسم غلط تھی۔ درست قسمیں webauthn.get اور webauthn.create ہیں۔" + challenge_mismatch_error: "فراہم کردہ دَعْویٰ تصدیقی سرور کے ذریعہ تیار کردہدَعْویٰ سے مماثل نہیں ہے۔" + invalid_origin_error: "تصدیق کی درخواست کی اصلیت سرور کی اصل سے مماثل نہیں ہے۔" + malformed_attestation_error: "تصدیقی ڈیٹا کو ڈی کوڈ کرنے میں ایک خامی تھی۔" + invalid_relying_party_id_error: "تصدیق کی درخواست کی ریلائینگ پارٹی آئی ڈی سرور ریلائینگ پارٹی آئی ڈی سے مماثل نہیں ہے۔" + user_verification_error: "صارف کی توثیق درکار ہے" + unsupported_public_key_algorithm_error: "فراہم کردہ عوامی چابی الگورتھم سرور کی تائِید یافتہ نہیں ہے۔" + unsupported_attestation_format_error: "تصدیقی فارمیٹ سرور کے ذریعہ تعاون یافتہ نہیں ہے۔" + credential_id_in_use_error: "فراہم کردہ شناختی ID پہلے سے ہی استعمال میں ہے۔" + public_key_error: "اسناد کے لیے عوامی چابی کی تصدیق ناکام ہو گئی۔" + ownership_error: "سیکیورٹی چابی صارف کی ملکیت نہیں۔" + not_found_error: "فراہم کردہ سندی ID کے ساتھ سیکیورٹی چابی نہیں مل سکی۔" + unknown_cose_algorithm_error: "سیکیورٹی چابی کے لیے استعمال ہونے والے الگورتھم کو تسلیم نہیں کیا گیا۔" topic_flag_types: spam: title: "سپَیم" @@ -738,24 +954,30 @@ ur: remove: "یہ ٹاپک اب بینر نہیں ہے۔ یہ اب ہر صفحے کے سب سے اوپر دکھایا نہیں جائے گا۔" unsubscribed: title: "ایمیل ترجیحات اَپ ڈیٹ کر دی گئیں!" + description: "%{email} کے لیے ای میل کی ترجیحات کو اپ ڈیٹ کر دیا گیا تھا۔ اپنی ای میل سیٹنگز کو تبدیل کرنے کے لیے اپنی صارف کی ترجیحاتدیکھیں۔" topic_description: "%{link} پر دوبارہ سَبسکرائب کرنے کیلئے، ٹاپک کے نچلے حصے یا دائیں جانب اطلاعات کنٹرول کا استعمال کریں۔" private_topic_description: "دوبارہ سَبسکرائب کرنے کیلئے، ٹاپک کے نچلے حصے یا دائیں جانب اطلاعات کنٹرول کا استعمال کریں۔" + uploads: + marked_insecure_from_theme_component_reason: "تھیم جزو میں استعمال شدہ اپ لوڈ" unsubscribe: title: "غیر سَبسکرائب" stop_watching_topic: "اس ٹاپک کو دیکھنا چھوڑ دیں، %{link}" mute_topic: "اِس ٹاپک کیلئے تمام اطلاعات کو خاموش کر دیں، %{link}" unwatch_category: "%{category} میں تمام ٹاپکس کو دیکھنا چھوڑ دیں" mailing_list_mode: "مَیلِنگ لِسٹ مَوڈ بند کریں" + all: "مجھے %{sitename}سے کوئی میل نہ بھیجیں" different_user_description: "جس صارف کو ہم نے اِی میل کی، آپ اُس سے مختلف صارف کے طور پر لاگ اِن ہیں۔ براہ مہربانی لاگ آوٹ کریں، یا گمنام مَوڈ میں داخل ہوں، اور دوبارہ کوشش کریں۔" + not_found_description: "معذرت، ہم اس اَن سبسکرائب کو تلاش نہیں کر سکے۔ کیا یہ ممکن ہے کہ آپ کے ای میل کا لنک بہت پرانا ہو اور اس کی میعاد ختم ہو گئی ہو؟" log_out: "لاگ آُوٹ" submit: "ترجیحات محفوظ کریں" digest_frequency: title: "آپ خلاصہ ای میلز وصول کررہے ہیں %{frequency}" + never_title: "آپ کو سمری ای میلز موصول نہیں ہو رہی ہیں" select_title: "خلاصہ ای میلز کی فریکوئنسی مقرر کریں:" - never: "کبھی نہیں " + never: "کبھی نہیں" every_30_minutes: "ہر 30 منٹ" every_hour: "گھنٹہ وار" - daily: "روزانہ " + daily: "روزانہ" weekly: "ہفتہ وار" every_month: "ہر مہینے" every_six_months: "ہر چھ ماہ" @@ -779,8 +1001,9 @@ ur: push: "بیرونی سروِسوں پر اطلاعات بھیجیں" session_info: "صارف سیشن کی معلومات پڑھیں" read: "تمام رِیڈ" - write: " تمام رائیٹ" + write: "سب لکھیں" one_time_password: "ایک بار والا لاگ اِن ٹَوکن تشکیل دیں" + bookmarks_calendar: "بک مارک یاددہانی پڑھیں" invalid_public_key: "معذرت، عوامی کِی غلط ہے۔" invalid_auth_redirect: "معذرت، اِس auth_redirect ہَوسٹ کی اجازت نہیں ہے۔" invalid_token: "غائب، غلط یا میعاد ختم شدہ ٹَوکن۔" @@ -852,7 +1075,14 @@ ur: yaxis: "نئے شراکت داروں کی تعداد" description: "اِس عرصہ میں صارفین کی تعداد جنہوں نے اپنی پہلی پوسٹ شائع کی۔" trust_level_growth: + title: "اعتماد کی سطح میں اضافہ" + xaxis: + tl1_reached: "TL1 تک پہنچ گیا" + tl2_reached: "TL2 تک پہنچ گیا" + tl3_reached: "TL3 تک پہنچ گیا" + tl4_reached: "TL4 تک پہنچ گیا" yaxis: "دن" + description: "صارفین کی تعداد جنہوں نے اس مدت کے دوران اپنے اعتماد کی سطح کو بڑھایا۔" consolidated_page_views: title: "مجموعی صفحہ ملاحظات" xaxis: @@ -1104,25 +1334,38 @@ ur: mutes_count: شمار کو خاموش description: "جو صارفین بہت سے دوسرے صارفین کی طرف سے خاموش اور/یا نظر انداز کیے گئے ہیں۔" top_users_by_likes_received: + title: "پسندیدگی کے لحاظ سے سرفہرست صارفین موصول ہوئے" labels: user: صارف qtt_like: لائیکس موصول ہوے + description: "سب سے اوپر 10 صارفین جنہیں پسندیدگی موصول ہوئی، ہیں۔" top_users_by_likes_received_from_inferior_trust_level: + title: "کم اعتماد کی سطح والے صارف کی طرف سے موصول ہونے والی پسندیدگیوں کے لحاظ سے سرفہرست صارفین" labels: user: صارف + trust_level: اعتماد کی سطح qtt_like: لائیکس موصول ہوے + description: "اعلی ٹرسٹ لیول میں سرفہرست 10 صارفین جنہیں کم اعتماد والے لوگ پسند کرتے ہیں۔" top_users_by_likes_received_from_a_variety_of_people: + title: "مختلف قسم کے لوگوں کی جانب سے موصول ہونے والی پسندیدگی کے لحاظ سے سرفہرست صارفین" labels: user: صارف qtt_like: لائیکس موصول ہوے + description: "سرفہرست 10 صارفین جنہیں لوگوں کی ایک وسیع رینج سے پسندیدگی ملی ہے۔" dashboard: + group_email_credentials_warning: 'گروپ %{group_full_name} کےلیے ای میل شناخت کے ساتھ ایک مسئلہ تھا۔ گروپ ان باکس سے اس وقت تک کوئی ای میل نہیں بھیجے جائیں گے جب تک اس مسئلے کو حل نہیں کیا جاتا۔ %{error}' rails_env_warning: "آپ کا سرور %{env} مَوڈ میں چل رہا ہے۔" host_names_warning: "آپ کی config/database.yml فائل ڈیفالٹ localhost نام استعمال کر رہا ہے۔ اِسے اپنے سائیٹ ہوسٹ کے نام پر اپ ڈیٹ کریں۔" sidekiq_warning: 'Sidekiq نہیں چل رہا۔ بہت سے کام، جیسا کہ ای میل بھیجنا، sidekq کی طرف سے اےسِنکرونسلی مکمل کیے جاتے ہیں۔ براہ کرم یقینی بنائیں کہ کم ازکم ایک sidekq پراسیس چل رہا ہے۔ Sidekiq کے بارے میں یہاں سے جانیے۔' - queue_size_warning: "قطار میں موجود جابز کی تعداد %{queue_size} ہے، جو کہ زیادہ ہے۔ یہ Sidekiq پراسیس کے ساتھ ایک مسئلہ کی نشاندہی کر سکتا ہے، یا آپ کو مزید Sidekiq کارکنوں کو شامل کرنے کی ضرورت ہوسکتی ہے۔" + queue_size_warning: "قطار میں لگی جوبز کی تعداد %{queue_size}ہے، جو زیادہ ہے۔ یہ Sidekiq کے عمل (وں) کے ساتھ کسی مسئلے کی نشاندہی کر سکتا ہے، یا آپ کو Sidekiq کے مزید کارکنوں کو شامل کرنے کی ضرورت پڑ سکتی ہے۔" memory_warning: "آپ کا سرور مجموعی طور پر 1 GB سے کم میموری کے ساتھ چل رہا ہے۔ کم ازکم 1 GB میموری تجویز کی گئی ہے۔" + google_oauth2_config_warning: 'سرور کو گوگل OAuth2 (enable_google_oauth2_logins) کے ساتھ سائن اپ اور لاگ ان کی اجازت دینے کے لیے کنفیگر کیا گیا ہے، لیکن کلائنٹ آئی ڈی اور کلائنٹ کی خفیہ اقدار سیٹ نہیں ہیں۔ سائٹ سیٹنگز پر جائیں اور سیٹنگز کو اپ ڈیٹ کریں۔ مزید جاننے کے لیے یہ گائیڈ دیکھیں۔' + facebook_config_warning: 'سرور کو فیس بک کے ساتھ سائن اپ اور لاگ ان (enable_facebook_logins) کی اجازت دینے کے لیے ترتیب دیا گیا ہے، لیکن ایپ آئی ڈی اور ایپ کی خفیہ اقدار سیٹ نہیں ہیں۔ سائٹ سیٹنگز پر جائیں اور سیٹنگز کو اپ ڈیٹ کریں۔ مزید جاننے کے لیے یہ گائیڈ دیکھیں۔' + twitter_config_warning: 'سرور کو ٹویٹر کے ساتھ سائن اپ اور لاگ ان کرنے کی اجازت دینے کے لیے ترتیب دیا گیا ہے (enable_twitter_logins)، لیکن چابی اور خفیہ اقدار سیٹ نہیں۔ سائٹ سیٹنگز پر جائیں اور سیٹنگز کو اپ ڈیٹ کریں۔ مزید جاننے کے لیے یہ ہِدایَات دیکھیں۔' + github_config_warning: 'سرور کو گٹ ہب (enable_github_logins) کے ساتھ سائن اپ اور لاگ ان کی اجازت دینے کے لیے ترتیب دیا گیا ہے، لیکن کلائنٹ آئی ڈی اور خفیہ اقدار سیٹ نہیں ہیں۔ سائٹ سیٹنگز پر جائیں اور سیٹنگز کو اپ ڈیٹ کریں۔ مزید جاننے کے لیے یہ گائیڈ دیکھیں۔' s3_config_warning: 'سرور کو S3 پر فائلوں کو اَپ لوڈ کرنے کیلئے ترتیب دیا گیا ہے، لیکن کم از کم ایک درج ذیل ترتیب سَیٹ نہیں کی گئی ہے: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_upload_bucket۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے "S3 پر تصویر اپ لوڈز کیسے سَیٹ کریں؟" ملاحظہ کریں۔' s3_backup_config_warning: 'سرور کو S3 پر بیک اَپس اَپ لوڈ کرنے کیلئے ترتیب دیا گیا ہے، لیکن کم از کم ایک درج ذیل ترتیب سَیٹ نہیں کی گئی ہے: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_backup_bucket۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے "S3 پر تصویر اپ لوڈز کیسے سَیٹ کریں؟" ملاحظہ کریں۔' + s3_cdn_warning: 'سرور کو S3 پر فائلیں اپ لوڈ کرنے کے لیے کنفیگر کیا گیا ہے، لیکن کوئی S3 CDN کنفیگر نہیں ہے۔ یہ مہنگے S3 اخراجات اور سائٹ کی سست کارکردگی کا باعث بن سکتا ہے۔ مزید جاننے کے لیے "اپ لوڈز کے لیے آبجیکٹ اسٹوریج کا استعمال" دیکھیں۔' image_magick_warning: 'سرور کو بڑی تصاویر کے تھَمب نَیل تخلیق کرنے کیلئے ترتیب دیا گیا ہے، لیکن ImageMagick اِنسٹال نہیں ہے۔ اپنے پسندیدہ پیکیج مینیجر کا استعمال کرکے ImageMagick اِنسٹال کریں یا تازہ ترین ورژن ڈاؤن لوڈ کریں۔' failing_emails_warning: 'ناکام ہونے والی %{num_failed_jobs} اِی مَیل جابز موجود ہیں۔ اپنا app.yml چیک کریں اور یہ یقینی بنائیں کہ میل سرور کی ترتیبات درست ہیں۔ Sidekiq میں ناکام جابز ملاحظہ کریں۔' subfolder_ends_in_slash: "آپ کا سب-فولڈر سیٹ اپ غلط ہے؛ DISCOURSE_RELATIVE_URL_ROOT ایک سلَیش میں ختم ہوتا ہے۔" @@ -1136,12 +1379,16 @@ ur: force_https_warning: "آپ کی ویب سائٹ SSL استعمال کر رہی ہے۔ لیکن `force_https` ابھی تک آپ کی سائٹ ترتیبات میں فعال نہیں ہے۔" out_of_date_themes: "مندرجہ ذیل تِھیمز کیلئے اَپ ڈیٹس دستیاب ہیں:" unreachable_themes: "ہم مندرجہ ذیل تِھیمز کیلئے اپ ڈیٹس کو چیک نہ کر سکے:" + watched_word_regexp_error: "'%{action}' دیکھے گئے الفاظ کے لیے ریگولر ایکسپریشن غلط ہے۔ براہ کرم اپنی دیکھے ہوئے لفظ کی ترتیباتکو چیک کریں، یا 'دیکھے ہوئے الفاظ کے ریگولر ایکسپریشنز' سائٹ کی ترتیب کو غیر فعال کریں۔" site_settings: + allow_bulk_invite: "CSV فائل اپ لوڈ کرکے کَثْرَتی دعوتوں کی اجازت دیں" disabled: "غیر فعال" + display_local_time_in_user_card: "جب صارف کا کارڈ کھولا جاتا ہے تو اس کے ٹائم زون کی بنیاد پر مقامی وقت دکھائیں۔" censored_words: "الفاظ جو خود بخود ■■■■ سے تبدیل ہوجائیں گے" delete_old_hidden_posts: "کوئی چھپی ہوئی پوسٹ جو 30 دن سے زائد عرصہ سے چھپا رکھی ہو اُس کو خود کار طریقے سے حذف کر دیں۔" default_locale: "اِس ڈِسکورس سَیٹ اپ کی پہلے سے طہ شدہ زبان۔ آپ سسٹم کی طرف سے بنائے گئے زُمرہ جات اور ٹاپکس کے متن کو مرضی کے مطابق / ٹَیکسٹ پر تبدیل کر سکتے ہیں۔" allow_user_locale: "صارفین کو اپنی زبان انٹرفیس ترجیح کو منتخب کرنے کی اجازت دیں" + set_locale_from_accept_language_header: "گمنام صارفین کے لیے ان کے ویب براؤزر کے لینگویج ہیڈر سے انٹرفیس کی زبان سیٹ کریں" support_mixed_text_direction: "مخلوط بائیں-سے-دائیں اور دائیں-سے-بائیں ٹیکسٹ سمتوں کی اجازت دیں۔" min_post_length: "حروف میں پوسٹ کی کم از کم لمبائی" min_first_post_length: "حروف میں پہلی پوسٹ (ٹاپک متن) کی کم از کم لمبائی" @@ -1149,11 +1396,14 @@ ur: max_post_length: "حروف میں پوسٹ کی زیادہ سے زیادہ لمبائی" topic_featured_link_enabled: "ٹاپکس کے ساتھ لِنک پوسٹ کرنا فعال کریں۔" show_topic_featured_link_in_digest: "ڈائجسٹ ای میل میں نمایاں ٹاپک کا لِنک دکھائیں۔" + min_topic_views_for_delete_confirm: "کسی موضوع کے حذف ہونے پر تصدیقی پاپ اپ ظاہر ہونے کے لیے کم از کم ملاحظات کا ہونا ضروری ہے" min_topic_title_length: "حروف میں ٹاپک کے عنوان کی کم از کم لمبائی" max_topic_title_length: "حروف میں ٹاپک کے عنوان کی زیادہ سے زیادہ لمبائی" min_personal_message_title_length: "حروف میں ایک پیغام کیلئے عنوان کی کم از کم لمبائی" max_emojis_in_title: "ٹاپک عنوان میں اِیمَوجیوں کی زیادہ سے زیادہ تعداد" min_search_term_length: "حروف میں درست سرچ ٹَرم کی کم از کم لمبائی" + search_tokenize_chinese: "غیر چینی سائٹوں پر بھی چینی کو ٹوکنائز کرنے کے لیے تلاش پر مجبور کریں" + search_tokenize_japanese: "غیر جاپانی سائٹس پر بھی جاپانیوں کو ٹوکنائز کرنے کے لیے تلاش پر مجبور کریں" search_prefer_recent_posts: "اگر آپ کے بڑے فورم پر سرچ سست ہے، تو یہ آپشن پہلے حالیہ پوسٹس کے ایک اِنڈَیکس پر کوشش کرتا ہے" search_recent_posts_size: "اِنڈَیکس میں کتنی حالیہ پوسٹس رکھی جائیں" log_search_queries: "صارفین کی طرف سے سرچ قُوَیریز کو لاگ کریں" @@ -1164,11 +1414,14 @@ ur: category_search_priority_high_weight: "اعلی زمرہ سرچ ترجیحات کیلئے درجہ بندی پر لاگو وزن۔" allow_uncategorized_topics: "ٹاپکس کو بغیر کسی زُمرہ کے تخلیق ہونے کی اجازت دیں۔ انتباہ: اگر کوئی بِلا زُمرہ ٹاپکس موجود ہوں، تو اِس کو بند کرنے سے پہلے آپ کو اُنہیں دوبارہ کسی زُمرہ میں ڈالنا ہوگا۔" allow_duplicate_topic_titles: "ایک جیسے، نقل عنوانات کے ساتھ ٹاپکس کی اجازت دیں۔" + allow_duplicate_topic_titles_category: "اگر زمرہ مختلف ہے تو ایک جیسے، ڈپلیکیٹ عنوانات کے ساتھ عنوانات کی اجازت دیں۔ allow_duplicate_topic_titles کو غیر فعال کرنا ضروری ہے۔" unique_posts_mins: "ایک ہی مواد والی دوبارہ پوسٹ صارف کتنے منٹ بعد بنا سکتا ہے" educate_until_posts: "جب صارف اپنی پہلی (ن) نئی پوسٹس کو ٹائپ کرنا شروع کرے، تو کمپوزر میں نیا صارف تعلیمی پینل کا پاپ-اپ دکھائیں۔" title: "اس سائٹ کا نام، جیسا کہ عنوان ٹَیگ میں استعمال ہوتا ہے۔" site_description: "اِس سائٹ کو ایک جملہ میں بیان کریں، جیسا کہ مَیٹا وضاحتی ٹَیگ میں استعمال کیا جاتا ہے۔" short_site_description: "مختصر وضاحت، جیسا کہ ہوم پیج پر عنوان ٹَیگ میں استعمال ہوتا ہے۔" + contact_email: "اس سائٹ کے ذمہ دار کلیدی رابطہ کا ای میل پتہ۔ اہم اطلاعات کے لیے استعمال کیا جاتا ہے، اور فوری معاملات کے لیے /متعلق پر بھی دکھایا جاتا ہے۔" + contact_url: "اس سائٹ کے لیے یو آر ایل سے رابطہ کریں۔ فوری معاملات کے لیے /متعلقہ صفحہ پر دکھایا گیا ہے۔" crawl_images: "صحیح چوڑائی اور اونچائی والے طول و عرض داخل کرنے کیلئے ریمَوٹ URL سے تصاویر حاصل کریں۔" download_remote_images_to_local: "ڈاؤن لوڈ کرکے ریمَوٹ تصاویر کو مقامی تصاویر میں تبدیل کریں؛ اِس طرح سے ٹوٹی ہوئی تصاویر کو روکا جا سکتا ہے۔" download_remote_images_threshold: "مقامی طور پر ریمَوٹ تصاویر کو ڈاؤن لَوڈ کرنے کیلئے ڈِسک میں کم از کم جگہ (فیصد میں)" @@ -1178,8 +1431,10 @@ ur: editing_grace_period_max_diff_high_trust: "ترمیم کی رعایتی مدت کے دوران حروف تبدیلیوں کی زیادہ سے زیادہ تعداد، اگر اِس سے زیادہ تبدیل کیے جائیں تو ایک اور پوسٹ رَوِیژن سٹور کریں (ٹرسٹ لَیول 2 اور اوپر)" staff_edit_locks_post: "پوسٹس کو ترمیم کرنے سے روک دیا جائے گا اگر وہ اسٹاف کے اراکین کی طرف سے ترمیم کی گئی ہوں" post_edit_time_limit: "ایک tl0 یا tl1 مصنف اشاعت کے بعد (ن) منٹ تک اپنی پوسٹ میں ترمیم یا اسے خارج کرسکتا ہے۔ ہمیشہ کیلئے 0 پر سَیٹ کریں۔" + tl2_post_edit_time_limit: "ایک tl2+ مصنف پوسٹ کرنے کے بعد (n) منٹ تک اپنی پوسٹ میں ترمیم کر سکتا ہے۔ ہمیشہ کے لیے 0 پر سیٹ کریں۔" edit_history_visible_to_public: "سب کو ایک ترمیم شدہ پوسٹ کے پچھلے ورژن دیکھنے کی اجازت دیں۔ غیر فعال ہونے پر، صرف اسٹاف ممبران دیکھ سکتے ہیں۔" delete_removed_posts_after: "مصنف کی طرف سے ہٹائی گئی پوسٹس خود کار طریقے سے (ن) گھنٹوں کے بعد حذف کردی جائیں گی۔ اگر 0 پر سیٹ ہو، تو پوسٹس کو فوری طور پر حذف کردیا جائے گا۔" + notify_users_after_responses_deleted_on_flagged_post: "جب کسی پوسٹ کو نشان لگایا جاتا ہے اور پھر ہٹا دیا جاتا ہے، تو ان تمام صارفین جنہوں نے پوسٹ کا جواب دیا اور انکے جوابات کو ہٹا دیا تھاکو مطلع کیا جائے گا۔" max_image_width: "پوسٹ میں تصاویر کی زیادہ سے زیادہ تھَمب نَیل چوڑائی" max_image_height: "پوسٹ میں تصاویر کی زیادہ سے زیادہ تھَمب نَیل اونچائی" responsive_post_image_sizes: "مندرجہ ذیل پکسل تناسب کے اعلی ڈی پی آئی اسکرینز کیلئے لائیٹ باکس پیش نظارہ تصاویر کا سائز تبدیل کریں۔ رِسپَونسِو تصاویر غیر فعال کرنے کیلئے تمام اقدار ہٹا دیں۔" @@ -1188,17 +1443,26 @@ ur: add_rel_nofollow_to_user_content: 'اندرونی لِنکس (بالائی ڈومینز سمیت) کے علاوہ، تمام شائع کردہ صارف کے مواد پر رَیل nofollow شامل کریں۔ اگر آپ اس کو تبدیل کرتے ہیں تو، آپ کو تمام پوسٹس کو دوبارہ رِیبَیک کرنا ہوگا: "rake posts:rebake"' exclude_rel_nofollow_domains: "اُن ڈومینز کی فہرست جن کے لنکس پر nofollow کو شامل نہیں کیا جانا چاہئے۔ example.com خود بخود sub.example.com کو بھی اجازت دیدے گا۔ وَیب کرالرز کو تمام مواد تلاش کرنے میں مدد دینے کیلئے آپ کو کم از کم، اِس سائٹ کا ڈومَین شامل کرلینا چاہئے۔ اگر آپ کی ویب سائٹ کے دوسرے حصے دوسرے ڈومینز پر ہیں، تو اُنہیں بھی شامل کر لیں۔" post_excerpt_maxlength: "پوسٹ اقتباس / خلاصہ کی زیادہ سے زیادہ لمبائی۔" + topic_excerpt_maxlength: "کسی موضوع کے اقتباس / خلاصے کی زیادہ سے زیادہ لمبائی، جو کسی موضوع میں پہلی پوسٹ سے تیار کی گئی ہے۔" show_pinned_excerpt_mobile: "موبائل وِیو میں پِن ہوے ٹاپکس پر اقتباس دکھائیں۔" show_pinned_excerpt_desktop: "ڈیسک ٹاپ وِیو میں پِن ہوے ٹاپکس پر اقتباس دکھائیں۔" post_onebox_maxlength: "ایک وَن باکسڈ ڈِسکورس پوسٹ کے حروف کی زیادہ سے زیادہ لمبائی۔" + blocked_onebox_domains: "ڈومینز کی ایک فہرست جو کبھی بھی ون باکس نہیں ہو گی مثلاً wikipedia.org\n(وائلڈ کارڈ کی علامتیں *؟ تعاون یافتہ نہیں)" allowed_inline_onebox_domains: "ڈومینز کی ایک فہرست جو چھوٹے فارم میں وَن باکسڈ کیے جائیں گے اگر وہ عنوان کے بغیر لنک کیے جائیں" + enable_inline_onebox_on_all_domains: "inline_onebox_domain_allowlist سائٹ کی ترتیب کو نظر انداز کریں اور تمام ڈومینز پر ان لائن onebox کی اجازت دیں۔" + force_custom_user_agent_hosts: "وہ میزبان جن کے لیے تمام درخواستوں پر حسب ضرورت ون باکس صارف ایجنٹ استعمال کرنا ہے۔ (خاص طور پر ان میزبانوں کے لیے مفید ہے جو صارف ایجنٹ کی رسائی کو محدود کرتے ہیں)۔" max_oneboxes_per_post: "ایک پوسٹ میں وَن باکس کی زیادہ سے زیادہ تعداد۔" + facebook_app_access_token: "آپ کی فیس بک ایپ آئی ڈی اور سیکریٹ سے تیار کردہ ایک ٹوکن۔ Instagram oneboxes بنانے کے لیے استعمال کیا جاتا ہے۔" logo: "آپ کی سائٹ کے سب سے اوپر بائیں پر لوگو کی تصویر۔ 120 کی اونچائی اور 3:1 سے زائد اَیسپَیکٹ رَیشو والی وسیع مستطیل تصویر کا استعمال کریں۔ اگر خالی چھوڑ دیا گیا ہو تو سائٹ کا عنوان دکھایا جائے گا۔" logo_small: "آپ کی سائٹ کے سب سے اوپر بائیں پر چھوٹی سی لوگو کی تصویر، نیچے سکرول کرنے پر نظر آتی ہے۔ ایک مربع 120 × 120 تصویر کا استعمال کریں۔ اگر خالی چھوڑ دیا گیا ہو تو ایک ہَوم گلِف دکھایا جائے گا۔" digest_logo: "آپ کی سائٹ کے ای میل خلاصہ کے اوپر استعبال ہونے والا متبادل کوگو۔ ایک وسیع مستطیل شکل استعمال کریں۔ ایک SVG تصویر استعمال نہ کریں۔ اگر خالی چھوڑ دیا گیا ہو، تو `لَوگَو` ترتیب والی تصویر کا استعمال کیا جائے گا۔" mobile_logo: "آپکی سائٹ کے موبائل ورژن پر استعمال ہونے والا لَوگَو۔ 120 کی اونچائی اور 3:1 سے زائد اَیسپَیکٹ رَیشو والی وسیع مستطیل تصویر کا استعمال کریں۔ اگر خالی چھوڑ دیا گیا ہو، تو `لَوگَو` ترتیب والی تصویر کا استعمال کیا جائے گا۔" + logo_dark: "'لوگو' سائٹ کی ترتیب کے لیے ڈارک اسکیم کا متبادل۔" + logo_small_dark: "'لوگو سمال' سائٹ ترتیب کے لیے ڈارک اسکیم کا متبادل۔" + mobile_logo_dark: "'موبائل لوگو' سائٹ ترتیب کے لیے ڈارک اسکیم کا متبادل۔" large_icon: "تصویر جو دوسرے مَیٹا ڈَیٹا آئیکن کیلئے بنیاد کے طور پر استعمال کی جاتی ہے۔ مثالی طور پر 512x512 سے بڑی ہونی چاہئیے۔ اگر خالی چھوڑ دیا گیا ہو، تو logo_small کا استعمال کیا جائے گا۔" manifest_icon: "تصویر جو اینڈرائڈ پر لوگو/سپلیش تصویر کے طور پر استعمال ہو۔ خود کار طریقے سے 512 × 512 کے سائیز پر دوبارہ تبدیل کر دی جائے گی۔ اگر خالی چھوڑ دیا گیا ہو، تو large_icon کا استعمال کیا جائے گا۔" + manifest_screenshots: "اسکرین شاٹس جو اس کے انسٹال پرامپٹ صفحہ پر آپ کی مثال کی خصوصیات اور فعالیت کو ظاہر کرتے ہیں۔ تمام تصاویر مقامی اپ لوڈز اور ایک جیسی ہونی چاہئیں۔" favicon: "آپ کی ویب سائٹ کیلئے ایک فَیوِکان، http://en.wikipedia.org/wiki/Favicon دیکھیے۔ ایک CDN پر صحیح کام کرنے کیلئے اِس کا png ہونا ضروری ہے۔ 32x32 کے سائیز پر دوبارہ تبدیل کر دی جائے گی۔ اگر خالی چھوڑ دیا گیا ہو، تو large_icon کا استعمال کیا جائے گا۔" apple_touch_icon: "ایپل ٹچ ڈیوائسوں کیلئے استعمال کیا جانے والا آئکن۔ خود بخود 180x180 کے سائیز پر دوبارہ تبدیل کر دیا جائے گا۔ اگر خالی چھوڑ دیا گیا ہو، تو large_icon کا استعمال کیا جائے گا۔" opengraph_image: "ڈِیفالٹ اَوپَن٘گراف تصویر، استعمال کی جاتی ہے جب صفحہ میں کوئی اور مناسب تصویر نہ ہو۔ اگر خالی چھوڑ دیا گیا ہو، تو large_icon کا استعمال کیا جائے گا۔" @@ -1206,14 +1470,19 @@ ur: notification_email: "تمام ضروری سِسٹم ای میلز بھیجنے کیلئے استعمال ہونے والا from: ای میل ایڈریس۔ یہاں درج کردہ ڈومین کا SPF ،DKIM اور ریورس 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 سے مطابقت رکھتے ہیں؟" + same_site_cookies: "ایک ہی سائٹ کوکیز کا استعمال کریں، وہ تعاون یافتہ براؤزرز (Lax یا Strict) پر تمام کراس سائٹ ریکوئسٹ فورجی ویکٹر کو ختم کر دیتے ہیں۔ انتباہ: Strict صرف ان سائٹس پر کام کرے گا جو لاگ ان کرنے اور بیرونی تصدیق کا طریقہ استعمال کرنے پر مجبور کرتی ہیں۔" summary_score_threshold: "'اِس ٹاپک کا خلاصہ کریں' میں شامل ہونے کیلئے ایک پوسٹ کا کم از کم سکور" + summary_posts_required: "'اس موضوع کا خلاصہ' فعال ہونے سے پہلے کسی موضوع میں کم از کم پوسٹس۔ اس ترتیب میں تبدیلیاں ایک ہفتے کے اندر اندر سابقہ طور پر لاگو ہو جائیں گی۔" + summary_likes_required: "'اس موضوع کا خلاصہ' فعال ہونے سے پہلے کسی موضوع میں کم از کم لائکس۔ اس ترتیب میں تبدیلیاں ایک ہفتے کے اندر اندر سابقہ طور پر لاگو ہو جائیں گی۔" summary_percent_filter: "جب صارف 'اِس ٹاپک کا خلاصہ کریں' پر کلک کرتا ہے، تو سب سے اوپر % پوسٹس دکھائیں" summary_max_results: "'اِس ٹاپک کا خلاصہ کریں' کی طرف سے لوٹائی جانے والی زیادہ سے زیادہ پوسٹس" + summary_timeline_button: "ٹائم لائن میں ایک 'خلاصہ' بٹن دکھائیں" + enable_personal_messages: "ٹرسٹ لَیول 1 (پیغامات بھیجنے کیلئے کم از کم ٹرسٹ لَیول کے ذریعہ ترتیب دے سکتے ہیں) والے صارفین کو پیغامات بنانے اور پیغامات کا جواب دینے کی اجازت دیں۔ نوٹ کریں کہ جوبھی ہو، اسٹاف ہمیشہ پیغامات بھیج سکتا ہے۔" enable_system_message_replies: "صارفین کو سِسٹم پیغامات کا جواب دینے کی اجازت دیں، یہاں تک کہ اگر ذاتی پیغامات بھی غیر فعال ہوں" - enable_long_polling: "نوٹیفکیشن کیلئے مَیسج بس استعمال ہو رہا ہے، لانگ پولِنگ کا استعمال کیا جا سکتا ہے" long_polling_base_url: "لانگ پولِنگ کیلئے استعمال ہونے والا بَیس URL (جب ایک CDN متحرک مواد فراہم کر رہا ہو، تو اِس کو اوریِجِن پُل پر سَیٹ کرنا یقینی بنائیں) مثال: http://origin.site.com" - long_polling_interval: "جب بھیجنے کیلئے کوئی ڈیٹا نہ ہو تو کلائینٹس کوجواب دینے سے پہلے سرور کو کتنا وقت انتظار کرنا چاہئے (صرف لاگ ہوئے صارفین)" polling_interval: "جب لانگ پولِنگ نہ ہو، تو لاگ ہوئے کلائنٹس کو مِلی سیکنڈوں میں کتنی دفعہ پَول کرنا چاہئے" anon_polling_interval: "گمنام کلائنٹس کو مِلی سیکنڈوں میں کتنی دفعہ پَول کرنا چاہئے" background_polling_interval: "کلائنٹس کو مِلی سیکنڈوں میں کتنی دفعہ پَول کرنا چاہئے (جب وِنڈو پسِ منظر میں ہو)" @@ -1277,6 +1546,9 @@ ur: 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: "ورژن اپ ڈیٹ کیلئے ڈِسکورس ہَب کو پِنگ کریں اور /admin ڈیش بورڈ پر نئے ورژن کا پیغام دکھائیں" new_version_emails: "جب ڈِسکورس کا نیا ورژن دستیاب ہو تو contact_email ایڈریس پر ایک ای میل بھیجیں۔" @@ -1286,12 +1558,33 @@ ur: min_username_length: "حروف میں صارف نام کی کم از کم لمبائی۔ انتباہ: اگر کوئی موجودہ صارفین یا گروپوں کے نام اِس سے چھوٹے ہیں، تو آپ کی سائٹ ٹوٹ جائے گی!" max_username_length: "حروف میں صارف نام کی زیادہ سے زیادہ لمبائی۔ انتباہ: اگر کوئی موجودہ صارفین یا گروپوں کے نام اِس سے زیادہ لمبے ہیں، تو آپ کی سائٹ ٹوٹ جائے گی!" unicode_usernames: "صارف اور گروپ ناموں میں یونیکَوڈ حروف اور نمبر شامل کرنے کی اجازت دیں۔" + allowed_unicode_username_characters: "صارف ناموں کے اندر صرف کچھ یکتا حروف کی اجازت دینے کے لیے باقاعدہ اظہار۔ ASCII حروف اور نمبروں کو ہمیشہ اجازت دی جائے گی اور اجازت کی فہرست میں شامل کرنے کی ضرورت نہیں ہے۔" reserved_usernames: "صارف نام جن کیلئے سائن اَپ کی اجازت نہیں ہے۔ وائلڈ کارڈ علامت * کسی بھی حرف کو صفر یا اِس سے زیادہ بار میچ کرنے کیلئے استعمال کیا جا سکتا ہے۔" min_password_length: "پاسورڈ کی کم از کم لمبائی۔" min_admin_password_length: "اَیڈمن کیلئے پاسورڈ کی کم از کم لمبائی۔" password_unique_characters: "منفرد حروف کی کم از کم تعداد جو پاسورڈ میں ہونا لاذمی ہے۔" block_common_passwords: "ایسا پاسورڈ جو 10،000 سب سے زیادہ عام پاسورڈز میںشامل ہو، اۃسے رکھنے کی اجازت نہ دیں۔" + auth_skip_create_confirm: بیرونی تصدیق کے ذریعے سائن اپ کرتے وقت، اکاؤنٹ بنائیں پاپ اپ کو چھوڑ دیں۔ auth_overrides_email، auth_overrides_username اور auth_overrides_name کے ساتھ بہترین استعمال کیا جاتا ہے۔ + auth_immediately: "صارف کے تعامل کے بغیر خودبخود بیرونی لاگ ان سسٹم پر ری ڈائریکٹ کریں۔ یہ تب ہی اثر انداز ہوتا ہے جب login_required درست ہو، اور صرف ایک بیرونی تصدیق کا طریقہ ہو" + enable_discourse_connect: "DiscourseConnect (سابقہ 'Discourse SSO') کے ذریعے سائن آن کو فعال کریں (انتباہ: صارفین کے ای میل ایڈریس *ضروری ہے* کی توثیق بیرونی سائٹ سے کی جائے!)" + verbose_discourse_connect_logging: "لاگ وربوز DiscourseConnect متعلقہ تشخیص کو /لاگزسے منسلک کریں۔" + enable_discourse_connect_provider: "/session/sso_provider اینڈ پوائنٹ پر DiscourseConnect (پہلے 'Discourse SSO') فراہم کنندہ پروٹوکول کو نافذ کریں، discourse_connect_provider_secrets کو سیٹ کرنے کی ضرورت ہے۔" + discourse_connect_url: "DiscourseConnect اینڈ پوائنٹ کا URL (اس میں http:// یا https:// شامل ہونا چاہیے)" + discourse_connect_secret: "خفیہ سٹرنگ کو خفیہ طور پر DiscourseConnect کی معلومات کی توثیق کرنے کے لیے استعمال کیا جاتا ہے، یقینی بنائیں کہ یہ 10 حروف یا اس سے زیادہ ہے۔" + discourse_connect_provider_secrets: "ڈومین خفیہ جوڑوں کی فہرست جو DiscourseConnect استعمال کر رہی ہے۔ یقینی بنائیں کہ DiscourseConnect سیکرٹ ۱۰ حروف یا اس سے زیادہ ہے۔ وائلڈ کارڈ کی علامت * کسی بھی ڈومین یا اس کے صرف ایک حصے سے ملنے کے لیے استعمال کیا جا سکتا ہے (جیسے *.example.com)۔" discourse_connect_overrides_bio: "صارف پروفائل میں صارف کی بائیو کی جگہ لے لیتا ہے اور اِس کو تبدیل کرنے سے صارف کو روک دیتا ہے" + discourse_connect_overrides_groups: "تمام دستی گروپ کی رکنیت کو گروپوں کے انتساب میں بیان کردہ گروپوں کے ساتھ ہم آہنگ کریں (انتباہ: اگر آپ گروپس کی وضاحت نہیں کرتے ہیں تو تمام دستی گروپ کی رکنیت صارف کے لیے صاف کر دی جائے گی)" + auth_overrides_email: "ہر لاگ ان پر بیرونی سائٹ ای میل کے ساتھ مقامی ای میل کو اوور رائیڈ کرتا ہے، اور مقامی تبدیلیوں کو روکتا ہے۔ تمام تصدیقی فراہم کنندگان پر لاگو ہوتا ہے۔ (انتباہ: مقامی ای میلز کو معمول پر لانے کی وجہ سے تضادات ہو سکتے ہیں)" + auth_overrides_username: "ہر لاگ ان پر بیرونی سائٹ کے صارف نام کے ساتھ مقامی صارف نام کو اوور رائیڈ کرتا ہے، اور مقامی تبدیلیوں کو روکتا ہے۔ تمام تصدیقی فراہم کنندگان پر لاگو ہوتا ہے۔ (انتباہ: صارف نام کی لمبائی/ضروریات میں فرق کی وجہ سے تضادات ہو سکتے ہیں)" + auth_overrides_name: "ہر لاگ ان پر بیرونی سائٹ کے مکمل نام کے ساتھ مقامی مکمل نام کو اوور رائیڈ کرتا ہے، اور مقامی تبدیلیوں کو روکتا ہے۔ تمام تصدیقی فراہم کنندگان پر لاگو ہوتا ہے۔" + discourse_connect_overrides_avatar: "DiscourseConnect پے لوڈ سے قدر کے ساتھ صارف کے اوتار کو اوور رائیڈ کرتا ہے۔ فعال ہونے پر، صارفین کو ڈسکورس پر اوتار اپ لوڈ کرنے کی اجازت نہیں ہوگی۔" + discourse_connect_overrides_location: "DiscourseConnect پے لوڈ سے قدر کے ساتھ صارف کے مقام کو اوور رائیڈ کرتا ہے اور مقامی تبدیلیوں کو روکتا ہے۔" + discourse_connect_overrides_website: "DiscourseConnect پے لوڈ سے قدر کے ساتھ صارف کی ویب سائٹ کو اوور رائیڈ کرتا ہے اور مقامی تبدیلیوں کو روکتا ہے۔" + discourse_connect_overrides_profile_background: "DiscourseConnect پے لوڈ سے قدر کے ساتھ صارف کے پروفائل کے پس منظر کو اوور رائیڈ کرتا ہے۔" + discourse_connect_overrides_card_background: "DiscourseConnect پے لوڈ سے قدر کے ساتھ صارف کارڈ کے پس منظر کو اوور رائیڈ کرتا ہے۔" + discourse_connect_not_approved_url: "غیر منظور شدہ DiscourseConnect اکاؤنٹس کو اس URL پر ری ڈائریکٹ کریں۔" + discourse_connect_allows_all_return_paths: "DiscourseConnect کی طرف سے فراہم کردہ return_paths کے لیے ڈومین کو محدود نہ کریں (بطور ڈیفالٹ واپسی کا راستہ موجودہ سائٹ پر ہونا چاہیے)" + enable_local_logins: "مقامی صارف نام اور پاس ورڈ لاگ ان پر مبنی اکاؤنٹس کو فعال کریں۔ انتباہ: اگر غیر فعال ہو تو، آپ لاگ ان کرنے سے قاصر ہو سکتے ہیں اگر آپ نے پہلے سے کم از کم ایک متبادل لاگ ان طریقہ ترتیب نہیں دیا ہے۔" enable_local_logins_via_email: "صارفین کو بذریعہ ای میل بھیجے جانے والے ایک کلِک لاگ اِن لِنک کی درخواست کرنے کی اجازت دیں۔" allow_new_registrations: "نئے صارف رجسٹریشنوں کی اجازت دیں۔ کسی کو نیا اکاؤنٹ بنانے سے روکنے کیلئے اس کو غیر چیک شدہ کریں۔" enable_signup_cta: "واپس آنے والے گمنام صارفین کو ایک نوٹس دکھائیں جو اُنہیں اکاؤنٹ بنانے کیلۓ قائل کرے۔" @@ -1300,21 +1593,29 @@ ur: google_oauth2_client_secret: "آپ کی گُوگل ایپلی کیشن کا کلائنٹ سیکرٹ." google_oauth2_prompt: "خالی جگہ سے علیحدہ کردہ سٹرنگ وَیلِیوز کی ایک اختیاری فہرست جو اِس بات کی وضاحت کرتی ہے کہ اَوتھرائزیشن سرور صارف کو دوبارہ تصدیق اور رضامندی کیلئے پوچھتا ہے کہ نہیں۔ ممکنہ اقدار کیلئے https://developers.google.com/identity/protocols/OpenIDConnect#prompt دیکھیے۔" google_oauth2_hd: "ایک اختیاری گُوگل اَیپس ہوسٹِڈ ڈومَین جس تک کہ سائن اِن محدود ہو گا۔ مزید تفصیلات کیلئے https://developers.google.com/identity/protocols/OpenIDConnect#hd-param ملاحظہ کریں۔" + google_oauth2_hd_groups: "(تجرباتی) توثیق پر میزبان ڈومین پر صارفین کے گوگل گروپس کو بازیافت کریں۔ بازیافت شدہ گوگل گروپس کو خودکار ڈسکورس گروپ ممبرشپ دینے کے لیے استعمال کیا جا سکتا ہے (گروپ سیٹنگز دیکھیں)۔" enable_twitter_logins: "ٹویٹر توثیق فعال کریں۔ twitter_consumer_key اور twitter_consumer_secret ضروری ہیں۔ دیکھیے ڈسکورس کیلئے ٹویٹر لاگ اِن ترتیب دیں (اور رِچ اَیمبَیڈ)۔" twitter_consumer_key: "ٹویٹر توثیق کیلئے کنزِیُومر کِی، https://developer.twitter.com/app پر رجسٹر کردہ" twitter_consumer_secret: "ٹویٹر توثیق کیلئے کنزِیُومر سیکرٹ، https://developer.twitter.com/app پر رجسٹر کردہ" enable_facebook_logins: "فَیسبُک توثیق فعال کریں۔ facebook_app_id اور facebook_app_secret ضروری ہیں۔ دیکھیے ڈسکورس کیلئے فَیسبُک لاگ اِن ترتیب دیں۔" + facebook_app_id: "فیس بک کی تصدیق اور اشتراک کے لیے ایپ آئی ڈی، https://developers.facebook.com/appsپر رجسٹرڈ" facebook_app_secret: "فَیسبُک توثیق کیلئے اَیپ سیکرٹ، https://developers.facebook.com/apps پر رجسٹر کردہ" + enable_github_logins: "گِٹ ہب کی توثیق کو فعال کریں، github_client_id اور github_client_secret کی ضرورت ہے۔ ڈسکورس کے لیے گِٹ ہب لاگ ان کو ترتیب دینا دیکھیں۔" + github_client_id: "گِت ہب تصدیق کے لیے کلائنٹ آئی ڈی، https://github.com/settings/developersپر رجسٹرڈ" + github_client_secret: "GitHub تصدیق کے لیے کلائنٹ کا راز، https://github.com/settings/developersپر رجسٹرڈ" enable_discord_logins: "صارفین کو بذریعہ ڈِسکَورڈ تصدیق کی اجازت دیں؟" discord_client_id: 'ڈِسکَورڈ کلائنٹ ID (ایک کی ضرورت ہے؟ ڈِسکَورڈ ڈَوَیلپر پَورٹل ملاحظہ کریں)' discord_secret: "ڈِسکَورڈ سیکرٹ کی۔" + discord_trusted_guilds: 'صرف ان Discord گلڈز کے اراکین کو Discord کے ذریعے لاگ ان ہونے دیں۔ گلڈ کے لیے عددی ID استعمال کریں۔ مزید معلومات کے لیے، ہدایات یہاںدیکھیں۔ کسی بھی جماعت کو اجازت دینے کے لیے خالی چھوڑ دیں۔' enable_backups: "ایڈمِنِسٹریٹروں کو فورم کے بیک اَپ بنانے کی اجازت دیں" + allow_restore: "بحالی کی اجازت دیں، جو سائٹ کے تمام ڈیٹا کو بدل سکتا ہے! غیر فعال رہنے دیں جب تک کہ آپ بیک اپ بحال کرنے کا ارادہ نہیں رکھتے" maximum_backups: "ڈِسک پر رکھے جانے بیک اَپس کی زیادہ سے زیادہ تعداد۔ پرانے بیک اَپس خود کار طریقے سے حذف کر دیے جاتے ہیں" automatic_backups_enabled: "خودکار بیک اَپس چلائیں جیسا کہ بیک اَپ فریکوئنسی میں بیان کیا گیا ہے" backup_frequency: "بَیک اَپس کے درمیان دنوں کی تعداد۔" s3_backup_bucket: "بیک اَپس رکھنے کیلئے ریمَوٹ بَکِّٹ۔ انتباہ: یقینی بنائیں کہ یہ ایک زاتی بَکِّٹ ہے۔" s3_endpoint: "اَینڈ پوائنٹ کو S3 سے مطابقت رکھنے والی سروس جیسے کہ ڈیجیٹل اَوشن سپَیسز یا مینیو پر بیک اَپ کرنے کیلئے ترمیم کیا جا سکتا ہے۔ انتباہ: اگر AWS S3 کا استعمال کر رہے ہوں تو خالی چھوڑ دیں۔" s3_configure_tombstone_policy: "سنگ مزار اپ لوڈوں کیلئے خود کار طریقے سے حذف کر دینے کی پالیسی فعال کریں۔ اہم: اگر غیر فعال ہو، تو اپلوڈ حذف ہونے پر کوئی جگہ دوبارہ حاصل نہیں کی جائے گی۔" + s3_disable_cleanup: "S3 سے پرانے بیک اپس کو ہٹانے سے روکیں جب زیادہ سے زیادہ اجازت سے زیادہ بیک اپ ہوں۔" enable_s3_inventory: "رپورٹیں بنائیں اور اَیمَیزَون S3 انوینٹری کا استعمال کرتے ہوئے اپلوڈ کی توثیق کریں۔ اہم: درست S3 اسناد ضروری ہیں (ایکسَیس قی آئی ڈی اور سیکرٹ ایکسَیس قی، دونوں)۔" backup_time_of_day: "دن کا وقت UTC جب بیک اَپ ہونا چاہئے۔" backup_with_uploads: "شیڈول کردہ بیک اَپس میں اپلوڈز شامل کریں۔ اِس کو غیر فعال کرنے پر صرف ڈَیٹا بَیس بیک اَپ کیا جائے گا۔" @@ -1337,12 +1638,18 @@ ur: max_bookmarks_per_day: "فی صارف ایک دن کے بُکمارکس کی زیادہ سے زیادہ تعداد۔" max_edits_per_day: "فی صارف ایک دن کے ترامیم کی زیادہ سے زیادہ تعداد۔" max_topics_per_day: "ایک صارف کے فی دن بنائے گئے ٹاپکس کی زیادہ سے زیادہ تعداد۔" + max_personal_messages_per_day: "نئے ذاتی پیغام کے عنوانات کی زیادہ سے زیادہ تعداد ایک صارف فی دن بنا سکتا ہے۔" max_invites_per_day: "ایک صارف کے فی دن بھیجی گئی دعوتوں کی زیادہ سے زیادہ تعداد۔" max_topic_invitations_per_day: "ایک صارف کے فی دن بھیجی گئی ٹاپک دعوتوں کی زیادہ سے زیادہ تعداد۔" 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 پر سیٹ کریں۔" + 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: "/ زمرہ جات کے صفحہ میں دکھانے کے لیے عنوانات کی تعداد۔ اگر 0 پر سیٹ کیا جاتا ہے، تو یہ خود بخود دو کالموں کو ہم آہنگ رکھنے کے لیے ایک قدر تلاش کرنے کی کوشش کرے گا (زمرے اور عنوانات)۔" suggested_topics: "ٹاپک کے نچلے حصے پر دکھائے گئے تجویز کردہ ٹاپکس کی تعداد۔" limit_suggested_to_category: "تجویز کردہ ٹاپکس میں صرف موجودہ زُمرہ سے ٹاپکس دکھائیں۔" suggested_topics_max_days_old: "تجویز کردہ ٹاپکس ن دنوں سے زیادہ پرانے نہیں ہونے چاہئیں۔" @@ -1361,12 +1668,21 @@ ur: avatar_sizes: "خود کار طریقے سے تیار کردہ اوتار کے سائزوں کی فہرست۔" external_system_avatars_enabled: "بیرونی سسٹم کی اوتار سروس کا استعمال کریں۔" external_system_avatars_url: "بیرونی سسٹم کی اوتار سروس کا URL۔ اجازت شدہ متبادلات {username} {first_letter} {color} {size} ہیں" + external_emoji_url: "ایموجی امیجز کے لیے بیرونی سروس کا URL۔ غیر فعال کرنے کے لیے خالی چھوڑ دیں۔" + use_site_small_logo_as_system_avatar: "سسٹم صارف کے اوتار کے بجائے سائٹ کا چھوٹا لوگو استعمال کریں۔ لوگو کا موجود ہونا ضروری ہے۔" restrict_letter_avatar_colors: "خط اوتار کے پس منظرمیں استعمال کیلئے 6-عددی ہیکسا ڈَیسیمل رنگوں کے اقدار کی ایک فہرست۔" + enable_listing_suspended_users_on_search: "معطل صارفین کو تلاش کرنے کے لیے باقاعدہ صارفین کو فعال کریں۔" + selectable_avatars_mode: "صارفین کو selectable_avatars کی فہرست سے ایک اوتار منتخب کرنے کی اجازت دیں اور اپنی مرضی کے اوتار اپ لوڈز کو منتخب اعتماد کی سطح تک محدود کریں۔" selectable_avatars: "اوتار کی فہرست جس میں سے صارفین منتخب کر سکتے ہیں۔" allow_all_attachments_for_group_messages: "گروپ کے پیغامات کیلئے تمام ای میل منسلکات کی اجازت دیں۔" png_to_jpg_quality: "تبدیل شدہ JPG فائل کا معیار (1 سب سے کم معیار ہے، 99 بہترین معیار ہے، 100 غیر فعال)۔" + recompress_original_jpg_quality: "اپ لوڈ کردہ تصویری فائلوں کا معیار (1 کم ترین معیار، 99 بہترین معیار، 100 غیر فعال)۔" + image_preview_jpg_quality: "تبدیل شدہ تصویری فائلوں کا معیار (1 کم ترین معیار، 99 بہترین معیار، 100 غیر فعال)۔" allow_staff_to_upload_any_file_in_pm: "سٹاف ارکان کو PM میں کوئی بھی فائل اَپ لوڈ کرنے کی اجازت دیں۔" strip_image_metadata: "تصویر مَیٹا ڈَیٹا کو ہٹائیں۔" + composer_media_optimization_image_enabled: "اپ لوڈ کردہ تصویری فائلوں کی کلائنٹ سائیڈ میڈیا آپٹیمائزیشن کو فعال کرتا ہے۔" + composer_media_optimization_image_bytes_optimization_threshold: "کلائنٹ سائیڈ آپٹیمائزیشن کو متحرک کرنے کے لیے کم از کم امیج فائل کا سائز" + composer_media_optimization_image_resize_dimensions_threshold: "کلائنٹ سائڈ کا سائز تبدیل کرنے کے لیے تصویر کی کم از کم چوڑائی" min_ratio_to_crop: "لمبی تصاویر کو کاٹنے کیلئے استعمال کیے جانے والا تناسب۔ چوڑائی / اونچائی کا نتیجہ درج کریں۔" simultaneous_uploads: "فائلوں کی زیادہ سے زیادہ تعداد جو کمپوزر میں ڈرَیگ & ڈراپ کی جا سکتی ہے" default_invitee_trust_level: "مدعو صارفین کیلئے ڈِیفالٹ ٹرسٹ لَیول (0-4)۔" @@ -1424,7 +1740,7 @@ ur: max_consecutive_replies: "ایک ہی ٹاپک میں لگاتار کتنی پوسٹس ایک صارف شائع کرے اِس سے پہلے کہ اُسے ایک اور جواب شامل کرنے سے روک دیا جائے۔" title_fancy_entities: "ٹاپک عنوانات میں عام ASCII حروف کو شوخ HTML اشیاء میں تبدیل کریں، بذریعہ سمارٹی پینٹس https://daringfireball.net/projects/smartypants/" min_title_similar_length: "عنوان کی کم از کم لمبائی، اِس سے پہلے کہاُس کو اُسی طرح کے دوسرے ٹاپکس کیلئے چیک کیا جائے گا۔" - desktop_category_page_style: " /categories صفحے کیلئے بصری سٹائل۔" + desktop_category_page_style: "/ زمرہ جات صفحہ کے لیے بصری انداز۔" category_colors: "زُمرہ جات کیلئے ہیکسا ڈَیسیمل رنگوں کے اقدار کی ایک فہرست۔" category_style: "زُمرہ بیَجوں کیلئے بصری سٹائل۔" dark_mode_none: "کوئی نہیں" @@ -1493,6 +1809,8 @@ ur: hard_bounce_score: "مستقل بائونس ہونے پر صارف کے ساتھ بائونس سکور شامل کر دیا جائے۔" bounce_score_threshold: "زیادہ سے زیادہ بائونس سکور اِس سے پہلے کہ ہم صارف کو ای میل کرنا روک دیں۔" reset_bounce_score_after_days: "X دنوں بعد بائونس سکور خود بخود رِی سَیٹ کریں۔" + blocked_attachment_content_types: "مواد کی قسم کی بنیاد پر منسلکات کو بلاک لسٹ کرنے کے لیے استعمال ہونے والے مطلوبہ الفاظ کی فہرست۔" + blocked_attachment_filenames: "فائل نام کی بنیاد پر منسلکات کو بلاک لسٹ کرنے کے لیے استعمال ہونے والے مطلوبہ الفاظ کی فہرست۔" forwarded_emails_behaviour: "ڈِسکَورس پر فاروَرڈ کردہ ایمیل کے ساتھ کیا کریں۔" always_show_trimmed_content: "ہمیشہ آنے والی ای میلز کا حصے دکھائیں۔ انتباہ: ای میل پتوں کو افشاں کر سکتا ہے۔" private_email: "مزید رازداری کیلئے ایمیل عنوان یا ایمیل متن میں پوسٹس یا ٹاپکس سے مواد شامل نہ کریں۔ نوٹ: ڈائجسٹ ایمیلز کو بھی غیر فعال کر دیتا ہے۔" @@ -1511,7 +1829,7 @@ ur: email_in: 'صارفین کو بذریعہ ای میل نئے ٹاپکس شائع کرنے کی اجازت دیں (دستی یا pop3 پَولنگ کی ضرورت ہے)۔ ہر قِسم کیلئے "ترتیبات" ٹَیب میں پتوں کو ترتیب دیں۔' email_in_min_trust: "نئے ٹاپک بذریعہ ای میل شائع کرنے کیلئے کم از کم ٹرسٹ لَیول جس کی صارف کو ضرورت ہے۔" email_in_spam_header: "سپیم کا پتہ لگانے کیلئے ایمیل ہیڈر۔" - email_prefix: "ای میلز کے موضوع میں استعمال کیا جانے والا [لیبل]۔ یہ سَیٹ کردہ نہ ہو تو 'عنوان' پر ڈِیفالٹ ہو جائے گا۔" + email_prefix: "ای میلز کے موضوع میں استعمال ہونے والا [label] ۔ اگر سیٹ نہیں کیا گیا تو یہ 'عنوان' ہوگا۔" email_site_title: "سائٹ سے ای میلز کے \"بھیجنے والا\" کے طور پر استعمال ہونے والا سائٹ کا عنوان۔ یہ سَیٹ نہ ہو تو 'عنوان' پر ڈِیفالٹ ہو جائے گا۔ اگر آپ کے 'عنوان' میں ایسے حروف شامل ہیں جن کی ای میل \"بھیجنے والا\" سٹرِنگ میں اجازت نہیں ہے، تو اِس ترتیب کو استعمال کریں۔" find_related_post_with_key: "جواب دی گئی پوسٹ کو تلاش کرنے کیلئے صرف 'جواب کلید' استعمال کریں۔ انتباہ: اِس کو غیر فعال کرنے سے ای میل ایڈریس پر مبنی صارف نقالی ممکن ہو جاتی ہے۔" minimum_topics_similar: "نئے ٹاپک تشکیل کرتے وقت اُسی طرح کے دوسرے ٹاپکس پیش کیے جانے سے پہلے کتنے ٹاپکس کے موجود ہونے کی ضرورت ہے۔" @@ -1601,7 +1919,7 @@ ur: emoji_set: "آپ اپنا اِیمَوجی کیسا پسند کریں گے؟" emoji_autocomplete_min_chars: "خود تکمیل اِیمَوجی پاپ اپ متحرک کرنے کیلئے مطلوبہ حروف کی کم از کم تعداد" enable_inline_emoji_translation: "اِن لائن اِیمَوجیوں کیلئے ترجمہ کو فعال بناتا ہے (پہلے کسی خالی جگہ یا وقفی علامت کے بغیر)" - approve_post_count: "ایک نئے یا بَیسِک صارف کی طرف سے پوسٹس کی تعداد جن کا منظور کیے جانا لاذمی ہے " + approve_post_count: "کسی نئے یا بنیادی صارف کی پوسٹس کی وہ مقدار جسے منظور ہونا ضروری ہے" approve_unless_trust_level: "اِس ٹرسٹ لَیول سے نیچے کے صارفین کیلئے پوسٹس کا منظور شدہ ہونا لازمی ہے" approve_new_topics_unless_trust_level: "اِس ٹرسٹ لَیول سے نیچے کے صارفین کیلئے نئے ٹاپکس کا منظور شدہ ہونا لازمی ہے" approve_unless_staged: "سٹَیجڈ صارفین کیلئے نئے ٹاپکس اور پوسٹس کا منظور شدہ ہونا لازمی ہے" @@ -1624,7 +1942,7 @@ ur: default_email_in_reply_to: "ڈِیفالٹ کے طور پر ای میلز میں جس پوسٹ کا جواب دیا گیا ہو، اُس کا اقتباس شامل کریں۔" default_other_new_topic_duration_minutes: "عالمی ڈِیفالٹ شرط جس کیلئے کوئی ٹاپک نیا سمجھا جاتا ہے۔" default_other_auto_track_topics_after_msecs: "عالمی ڈِیفالٹ وقت جس کے بعد ٹاپک خود کار طریقہ سے ٹرَیک کر دیا جائے گا۔" - default_other_notification_level_when_replying: " گلوبل ڈیفالٹ اطلاعات کا لَیول جب صارف کسی ٹاپک پر جواب دے۔" + default_other_notification_level_when_replying: "عالمی ڈیفالٹ اطلاع کی سطح جب صارف کسی موضوع کا جواب دیتا ہے۔" default_other_external_links_in_new_tab: "ڈِیفالٹ کے طور پر تمام بیرونی ویب سائٹ کے لنکس ایک نئے ٹیب میں کھولیں۔" default_other_enable_quoting: "ڈِیفالٹ کے طور پر روشنی ڈالے گئے ٹَیکسٹ کے لئے اقتباسی جواب فعال کریں۔" default_other_enable_defer: "ڈیفالٹ سے ٹاپک ملتوی خصوصیت فعال کریں۔" @@ -1722,7 +2040,7 @@ ur: unknown_error: "آپ کے اکاؤنٹ کے ساتھ ایک مسئلہ درپیش ہے۔ براہ مہربانی سائٹ کے ایڈمِنِسٹریٹر سے رابطہ کریں۔" timeout_expired: "اکاؤنٹ لاگ اِن کا وقت ختم ہوگیا، براہ مہربانی دوبارہ لاگ اِن کرنے کی کوشش کریں۔" no_email: "کوئی ای میل پتہ فراہم نہیں گیا تھا۔ براہ مہربانی سائٹ کے ایڈمِنِسٹریٹر سے رابطہ کریں۔" - email_error: "%{email} ای میل ایڈریس کے ساتھ اکاؤنٹ رجسٹر نہیں کیا جا سکا ۔ براہ مہربانی سائٹ کے ایڈمِنِسٹریٹر سے رابطہ کریں۔" + email_error: "ای میل ایڈریس %{email}کے ساتھ اکاؤنٹ رجسٹر نہیں کیا جا سکا۔ براہ کرم سائٹ کے منتظم سے رابطہ کریں۔" original_poster: "اصل پوسٹ کرنے والا" most_recent_poster: "سب سے حالیہ پوسٹ کرنے والا" frequent_poster: "اکثر پوسٹ کرنے والا" @@ -1989,7 +2307,7 @@ ur: subject_template: "[%{email_prefix}] اَپ ڈیٹ دستیاب ہے" flag_reasons: off_topic: "آپ کی پوسٹ کو **موضوع سے ہٹ کر** کے طور پر فلَیگ کیا گیا تھا: کمیونٹی سمجھتی ہے کہ یہ ٹاپک کیلئے اچھی فِٹ نہیں ہے، جیسا کہ عنوان اور پہلی پوسٹ کی طرف سے فی الحال بیان کیا گیا ہے۔" - inappropriate: "آپ کی پوسٹ کو **نامناسب** کے طور پر فلَیگ کیا گیا تھا: کمیونٹی سمجھتی ہے کہ یہ توہین آمیز، بدسلوکی، یا [ہماری کمیونٹی کے قواعد و ضوابط]() کی خلاف ورزی ہے۔" + inappropriate: "آپ کی پوسٹ کو **نامناسب** کے طور پر نشان زد کیا گیا تھا: کمیونٹی کو لگتا ہے کہ یہ ناگوار، بدسلوکی، یا [ہماری کمیونٹی گائیڈلائنز](%{base_path}/guidelines) کی خلاف ورزی ہے۔" spam: "آپ کی پوسٹ کو **سپَیم** کے طور پر فلَیگ کیا گیا تھا: کمیونٹی سمجھتی ہے کہ یہ ایک اشتہار ہے، ایسی چیز جو کہ بجائے توقع کے مطابق مفید یا موضوع سے متعلقہ ہونے، کے فطرت میں زیادہ تر پرَومَوشنل ہے۔" notify_moderators: "آپ کی پوسٹ کو **ماڈریٹر کی توجہ کیلئے** فلَیگ کیا گیا تھا: کمیونٹی سمجھتی ہے کہ اِس پوسٹ میں کچھ ایسا ہے جس کیلئے سٹاف ممبر کی طرد سے دستی مداخلت کی ضرورت ہے۔" flags_dispositions: @@ -2450,8 +2768,8 @@ ur: آپ کو یہ موصول ہو رہا ہے کیونکہ آپ نے مَیلنگ لِسٹ مَوڈ فعال کیا ہوا ہے۔ اِن ای میلز سے غیر سَبسکرائب کرنے کیلئے، [یہاں کلِک کریں](%{unsubscribe_url})۔ - subject_re: "جواب:" - subject_pm: "[PM]" + subject_re: "جواب: " + subject_pm: "[PM] " email_from: "%{user_name} بذریعہ %{site_name}" user_notifications: previous_discussion: "پچھلے جوابات" @@ -2519,7 +2837,7 @@ ur: %{respond_instructions} user_replied_pm: title: "صارف کی طرف سے جواب دیا گیا ذاتی پیغام" - subject_template: "[%{email_prefix}] [ذاتی پیغام] %{topic_title}" + subject_template: "[%{email_prefix}] [PM] %{topic_title}" text_body_template: | %{header_instructions} @@ -2563,7 +2881,7 @@ ur: %{respond_instructions} user_mentioned_pm: title: "صارف ذکر کردہ ذاتی پیغام" - subject_template: "[%{email_prefix}] [ذاتی پیغام] %{topic_title}" + subject_template: "[%{email_prefix}] [PM] %{topic_title}" text_body_template: | %{header_instructions} @@ -2584,7 +2902,7 @@ ur: %{respond_instructions} user_group_mentioned_pm: - subject_template: "[%{email_prefix}] [ذاتی پیغام] %{topic_title}" + subject_template: "[%{email_prefix}] [PM] %{topic_title}" text_body_template: | %{header_instructions} @@ -2594,7 +2912,7 @@ ur: %{respond_instructions} user_group_mentioned_pm_group: - subject_template: "[%{email_prefix}] [ذاتی پیغام] %{topic_title}" + subject_template: "[%{email_prefix}] [PM] %{topic_title}" text_body_template: | %{header_instructions} @@ -2627,7 +2945,7 @@ ur: %{respond_instructions} user_posted_pm: title: "صارف نے ذاتی پیغام شائع کیا" - subject_template: "[%{email_prefix}] [ذاتی پیغام] %{topic_title}" + subject_template: "[%{email_prefix}] [PM] %{topic_title}" text_body_template: | %{header_instructions} @@ -2674,7 +2992,7 @@ ur: new_users: "نئے صارفین" popular_topics: "مقبول ٹاپک" follow_topic: "اِس ٹاپک کی پیروی کریں" - join_the_discussion: "مزید پڑھیں " + join_the_discussion: "مزید پڑھیں" popular_posts: "مقبول پوسٹس" more_new: "آپ کیلئے نیا" subject_template: "[%{email_prefix}] خلاصہ" @@ -2746,7 +3064,7 @@ ur: %{new_email} signup_after_approval: - title: "سائن اَپ بعد از منظوری " + title: "سائن اَپ بعد از منظوری" subject_template: "آپ کو %{site_name} پر منظور کر دیا گیا ہے!" text_body_template: | %{site_name} میں خوش آمدید! @@ -2787,7 +3105,7 @@ ur: اگر اوپر والا لِنک کلک نہیں ہوتا ہو، تو اپنے ویب براؤزر کے ایڈریس بار میں کاپی اور پَیسٹ کرنے کی کوشش کریں۔ suspicious_login: title: "نیا لاگ اِن انتباہ" - subject_template: " [%{site_name}] نیا لاگ اِن %{location} سے" + subject_template: "[%{site_name}] %{location}سے نیا لاگ ان" text_body_template: | ہیلو، @@ -2840,7 +3158,7 @@ ur: avatar: missing: "معذرت، ہم اس ای میل ایڈریس سے منسلک کوئی اوتار تلاش نہیں کر سکے۔ کیا آپ اُسے دوبارہ اَپ لوڈ کرنے کی کوشش کر سکتے ہیں؟" flag_reason: - sockpuppet: "ایک نئے صارف نے ایک ٹاپک بنایا، اور اُسی IP ایڈریس (%{ip_address}) پر ایک نئے صارف نے جواب دیا۔ `flag_sockpuppets` سائٹ ترتیب دیکھیے۔" + sockpuppet: "ایک نئے صارف نے ایک موضوع بنایا، اور اسی آئی پی ایڈریس (%{ip_address}) پر ایک اور نئے صارف نے جواب دیا۔ `flag_sockpuppets`سائٹ کی ترتیب دیکھیں۔" skipped_email_log: exceeded_emails_limit: "max_emails_per_day_per_user سے تجاوز کر گیا" exceeded_bounces_limit: "bounce_score_threshold سے تجاوز کر گیا" @@ -2883,10 +3201,10 @@ ur: latte_theme_name: "لا ٹیے" summer_theme_name: "موسمِ گرما" dark_rose_theme_name: "گہرا گلابی" - edit_this_page: " اِس صفحہ میں ترمیم کریں" + edit_this_page: "اِس صفحہ میں ترمیم کریں" csv_export: boolean_yes: "جی ہاں" - boolean_no: "نہیں " + boolean_no: "نہیں" rate_limit_error: "پوسٹس فی دن ایک بار ہی ڈاؤن لوڈ کی جا سکتی ہیں، براہ مہربانی کل دوبارہ کوشش کریں۔" static_topic_first_reply: | %{page_name} صفحے کے مواد کو تبدیل کرنے کیلئے اس ٹاپک میں پہلی پوسٹ میں ترمیم کریں۔ @@ -2894,7 +3212,7 @@ ur: title: "عمومی سوالات کے جوابات / ہدایات" tos_topic: title: "سروس کی شرائط" - body: "یہ شرائط <%{base_url}> پرانٹرنیٹ فورم کے استعمال کیلئے لاگو ہیں۔ فورم کا استعمال کرنے کیلئے، آپ کو %{company_name}، کمپنی جو اِس فورم کو چلاتی ہے، کے ساتھ اِن شرائط پر متفق ہونا ضروری ہے۔\n\nکمپنی مختلف شرائط کے تحت دیگر مصنوعات اور خدمات پیش کر سکتی ہے۔ یہ شرائط صرف فورم کے استعمال کیلئے لاگو ہوتی ہیں۔\n\nپر جائیں:\n\n- [اہم شرائط](#heading--اہم-شرائط)\n- [آپ کیلئے فورم کے استعمال کی اجازت](#heading--اجازت)\n- [فورم کے استعمال کیلئے شرائط](#heading--شرائط)\n- [قابلِ قبول استعمال](#heading--قابلِ-قبول-استعمال)\n- [مواد کے معیار](#heading--مواد-معیار)\n- [نفاذ](#heading--نفاذ)\n- [آپ کا اکاؤنٹ](#heading--آپ-کا-اکاؤنٹ)\n- [آپ کا مواد](#heading-- آپ-کا-مواد)\n- [آپ کی ذمہ داری](#heading--ذمہ داری)\n- [ڈِسکَلَیمرز](#heading--ڈِسکَلَیمرز)\n- [جواب دہی کی حد](#heading--جواب-دہی)\n- [تاثرات](#heading--تاثرات)\n- [اختتام] (#heading--اختتام)\n- [تنازعات](#heading--تنازعات)\n- [عمومی شرائط](#heading--عام)\n- [رابطہ](#heading--رابطہ)\n- [تبدیلیاں](#heading--تبدیلیاں)\n\n

    اہم شرائط

    \n\n***اِن شرائط میں شامل کئی اہم دفعات ہیں جو آپ کے حقوق اور فرائیض پر اثر انداز ہوتے ہیں، جیسے کہ [ڈِسکَلَیمرز](#heading--ڈِسکَلَیمرز) میں ڈِسکَلَیمرز، [جواب دہی کی حد](#heading--جواب-دہی) میں کمپنی کی آپ کیلئے جواب دہی کی حدیں، [آپ کے استعمال کیلئے ذمہ داری](#heading--ذمہ داری) میں آپ کے فورم کے غلط استعمال سے ہونے والے نقصانات کیلئے کمپنی کا احاطہ کرنے کا معاہدہ، اور [تنازعات](#heading--تنازعات) میں اختلافات کو ثالثین کے ذریعہ حل کروانے کا ایک معاہدہ۔***\n\n

    آپ کیلئے فورم کے استعمال کی اجازت

    \n\nاِن شرائط کے تحت، کمپنی آپ کو فورم استعمال کرنے کی اجازت دیتی ہے۔ فورم کو استعمال کرنے کیلئے ہر ایک کا اِن شرائط سے متفق ہونا ضروری ہے۔\n\n

    فورم کے استعمال کیلئے شرائط

    \n\nفورم استعمال کرنے کی آپ کیلئے اجازت مندرجہ ذیل شرائط کے تابع ہے:\n\n1. آپ کا کم از کم تیرہ سال کا ہونا لازمی ہے۔\n\n2. آپ مزید فورم کا استعمال نہ کریں اگر کمپنی براہ راست آپ سے رابطہ کرے کہ آپ ایسا نہ کریں۔\n\n3. آپ کا [قابلِ قبول استعمال](#heading--قابلِ-قبول-استعمال) اور [مواد کے معیار](#heading--مواد-معیار) کے مطابق فورم استعمال کرنا لازمی ہے۔\n\n

    قابلِ قبول استعمال

    \n\n1. آپ فورم کا استعمال کرتے ہوئے قانون کو توڑ نہیں سکتے۔\n\n2. آپ کسی دوسرے کی خصوصی اجازت کے بغیر فورم پر اُن کے اکاؤنٹ کو استعمال یا استعمال کی کوشش نہیں کر سکتے۔\n\n3. آپ صارف نام یا فورم کے دیگر منفرد شناخت کنندہ پر خریدوفروخت یا دوسری صورت میں تجارت نہیں کرسکتے۔\n\n4. آپ فورم کے ذریعہ اشتھارات، چَین خطوط، یا دیگر درخواستیں نہیں بھیج سکتے، یا تجارتی ایمیل فہرستوں یا ڈیٹا بَیس کیلئے پتے یا دوسری ذاتی معلومات جمع کرنے کیلئے بھی فورم کا استعمال نہیں کرسکتے۔\n\n5. آپ فورم تک رسائی کو خودکار نہیں کرسکتے، یا فورم کی نگرانی، جیسے کہ بذریعہ وَیب کرالر، براؤزر پلگ اِن یا اَیڈ آن، یا دیگر کمپیوٹر پروگرام جو ویب براؤزر نہیں ہے۔ آپ عوامی طور پر دستیاب سرچ اِنجَن، اگر آپ ایک چلاتے ہوں، کیلئے فورم کو اِنڈیکس کرنے کیلئے اِسے کرال کر سکتے ہیں۔\n\n6. آپ ایمیل فہرستوں، نیوز گروپوں، یا گروپ ایمیل عرفات پر ایمیل بھیجنے کیلئے فورم کا استعمال نہیں کر سکتے۔\n\n7. آپ غلط اشارہ نہیں دے سکتے کہ آپ کمپنی کے ساتھ منسلک یا اُس کے تائيد کردہ ہیں۔\n\n8. آپ فورم پر موجود تصاویر یا دیگر غیر ہائپر ٹیکسٹ مواد کو دوسرے ویب صفحات پر ہائپر لنک نہیں کرسکتے۔\n\n9. آپ اس فورم سے ڈاؤن لوڈ کردہ مواد پر سے ملکیت ظاہر کرنے کے کسی بھی نشان کو نہیں نکال سکتے۔\n\n10. آپ دوسری ویب سائٹوں پر بذریعہ `
  • - likes + likes +  <%= t.like_count -%> - replies + replies +  <%= t.posts_count - 1 -%> - <% t.posters_summary.each do |ps| %> - <% if ps.user %> - <%= ps.user.username -%> - <% end %> - <% end %> - @@ -245,7 +240,8 @@
    - + +
    - <% t.posters_summary[0,2].each do |ps| %> - <% if ps.user %> - <%= ps.user.username -%> - <% end %> - <% end %> - - likes + likes +

    <%= t.like_count -%>

    - replies + replies +

    <%= t.posts_count - 1 -%>