diff --git a/.dockerignore b/.dockerignore index 9d990ab9ce6..fe87f6e6006 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ .gitattributes .gitignore .github +.vscode public/system public/assets public/packs @@ -20,6 +21,7 @@ postgres14 redis elasticsearch chart +storybook-static .yarn/ !.yarn/patches !.yarn/plugins diff --git a/.github/workflows/crowdin-download-stable.yml b/.github/workflows/crowdin-download-stable.yml index 6d9a0586298..8e18a9d0a0f 100644 --- a/.github/workflows/crowdin-download-stable.yml +++ b/.github/workflows/crowdin-download-stable.yml @@ -50,7 +50,7 @@ jobs: # Create or update the pull request - name: Create Pull Request - uses: peter-evans/create-pull-request@v7.0.6 + uses: peter-evans/create-pull-request@v7.0.8 with: commit-message: 'New Crowdin translations' title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)' diff --git a/.nvmrc b/.nvmrc index 4a203c23d83..f1c8f6b0d0f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.17 +22.18 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4ec92f34121..625fbf17ab4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,15 +1,11 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.77.0. +# using RuboCop version 1.79.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -Lint/NonLocalExitFromIterator: - Exclude: - - 'app/helpers/json_ld_helper.rb' - # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 82 diff --git a/CHANGELOG.md b/CHANGELOG.md index 19be8ea68e5..b3af469bb35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,48 @@ All notable changes to this project will be documented in this file. +## [4.4.3] - 2025-08-05 + +### Security + +- Update dependencies +- Fix incorrect rate-limit handling [GHSA-84ch-6436-c7mg](https://github.com/mastodon/mastodon/security/advisories/GHSA-84ch-6436-c7mg) + +### Fixed + +- Fix race condition caused by ActiveRecord query cache in `Create` critical path (#35662 by @ClearlyClaire) +- Fix race condition caused by quote post processing (#35657 by @ClearlyClaire) +- Fix WebUI crashing for accounts with `null` URL (#35651 by @ClearlyClaire) +- Fix friends-of-friends recommendations suggesting already-requested accounts (#35604 by @ClearlyClaire) +- Fix synchronous recursive fetching of deeply-nested quoted posts (#35600 by @ClearlyClaire) +- Fix “Expand this post” link including user `@undefined` (#35478 by @ClearlyClaire) + +### Changed + +- Change `StatusReachFinder` to consider quotes as well as reblogs (#35601 by @ClearlyClaire) +- Add restrictions on which quote posts can trend (#35507 by @ClearlyClaire) +- Change quote verification to not bypass authorization flow for mentions (#35528 by @ClearlyClaire) + +## [4.4.2] - 2025-07-23 + +### Security + +- Update dependencies + +### Fixed + +- Fix menu not clickable in Firefox (#35390 and #35414 by @diondiondion) +- Add `lang` attribute to current composer language in alt text modal (#35412 by @diondiondion) +- Fix quote posts styling on notifications page (#35411 by @diondiondion) +- Improve a11y of custom select menus in notifications settings (#35403 by @diondiondion) +- Fix selected item in poll select menus is unreadable in Firefox (#35402 by @diondiondion) +- Update age limit wording (#35387 by @diondiondion) +- Fix support for quote verification in implicit status updates (#35384 by @ClearlyClaire) +- Improve `Dropdown` component accessibility (#35373 by @diondiondion) +- Fix processing some incoming quotes failing because of missing JSON-LD context (#35354 and #35380 by @ClearlyClaire) +- Make bio hashtags open the local page instead of the remote instance (#35349 by @ChaosExAnima) +- Fix styling of external log-in button (#35320 by @ClearlyClaire) + ## [4.4.1] - 2025-07-09 ### Fixed diff --git a/Gemfile b/Gemfile index ce775fc57bc..ee2369921de 100644 --- a/Gemfile +++ b/Gemfile @@ -84,7 +84,7 @@ gem 'sanitize', '~> 7.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '< 8' gem 'sidekiq-bulk', '~> 0.2.0' -gem 'sidekiq-scheduler', '~> 5.0' +gem 'sidekiq-scheduler', '~> 6.0' gem 'sidekiq-unique-jobs', '> 8' gem 'simple_form', '~> 5.2' gem 'simple-navigation', '~> 4.4' diff --git a/Gemfile.lock b/Gemfile.lock index 4c232743bf1..943334b03d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,13 +90,13 @@ GEM public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) android_key_attestation (0.3.0) - annotaterb (4.17.0) + annotaterb (4.18.0) activerecord (>= 6.0.0) activesupport (>= 6.0.0) ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1131.0) + aws-partitions (1.1135.0) aws-sdk-core (3.215.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -144,7 +144,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - capybara-playwright-driver (0.5.6) + capybara-playwright-driver (0.5.7) addressable capybara playwright-ruby-client (>= 1.16.0) @@ -175,9 +175,9 @@ GEM css_parser (1.21.1) addressable csv (3.3.5) - database_cleaner-active_record (2.2.1) + database_cleaner-active_record (2.2.2) activerecord (>= 5.a) - database_cleaner-core (~> 2.0.0) + database_cleaner-core (~> 2.0) database_cleaner-core (2.0.1) date (3.4.1) debug (1.11.0) @@ -233,7 +233,7 @@ GEM fabrication (3.0.0) faker (3.5.2) i18n (>= 1.8.11, < 2) - faraday (2.13.2) + faraday (2.13.4) faraday-net_http (>= 2.0, < 3.5) json logger @@ -287,7 +287,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.65.0) + haml_lint (0.66.0) haml (>= 5.0) parallel (~> 1.10) rainbow @@ -315,7 +315,7 @@ GEM http_accept_language (2.1.1) httpclient (2.9.0) mutex_m - httplog (1.7.1) + httplog (1.7.2) rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.7) @@ -345,7 +345,7 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.13.0) + json (2.13.2) json-canonicalization (1.0.0) json-jwt (1.16.7) activesupport (>= 4.2) @@ -438,7 +438,7 @@ GEM mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2025.0715) + mime-types-data (3.2025.0729) mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) @@ -468,7 +468,7 @@ GEM hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-cas (3.0.1) + omniauth-cas (3.0.2) addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) @@ -601,16 +601,16 @@ GEM ox (2.14.23) bigdecimal (>= 3.0) parallel (1.27.0) - parser (3.3.8.0) + parser (3.3.9.0) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.9) + pg (1.6.1) pghero (3.7.0) activerecord (>= 7.1) - playwright-ruby-client (1.54.0) + playwright-ruby-client (1.54.1) concurrent-ruby (>= 1.1.6) mime-types (>= 3.0) pp (0.6.2) @@ -635,7 +635,7 @@ GEM date stringio public_suffix (6.0.2) - puma (6.6.0) + puma (6.6.1) nio4r (~> 2.0) pundit (2.5.0) activesupport (>= 3.0.0) @@ -721,7 +721,7 @@ GEM connection_pool redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.10.0) + regexp_parser (2.11.0) reline (0.6.2) io-console (~> 0.5) request_store (1.7.0) @@ -731,7 +731,7 @@ GEM railties (>= 5.2) rexml (3.4.1) rotp (6.3.0) - rouge (4.5.2) + rouge (4.6.0) rpam2 (4.0.2) rqrcode (3.1.0) chunky_png (~> 1.0) @@ -765,7 +765,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.4) - rubocop (1.78.0) + rubocop (1.79.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -773,7 +773,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.45.1, < 2.0) + rubocop-ast (>= 1.46.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.46.0) @@ -805,7 +805,7 @@ GEM ruby-prof (1.7.2) base64 ruby-progressbar (1.13.0) - ruby-saml (1.18.0) + ruby-saml (1.18.1) nokogiri (>= 1.13.10) rexml ruby-vips (2.2.4) @@ -833,10 +833,9 @@ GEM redis-client (>= 0.22.2) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (5.0.6) + sidekiq-scheduler (6.0.1) rufus-scheduler (~> 3.2) - sidekiq (>= 6, < 8) - tilt (>= 1.4.0, < 3) + sidekiq (>= 7.3, < 9) sidekiq-unique-jobs (8.0.11) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 7.0.0, < 9.0.0) @@ -859,7 +858,7 @@ GEM stoplight (4.1.1) redlock (~> 1.0) stringio (3.1.7) - strong_migrations (2.4.0) + strong_migrations (2.5.0) activerecord (>= 7.1) swd (2.0.3) activesupport (>= 3) @@ -867,7 +866,7 @@ GEM faraday (~> 2.0) faraday-follow_redirects sysexits (1.2.0) - temple (0.10.3) + temple (0.10.4) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) terrapin (1.1.1) @@ -1082,7 +1081,7 @@ DEPENDENCIES shoulda-matchers sidekiq (< 8) sidekiq-bulk (~> 0.2.0) - sidekiq-scheduler (~> 5.0) + sidekiq-scheduler (~> 6.0) sidekiq-unique-jobs (> 8) simple-navigation (~> 4.4) simple_form (~> 5.2) @@ -1106,4 +1105,4 @@ RUBY VERSION ruby 3.4.1p0 BUNDLED WITH - 2.7.0 + 2.7.1 diff --git a/app/controllers/activitypub/quote_authorizations_controller.rb b/app/controllers/activitypub/quote_authorizations_controller.rb new file mode 100644 index 00000000000..fa635d636a6 --- /dev/null +++ b/app/controllers/activitypub/quote_authorizations_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController + include Authorization + + vary_by -> { 'Signature' if authorized_fetch_mode? } + + before_action :require_account_signature!, if: :authorized_fetch_mode? + before_action :set_quote_authorization + + def show + expires_in 0, public: @quote.status.distributable? && public_fetch_mode? + render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' + end + + private + + def pundit_user + signed_request_account + end + + def set_quote_authorization + @quote = Quote.accepted.where(quoted_account: @account).find(params[:id]) + authorize @quote.status, :show? + rescue Mastodon::NotPermittedError + not_found + end +end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb index 8b8e83fde77..61ca1661898 100644 --- a/app/controllers/admin/action_logs_controller.rb +++ b/app/controllers/admin/action_logs_controller.rb @@ -6,7 +6,7 @@ module Admin def index authorize :audit_log, :index? - @auditable_accounts = Account.auditable.select(:id, :username) + @auditable_accounts = Account.auditable.select(:id, :username).order(username: :asc) end private diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 702550eecc1..5d1555796f4 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -19,15 +19,13 @@ module Admin log_action :resend, @user - flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success') - redirect_to admin_accounts_path + redirect_to admin_accounts_path, notice: t('admin.accounts.resend_confirmation.success') end private def redirect_confirmed_user - flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') - redirect_to admin_accounts_path + redirect_to admin_accounts_path, flash: { error: t('admin.accounts.resend_confirmation.already_confirmed') } end def user_confirmed? diff --git a/app/controllers/admin/disputes/appeals_controller.rb b/app/controllers/admin/disputes/appeals_controller.rb index 0c415536767..7c70603e231 100644 --- a/app/controllers/admin/disputes/appeals_controller.rb +++ b/app/controllers/admin/disputes/appeals_controller.rb @@ -18,7 +18,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController end def reject - authorize @appeal, :approve? + authorize @appeal, :reject? log_action :reject, @appeal @appeal.reject!(current_account) UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index c3443b70776..5e1074b224a 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -36,7 +36,7 @@ module Admin end def edit - authorize :domain_block, :create? + authorize :domain_block, :update? end def create @@ -129,7 +129,7 @@ module Admin end def requires_confirmation? - @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm] + @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.suspend? && !params[:confirm] end end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 2ae5ec82556..a08375e0a41 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -14,8 +14,7 @@ module Admin @admin_settings = Form::AdminSettings.new(settings_params) if @admin_settings.save - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path + redirect_to after_update_redirect_path, notice: t('generic.changes_saved_msg') else render :show end diff --git a/app/controllers/admin/username_blocks_controller.rb b/app/controllers/admin/username_blocks_controller.rb new file mode 100644 index 00000000000..22ac9408178 --- /dev/null +++ b/app/controllers/admin/username_blocks_controller.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +class Admin::UsernameBlocksController < Admin::BaseController + before_action :set_username_block, only: [:edit, :update] + + def index + authorize :username_block, :index? + @username_blocks = UsernameBlock.order(username: :asc).page(params[:page]) + @form = Form::UsernameBlockBatch.new + end + + def batch + authorize :username_block, :index? + + @form = Form::UsernameBlockBatch.new(form_username_block_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.username_blocks.no_username_block_selected') + rescue Mastodon::NotPermittedError + flash[:alert] = I18n.t('admin.username_blocks.not_permitted') + ensure + redirect_to admin_username_blocks_path + end + + def new + authorize :username_block, :create? + @username_block = UsernameBlock.new(exact: true) + end + + def edit + authorize @username_block, :update? + end + + def create + authorize :username_block, :create? + + @username_block = UsernameBlock.new(resource_params) + + if @username_block.save + log_action :create, @username_block + redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.created_msg') + else + render :new + end + end + + def update + authorize @username_block, :update? + + if @username_block.update(resource_params) + log_action :update, @username_block + redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.updated_msg') + else + render :new + end + end + + private + + def set_username_block + @username_block = UsernameBlock.find(params[:id]) + end + + def form_username_block_batch_params + params + .expect(form_username_block_batch: [username_block_ids: []]) + end + + def resource_params + params + .expect(username_block: [:username, :comparison, :allow_with_approval]) + end + + def action_from_button + 'delete' if params[:delete] + end +end diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb index 283383acb4a..dd272120e21 100644 --- a/app/controllers/api/v1/admin/tags_controller.rb +++ b/app/controllers/api/v1/admin/tags_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Admin::TagsController < Api::BaseController include Authorization + before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write' }, only: :update diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index f2c52f2846e..3b0cda7d931 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -16,16 +16,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController def create with_redis_lock("push_subscription:#{current_user.id}") do destroy_web_push_subscriptions! - - @push_subscription = Web::PushSubscription.create!( - endpoint: subscription_params[:endpoint], - key_p256dh: subscription_params[:keys][:p256dh], - key_auth: subscription_params[:keys][:auth], - standard: subscription_params[:standard] || false, - data: data_params, - user_id: current_user.id, - access_token_id: doorkeeper_token.id - ) + @push_subscription = Web::PushSubscription.create!(web_push_subscription_params) end render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer @@ -55,6 +46,18 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController not_found if @push_subscription.nil? end + def web_push_subscription_params + { + access_token_id: doorkeeper_token.id, + data: data_params, + endpoint: subscription_params[:endpoint], + key_auth: subscription_params[:keys][:auth], + key_p256dh: subscription_params[:keys][:p256dh], + standard: subscription_params[:standard] || false, + user_id: current_user.id, + } + end + def subscription_params params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]]) end diff --git a/app/controllers/api/v1/statuses/quotes_controller.rb b/app/controllers/api/v1/statuses/quotes_controller.rb new file mode 100644 index 00000000000..962855884ec --- /dev/null +++ b/app/controllers/api/v1/statuses/quotes_controller.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index + before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: :revoke + + before_action :check_owner! + before_action :set_quote, only: :revoke + after_action :insert_pagination_headers, only: :index + + def index + cache_if_unauthenticated! + @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer + end + + def revoke + authorize @quote, :revoke? + + RevokeQuoteService.new.call(@quote) + + render json: @quote.status, serializer: REST::StatusSerializer + end + + private + + def check_owner! + authorize @status, :list_quotes? + end + + def set_quote + @quote = @status.quotes.find_by!(status_id: params[:id]) + end + + def load_statuses + scope = default_statuses + scope = scope.not_excluded_by_account(current_account) unless current_account.nil? + scope.merge(paginated_quotes).to_a + end + + def default_statuses + Status.includes(:quote).references(:quote) + end + + def paginated_quotes + @status.quotes.accepted.paginate_by_max_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def next_path + api_v1_status_quotes_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_status_quotes_url pagination_params(since_id: pagination_since_id) unless @statuses.empty? + end + + def pagination_max_id + @statuses.last.quote.id + end + + def pagination_since_id + @statuses.first.quote.id + end + + def records_continue? + @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) + end +end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index e25b161afd8..fdf1e7a4685 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -10,6 +10,7 @@ class Api::V1::StatusesController < Api::BaseController before_action :set_statuses, only: [:index] before_action :set_status, only: [:show, :context] before_action :set_thread, only: [:create] + before_action :set_quoted_status, only: [:create] before_action :check_statuses_limit, only: [:index] override_rate_limit_headers :create, family: :statuses @@ -65,7 +66,11 @@ class Api::V1::StatusesController < Api::BaseController add_async_refresh_header(async_refresh) elsif !current_account.nil? && @status.should_fetch_replies? add_async_refresh_header(AsyncRefresh.create(refresh_key)) - ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) + + WorkerBatch.new.within do |batch| + batch.connect(refresh_key, threshold: 1.0) + ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id }) + end end render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) @@ -76,6 +81,8 @@ class Api::V1::StatusesController < Api::BaseController current_user.account, text: status_params[:status], thread: @thread, + quoted_status: @quoted_status, + quote_approval_policy: quote_approval_policy, media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], spoiler_text: status_params[:spoiler_text], @@ -107,7 +114,8 @@ class Api::V1::StatusesController < Api::BaseController sensitive: status_params[:sensitive], language: status_params[:language], spoiler_text: status_params[:spoiler_text], - poll: status_params[:poll] + poll: status_params[:poll], + quote_approval_policy: quote_approval_policy ) render json: @status, serializer: REST::StatusSerializer @@ -147,6 +155,16 @@ class Api::V1::StatusesController < Api::BaseController render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404 end + def set_quoted_status + return unless Mastodon::Feature.outgoing_quotes_enabled? + + @quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present? + authorize(@quoted_status, :quote?) if @quoted_status.present? + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError + # TODO: distinguish between non-existing and non-quotable posts + render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404 + end + def check_statuses_limit raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT end @@ -163,6 +181,8 @@ class Api::V1::StatusesController < Api::BaseController params.permit( :status, :in_reply_to_id, + :quoted_status_id, + :quote_approval_policy, :sensitive, :spoiler_text, :visibility, @@ -185,6 +205,23 @@ class Api::V1::StatusesController < Api::BaseController ) end + def quote_approval_policy + # TODO: handle `nil` separately + return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present? + + case status_params[:quote_approval_policy] + when 'public' + Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 + when 'followers' + Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16 + when 'nobody' + 0 + else + # TODO: raise more useful message + raise ActiveRecord::RecordInvalid + end + end + def serializer_for_status @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 3ca13cc4275..c00ddf92cc0 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -20,7 +20,7 @@ class Api::V2::SearchController < Api::BaseController @search = Search.new(search_results) render json: @search, serializer: REST::SearchSerializer rescue Mastodon::SyntaxError - unprocessable_entity + unprocessable_content rescue ActiveRecord::RecordNotFound not_found end diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 2711071b4a5..ced68d39fc7 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -49,7 +49,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController { policy: 'all', alerts: Notification::TYPES.index_with { alerts_enabled }, - } + }.deep_stringify_keys end def alerts_enabled diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 42abe990483..82d9e8380fc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -28,7 +28,7 @@ class ApplicationController < ActionController::Base rescue_from Mastodon::NotPermittedError, with: :forbidden rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::UnknownFormat, with: :not_acceptable - rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity + rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_content rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) @@ -123,7 +123,7 @@ class ApplicationController < ActionController::Base respond_with_error(410) end - def unprocessable_entity + def unprocessable_content respond_with_error(422) end diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index 7c1ff59671d..2680a1c5fdc 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -19,8 +19,7 @@ class Auth::PasswordsController < Devise::PasswordsController private def redirect_invalid_reset_token - flash[:error] = I18n.t('auth.invalid_reset_password_token') - redirect_to new_password_path(resource_name) + redirect_to new_password_path(resource_name), flash: { error: t('auth.invalid_reset_password_token') } end def reset_password_token_is_valid? diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index d850e05e94f..08b01d6b108 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -22,7 +22,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController end def destroy - if current_account.moved_to_account_id.present? + if current_account.moved? current_account.update!(moved_to_account: nil) ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) end diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index ee2fc5dc80f..fe59bdc4917 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -8,8 +8,7 @@ class Settings::SessionsController < Settings::BaseController def destroy @session.destroy! - flash[:notice] = I18n.t('sessions.revoke_success') - redirect_to edit_user_registration_path + redirect_to edit_user_registration_path, notice: t('sessions.revoke_success') end private diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index 9714d54f954..83dedb411d4 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -52,7 +52,7 @@ module Settings end else flash[:error] = I18n.t('webauthn_credentials.create.error') - status = :unprocessable_entity + status = :unprocessable_content end else flash[:error] = t('webauthn_credentials.create.error') @@ -86,13 +86,11 @@ module Settings private def redirect_invalid_otp - flash[:error] = t('webauthn_credentials.otp_required') - redirect_to settings_two_factor_authentication_methods_path + redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.otp_required') } end def redirect_invalid_webauthn - flash[:error] = t('webauthn_credentials.not_enabled') - redirect_to settings_two_factor_authentication_methods_path + redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.not_enabled') } end end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 341b0e64729..af6bebf36fd 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -11,6 +11,7 @@ class StatusesController < ApplicationController before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :redirect_to_original, only: :show + before_action :verify_embed_allowed, only: :embed after_action :set_link_headers @@ -40,8 +41,6 @@ class StatusesController < ApplicationController end def embed - return not_found if @status.hidden? || @status.reblog? - expires_in 180, public: true response.headers.delete('X-Frame-Options') @@ -50,6 +49,10 @@ class StatusesController < ApplicationController private + def verify_embed_allowed + not_found if @status.hidden? || @status.reblog? + end + def set_link_headers response.headers['Link'] = LinkHeader.new( [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]] diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 859f9246876..4a55a36ecd1 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -13,6 +13,8 @@ module Admin::ActionLogsHelper end when 'UserRole' link_to log.human_identifier, admin_roles_path(log.target_id) + when 'UsernameBlock' + link_to log.human_identifier, edit_admin_username_block_path(log.target_id) when 'Report' link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id) when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain' diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 77ddee1122c..885f578fd0d 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -39,6 +39,12 @@ module ContextHelper 'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' }, 'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' }, }, + quote_authorizations: { + 'gts' => 'https://gotosocial.org/ns#', + 'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' }, + 'interactingObject' => { '@id' => 'gts:interactingObject' }, + 'interactionTarget' => { '@id' => 'gts:interactionTarget' }, + }, }.freeze def full_context diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb deleted file mode 100644 index 0800601f98b..00000000000 --- a/app/helpers/email_helper.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module EmailHelper - def self.included(base) - base.extend(self) - end - - def email_to_canonical_email(str) - username, domain = str.downcase.split('@', 2) - username, = username.delete('.').split('+', 2) - - "#{username}@#{domain}" - end - - def email_to_canonical_email_hash(str) - Digest::SHA2.new(256).hexdigest(email_to_canonical_email(str)) - end -end diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 1b364a000cf..c27edbb0730 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -65,12 +65,12 @@ module FormattingHelper end def rss_content_preroll(status) - if status.spoiler_text? - safe_join [ - tag.p { spoiler_with_warning(status) }, - tag.hr, - ] - end + return unless status.spoiler_text? + + safe_join [ + tag.p { spoiler_with_warning(status) }, + tag.hr, + ] end def spoiler_with_warning(status) @@ -81,10 +81,10 @@ module FormattingHelper end def rss_content_postroll(status) - if status.preloadable_poll - tag.p do - poll_option_tags(status) - end + return unless status.preloadable_poll + + tag.p do + poll_option_tags(status) end end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index c5b83326db3..79e28c983af 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -39,18 +39,8 @@ module HomeHelper end end - def obscured_counter(count) - if count <= 0 - '0' - elsif count == 1 - '1' - else - '1+' - end - end - - def custom_field_classes(field) - if field.verified? + def field_verified_class(verified) + if verified 'verified' else 'emojify' diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/json_ld_helper.rb index 078aba456ae..675d8b87309 100644 --- a/app/helpers/json_ld_helper.rb +++ b/app/helpers/json_ld_helper.rb @@ -134,7 +134,7 @@ module JsonLdHelper patch_for_forwarding!(value, compacted_value) elsif value.is_a?(Array) compacted_value = [compacted_value] unless compacted_value.is_a?(Array) - return if value.size != compacted_value.size + return nil if value.size != compacted_value.size compacted[key] = value.zip(compacted_value).map do |v, vc| if v.is_a?(Hash) && vc.is_a?(Hash) diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index 0f24063385b..00b4a6d2b3f 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -24,24 +24,24 @@ module ThemeHelper end def custom_stylesheet - if active_custom_stylesheet.present? - stylesheet_link_tag( - custom_css_path(active_custom_stylesheet), - host: root_url, - media: :all, - skip_pipeline: true - ) - end + return if active_custom_stylesheet.blank? + + stylesheet_link_tag( + custom_css_path(active_custom_stylesheet), + host: root_url, + media: :all, + skip_pipeline: true + ) end private def active_custom_stylesheet - if cached_custom_css_digest.present? - [:custom, cached_custom_css_digest.to_s.first(8)] - .compact_blank - .join('-') - end + return if cached_custom_css_digest.blank? + + [:custom, cached_custom_css_digest.to_s.first(8)] + .compact_blank + .join('-') end def cached_custom_css_digest diff --git a/app/javascript/images/mailer-new/heading/README.md b/app/javascript/images/mailer-new/heading/README.md index ecd4b949e76..9fb6841f14b 100644 --- a/app/javascript/images/mailer-new/heading/README.md +++ b/app/javascript/images/mailer-new/heading/README.md @@ -1 +1,3 @@ Images in this folder are based on [Tabler.io icons](https://tabler.io/icons). + +Seems to be 1.5 width icons scaled to 64×64px and centered above a blue square with round corners (24px). diff --git a/app/javascript/images/mailer-new/heading/quote.png b/app/javascript/images/mailer-new/heading/quote.png new file mode 100644 index 00000000000..c2af73282f1 Binary files /dev/null and b/app/javascript/images/mailer-new/heading/quote.png differ diff --git a/app/javascript/mastodon/actions/interactions_typed.ts b/app/javascript/mastodon/actions/interactions_typed.ts index f58faffa86d..832ea189104 100644 --- a/app/javascript/mastodon/actions/interactions_typed.ts +++ b/app/javascript/mastodon/actions/interactions_typed.ts @@ -1,4 +1,8 @@ -import { apiReblog, apiUnreblog } from 'mastodon/api/interactions'; +import { + apiReblog, + apiUnreblog, + apiRevokeQuote, +} from 'mastodon/api/interactions'; import type { StatusVisibility } from 'mastodon/models/status'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; @@ -33,3 +37,19 @@ export const unreblog = createDataLoadingThunk( return discardLoadData; }, ); + +export const revokeQuote = createDataLoadingThunk( + 'status/revoke_quote', + ({ + statusId, + quotedStatusId, + }: { + statusId: string; + quotedStatusId: string; + }) => apiRevokeQuote(quotedStatusId, statusId), + (data, { dispatch, discardLoadData }) => { + dispatch(importFetchedStatus(data)); + + return discardLoadData; + }, +); diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 43863254818..7e162e5e51c 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -31,7 +31,9 @@ import { NOTIFICATIONS_FILTER_SET } from './notifications'; import { saveSettings } from './settings'; function excludeAllTypesExcept(filter: string) { - return allNotificationTypes.filter((item) => item !== filter); + return allNotificationTypes.filter( + (item) => item !== filter && !(item === 'quote' && filter === 'mention'), + ); } function getExcludedTypes(state: RootState) { @@ -156,12 +158,15 @@ export const processNewNotificationForGroups = createAppAsyncThunk( const showInColumn = activeFilter === 'all' ? notificationShows[notification.type] !== false - : activeFilter === notification.type; + : activeFilter === notification.type || + (activeFilter === 'mention' && notification.type === 'quote'); if (!showInColumn) return; if ( - (notification.type === 'mention' || notification.type === 'update') && + (notification.type === 'mention' || + notification.type === 'update' || + notification.type === 'quote') && notification.status?.filtered ) { const filters = notification.status.filtered.filter((result) => diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 2499b8da1d7..cbfddc750f3 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -31,7 +31,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; - if (['mention', 'status'].includes(notification.type) && notification.status.filtered) { + if (['mention', 'status', 'quote'].includes(notification.type) && notification.status.filtered) { const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); if (filters.some(result => result.filter.filter_action === 'hide')) { diff --git a/app/javascript/mastodon/api/interactions.ts b/app/javascript/mastodon/api/interactions.ts index 118b5f06d20..5ffa5d15076 100644 --- a/app/javascript/mastodon/api/interactions.ts +++ b/app/javascript/mastodon/api/interactions.ts @@ -8,3 +8,8 @@ export const apiReblog = (statusId: string, visibility: StatusVisibility) => export const apiUnreblog = (statusId: string) => apiRequestPost(`v1/statuses/${statusId}/unreblog`); + +export const apiRevokeQuote = (quotedStatusId: string, statusId: string) => + apiRequestPost( + `v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`, + ); diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index b93054a1f6f..913a201fef4 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -37,7 +37,7 @@ export interface BaseApiAccountJSON { roles?: ApiAccountJSON[]; statuses_count: number; uri: string; - url: string; + url?: string; username: string; moved?: ApiAccountJSON; suspended?: boolean; diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts index 190d8c83966..69fd17a2563 100644 --- a/app/javascript/mastodon/api_types/notifications.ts +++ b/app/javascript/mastodon/api_types/notifications.ts @@ -13,6 +13,7 @@ export const allNotificationTypes = [ 'favourite', 'reblog', 'mention', + 'quote', 'poll', 'status', 'update', @@ -28,6 +29,7 @@ export type NotificationWithStatusType = | 'reblog' | 'status' | 'mention' + | 'quote' | 'poll' | 'update'; diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx index cdac41b8a7d..b720b4746d0 100644 --- a/app/javascript/mastodon/components/account_bio.tsx +++ b/app/javascript/mastodon/components/account_bio.tsx @@ -3,8 +3,8 @@ import { useCallback } from 'react'; import { useLinks } from 'mastodon/hooks/useLinks'; import { EmojiHTML } from '../features/emoji/emoji_html'; -import { isFeatureEnabled } from '../initial_state'; import { useAppSelector } from '../store'; +import { isModernEmojiEnabled } from '../utils/environment'; interface AccountBioProps { className: string; @@ -32,9 +32,7 @@ export const AccountBio: React.FC = ({ if (!account) { return ''; } - return isFeatureEnabled('modern_emojis') - ? account.note - : account.note_emojified; + return isModernEmojiEnabled() ? account.note : account.note_emojified; }); const extraEmojis = useAppSelector((state) => { const account = state.accounts.get(accountId); diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx index 8e3a434c14b..d7d0b5f2ce0 100644 --- a/app/javascript/mastodon/components/gifv.tsx +++ b/app/javascript/mastodon/components/gifv.tsx @@ -37,7 +37,6 @@ export const GIFV = forwardRef( role='button' tabIndex={0} aria-label={alt} - title={alt} lang={lang} onClick={handleClick} /> @@ -49,7 +48,6 @@ export const GIFV = forwardRef( role='button' tabIndex={0} aria-label={alt} - title={alt} lang={lang} width={width} height={height} diff --git a/app/javascript/mastodon/components/hotkeys/index.tsx b/app/javascript/mastodon/components/hotkeys/index.tsx index b5e0de4c594..7e840ab9558 100644 --- a/app/javascript/mastodon/components/hotkeys/index.tsx +++ b/app/javascript/mastodon/components/hotkeys/index.tsx @@ -40,7 +40,11 @@ type KeyMatcher = ( */ function just(keyName: string): KeyMatcher { return (event) => ({ - isMatch: normalizeKey(event.key) === keyName, + isMatch: + normalizeKey(event.key) === keyName && + !event.altKey && + !event.ctrlKey && + !event.metaKey, priority: hotkeyPriority.singleKey, }); } diff --git a/app/javascript/mastodon/components/learn_more_link.tsx b/app/javascript/mastodon/components/learn_more_link.tsx new file mode 100644 index 00000000000..b5337794c95 --- /dev/null +++ b/app/javascript/mastodon/components/learn_more_link.tsx @@ -0,0 +1,63 @@ +import { useState, useRef, useCallback, useId } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import Overlay from 'react-overlays/Overlay'; + +export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const accessibilityId = useId(); + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + + const handleClick = useCallback(() => { + setOpen(!open); + }, [open, setOpen]); + + return ( + <> + + + + {({ props }) => ( +
+
{children}
+ +
+ +
+
+ )} +
+ + ); +}; diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index ec5a9780cb7..663fc53407c 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -67,21 +67,28 @@ const messages = defineMessages({ unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, filter: { id: 'status.filter', defaultMessage: 'Filter this post' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, + revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}’s post' }, }); -const mapStateToProps = (state, { status }) => ({ - relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), -}); +const mapStateToProps = (state, { status }) => { + const quotedStatusId = status.getIn(['quote', 'quoted_status']); + return ({ + relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), + quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null, + }); +}; class StatusActionBar extends ImmutablePureComponent { static propTypes = { identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, relationship: ImmutablePropTypes.record, + quotedAccountId: ImmutablePropTypes.string, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onRevokeQuote: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, onMute: PropTypes.func, @@ -110,6 +117,7 @@ class StatusActionBar extends ImmutablePureComponent { updateOnProps = [ 'status', 'relationship', + 'quotedAccountId', 'withDismiss', ]; @@ -190,6 +198,10 @@ class StatusActionBar extends ImmutablePureComponent { } }; + handleRevokeQuoteClick = () => { + this.props.onRevokeQuote(this.props.status); + } + handleBlockClick = () => { const { status, relationship, onBlock, onUnblock } = this.props; const account = status.get('account'); @@ -241,7 +253,7 @@ class StatusActionBar extends ImmutablePureComponent { }; render () { - const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; + const { status, relationship, quotedAccountId, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -291,6 +303,10 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); menu.push(null); + if (quotedAccountId === me) { + menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true }); + } + if (relationship && relationship.get('muting')) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); } else { diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index c82b23269bf..288927977ca 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -15,8 +15,9 @@ import { undoStatusTranslation } from 'mastodon/actions/statuses'; import { Icon } from 'mastodon/components/icon'; import { Poll } from 'mastodon/components/poll'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import { autoPlayGif, isFeatureEnabled, languages as preloadedLanguages } from 'mastodon/initial_state'; +import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; import { EmojiHTML } from '../features/emoji/emoji_html'; +import { isModernEmojiEnabled } from '../utils/environment'; const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) @@ -28,7 +29,7 @@ const supportsTranslator = 'Translator' in globalThis; * @returns {string} */ export function getStatusContent(status) { - if (isFeatureEnabled('modern_emojis')) { + if (isModernEmojiEnabled()) { return status.getIn(['translation', 'content']) || status.get('content'); } return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml'); @@ -51,13 +52,13 @@ class TranslateButton extends PureComponent { return (
-
- -
- + +
+ +
); } @@ -149,6 +150,16 @@ class StatusContent extends PureComponent { onCollapsedToggle(collapsed); } + + // Remove quote fallback link from the DOM so it doesn't + // mess with paragraph margins + if (!!status.get('quote')) { + const inlineQuote = node.querySelector('.quote-inline'); + + if (inlineQuote) { + inlineQuote.remove(); + } + } } handleMouseEnter = ({ currentTarget }) => { diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx index 70b7968fba1..c3055aeeab5 100644 --- a/app/javascript/mastodon/components/status_list.jsx +++ b/app/javascript/mastodon/components/status_list.jsx @@ -41,9 +41,11 @@ export default class StatusList extends ImmutablePureComponent { }; componentDidMount() { - this.columnHeaderHeight = parseFloat( - getComputedStyle(this.node.node).getPropertyValue('--column-header-height') - ) || 0; + this.columnHeaderHeight = this.node?.node + ? parseFloat( + getComputedStyle(this.node.node).getPropertyValue('--column-header-height') + ) || 0 + : 0; } getFeaturedStatusCount = () => { @@ -69,8 +71,8 @@ export default class StatusList extends ImmutablePureComponent { }; _selectChild = (id, index, direction) => { - const listContainer = this.node.node; - let listItem = listContainer.querySelector( + const listContainer = this.node?.node; + let listItem = listContainer?.querySelector( // :nth-child uses 1-based indexing `.item-list > :nth-child(${index + 1 + direction})` ); diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index d3d8b58c33d..8d43ea1819a 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -3,19 +3,15 @@ import { useEffect, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { Link } from 'react-router-dom'; import type { Map as ImmutableMap } from 'immutable'; -import ArticleIcon from '@/material-icons/400-24px/article.svg?react'; -import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; -import { Icon } from 'mastodon/components/icon'; +import { LearnMoreLink } from 'mastodon/components/learn_more_link'; import StatusContainer from 'mastodon/containers/status_container'; import type { Status } from 'mastodon/models/status'; import type { RootState } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import QuoteIcon from '../../images/quote.svg?react'; import { fetchStatus } from '../actions/statuses'; import { makeGetStatus } from '../selectors'; @@ -31,7 +27,6 @@ const QuoteWrapper: React.FC<{ 'status__quote--error': isError, })} > - {children} ); @@ -45,27 +40,20 @@ const NestedQuoteLink: React.FC<{ accountId ? state.accounts.get(accountId) : undefined, ); - const quoteAuthorName = account?.display_name_html; + const quoteAuthorName = account?.acct; if (!quoteAuthorName) { return null; } - const quoteAuthorElement = ( - - ); - const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`; - return ( - +
- - - +
); }; @@ -112,39 +100,42 @@ export const QuotedStatus: React.FC<{ defaultMessage='Hidden due to one of your filters' /> ); - } else if (quoteState === 'deleted') { - quoteError = ( - - ); - } else if (quoteState === 'unauthorized') { - quoteError = ( - - ); } else if (quoteState === 'pending') { quoteError = ( - + <> + + + +
+ +
+

+ +

+
+ ); - } else if (quoteState === 'rejected' || quoteState === 'revoked') { + } else if ( + !status || + !quotedStatusId || + quoteState === 'deleted' || + quoteState === 'rejected' || + quoteState === 'revoked' || + quoteState === 'unauthorized' + ) { quoteError = ( - ); - } else if (!status || !quotedStatusId) { - quoteError = ( - ); } @@ -168,7 +159,7 @@ export const QuotedStatus: React.FC<{ isQuotedPost id={quotedStatusId} contextType={contextType} - avatarSize={40} + avatarSize={32} > {canRenderChildQuote && ( ({ } }, + onRevokeQuote (status) { + dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }})); + }, + onEdit (status) { dispatch((_, getState) => { let state = getState(); diff --git a/app/javascript/mastodon/features/emoji/database.test.ts b/app/javascript/mastodon/features/emoji/database.test.ts new file mode 100644 index 00000000000..0689fd7c542 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/database.test.ts @@ -0,0 +1,139 @@ +import { IDBFactory } from 'fake-indexeddb'; + +import { unicodeEmojiFactory } from '@/testing/factories'; + +import { + putEmojiData, + loadEmojiByHexcode, + searchEmojisByHexcodes, + searchEmojisByTag, + testClear, + testGet, +} from './database'; + +describe('emoji database', () => { + afterEach(() => { + testClear(); + indexedDB = new IDBFactory(); + }); + describe('putEmojiData', () => { + test('adds to loaded locales', async () => { + const { loadedLocales } = await testGet(); + expect(loadedLocales).toHaveLength(0); + await putEmojiData([], 'en'); + expect(loadedLocales).toContain('en'); + }); + + test('loads emoji into indexedDB', async () => { + await putEmojiData([unicodeEmojiFactory()], 'en'); + const { db } = await testGet(); + await expect(db.get('en', 'test')).resolves.toEqual( + unicodeEmojiFactory(), + ); + }); + }); + + describe('loadEmojiByHexcode', () => { + test('throws if the locale is not loaded', async () => { + await expect(loadEmojiByHexcode('en', 'test')).rejects.toThrowError( + 'Locale en', + ); + }); + + test('retrieves the emoji', async () => { + await putEmojiData([unicodeEmojiFactory()], 'en'); + await expect(loadEmojiByHexcode('test', 'en')).resolves.toEqual( + unicodeEmojiFactory(), + ); + }); + + test('returns undefined if not found', async () => { + await putEmojiData([], 'en'); + await expect(loadEmojiByHexcode('test', 'en')).resolves.toBeUndefined(); + }); + }); + + describe('searchEmojisByHexcodes', () => { + const data = [ + unicodeEmojiFactory({ hexcode: 'not a number' }), + unicodeEmojiFactory({ hexcode: '1' }), + unicodeEmojiFactory({ hexcode: '2' }), + unicodeEmojiFactory({ hexcode: '3' }), + unicodeEmojiFactory({ hexcode: 'another not a number' }), + ]; + beforeEach(async () => { + await putEmojiData(data, 'en'); + }); + test('finds emoji in consecutive range', async () => { + const actual = await searchEmojisByHexcodes(['1', '2', '3'], 'en'); + expect(actual).toHaveLength(3); + }); + + test('finds emoji in split range', async () => { + const actual = await searchEmojisByHexcodes(['1', '3'], 'en'); + expect(actual).toHaveLength(2); + expect(actual).toContainEqual(data.at(1)); + expect(actual).toContainEqual(data.at(3)); + }); + + test('finds emoji with non-numeric range', async () => { + const actual = await searchEmojisByHexcodes( + ['3', 'not a number', '1'], + 'en', + ); + expect(actual).toHaveLength(3); + expect(actual).toContainEqual(data.at(0)); + expect(actual).toContainEqual(data.at(1)); + expect(actual).toContainEqual(data.at(3)); + }); + + test('not found emoji are not returned', async () => { + const actual = await searchEmojisByHexcodes(['not found'], 'en'); + expect(actual).toHaveLength(0); + }); + + test('only found emojis are returned', async () => { + const actual = await searchEmojisByHexcodes( + ['another not a number', 'not found'], + 'en', + ); + expect(actual).toHaveLength(1); + expect(actual).toContainEqual(data.at(4)); + }); + }); + + describe('searchEmojisByTag', () => { + const data = [ + unicodeEmojiFactory({ hexcode: 'test1', tags: ['test 1'] }), + unicodeEmojiFactory({ + hexcode: 'test2', + tags: ['test 2', 'something else'], + }), + unicodeEmojiFactory({ hexcode: 'test3', tags: ['completely different'] }), + ]; + beforeEach(async () => { + await putEmojiData(data, 'en'); + }); + test('finds emojis with tag', async () => { + const actual = await searchEmojisByTag('test 1', 'en'); + expect(actual).toHaveLength(1); + expect(actual).toContainEqual(data.at(0)); + }); + + test('finds emojis starting with tag', async () => { + const actual = await searchEmojisByTag('test', 'en'); + expect(actual).toHaveLength(2); + expect(actual).not.toContainEqual(data.at(2)); + }); + + test('does not find emojis ending with tag', async () => { + const actual = await searchEmojisByTag('else', 'en'); + expect(actual).toHaveLength(0); + }); + + test('finds nothing with invalid tag', async () => { + const actual = await searchEmojisByTag('not found', 'en'); + expect(actual).toHaveLength(0); + }); + }); +}); diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts index 0b8ddd34fbe..0e8ada1d0e0 100644 --- a/app/javascript/mastodon/features/emoji/database.ts +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -9,6 +9,7 @@ import type { UnicodeEmojiData, LocaleOrCustom, } from './types'; +import { emojiLogger } from './utils'; interface EmojiDB extends LocaleTables, DBSchema { custom: { @@ -36,40 +37,63 @@ interface LocaleTable { } type LocaleTables = Record; +type Database = IDBPDatabase; + const SCHEMA_VERSION = 1; -let db: IDBPDatabase | null = null; +const loadedLocales = new Set(); -async function loadDB() { - if (db) { - return db; - } - db = await openDB('mastodon-emoji', SCHEMA_VERSION, { - upgrade(database) { - const customTable = database.createObjectStore('custom', { - keyPath: 'shortcode', - autoIncrement: false, - }); - customTable.createIndex('category', 'category'); +const log = emojiLogger('database'); - database.createObjectStore('etags'); +// Loads the database in a way that ensures it's only loaded once. +const loadDB = (() => { + let dbPromise: Promise | null = null; - for (const locale of SUPPORTED_LOCALES) { - const localeTable = database.createObjectStore(locale, { - keyPath: 'hexcode', + // Actually load the DB. + async function initDB() { + const db = await openDB('mastodon-emoji', SCHEMA_VERSION, { + upgrade(database) { + const customTable = database.createObjectStore('custom', { + keyPath: 'shortcode', autoIncrement: false, }); - localeTable.createIndex('group', 'group'); - localeTable.createIndex('label', 'label'); - localeTable.createIndex('order', 'order'); - localeTable.createIndex('tags', 'tags', { multiEntry: true }); - } - }, - }); - return db; -} + customTable.createIndex('category', 'category'); + + database.createObjectStore('etags'); + + for (const locale of SUPPORTED_LOCALES) { + const localeTable = database.createObjectStore(locale, { + keyPath: 'hexcode', + autoIncrement: false, + }); + localeTable.createIndex('group', 'group'); + localeTable.createIndex('label', 'label'); + localeTable.createIndex('order', 'order'); + localeTable.createIndex('tags', 'tags', { multiEntry: true }); + } + }, + }); + await syncLocales(db); + return db; + } + + // Loads the database, or returns the existing promise if it hasn't resolved yet. + const loadPromise = async (): Promise => { + if (dbPromise) { + return dbPromise; + } + dbPromise = initDB(); + return dbPromise; + }; + // Special way to reset the database, used for unit testing. + loadPromise.reset = () => { + dbPromise = null; + }; + return loadPromise; +})(); export async function putEmojiData(emojis: UnicodeEmojiData[], locale: Locale) { + loadedLocales.add(locale); const db = await loadDB(); const trx = db.transaction(locale, 'readwrite'); await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); @@ -86,15 +110,15 @@ export async function putCustomEmojiData(emojis: CustomEmojiData[]) { export async function putLatestEtag(etag: string, localeString: string) { const locale = toSupportedLocaleOrCustom(localeString); const db = await loadDB(); - return db.put('etags', etag, locale); + await db.put('etags', etag, locale); } -export async function searchEmojiByHexcode( +export async function loadEmojiByHexcode( hexcode: string, localeString: string, ) { - const locale = toSupportedLocale(localeString); const db = await loadDB(); + const locale = toLoadedLocale(localeString); return db.get(locale, hexcode); } @@ -102,45 +126,39 @@ export async function searchEmojisByHexcodes( hexcodes: string[], localeString: string, ) { - const locale = toSupportedLocale(localeString); const db = await loadDB(); - return db.getAll( + const locale = toLoadedLocale(localeString); + const sortedCodes = hexcodes.toSorted(); + const results = await db.getAll( locale, - IDBKeyRange.bound(hexcodes[0], hexcodes[hexcodes.length - 1]), + IDBKeyRange.bound(sortedCodes.at(0), sortedCodes.at(-1)), ); + return results.filter((emoji) => hexcodes.includes(emoji.hexcode)); } -export async function searchEmojiByTag(tag: string, localeString: string) { - const locale = toSupportedLocale(localeString); - const range = IDBKeyRange.only(tag.toLowerCase()); +export async function searchEmojisByTag(tag: string, localeString: string) { const db = await loadDB(); + const locale = toLoadedLocale(localeString); + const range = IDBKeyRange.bound( + tag.toLowerCase(), + `${tag.toLowerCase()}\uffff`, + ); return db.getAllFromIndex(locale, 'tags', range); } -export async function searchCustomEmojiByShortcode(shortcode: string) { +export async function loadCustomEmojiByShortcode(shortcode: string) { const db = await loadDB(); return db.get('custom', shortcode); } export async function searchCustomEmojisByShortcodes(shortcodes: string[]) { const db = await loadDB(); - return db.getAll( + const sortedCodes = shortcodes.toSorted(); + const results = await db.getAll( 'custom', - IDBKeyRange.bound(shortcodes[0], shortcodes[shortcodes.length - 1]), + IDBKeyRange.bound(sortedCodes.at(0), sortedCodes.at(-1)), ); -} - -export async function findMissingLocales(localeStrings: string[]) { - const locales = new Set(localeStrings.map(toSupportedLocale)); - const missingLocales: Locale[] = []; - const db = await loadDB(); - for (const locale of locales) { - const rowCount = await db.count(locale); - if (!rowCount) { - missingLocales.push(locale); - } - } - return missingLocales; + return results.filter((emoji) => shortcodes.includes(emoji.shortcode)); } export async function loadLatestEtag(localeString: string) { @@ -153,3 +171,51 @@ export async function loadLatestEtag(localeString: string) { const etag = await db.get('etags', locale); return etag ?? null; } + +// Private functions + +async function syncLocales(db: Database) { + const locales = await Promise.all( + SUPPORTED_LOCALES.map( + async (locale) => + [locale, await hasLocale(locale, db)] satisfies [Locale, boolean], + ), + ); + for (const [locale, loaded] of locales) { + if (loaded) { + loadedLocales.add(locale); + } else { + loadedLocales.delete(locale); + } + } + log('Loaded %d locales: %o', loadedLocales.size, loadedLocales); +} + +function toLoadedLocale(localeString: string) { + const locale = toSupportedLocale(localeString); + if (localeString !== locale) { + log(`Locale ${locale} is different from provided ${localeString}`); + } + if (!loadedLocales.has(locale)) { + throw new Error(`Locale ${locale} is not loaded in emoji database`); + } + return locale; +} + +async function hasLocale(locale: Locale, db: Database): Promise { + if (loadedLocales.has(locale)) { + return true; + } + const rowCount = await db.count(locale); + return !!rowCount; +} + +// Testing helpers +export async function testGet() { + const db = await loadDB(); + return { db, loadedLocales }; +} +export function testClear() { + loadedLocales.clear(); + loadDB.reset(); +} diff --git a/app/javascript/mastodon/features/emoji/emoji_html.tsx b/app/javascript/mastodon/features/emoji/emoji_html.tsx index 27af2dda279..ed6f7a20465 100644 --- a/app/javascript/mastodon/features/emoji/emoji_html.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_html.tsx @@ -1,81 +1,48 @@ -import type { HTMLAttributes } from 'react'; -import { useEffect, useMemo, useState } from 'react'; +import type { ComponentPropsWithoutRef, ElementType } from 'react'; -import type { List as ImmutableList } from 'immutable'; -import { isList } from 'immutable'; +import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; -import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; -import { isFeatureEnabled } from '@/mastodon/initial_state'; -import type { CustomEmoji } from '@/mastodon/models/custom_emoji'; +import { useEmojify } from './hooks'; +import type { CustomEmojiMapArg } from './types'; -import { useEmojiAppState } from './hooks'; -import { emojifyElement } from './render'; -import type { ExtraCustomEmojiMap } from './types'; - -type EmojiHTMLProps = Omit< - HTMLAttributes, +type EmojiHTMLProps = Omit< + ComponentPropsWithoutRef, 'dangerouslySetInnerHTML' > & { htmlString: string; - extraEmojis?: ExtraCustomEmojiMap | ImmutableList; + extraEmojis?: CustomEmojiMapArg; + as?: Element; }; -export const EmojiHTML: React.FC = ({ - htmlString, +export const ModernEmojiHTML = ({ extraEmojis, + htmlString, + as: asElement, // Rename for syntax highlighting ...props -}) => { - if (isFeatureEnabled('modern_emojis')) { - return ( - - ); - } - return
; -}; +}: EmojiHTMLProps) => { + const Wrapper = asElement ?? 'div'; + const emojifiedHtml = useEmojify(htmlString, extraEmojis); -const ModernEmojiHTML: React.FC = ({ - extraEmojis: rawEmojis, - htmlString: text, - ...props -}) => { - const appState = useEmojiAppState(); - const [innerHTML, setInnerHTML] = useState(''); - - const extraEmojis: ExtraCustomEmojiMap = useMemo(() => { - if (!rawEmojis) { - return {}; - } - if (isList(rawEmojis)) { - return ( - rawEmojis.toJS() as ApiCustomEmojiJSON[] - ).reduce( - (acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }), - {}, - ); - } - return rawEmojis; - }, [rawEmojis]); - - useEffect(() => { - if (!text) { - return; - } - const cb = async () => { - const div = document.createElement('div'); - div.innerHTML = text; - const ele = await emojifyElement(div, appState, extraEmojis); - setInnerHTML(ele.innerHTML); - }; - void cb(); - }, [text, appState, extraEmojis]); - - if (!innerHTML) { + if (emojifiedHtml === null) { return null; } - return
; + return ( + + ); +}; + +export const EmojiHTML = ( + props: EmojiHTMLProps, +) => { + if (isModernEmojiEnabled()) { + return ; + } + const Wrapper = props.as ?? 'div'; + return ( + + ); }; diff --git a/app/javascript/mastodon/features/emoji/emoji_text.tsx b/app/javascript/mastodon/features/emoji/emoji_text.tsx deleted file mode 100644 index 253371391a4..00000000000 --- a/app/javascript/mastodon/features/emoji/emoji_text.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { useEmojiAppState } from './hooks'; -import { emojifyText } from './render'; - -interface EmojiTextProps { - text: string; -} - -export const EmojiText: React.FC = ({ text }) => { - const appState = useEmojiAppState(); - const [rendered, setRendered] = useState<(string | HTMLImageElement)[]>([]); - - useEffect(() => { - const cb = async () => { - const rendered = await emojifyText(text, appState); - setRendered(rendered ?? []); - }; - void cb(); - }, [text, appState]); - - if (rendered.length === 0) { - return null; - } - - return ( - <> - {rendered.map((fragment, index) => { - if (typeof fragment === 'string') { - return {fragment}; - } - return ( - {fragment.alt} - ); - })} - - ); -}; diff --git a/app/javascript/mastodon/features/emoji/hooks.ts b/app/javascript/mastodon/features/emoji/hooks.ts index fd38129a19b..3f397f4ef03 100644 --- a/app/javascript/mastodon/features/emoji/hooks.ts +++ b/app/javascript/mastodon/features/emoji/hooks.ts @@ -1,8 +1,64 @@ +import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; + +import { isList } from 'immutable'; + +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { useAppSelector } from '@/mastodon/store'; +import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { toSupportedLocale } from './locale'; import { determineEmojiMode } from './mode'; -import type { EmojiAppState } from './types'; +import type { + CustomEmojiMapArg, + EmojiAppState, + ExtraCustomEmojiMap, +} from './types'; +import { stringHasAnyEmoji } from './utils'; + +export function useEmojify(text: string, extraEmojis?: CustomEmojiMapArg) { + const [emojifiedText, setEmojifiedText] = useState(null); + + const appState = useEmojiAppState(); + const extra: ExtraCustomEmojiMap = useMemo(() => { + if (!extraEmojis) { + return {}; + } + if (isList(extraEmojis)) { + return ( + extraEmojis.toJS() as ApiCustomEmojiJSON[] + ).reduce( + (acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }), + {}, + ); + } + return extraEmojis; + }, [extraEmojis]); + + const emojify = useCallback( + async (input: string) => { + const wrapper = document.createElement('div'); + wrapper.innerHTML = input; + const { emojifyElement } = await import('./render'); + const result = await emojifyElement(wrapper, appState, extra); + if (result) { + setEmojifiedText(result.innerHTML); + } else { + setEmojifiedText(input); + } + }, + [appState, extra], + ); + useLayoutEffect(() => { + if (isModernEmojiEnabled() && !!text.trim() && stringHasAnyEmoji(text)) { + void emojify(text); + } else { + // If no emoji or we don't want to render, fall back. + setEmojifiedText(text); + } + }, [emojify, text]); + + return emojifiedText; +} export function useEmojiAppState(): EmojiAppState { const locale = useAppSelector((state) => @@ -12,5 +68,10 @@ export function useEmojiAppState(): EmojiAppState { determineEmojiMode(state.meta.get('emoji_style') as string), ); - return { currentLocale: locale, locales: [locale], mode }; + return { + currentLocale: locale, + locales: [locale], + mode, + darkTheme: document.body.classList.contains('theme-default'), + }; } diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts index 4f23dc5395e..99c16fe361c 100644 --- a/app/javascript/mastodon/features/emoji/index.ts +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -1,17 +1,21 @@ import initialState from '@/mastodon/initial_state'; +import { loadWorker } from '@/mastodon/utils/workers'; import { toSupportedLocale } from './locale'; +import { emojiLogger } from './utils'; const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en'); let worker: Worker | null = null; -export async function initializeEmoji() { +const log = emojiLogger('index'); + +export function initializeEmoji() { + log('initializing emojis'); if (!worker && 'Worker' in window) { try { - worker = new Worker(new URL('./worker', import.meta.url), { + worker = loadWorker(new URL('./worker', import.meta.url), { type: 'module', - credentials: 'omit', }); } catch (err) { console.warn('Error creating web worker:', err); @@ -21,9 +25,16 @@ export async function initializeEmoji() { if (worker) { // Assign worker to const to make TS happy inside the event listener. const thisWorker = worker; + const timeoutId = setTimeout(() => { + log('worker is not ready after timeout'); + worker = null; + void fallbackLoad(); + }, 500); thisWorker.addEventListener('message', (event: MessageEvent) => { const { data: message } = event; if (message === 'ready') { + log('worker ready, loading data'); + clearTimeout(timeoutId); thisWorker.postMessage('custom'); void loadEmojiLocale(userLocale); // Load English locale as well, because people are still used to @@ -31,15 +42,22 @@ export async function initializeEmoji() { if (userLocale !== 'en') { void loadEmojiLocale('en'); } + } else { + log('got worker message: %s', message); } }); } else { - const { importCustomEmojiData } = await import('./loader'); - await importCustomEmojiData(); - await loadEmojiLocale(userLocale); - if (userLocale !== 'en') { - await loadEmojiLocale('en'); - } + void fallbackLoad(); + } +} + +async function fallbackLoad() { + log('falling back to main thread for loading'); + const { importCustomEmojiData } = await import('./loader'); + await importCustomEmojiData(); + await loadEmojiLocale(userLocale); + if (userLocale !== 'en') { + await loadEmojiLocale('en'); } } diff --git a/app/javascript/mastodon/features/emoji/loader.ts b/app/javascript/mastodon/features/emoji/loader.ts index 482d9e5c359..72f57b6f6c0 100644 --- a/app/javascript/mastodon/features/emoji/loader.ts +++ b/app/javascript/mastodon/features/emoji/loader.ts @@ -2,7 +2,6 @@ import { flattenEmojiData } from 'emojibase'; import type { CompactEmoji, FlatCompactEmoji } from 'emojibase'; import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; -import { isDevelopment } from '@/mastodon/utils/environment'; import { putEmojiData, @@ -12,6 +11,9 @@ import { } from './database'; import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; import type { LocaleOrCustom } from './types'; +import { emojiLogger } from './utils'; + +const log = emojiLogger('loader'); export async function importEmojiData(localeString: string) { const locale = toSupportedLocale(localeString); @@ -20,6 +22,7 @@ export async function importEmojiData(localeString: string) { return; } const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis); + log('loaded %d for %s locale', flattenedEmojis.length, locale); await putEmojiData(flattenedEmojis, locale); } @@ -28,6 +31,7 @@ export async function importCustomEmojiData() { if (!emojis) { return; } + log('loaded %d custom emojis', emojis.length); await putCustomEmojiData(emojis); } @@ -36,15 +40,18 @@ async function fetchAndCheckEtag( ): Promise { const locale = toSupportedLocaleOrCustom(localeOrCustom); - let uri: string; + // Use location.origin as this script may be loaded from a CDN domain. + const url = new URL(location.origin); if (locale === 'custom') { - uri = '/api/v1/custom_emojis'; + url.pathname = '/api/v1/custom_emojis'; } else { - uri = `/packs${isDevelopment() ? '-dev' : ''}/emoji/${locale}.json`; + // This doesn't use isDevelopment() as that module loads initial state + // which breaks workers, as they cannot access the DOM. + url.pathname = `/packs${import.meta.env.DEV ? '-dev' : ''}/emoji/${locale}.json`; } const oldEtag = await loadLatestEtag(locale); - const response = await fetch(uri, { + const response = await fetch(url, { headers: { 'Content-Type': 'application/json', 'If-None-Match': oldEtag ?? '', // Send the old ETag to check for modifications diff --git a/app/javascript/mastodon/features/emoji/render.test.ts b/app/javascript/mastodon/features/emoji/render.test.ts index 23f85c36b3e..e9609e15dc5 100644 --- a/app/javascript/mastodon/features/emoji/render.test.ts +++ b/app/javascript/mastodon/features/emoji/render.test.ts @@ -1,94 +1,184 @@ +import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories'; + import { EMOJI_MODE_NATIVE, EMOJI_MODE_NATIVE_WITH_FLAGS, EMOJI_MODE_TWEMOJI, } from './constants'; -import { emojifyElement, tokenizeText } from './render'; -import type { CustomEmojiData, UnicodeEmojiData } from './types'; +import * as db from './database'; +import { + emojifyElement, + emojifyText, + testCacheClear, + tokenizeText, +} from './render'; +import type { EmojiAppState, ExtraCustomEmojiMap } from './types'; -vitest.mock('./database', () => ({ - searchCustomEmojisByShortcodes: vitest.fn( - () => - [ - { - shortcode: 'custom', - static_url: 'emoji/static', - url: 'emoji/custom', - category: 'test', - visible_in_picker: true, - }, - ] satisfies CustomEmojiData[], - ), - searchEmojisByHexcodes: vitest.fn( - () => - [ - { +function mockDatabase() { + return { + searchCustomEmojisByShortcodes: vi + .spyOn(db, 'searchCustomEmojisByShortcodes') + .mockResolvedValue([customEmojiFactory()]), + searchEmojisByHexcodes: vi + .spyOn(db, 'searchEmojisByHexcodes') + .mockResolvedValue([ + unicodeEmojiFactory({ hexcode: '1F60A', - group: 0, label: 'smiling face with smiling eyes', - order: 0, - tags: ['smile', 'happy'], unicode: '😊', - }, - { + }), + unicodeEmojiFactory({ hexcode: '1F1EA-1F1FA', - group: 0, label: 'flag-eu', - order: 0, - tags: ['flag', 'european union'], unicode: '🇪🇺', - }, - ] satisfies UnicodeEmojiData[], - ), - findMissingLocales: vitest.fn(() => []), -})); + }), + ]), + }; +} + +const expectedSmileImage = + '😊'; +const expectedFlagImage = + '🇪🇺'; +const expectedCustomEmojiImage = + ':custom:'; +const expectedRemoteCustomEmojiImage = + ':remote:'; + +const mockExtraCustom: ExtraCustomEmojiMap = { + remote: { + shortcode: 'remote', + static_url: 'remote.social/static', + url: 'remote.social/custom', + }, +}; + +function testAppState(state: Partial = {}) { + return { + locales: ['en'], + mode: EMOJI_MODE_TWEMOJI, + currentLocale: 'en', + darkTheme: false, + ...state, + } satisfies EmojiAppState; +} describe('emojifyElement', () => { - const testElement = document.createElement('div'); - testElement.innerHTML = '

Hello 😊🇪🇺!

:custom:

'; - - const expectedSmileImage = - '😊'; - const expectedFlagImage = - '🇪🇺'; - const expectedCustomEmojiImage = - ':custom:'; - - function cloneTestElement() { - return testElement.cloneNode(true) as HTMLElement; + function testElement(text = '

Hello 😊🇪🇺!

:custom:

') { + const testElement = document.createElement('div'); + testElement.innerHTML = text; + return testElement; } + afterEach(() => { + testCacheClear(); + vi.restoreAllMocks(); + }); + + test('caches element rendering results', async () => { + const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } = + mockDatabase(); + await emojifyElement(testElement(), testAppState()); + await emojifyElement(testElement(), testAppState()); + await emojifyElement(testElement(), testAppState()); + expect(searchEmojisByHexcodes).toHaveBeenCalledExactlyOnceWith( + ['1F1EA-1F1FA', '1F60A'], + 'en', + ); + expect(searchCustomEmojisByShortcodes).toHaveBeenCalledExactlyOnceWith([ + 'custom', + ]); + }); + test('emojifies custom emoji in native mode', async () => { - const emojifiedElement = await emojifyElement(cloneTestElement(), { - locales: ['en'], - mode: EMOJI_MODE_NATIVE, - currentLocale: 'en', - }); - expect(emojifiedElement.innerHTML).toBe( + const { searchEmojisByHexcodes } = mockDatabase(); + const actual = await emojifyElement( + testElement(), + testAppState({ mode: EMOJI_MODE_NATIVE }), + ); + assert(actual); + expect(actual.innerHTML).toBe( `

Hello 😊🇪🇺!

${expectedCustomEmojiImage}

`, ); + expect(searchEmojisByHexcodes).not.toHaveBeenCalled(); }); test('emojifies flag emoji in native-with-flags mode', async () => { - const emojifiedElement = await emojifyElement(cloneTestElement(), { - locales: ['en'], - mode: EMOJI_MODE_NATIVE_WITH_FLAGS, - currentLocale: 'en', - }); - expect(emojifiedElement.innerHTML).toBe( + const { searchEmojisByHexcodes } = mockDatabase(); + const actual = await emojifyElement( + testElement(), + testAppState({ mode: EMOJI_MODE_NATIVE_WITH_FLAGS }), + ); + assert(actual); + expect(actual.innerHTML).toBe( `

Hello 😊${expectedFlagImage}!

${expectedCustomEmojiImage}

`, ); + expect(searchEmojisByHexcodes).toHaveBeenCalledOnce(); }); test('emojifies everything in twemoji mode', async () => { - const emojifiedElement = await emojifyElement(cloneTestElement(), { - locales: ['en'], - mode: EMOJI_MODE_TWEMOJI, - currentLocale: 'en', - }); - expect(emojifiedElement.innerHTML).toBe( + const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } = + mockDatabase(); + const actual = await emojifyElement(testElement(), testAppState()); + assert(actual); + expect(actual.innerHTML).toBe( `

Hello ${expectedSmileImage}${expectedFlagImage}!

${expectedCustomEmojiImage}

`, ); + expect(searchEmojisByHexcodes).toHaveBeenCalledOnce(); + expect(searchCustomEmojisByShortcodes).toHaveBeenCalledOnce(); + }); + + test('emojifies with provided custom emoji', async () => { + const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } = + mockDatabase(); + const actual = await emojifyElement( + testElement('

hi :remote:

'), + testAppState(), + mockExtraCustom, + ); + assert(actual); + expect(actual.innerHTML).toBe( + `

hi ${expectedRemoteCustomEmojiImage}

`, + ); + expect(searchEmojisByHexcodes).not.toHaveBeenCalled(); + expect(searchCustomEmojisByShortcodes).not.toHaveBeenCalled(); + }); + + test('returns null when no emoji are found', async () => { + mockDatabase(); + const actual = await emojifyElement( + testElement('

here is just text :)

'), + testAppState(), + ); + expect(actual).toBeNull(); + }); +}); + +describe('emojifyText', () => { + test('returns original input when no emoji are in string', async () => { + const actual = await emojifyText('nothing here', testAppState()); + expect(actual).toBe('nothing here'); + }); + + test('renders Unicode emojis to twemojis', async () => { + mockDatabase(); + const actual = await emojifyText('Hello 😊🇪🇺!', testAppState()); + expect(actual).toBe(`Hello ${expectedSmileImage}${expectedFlagImage}!`); + }); + + test('renders custom emojis', async () => { + mockDatabase(); + const actual = await emojifyText('Hello :custom:!', testAppState()); + expect(actual).toBe(`Hello ${expectedCustomEmojiImage}!`); + }); + + test('renders provided extra emojis', async () => { + const actual = await emojifyText( + 'remote emoji :remote:', + testAppState(), + mockExtraCustom, + ); + expect(actual).toBe(`remote emoji ${expectedRemoteCustomEmojiImage}`); }); }); diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index 6ef9492147c..8d2299fd89e 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -1,8 +1,7 @@ -import type { Locale } from 'emojibase'; -import EMOJI_REGEX from 'emojibase-regex/emoji-loose'; - import { autoPlayGif } from '@/mastodon/initial_state'; +import { createLimitedCache } from '@/mastodon/utils/cache'; import { assetHost } from '@/mastodon/utils/config'; +import * as perf from '@/mastodon/utils/performance'; import { EMOJI_MODE_NATIVE, @@ -12,11 +11,9 @@ import { EMOJI_STATE_MISSING, } from './constants'; import { - findMissingLocales, searchCustomEmojisByShortcodes, searchEmojisByHexcodes, } from './database'; -import { loadEmojiLocale } from './index'; import { emojiToUnicodeHex, twemojiHasBorder, @@ -34,18 +31,38 @@ import type { LocaleOrCustom, UnicodeEmojiToken, } from './types'; -import { stringHasUnicodeFlags } from './utils'; +import { + anyEmojiRegex, + emojiLogger, + stringHasAnyEmoji, + stringHasUnicodeFlags, +} from './utils'; -const localeCacheMap = new Map([ - [EMOJI_TYPE_CUSTOM, new Map()], -]); +const log = emojiLogger('render'); -// Emojifies an element. This modifies the element in place, replacing text nodes with emojified versions. +/** + * Emojifies an element. This modifies the element in place, replacing text nodes with emojified versions. + */ export async function emojifyElement( element: Element, appState: EmojiAppState, extraEmojis: ExtraCustomEmojiMap = {}, -): Promise { +): Promise { + const cacheKey = createCacheKey(element, appState, extraEmojis); + const cached = getCached(cacheKey); + if (cached !== undefined) { + log('Cache hit on %s', element.outerHTML); + if (cached === null) { + return null; + } + element.innerHTML = cached; + return element; + } + if (!stringHasAnyEmoji(element.innerHTML)) { + updateCache(cacheKey, null); + return null; + } + perf.start('emojifyElement()'); const queue: (HTMLElement | Text)[] = [element]; while (queue.length > 0) { const current = queue.shift(); @@ -61,7 +78,7 @@ export async function emojifyElement( current.textContent && (current instanceof Text || !current.hasChildNodes()) ) { - const renderedContent = await emojifyText( + const renderedContent = await textToElementArray( current.textContent, appState, extraEmojis, @@ -70,7 +87,7 @@ export async function emojifyElement( if (!(current instanceof Text)) { current.textContent = null; // Clear the text content if it's not a Text node. } - current.replaceWith(renderedToHTMLFragment(renderedContent)); + current.replaceWith(renderedToHTML(renderedContent)); } continue; } @@ -81,6 +98,8 @@ export async function emojifyElement( } } } + updateCache(cacheKey, element.innerHTML); + perf.stop('emojifyElement()'); return element; } @@ -88,7 +107,54 @@ export async function emojifyText( text: string, appState: EmojiAppState, extraEmojis: ExtraCustomEmojiMap = {}, +): Promise { + const cacheKey = createCacheKey(text, appState, extraEmojis); + const cached = getCached(cacheKey); + if (cached !== undefined) { + log('Cache hit on %s', text); + return cached ?? text; + } + if (!stringHasAnyEmoji(text)) { + updateCache(cacheKey, null); + return text; + } + const eleArray = await textToElementArray(text, appState, extraEmojis); + if (!eleArray) { + updateCache(cacheKey, null); + return text; + } + const rendered = renderedToHTML(eleArray, document.createElement('div')); + updateCache(cacheKey, rendered.innerHTML); + return rendered.innerHTML; +} + +// Private functions + +const { + set: updateCache, + get: getCached, + clear: cacheClear, +} = createLimitedCache({ log: log.extend('cache') }); + +function createCacheKey( + input: HTMLElement | string, + appState: EmojiAppState, + extraEmojis: ExtraCustomEmojiMap, ) { + return JSON.stringify([ + input instanceof HTMLElement ? input.outerHTML : input, + appState, + extraEmojis, + ]); +} + +type EmojifiedTextArray = (string | HTMLImageElement)[]; + +async function textToElementArray( + text: string, + appState: EmojiAppState, + extraEmojis: ExtraCustomEmojiMap = {}, +): Promise { // Exit if no text to convert. if (!text.trim()) { return null; @@ -102,10 +168,9 @@ export async function emojifyText( } // Get all emoji from the state map, loading any missing ones. - await ensureLocalesAreLoaded(appState.locales); - await loadMissingEmojiIntoCache(tokens, appState.locales); + await loadMissingEmojiIntoCache(tokens, appState, extraEmojis); - const renderedFragments: (string | HTMLImageElement)[] = []; + const renderedFragments: EmojifiedTextArray = []; for (const token of tokens) { if (typeof token !== 'string' && shouldRenderImage(token, appState.mode)) { let state: EmojiState | undefined; @@ -125,7 +190,7 @@ export async function emojifyText( // If the state is valid, create an image element. Otherwise, just append as text. if (state && typeof state !== 'string') { - const image = stateToImage(state); + const image = stateToImage(state, appState); renderedFragments.push(image); continue; } @@ -137,21 +202,6 @@ export async function emojifyText( return renderedFragments; } -// Private functions - -async function ensureLocalesAreLoaded(locales: Locale[]) { - const missingLocales = await findMissingLocales(locales); - for (const locale of missingLocales) { - await loadEmojiLocale(locale); - } -} - -const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i; -const TOKENIZE_REGEX = new RegExp( - `(${EMOJI_REGEX.source}|${CUSTOM_EMOJI_REGEX.source})`, - 'g', -); - type TokenizedText = (string | EmojiToken)[]; export function tokenizeText(text: string): TokenizedText { @@ -161,7 +211,7 @@ export function tokenizeText(text: string): TokenizedText { const tokens = []; let lastIndex = 0; - for (const match of text.matchAll(TOKENIZE_REGEX)) { + for (const match of text.matchAll(anyEmojiRegex())) { if (match.index > lastIndex) { tokens.push(text.slice(lastIndex, match.index)); } @@ -189,8 +239,18 @@ export function tokenizeText(text: string): TokenizedText { return tokens; } +const localeCacheMap = new Map([ + [ + EMOJI_TYPE_CUSTOM, + createLimitedCache({ log: log.extend('custom') }), + ], +]); + function cacheForLocale(locale: LocaleOrCustom): EmojiStateMap { - return localeCacheMap.get(locale) ?? (new Map() as EmojiStateMap); + return ( + localeCacheMap.get(locale) ?? + createLimitedCache({ log: log.extend(locale) }) + ); } function emojiForLocale( @@ -203,7 +263,8 @@ function emojiForLocale( async function loadMissingEmojiIntoCache( tokens: TokenizedText, - locales: Locale[], + { mode, currentLocale }: EmojiAppState, + extraEmojis: ExtraCustomEmojiMap, ) { const missingUnicodeEmoji = new Set(); const missingCustomEmoji = new Set(); @@ -217,42 +278,41 @@ async function loadMissingEmojiIntoCache( // If this is a custom emoji, check it separately. if (token.type === EMOJI_TYPE_CUSTOM) { const code = token.code; + if (code in extraEmojis) { + continue; // We don't care about extra emoji. + } const emojiState = emojiForLocale(code, EMOJI_TYPE_CUSTOM); if (!emojiState) { missingCustomEmoji.add(code); } // Otherwise this is a unicode emoji, so check it against all locales. - } else { + } else if (shouldRenderImage(token, mode)) { const code = emojiToUnicodeHex(token.code); if (missingUnicodeEmoji.has(code)) { continue; // Already marked as missing. } - for (const locale of locales) { - const emojiState = emojiForLocale(code, locale); - if (!emojiState) { - // If it's missing in one locale, we consider it missing for all. - missingUnicodeEmoji.add(code); - } + const emojiState = emojiForLocale(code, currentLocale); + if (!emojiState) { + // If it's missing in one locale, we consider it missing for all. + missingUnicodeEmoji.add(code); } } } if (missingUnicodeEmoji.size > 0) { const missingEmojis = Array.from(missingUnicodeEmoji).toSorted(); - for (const locale of locales) { - const emojis = await searchEmojisByHexcodes(missingEmojis, locale); - const cache = cacheForLocale(locale); - for (const emoji of emojis) { - cache.set(emoji.hexcode, { type: EMOJI_TYPE_UNICODE, data: emoji }); - } - const notFoundEmojis = missingEmojis.filter((code) => - emojis.every((emoji) => emoji.hexcode !== code), - ); - for (const code of notFoundEmojis) { - cache.set(code, EMOJI_STATE_MISSING); // Mark as missing if not found, as it's probably not a valid emoji. - } - localeCacheMap.set(locale, cache); + const emojis = await searchEmojisByHexcodes(missingEmojis, currentLocale); + const cache = cacheForLocale(currentLocale); + for (const emoji of emojis) { + cache.set(emoji.hexcode, { type: EMOJI_TYPE_UNICODE, data: emoji }); } + const notFoundEmojis = missingEmojis.filter((code) => + emojis.every((emoji) => emoji.hexcode !== code), + ); + for (const code of notFoundEmojis) { + cache.set(code, EMOJI_STATE_MISSING); // Mark as missing if not found, as it's probably not a valid emoji. + } + localeCacheMap.set(currentLocale, cache); } if (missingCustomEmoji.size > 0) { @@ -288,22 +348,24 @@ function shouldRenderImage(token: EmojiToken, mode: EmojiMode): boolean { return true; } -function stateToImage(state: EmojiLoadedState) { +function stateToImage(state: EmojiLoadedState, appState: EmojiAppState) { const image = document.createElement('img'); image.draggable = false; image.classList.add('emojione'); if (state.type === EMOJI_TYPE_UNICODE) { const emojiInfo = twemojiHasBorder(unicodeToTwemojiHex(state.data.hexcode)); - if (emojiInfo.hasLightBorder) { - image.dataset.lightCode = `${emojiInfo.hexCode}_BORDER`; - } else if (emojiInfo.hasDarkBorder) { - image.dataset.darkCode = `${emojiInfo.hexCode}_BORDER`; + let fileName = emojiInfo.hexCode; + if ( + (appState.darkTheme && emojiInfo.hasDarkBorder) || + (!appState.darkTheme && emojiInfo.hasLightBorder) + ) { + fileName = `${emojiInfo.hexCode}_border`; } image.alt = state.data.unicode; image.title = state.data.label; - image.src = `${assetHost}/emoji/${emojiInfo.hexCode}.svg`; + image.src = `${assetHost}/emoji/${fileName}.svg`; } else { // Custom emoji const shortCode = `:${state.data.shortcode}:`; @@ -318,8 +380,16 @@ function stateToImage(state: EmojiLoadedState) { return image; } -function renderedToHTMLFragment(renderedArray: (string | HTMLImageElement)[]) { - const fragment = document.createDocumentFragment(); +function renderedToHTML(renderedArray: EmojifiedTextArray): DocumentFragment; +function renderedToHTML( + renderedArray: EmojifiedTextArray, + parent: ParentType, +): ParentType; +function renderedToHTML( + renderedArray: EmojifiedTextArray, + parent: ParentNode | null = null, +) { + const fragment = parent ?? document.createDocumentFragment(); for (const fragmentItem of renderedArray) { if (typeof fragmentItem === 'string') { fragment.appendChild(document.createTextNode(fragmentItem)); @@ -329,3 +399,9 @@ function renderedToHTMLFragment(renderedArray: (string | HTMLImageElement)[]) { } return fragment; } + +// Testing helpers +export const testCacheClear = () => { + cacheClear(); + localeCacheMap.clear(); +}; diff --git a/app/javascript/mastodon/features/emoji/types.ts b/app/javascript/mastodon/features/emoji/types.ts index f5932ed97fd..85bbe6d1a56 100644 --- a/app/javascript/mastodon/features/emoji/types.ts +++ b/app/javascript/mastodon/features/emoji/types.ts @@ -1,6 +1,10 @@ +import type { List as ImmutableList } from 'immutable'; + import type { FlatCompactEmoji, Locale } from 'emojibase'; import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; +import type { CustomEmoji } from '@/mastodon/models/custom_emoji'; +import type { LimitedCache } from '@/mastodon/utils/cache'; import type { EMOJI_MODE_NATIVE, @@ -22,6 +26,7 @@ export interface EmojiAppState { locales: Locale[]; currentLocale: Locale; mode: EmojiMode; + darkTheme: boolean; } export interface UnicodeEmojiToken { @@ -45,7 +50,7 @@ export interface EmojiStateUnicode { } export interface EmojiStateCustom { type: typeof EMOJI_TYPE_CUSTOM; - data: CustomEmojiData; + data: CustomEmojiRenderFields; } export type EmojiState = | EmojiStateMissing @@ -53,9 +58,16 @@ export type EmojiState = | EmojiStateCustom; export type EmojiLoadedState = EmojiStateUnicode | EmojiStateCustom; -export type EmojiStateMap = Map; +export type EmojiStateMap = LimitedCache; -export type ExtraCustomEmojiMap = Record; +export type CustomEmojiMapArg = + | ExtraCustomEmojiMap + | ImmutableList; +export type CustomEmojiRenderFields = Pick< + CustomEmojiData, + 'shortcode' | 'static_url' | 'url' +>; +export type ExtraCustomEmojiMap = Record; export interface TwemojiBorderInfo { hexCode: string; diff --git a/app/javascript/mastodon/features/emoji/utils.test.ts b/app/javascript/mastodon/features/emoji/utils.test.ts index 75cac8c5b4c..b9062294c47 100644 --- a/app/javascript/mastodon/features/emoji/utils.test.ts +++ b/app/javascript/mastodon/features/emoji/utils.test.ts @@ -1,8 +1,14 @@ -import { stringHasUnicodeEmoji, stringHasUnicodeFlags } from './utils'; +import { + stringHasAnyEmoji, + stringHasCustomEmoji, + stringHasUnicodeEmoji, + stringHasUnicodeFlags, +} from './utils'; -describe('stringHasEmoji', () => { +describe('stringHasUnicodeEmoji', () => { test.concurrent.for([ ['only text', false], + ['text with non-emoji symbols ™©', false], ['text with emoji 😀', true], ['multiple emojis 😀😃😄', true], ['emoji with skin tone 👍🏽', true], @@ -19,14 +25,14 @@ describe('stringHasEmoji', () => { ['emoji with enclosing keycap #️⃣', true], ['emoji with no visible glyph \u200D', false], ] as const)( - 'stringHasEmoji has emojis in "%s": %o', + 'stringHasUnicodeEmoji has emojis in "%s": %o', ([text, expected], { expect }) => { expect(stringHasUnicodeEmoji(text)).toBe(expected); }, ); }); -describe('stringHasFlags', () => { +describe('stringHasUnicodeFlags', () => { test.concurrent.for([ ['EU 🇪🇺', true], ['Germany 🇩🇪', true], @@ -45,3 +51,27 @@ describe('stringHasFlags', () => { }, ); }); + +describe('stringHasCustomEmoji', () => { + test('string with custom emoji returns true', () => { + expect(stringHasCustomEmoji(':custom: :test:')).toBeTruthy(); + }); + test('string without custom emoji returns false', () => { + expect(stringHasCustomEmoji('🏳️‍🌈 :🏳️‍🌈: text ™')).toBeFalsy(); + }); +}); + +describe('stringHasAnyEmoji', () => { + test('string without any emoji or characters', () => { + expect(stringHasAnyEmoji('normal text. 12356?!')).toBeFalsy(); + }); + test('string with non-emoji characters', () => { + expect(stringHasAnyEmoji('™©')).toBeFalsy(); + }); + test('has unicode emoji', () => { + expect(stringHasAnyEmoji('🏳️‍🌈🔥🇸🇹 👩‍🔬')).toBeTruthy(); + }); + test('has custom emoji', () => { + expect(stringHasAnyEmoji(':test: :custom:')).toBeTruthy(); + }); +}); diff --git a/app/javascript/mastodon/features/emoji/utils.ts b/app/javascript/mastodon/features/emoji/utils.ts index d00accea8c5..ce359199296 100644 --- a/app/javascript/mastodon/features/emoji/utils.ts +++ b/app/javascript/mastodon/features/emoji/utils.ts @@ -1,13 +1,56 @@ -import EMOJI_REGEX from 'emojibase-regex/emoji-loose'; +import debug from 'debug'; -export function stringHasUnicodeEmoji(text: string): boolean { - return EMOJI_REGEX.test(text); +import { emojiRegexPolyfill } from '@/mastodon/polyfills'; + +export function emojiLogger(segment: string) { + return debug(`emojis:${segment}`); } -// From https://github.com/talkjs/country-flag-emoji-polyfill/blob/master/src/index.ts#L49-L50 -const EMOJIS_FLAGS_REGEX = - /[\u{1F1E6}-\u{1F1FF}|\u{E0062}-\u{E0063}|\u{E0065}|\u{E0067}|\u{E006C}|\u{E006E}|\u{E0073}-\u{E0074}|\u{E0077}|\u{E007F}]+/u; - -export function stringHasUnicodeFlags(text: string): boolean { - return EMOJIS_FLAGS_REGEX.test(text); +export function stringHasUnicodeEmoji(input: string): boolean { + return new RegExp(EMOJI_REGEX, supportedFlags()).test(input); } + +export function stringHasUnicodeFlags(input: string): boolean { + if (supportsRegExpSets()) { + return new RegExp( + '\\p{RGI_Emoji_Flag_Sequence}|\\p{RGI_Emoji_Tag_Sequence}', + 'v', + ).test(input); + } + return new RegExp( + // First range is regional indicator symbols, + // Second is a black flag + 0-9|a-z tag chars + cancel tag. + // See: https://en.wikipedia.org/wiki/Regional_indicator_symbol + '(?:\uD83C[\uDDE6-\uDDFF]){2}|\uD83C\uDFF4(?:\uDB40[\uDC30-\uDC7A])+\uDB40\uDC7F', + ).test(input); +} + +// Constant as this is supported by all browsers. +const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i; +export function stringHasCustomEmoji(input: string) { + return CUSTOM_EMOJI_REGEX.test(input); +} + +export function stringHasAnyEmoji(input: string) { + return stringHasUnicodeEmoji(input) || stringHasCustomEmoji(input); +} + +export function anyEmojiRegex() { + return new RegExp( + `${EMOJI_REGEX}|${CUSTOM_EMOJI_REGEX.source}`, + supportedFlags('gi'), + ); +} + +function supportsRegExpSets() { + return 'unicodeSets' in RegExp.prototype; +} + +function supportedFlags(flags = '') { + if (supportsRegExpSets()) { + return `${flags}v`; + } + return flags; +} + +const EMOJI_REGEX = emojiRegexPolyfill?.source ?? '\\p{RGI_Emoji}'; diff --git a/app/javascript/mastodon/features/emoji/worker.ts b/app/javascript/mastodon/features/emoji/worker.ts index 1c48a077730..6fb7d36e936 100644 --- a/app/javascript/mastodon/features/emoji/worker.ts +++ b/app/javascript/mastodon/features/emoji/worker.ts @@ -5,9 +5,14 @@ self.postMessage('ready'); // After the worker is ready, notify the main thread function handleMessage(event: MessageEvent) { const { data: locale } = event; - if (locale !== 'custom') { - void importEmojiData(locale); - } else { - void importCustomEmojiData(); - } + void loadData(locale); +} + +async function loadData(locale: string) { + if (locale !== 'custom') { + await importEmojiData(locale); + } else { + await importCustomEmojiData(); + } + self.postMessage(`loaded ${locale}`); } diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index 9616adcb937..b1f4e598185 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -143,6 +143,17 @@ class ColumnSettings extends PureComponent {
+
+

+ +
+ + {showPushSettings && } + + +
+
+

diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index b38e5da1594..ced09881a43 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -8,9 +8,9 @@ import { Link, withRouter } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; - import EditIcon from '@/material-icons/400-24px/edit.svg?react'; import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; +import FormatQuoteIcon from '@/material-icons/400-24px/format_quote.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react'; @@ -42,6 +42,7 @@ const messages = defineMessages({ adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' }, relationshipsSevered: { id: 'notification.relationships_severance_event', defaultMessage: 'Lost connections with {name}' }, moderationWarning: { id: 'notification.moderation_warning', defaultMessage: 'You have received a moderation warning' }, + quote: { id: 'notification.label.quote', defaultMessage: '{name} quoted your post'} }); const notificationForScreenReader = (intl, message, timestamp) => { @@ -251,6 +252,36 @@ class Notification extends ImmutablePureComponent { ); } + renderQuote (notification, link) { + const { intl, unread } = this.props; + + return ( + +
+
+ + + + + +
+ +
+
+ ); + } + renderStatus (notification, link) { const { intl, unread, status } = this.props; @@ -467,6 +498,8 @@ class Notification extends ImmutablePureComponent { return this.renderFollowRequest(notification, account, link); case 'mention': return this.renderMention(notification); + case 'quote': + return this.renderQuote(notification); case 'favourite': return this.renderFavourite(notification, link); case 'reblog': diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx index f0f2139ad21..eba39e17b7a 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx @@ -15,6 +15,7 @@ import { NotificationFollowRequest } from './notification_follow_request'; import { NotificationMention } from './notification_mention'; import { NotificationModerationWarning } from './notification_moderation_warning'; import { NotificationPoll } from './notification_poll'; +import { NotificationQuote } from './notification_quote'; import { NotificationReblog } from './notification_reblog'; import { NotificationSeveredRelationships } from './notification_severed_relationships'; import { NotificationStatus } from './notification_status'; @@ -91,6 +92,11 @@ export const NotificationGroup: React.FC<{ ); break; + case 'quote': + content = ( + + ); + break; case 'follow': content = ( diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_quote.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_quote.tsx new file mode 100644 index 00000000000..595bed806c6 --- /dev/null +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_quote.tsx @@ -0,0 +1,33 @@ +import { FormattedMessage } from 'react-intl'; + +import FormatQuoteIcon from '@/material-icons/400-24px/format_quote.svg?react'; +import type { NotificationGroupQuote } from 'mastodon/models/notification_group'; + +import type { LabelRenderer } from './notification_group_with_status'; +import { NotificationWithStatus } from './notification_with_status'; + +const quoteLabelRenderer: LabelRenderer = (displayName) => ( + +); + +export const NotificationQuote: React.FC<{ + notification: NotificationGroupQuote; + unread: boolean; +}> = ({ notification, unread }) => { + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx index 080aaca4512..24c88f95050 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx @@ -21,6 +21,7 @@ import { openModal } from 'mastodon/actions/modal'; import { IconButton } from 'mastodon/components/icon_button'; import { useIdentity } from 'mastodon/identity_context'; import { me } from 'mastodon/initial_state'; +import type { Account } from 'mastodon/models/account'; import type { Status } from 'mastodon/models/status'; import { makeGetStatus } from 'mastodon/selectors'; import type { RootState } from 'mastodon/store'; @@ -66,10 +67,7 @@ export const Footer: React.FC<{ const dispatch = useAppDispatch(); const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector; const status = useAppSelector((state) => getStatus(state, { id: statusId })); - const accountId = status?.get('account') as string | undefined; - const account = useAppSelector((state) => - accountId ? state.accounts.get(accountId) : undefined, - ); + const account = status?.get('account') as Account | undefined; const askReplyConfirmation = useAppSelector( (state) => (state.compose.get('text') as string).trim().length !== 0, ); diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 81f8163fffe..5d6625fc103 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -61,22 +61,29 @@ const messages = defineMessages({ unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, + revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}’s post' }, }); -const mapStateToProps = (state, { status }) => ({ - relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), -}); +const mapStateToProps = (state, { status }) => { + const quotedStatusId = status.getIn(['quote', 'quoted_status']); + return ({ + relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), + quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null, + }); +}; class ActionBar extends PureComponent { static propTypes = { identity: identityContextPropShape, status: ImmutablePropTypes.map.isRequired, relationship: ImmutablePropTypes.record, + quotedAccountId: ImmutablePropTypes.string, onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, + onRevokeQuote: PropTypes.func, onEdit: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -113,6 +120,10 @@ class ActionBar extends PureComponent { this.props.onDelete(this.props.status); }; + handleRevokeQuoteClick = () => { + this.props.onRevokeQuote(this.props.status); + } + handleRedraftClick = () => { this.props.onDelete(this.props.status, true); }; @@ -193,7 +204,7 @@ class ActionBar extends PureComponent { }; render () { - const { status, relationship, intl } = this.props; + const { status, relationship, quotedAccountId, intl } = this.props; const { signedIn, permissions } = this.props.identity; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -237,6 +248,10 @@ class ActionBar extends PureComponent { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push(null); + if (quotedAccountId === me) { + menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true }); + } + if (relationship && relationship.get('muting')) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); } else { diff --git a/app/javascript/mastodon/features/status/components/refresh_controller.tsx b/app/javascript/mastodon/features/status/components/refresh_controller.tsx index 04046302b62..9788b2849f3 100644 --- a/app/javascript/mastodon/features/status/components/refresh_controller.tsx +++ b/app/javascript/mastodon/features/status/components/refresh_controller.tsx @@ -2,8 +2,6 @@ import { useEffect, useState, useCallback } from 'react'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; - import { fetchContext, completeContextRefresh, @@ -22,11 +20,15 @@ const messages = defineMessages({ export const RefreshController: React.FC<{ statusId: string; - withBorder?: boolean; -}> = ({ statusId, withBorder }) => { +}> = ({ statusId }) => { const refresh = useAppSelector( (state) => state.contexts.refreshing[statusId], ); + const autoRefresh = useAppSelector( + (state) => + !state.contexts.replies[statusId] || + state.contexts.replies[statusId].length === 0, + ); const dispatch = useAppDispatch(); const intl = useIntl(); const [ready, setReady] = useState(false); @@ -42,6 +44,11 @@ export const RefreshController: React.FC<{ dispatch(completeContextRefresh({ statusId })); if (result.async_refresh.result_count > 0) { + if (autoRefresh) { + void dispatch(fetchContext({ statusId })); + return ''; + } + setReady(true); } } else { @@ -60,7 +67,7 @@ export const RefreshController: React.FC<{ return () => { clearTimeout(timeoutId); }; - }, [dispatch, setReady, statusId, refresh]); + }, [dispatch, setReady, statusId, refresh, autoRefresh]); const handleClick = useCallback(() => { setLoading(true); @@ -78,12 +85,7 @@ export const RefreshController: React.FC<{ if (ready && !loading) { return ( -
- {descendants} {remoteHint} + {descendants} diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts index 25ffb3b6291..139b6f8ba25 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts @@ -10,3 +10,4 @@ export { ConfirmClearNotificationsModal } from './clear_notifications'; export { ConfirmLogOutModal } from './log_out'; export { ConfirmFollowToListModal } from './follow_to_list'; export { ConfirmMissingAltTextModal } from './missing_alt_text'; +export { ConfirmRevokeQuoteModal } from './revoke_quote'; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx new file mode 100644 index 00000000000..83964aa5fe8 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx @@ -0,0 +1,48 @@ +import { useCallback } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { revokeQuote } from 'mastodon/actions/interactions_typed'; +import { useAppDispatch } from 'mastodon/store'; + +import type { BaseConfirmationModalProps } from './confirmation_modal'; +import { ConfirmationModal } from './confirmation_modal'; + +const messages = defineMessages({ + revokeQuoteTitle: { + id: 'confirmations.revoke_quote.title', + defaultMessage: 'Remove post?', + }, + revokeQuoteMessage: { + id: 'confirmations.revoke_quote.message', + defaultMessage: 'This action cannot be undone.', + }, + revokeQuoteConfirm: { + id: 'confirmations.revoke_quote.confirm', + defaultMessage: 'Remove post', + }, +}); + +export const ConfirmRevokeQuoteModal: React.FC< + { + statusId: string; + quotedStatusId: string; + } & BaseConfirmationModalProps +> = ({ statusId, quotedStatusId, onClose }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const onConfirm = useCallback(() => { + void dispatch(revokeQuote({ quotedStatusId, statusId })); + }, [dispatch, statusId, quotedStatusId]); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 4a98de0a31a..3b7a24faaf4 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -37,6 +37,7 @@ import { ConfirmLogOutModal, ConfirmFollowToListModal, ConfirmMissingAltTextModal, + ConfirmRevokeQuoteModal, } from './confirmation_modals'; import { ImageModal } from './image_modal'; import MediaModal from './media_modal'; @@ -59,6 +60,7 @@ export const MODAL_COMPONENTS = { 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }), 'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }), 'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }), + 'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }), 'MUTE': MuteModal, 'BLOCK': BlockModal, 'DOMAIN_BLOCK': DomainBlockModal, diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.tsx b/app/javascript/mastodon/features/ui/components/zoomable_image.tsx index 09b39d3efab..1297d243d0d 100644 --- a/app/javascript/mastodon/features/ui/components/zoomable_image.tsx +++ b/app/javascript/mastodon/features/ui/components/zoomable_image.tsx @@ -306,10 +306,8 @@ export const ZoomableImage: React.FC = ({ {title}
” filtri ilə uyuşur", "follow_suggestions.hints.friends_of_friends": "Bu profil izlədiyiniz insanlar arasında populyardır.", - "follow_suggestions.hints.most_followed": "Bu profil {domain} serverində ən çox izlənilənlərdən biridir." + "follow_suggestions.hints.most_followed": "Bu profil {domain} serverində ən çox izlənilənlərdən biridir.", + "generic.saved": "Saxlanıldı", + "getting_started.heading": "Başlayaq", + "hashtag.admin_moderation": "#{name} üçün moderasiya interfeysini aç", + "hashtag.browse": "#{hashtag} göndərişlərinə bax", + "hashtag.browse_from_account": "@{name} - #{hashtag} göndərişlərinə bax", + "hashtag.column_header.tag_mode.all": "və {additional}", + "hashtag.column_header.tag_mode.any": "və ya {additional}", + "hashtag.column_header.tag_mode.none": "{additional} olmadan", + "hashtag.column_settings.select.no_options_message": "Heç bir təklif tapılmadı", + "hashtag.column_settings.select.placeholder": "Mövzu etiketlərini daxil edin…", + "hashtag.column_settings.tag_mode.all": "Bunların hamısı", + "hashtag.column_settings.tag_mode.any": "Bunlardan hər hansısa biri", + "hashtag.column_settings.tag_mode.none": "Bunların heç biri", + "hashtag.column_settings.tag_toggle": "Bu sütun üçün əlavə etiketləri daxil et", + "hashtag.counter_by_accounts": "{count, plural, one {{counter} iştirakçı} other {{counter} iştirakçı}}", + "hashtag.counter_by_uses": "{count, plural, one {{counter} göndəriş} other {{counter} göndəriş}}", + "hashtag.counter_by_uses_today": "Bu gün {count, plural, one {{counter} göndəriş} other {{counter} göndəriş}}", + "hashtag.follow": "Mövzu etiketini izlə", + "hashtag.mute": "#{hashtag} - səssizə al", + "hashtag.unfollow": "Mövzu etiketini izləmə", + "hashtags.and_other": "…və daha {count, plural, one {}other {# ədəd}}", + "hints.profiles.followers_may_be_missing": "Bu profilin izləyiciləri əskik ola bilər.", + "hints.profiles.follows_may_be_missing": "Bu profilin izləyənləri əskik ola bilər.", + "home.column_settings.show_quotes": "Sitatları göstər", + "home.column_settings.show_replies": "Cavabları göstər", + "home.hide_announcements": "Elanları gizlət", + "home.pending_critical_update.body": "Lütfən Mastodon serverinizi mümkün olan ən qısa müddətdə güncəlləyin!", + "home.pending_critical_update.link": "Güncəlləmələrə bax", + "home.pending_critical_update.title": "Kritik güvənlik güncəlləməsi mövcuddur!", + "home.show_announcements": "Elanları göstər", + "ignore_notifications_modal.ignore": "Bildirişləri yox say", + "ignore_notifications_modal.limited_accounts_title": "Moderasiya edilmiş hesabların bildirişləri yox sayılsın?", + "ignore_notifications_modal.new_accounts_title": "Yeni hesabların bildirişləri yox sayılsın?", + "ignore_notifications_modal.not_followers_title": "Sizi izləməyən şəxslərin bildirişləri yox sayılsın?", + "ignore_notifications_modal.not_following_title": "İzləmədiyiniz şəxslərin bildirişləri yox sayılsın?", + "ignore_notifications_modal.private_mentions_title": "İstənilməyən Şəxsi Adçəkmələrdən gələn bildirişlər yox sayılsın?", + "info_button.label": "Kömək", + "interaction_modal.action.favourite": "Davam etmək üçün hesabınızdan sevimlilərə əlavə etməlisiniz.", + "interaction_modal.action.follow": "Davam etmək üçün hesabınızdan izləməlisiniz.", + "interaction_modal.action.reblog": "Davam etmək üçün hesabınızdan təkrar göndərməlisiniz.", + "interaction_modal.action.reply": "Davam etmək üçün hesabınızdan cavab verməlisiniz.", + "interaction_modal.action.vote": "Davam etmək üçün hesabınızdan səs verməlisiniz.", + "keyboard_shortcuts.profile": "Müəllifin profilini aç", + "keyboard_shortcuts.reply": "Göndərişə cavab ver", + "learn_more_link.got_it": "Anladım", + "learn_more_link.learn_more": "Daha ətraflı", + "lightbox.close": "Bağla", + "lightbox.next": "Növbəti", + "lightbox.previous": "Əvvəlki", + "lightbox.zoom_in": "Həqiqi ölçüyə qayıt", + "limited_account_hint.action": "Yenə də profili göstər", + "limited_account_hint.title": "Bu profil, {domain} moderatorları tərəfindən gizlədildi.", + "navigation_bar.account_settings": "Parol və təhlükəsizlik", + "navigation_bar.moderation": "Moderasiya", + "not_signed_in_indicator.not_signed_in": "Bu resursa erişmək üçün giriş etməlisiniz.", + "notification.moderation-warning.learn_more": "Daha ətraflı", + "notification.moderation_warning": "Bir moderasiya xəbərdarlığı aldınız", + "notification.moderation_warning.action_delete_statuses": "Bəzi göndərişləriniz silindi.", + "notification.moderation_warning.action_disable": "Hesabınız sıradan çıxarılıb.", + "notification.moderation_warning.action_mark_statuses_as_sensitive": "Bəzi göndərişləriniz həssas olaraq işarələnib.", + "notification.moderation_warning.action_none": "Hesabınız bir moderasiya xəbərdarlığı aldı.", + "notification.moderation_warning.action_sensitive": "Göndərişləriniz artıq həssas olaraq işarələnəcək.", + "notification.moderation_warning.action_silence": "Hesabınız məhdudlaşdırılıb.", + "notification.moderation_warning.action_suspend": "Hesabınızın fəaliyyəti dayandırılıb.", + "notification_requests.confirm_dismiss_multiple.message": "{count, plural, one {bir bildiriş sorğusunu} other {# bildiriş sorğusunu}} bağlamaq üzrəsiniz. {count, plural, one {Ona} other {Onlara}} yenidən asanlıqla erişə bilməyəcəksiniz. Davam etmək istədiyinizə əminsiniz?", + "notification_requests.explainer_for_limited_account": "Hesab, bir moderator tərəfindən məhdudlaşdırıldığı üçün bu hesabın bildirişləri filtrləndi.", + "notification_requests.explainer_for_limited_remote_account": "Hesab və ya onun serveri, bir moderator tərəfindən məhdudlaşdırıldığı üçün bu hesabın bildirişləri filtrləndi.", + "notifications.filter.statuses": "İzlədiyiniz şəxslərdən güncəlləmələr", + "notifications.policy.filter_limited_accounts_hint": "Server moderatorları tərəfindən məhdudlaşdırılıb", + "notifications.policy.filter_limited_accounts_title": "Moderasiya edilmiş hesablar", + "password_confirmation.exceeds_maxlength": "Parol təsdiqi, maksimum parol uzunluğunu aşır", + "password_confirmation.mismatching": "Parol təsdiqi uyuşmur", + "privacy_policy.last_updated": "Son güncəlləmə {date}", + "report.category.subtitle": "Ən çox uyuşanı seçin", + "report_notification.categories.spam_sentence": "spam", + "report_notification.categories.violation": "Qayda pozuntusu", + "report_notification.categories.violation_sentence": "qayda pozuntusu", + "report_notification.open": "Hesabatı aç", + "search.clear": "Axtarışı təmizlə", + "search.no_recent_searches": "Son axtarışlar yoxdur", + "search.placeholder": "Axtar", + "search.quick_action.account_search": "Uyuşan profillər {x}", + "search.quick_action.go_to_account": "{x} profilinə get", + "search.quick_action.go_to_hashtag": "{x} mövzu etiketinə get", + "search.quick_action.open_url": "URL-ni Mastodon-da aç", + "search.quick_action.status_search": "Uyuşan göndərişlər {x}", + "search.search_or_paste": "Axtar və ya URL-ni yapışdır", + "search_popout.full_text_search_disabled_message": "{domain} domenində mövcud deyil.", + "search_popout.full_text_search_logged_out_message": "Yalnız giriş edildiyi zaman əlçatandır.", + "search_popout.language_code": "ISO dil kodu", + "search_popout.options": "Axtarış seçimləri", + "search_popout.quick_actions": "Cəld əməliyyatlar", + "search_popout.recent": "Son axtarışlar", + "search_popout.specific_date": "müəyyən tarix", + "search_popout.user": "istifadəçi", + "search_results.accounts": "Profillər", + "search_results.all": "Hamısı", + "search_results.hashtags": "Mövzu etiketləri", + "search_results.no_results": "Nəticə yoxdur.", + "search_results.no_search_yet": "Göndərişləri, profilləri və ya mövzu etiketlərini axtarmağa çalışın.", + "search_results.see_all": "Hamısına bax", + "search_results.statuses": "Göndərişlər", + "search_results.title": "\"{q}\" axtar", + "server_banner.about_active_users": "Son 30 gündə bu serveri istifadə edənlər (aylıq aktiv istifadəçilər)", + "server_banner.active_users": "aktiv istifadəçilər", + "server_banner.is_one_of_many": "{domain}, fediverse-də iştirak etmək üçün istifadə edə biləcəyiniz bir neçə müstəqil Mastodon serverlərindən biridir.", + "server_banner.server_stats": "Server statistikaları:", + "sign_in_banner.create_account": "Hesab yarat", + "sign_in_banner.follow_anyone": "fediverse-dəki hər kəsi izləyin və hamısına xronoloji ardıcıllıqla baxın. Heç bir alqoritm, reklam və ya klikləmə tələsi yoxdur.", + "sign_in_banner.mastodon_is": "Mastodon, baş verənlərdən xəbərdar olmağın ən yaxşı yoldur.", + "sign_in_banner.sign_in": "Giriş", + "sign_in_banner.sso_redirect": "Giriş və ya Qeydiyyat", + "status.admin_account": "@{name} üçün moderasiya interfeysini aç", + "status.admin_domain": "{domain} üçün moderasiya interfeysini aç", + "status.admin_status": "Moderasiya interfeysində bu göndərişi aç", + "status.block": "Əngəllə: @{name}", + "status.bookmark": "Əlfəcin", + "status.context.load_new_replies": "Yeni cavablar mövcuddur", + "status.context.loading": "Daha çox cavab yoxlanılır", + "status.delete": "Sil", + "status.direct": "Şəxsi olaraq adını çək: @{name}", + "status.direct_indicator": "Şəxsi olaraq adını çək", + "status.edit": "Düzəliş et", + "status.edited": "Son düzəliş {date}", + "status.edited_x_times": "{count, plural, one {{count} dəfə} other {{count} dəfə}} düzəliş edilib", + "status.favourite": "Sevimli", + "status.favourites": "{count, plural, one {sevimli} other {sevimli}}", + "status.filter": "Bu göndərişi filtrlə", + "status.history.created": "{name}, {date} yaratdı", + "status.history.edited": "{name}, {date} düzəliş etdi", + "status.load_more": "Daha çoxunu yüklə", + "status.media.open": "Açmaq üçün kliklə", + "status.media.show": "Göstərmək üçün kliklə", + "status.media_hidden": "Media gizlidir", + "status.mention": "Adını çək: @{name}", + "status.more": "Daha çox", + "status.mute": "@{name} - səssizə al", + "status.mute_conversation": "Danışığın səsini kəs", + "status.open": "Bu göndərişi genişləndir", + "status.quote_error.filtered": "Bəzi filtrlərinizə görə gizlidir", + "status.quote_error.not_available": "Göndəriş əlçatmazdır", + "status.quote_error.pending_approval": "Göndəriş gözləmədədir", + "status.read_more": "Daha çoxunu oxu", + "status.remove_bookmark": "Əlfəcini sil", + "status.remove_favourite": "Sevimlilərdən sil", + "status.replied_to": "Cavab verildi: {name}", + "status.reply": "Cavabla", + "status.report": "Bildir: @{name}", + "status.sensitive_warning": "Həssas məzmun", + "status.share": "Paylaş", + "status.show_less_all": "Hamısı üçün daha az göstər", + "status.show_more_all": "Hamısı üçün daha çox göstər", + "status.show_original": "Orijinalı göstər", + "status.translate": "Tərcümə et", + "status.translated_from_with": "{provider} ilə {lang} dilindən tərcümə edilib", + "status.uncached_media_warning": "Önizləmə mövcud deyil", + "status.unmute_conversation": "Danışığın səsini aç", + "subscribed_languages.save": "Dəyişiklikləri saxla", + "subscribed_languages.target": "{target} üçün abunə olunmuş dilləri dəyişdir", + "tabs_bar.home": "Ana səhifə", + "tabs_bar.menu": "Menyu", + "tabs_bar.notifications": "Bildirişlər", + "tabs_bar.publish": "Yeni göndəriş", + "tabs_bar.search": "Axtar", + "terms_of_service.effective_as_of": "{date} etibarilə qüvvədə", + "terms_of_service.title": "Xidmət Şərtləri", + "terms_of_service.upcoming_changes_on": "{date} tarixində ediləcək dəyişikliklər", + "time_remaining.days": "{number, plural, one {# gün} other {# gün}} qalıb", + "time_remaining.hours": "{number, plural, one {# saat} other {# saat}} qalıb", + "time_remaining.minutes": "{number, plural, one {# dəqiqə} other {# dəqiqə}} qalıb", + "time_remaining.moments": "Bir neçə dəqiqə qalıb", + "time_remaining.seconds": "{number, plural, one {# saniyə} other {# saniyə}} qalıb", + "trends.trending_now": "İndi trenddədir", + "ui.beforeunload": "Mastodon-u tərk etsəniz, qaralamanız itəcək.", + "units.short.billion": "{count} mlyrd", + "units.short.million": "{count} mlyn", + "units.short.thousand": "{count} min", + "upload_area.title": "Yükləmək üçün sürüklə və burax", + "upload_button.label": "Təsvir, video və ya səs faylı əlavə et", + "upload_error.limit": "Fayl yükləmə limiti aşılıb.", + "upload_error.poll": "Anketlərdə fayl yükləməyə icazə verilmir.", + "upload_form.drag_and_drop.instructions": "Bir media qoşmasını daşımaq üçün boşluq və ya enter düyməsinə basın. Sürükləmə zamanı, media qoşmasını hər hansısa bir yönə hərəkət etdirmək üçün ox düymələrini istifadə edin. Media qoşmasını yeni mövqeyinə buraxmaq üçün təkrar boşluq və ya enter düyməsinə basın, ləğv etmək üçün escape düyməsinə basın.", + "upload_form.drag_and_drop.on_drag_cancel": "Sürükləmə ləğv edilib. {item} media qoşması buraxıldı.", + "upload_form.drag_and_drop.on_drag_end": "{item} media qoşması buraxıldı.", + "upload_form.drag_and_drop.on_drag_over": "{item} media qoşması daşındı.", + "upload_form.drag_and_drop.on_drag_start": "{item} media qoşması alındı.", + "upload_form.edit": "Düzəliş et", + "upload_progress.label": "Yüklənir...", + "upload_progress.processing": "Emal edilir…", + "username.taken": "Bu istifadəçi adı götürülüb. Başqasını sınayın", + "video.close": "Videonu bağla", + "video.download": "Faylı endir", + "video.exit_fullscreen": "Tam ekrandan çıx", + "video.expand": "Videonu genişləndir", + "video.fullscreen": "Tam ekran", + "video.hide": "Videonu gizlət", + "video.mute": "Səsi kəs", + "video.pause": "Fasilə ver", + "video.play": "Oxut", + "video.skip_backward": "Geri ötür", + "video.skip_forward": "İrəli ötür", + "video.unmute": "Səsi aç", + "video.volume_down": "Həcmi azalt", + "video.volume_up": "Həcmi artır" } diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 213ec61aca6..706e746988f 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -404,8 +404,6 @@ "hints.profiles.see_more_followers": "Глядзець больш падпісаных на {domain}", "hints.profiles.see_more_follows": "Глядзець больш падпісак на {domain}", "hints.profiles.see_more_posts": "Глядзець больш допісаў на {domain}", - "hints.threads.replies_may_be_missing": "Адказы з іншых сервераў могуць адсутнічаць.", - "hints.threads.see_more": "Глядзіце больш адказаў на {domain}", "home.column_settings.show_quotes": "Паказаць цытаты", "home.column_settings.show_reblogs": "Паказваць пашырэнні", "home.column_settings.show_replies": "Паказваць адказы", @@ -823,7 +821,6 @@ "status.mute_conversation": "Ігнараваць размову", "status.open": "Разгарнуць гэты допіс", "status.pin": "Замацаваць у профілі", - "status.quote_post_author": "Допіс карыстальніка @{name}", "status.read_more": "Чытаць болей", "status.reblog": "Пашырыць", "status.reblog_private": "Пашырыць з першапачатковай бачнасцю", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 8628b68954c..6f0cf6f54fe 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -419,8 +419,6 @@ "hints.profiles.see_more_followers": "Преглед на още последователи на {domain}", "hints.profiles.see_more_follows": "Преглед на още последвания на {domain}", "hints.profiles.see_more_posts": "Преглед на още публикации на {domain}", - "hints.threads.replies_may_be_missing": "Отговори от други сървъри може да липсват.", - "hints.threads.see_more": "Преглед на още отговори на {domain}", "home.column_settings.show_quotes": "Показване на цитираното", "home.column_settings.show_reblogs": "Показване на подсилванията", "home.column_settings.show_replies": "Показване на отговорите", @@ -862,12 +860,6 @@ "status.open": "Разширяване на публикацията", "status.pin": "Закачане в профила", "status.quote_error.filtered": "Скрито поради един от филтрите ви", - "status.quote_error.not_found": "Публикацията не може да се показва.", - "status.quote_error.pending_approval": "Публикацията чака одобрение от първоначалния автор.", - "status.quote_error.rejected": "Публикацията не може да се показва като първоначалния автор не позволява цитирането ѝ.", - "status.quote_error.removed": "Публикацията е премахната от автора ѝ.", - "status.quote_error.unauthorized": "Публикацията не може да се показва, тъй като не сте упълномощени да я гледате.", - "status.quote_post_author": "Публикация от {name}", "status.read_more": "Още за четене", "status.reblog": "Подсилване", "status.reblog_private": "Подсилване с оригиналната видимост", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index b0e21ab3be0..cdca2446ca0 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -558,6 +558,8 @@ "status.bookmark": "Ouzhpennañ d'ar sinedoù", "status.cancel_reblog_private": "Nac'hañ ar skignadenn", "status.cannot_reblog": "Ar c'hannad-se na c'hall ket bezañ skignet", + "status.context.load_new_replies": "Respontoù nevez zo", + "status.context.loading": "O kerc'hat muioc'h a respontoù", "status.copy": "Eilañ liamm ar c'hannad", "status.delete": "Dilemel", "status.detailed_status": "Gwel kaozeadenn munudek", @@ -580,7 +582,6 @@ "status.mute_conversation": "Kuzhat ar gaozeadenn", "status.open": "Digeriñ ar c'hannad-mañ", "status.pin": "Spilhennañ d'ar profil", - "status.quote_post_author": "Embannadenn gant {name}", "status.read_more": "Lenn muioc'h", "status.reblog": "Skignañ", "status.reblog_private": "Skignañ gant ar weledenn gentañ", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 7fab405f8e2..103b4f3715d 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -245,6 +245,7 @@ "confirmations.remove_from_followers.confirm": "Elimina el seguidor", "confirmations.remove_from_followers.message": "{name} deixarà de seguir-vos. Tirem endavant?", "confirmations.remove_from_followers.title": "Eliminem el seguidor?", + "confirmations.revoke_quote.message": "Aquesta acció no es pot desfer.", "confirmations.unfollow.confirm": "Deixa de seguir", "confirmations.unfollow.message": "Segur que vols deixar de seguir {name}?", "confirmations.unfollow.title": "Deixar de seguir l'usuari?", @@ -424,8 +425,6 @@ "hints.profiles.see_more_followers": "Vegeu més seguidors a {domain}", "hints.profiles.see_more_follows": "Vegeu més seguiments a {domain}", "hints.profiles.see_more_posts": "Vegeu més publicacions a {domain}", - "hints.threads.replies_may_be_missing": "Es poden haver perdut respostes d'altres servidors.", - "hints.threads.see_more": "Vegeu més respostes a {domain}", "home.column_settings.show_quotes": "Mostrar les cites", "home.column_settings.show_reblogs": "Mostra els impulsos", "home.column_settings.show_replies": "Mostra les respostes", @@ -499,6 +498,8 @@ "keyboard_shortcuts.translate": "per a traduir una publicació", "keyboard_shortcuts.unfocus": "Descentra l'àrea de composició de text/cerca", "keyboard_shortcuts.up": "Apuja a la llista", + "learn_more_link.got_it": "Entesos", + "learn_more_link.learn_more": "Per a saber-ne més", "lightbox.close": "Tanca", "lightbox.next": "Següent", "lightbox.previous": "Anterior", @@ -599,6 +600,7 @@ "notification.label.mention": "Menció", "notification.label.private_mention": "Menció privada", "notification.label.private_reply": "Resposta en privat", + "notification.label.quote": "{name} ha citat la vostra publicació", "notification.label.reply": "Resposta", "notification.mention": "Menció", "notification.mentioned_you": "{name} us ha mencionat", @@ -656,6 +658,7 @@ "notifications.column_settings.mention": "Mencions:", "notifications.column_settings.poll": "Resultats de l’enquesta:", "notifications.column_settings.push": "Notificacions push", + "notifications.column_settings.quote": "Cites:", "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostra a la columna", "notifications.column_settings.sound": "Reprodueix so", @@ -846,6 +849,8 @@ "status.bookmark": "Marca", "status.cancel_reblog_private": "Desfés l'impuls", "status.cannot_reblog": "No es pot impulsar aquest tut", + "status.context.load_new_replies": "Hi ha respostes noves", + "status.context.loading": "Comprovació de més respostes", "status.continued_thread": "Continuació del fil", "status.copy": "Copia l'enllaç al tut", "status.delete": "Elimina", @@ -872,12 +877,11 @@ "status.open": "Amplia el tut", "status.pin": "Fixa en el perfil", "status.quote_error.filtered": "No es mostra a causa d'un dels vostres filtres", - "status.quote_error.not_found": "No es pot mostrar aquesta publicació.", - "status.quote_error.pending_approval": "Aquesta publicació està pendent d'aprovació per l'autor original.", - "status.quote_error.rejected": "No es pot mostrar aquesta publicació perquè l'autor original no en permet la citació.", - "status.quote_error.removed": "Aquesta publicació ha estat eliminada per l'autor.", - "status.quote_error.unauthorized": "No es pot mostrar aquesta publicació perquè no teniu autorització per a veure-la.", - "status.quote_post_author": "Publicació de {name}", + "status.quote_error.not_available": "Publicació no disponible", + "status.quote_error.pending_approval": "Publicació pendent", + "status.quote_error.pending_approval_popout.body": "Les citacions compartides a través del Fediverse poden trigar en aparèixer, perquè diferents servidors tenen diferents protocols.", + "status.quote_error.pending_approval_popout.title": "Publicació pendent? Mantinguem la calma", + "status.quote_post_author": "S'ha citat una publicació de @{name}", "status.read_more": "Més informació", "status.reblog": "Impulsa", "status.reblog_private": "Impulsa amb la visibilitat original", @@ -892,6 +896,7 @@ "status.reply": "Respon", "status.replyAll": "Respon al fil", "status.report": "Denuncia @{name}", + "status.revoke_quote": "Elimina la meva publicació de la de @{name}", "status.sensitive_warning": "Contingut sensible", "status.share": "Comparteix", "status.show_less_all": "Mostra'n menys per a tot", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index f64a6196074..a6e82882953 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Odstranit sledujícího", "confirmations.remove_from_followers.message": "{name} vás přestane sledovat. Jste si jisti, že chcete pokračovat?", "confirmations.remove_from_followers.title": "Odstranit sledujícího?", + "confirmations.revoke_quote.confirm": "Odstranit příspěvek", + "confirmations.revoke_quote.message": "Tuto akci nelze vrátit zpět.", + "confirmations.revoke_quote.title": "Odstranit příspěvek?", "confirmations.unfollow.confirm": "Přestat sledovat", "confirmations.unfollow.message": "Opravdu chcete {name} přestat sledovat?", "confirmations.unfollow.title": "Přestat sledovat uživatele?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Zobrazit více sledujících na {domain}", "hints.profiles.see_more_follows": "Zobrazit další sledování na {domain}", "hints.profiles.see_more_posts": "Zobrazit další příspěvky na {domain}", - "hints.threads.replies_may_be_missing": "Odpovědi z jiných serverů mohou chybět.", - "hints.threads.see_more": "Zobrazit další odpovědi na {domain}", "home.column_settings.show_quotes": "Zobrazit citace", "home.column_settings.show_reblogs": "Zobrazit boosty", "home.column_settings.show_replies": "Zobrazit odpovědi", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "k přeložení příspěvku", "keyboard_shortcuts.unfocus": "Zrušit zaměření na nový příspěvek/hledání", "keyboard_shortcuts.up": "Posunout v seznamu nahoru", + "learn_more_link.got_it": "Rozumím", + "learn_more_link.learn_more": "Zjistit více", "lightbox.close": "Zavřít", "lightbox.next": "Další", "lightbox.previous": "Předchozí", @@ -600,6 +603,7 @@ "notification.label.mention": "Zmínka", "notification.label.private_mention": "Soukromá zmínka", "notification.label.private_reply": "Privátní odpověď", + "notification.label.quote": "{name} citovali váš příspěvek", "notification.label.reply": "Odpověď", "notification.mention": "Zmínka", "notification.mentioned_you": "{name} vás zmínil", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Zmínky:", "notifications.column_settings.poll": "Výsledky anket:", "notifications.column_settings.push": "Push oznámení", + "notifications.column_settings.quote": "Citace:", "notifications.column_settings.reblog": "Boosty:", "notifications.column_settings.show": "Zobrazit ve sloupci", "notifications.column_settings.sound": "Přehrát zvuk", @@ -847,6 +852,8 @@ "status.bookmark": "Přidat do záložek", "status.cancel_reblog_private": "Zrušit boostnutí", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", + "status.context.load_new_replies": "K dispozici jsou nové odpovědi", + "status.context.loading": "Hledání dalších odpovědí", "status.continued_thread": "Pokračuje ve vlákně", "status.copy": "Zkopírovat odkaz na příspěvek", "status.delete": "Smazat", @@ -873,12 +880,11 @@ "status.open": "Rozbalit tento příspěvek", "status.pin": "Připnout na profil", "status.quote_error.filtered": "Skryté kvůli jednomu z vašich filtrů", - "status.quote_error.not_found": "Tento příspěvek nelze zobrazit.", - "status.quote_error.pending_approval": "Tento příspěvek čeká na schválení od původního autora.", - "status.quote_error.rejected": "Tento příspěvek nemůže být zobrazen, protože původní autor neumožňuje, aby byl citován.", - "status.quote_error.removed": "Tento příspěvek byl odstraněn jeho autorem.", - "status.quote_error.unauthorized": "Tento příspěvek nelze zobrazit, protože nemáte oprávnění k jeho zobrazení.", - "status.quote_post_author": "Příspěvek od {name}", + "status.quote_error.not_available": "Příspěvek není dostupný", + "status.quote_error.pending_approval": "Příspěvek čeká na schválení", + "status.quote_error.pending_approval_popout.body": "Zobrazení citátů sdílených napříč Fediversem může chvíli trvat, protože různé servery používají různé protokoly.", + "status.quote_error.pending_approval_popout.title": "Příspěvek čeká na schválení? Buďte klidní", + "status.quote_post_author": "Citovali příspěvek od @{name}", "status.read_more": "Číst více", "status.reblog": "Boostnout", "status.reblog_private": "Boostnout s původní viditelností", @@ -893,6 +899,7 @@ "status.reply": "Odpovědět", "status.replyAll": "Odpovědět na vlákno", "status.report": "Nahlásit @{name}", + "status.revoke_quote": "Odstranit můj příspěvek z příspěvku @{name}", "status.sensitive_warning": "Citlivý obsah", "status.share": "Sdílet", "status.show_less_all": "Zobrazit méně pro všechny", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index da40f1e010b..9dfc6b35a60 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Gweld mwy o ddilynwyr ar {domain}", "hints.profiles.see_more_follows": "Gweld mwy o 'yn dilyn' ar {domain}", "hints.profiles.see_more_posts": "Gweld mwy o bostiadau ar {domain}", - "hints.threads.replies_may_be_missing": "Mae'n bosibl y bydd ymatebion gan weinyddion eraill ar goll.", - "hints.threads.see_more": "Gweld mwy o ymatebion ar {domain}", "home.column_settings.show_quotes": "Dangos dyfyniadau", "home.column_settings.show_reblogs": "Dangos hybiau", "home.column_settings.show_replies": "Dangos ymatebion", @@ -873,12 +871,6 @@ "status.open": "Ehangu'r post hwn", "status.pin": "Pinio ar y proffil", "status.quote_error.filtered": "Wedi'i guddio oherwydd un o'ch hidlwyr", - "status.quote_error.not_found": "Does dim modd dangos y postiad hwn.", - "status.quote_error.pending_approval": "Mae'r postiad hwn yn aros am gymeradwyaeth yr awdur gwreiddiol.", - "status.quote_error.rejected": "Does dim modd dangos y postiad hwn gan nad yw'r awdur gwreiddiol yn caniatáu iddo gael ei ddyfynnu.", - "status.quote_error.removed": "Cafodd y postiad hwn ei ddileu gan ei awdur.", - "status.quote_error.unauthorized": "Does dim modd dangos y postiad hwn gan nad oes gennych awdurdod i'w weld.", - "status.quote_post_author": "Postiad gan {name}", "status.read_more": "Darllen rhagor", "status.reblog": "Hybu", "status.reblog_private": "Hybu i'r gynulleidfa wreiddiol", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 6a3542fee35..96a6eecf8c6 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Fjern følger", "confirmations.remove_from_followers.message": "{name} vil ikke længere følge dig. Er du sikker på, at du vil fortsætte?", "confirmations.remove_from_followers.title": "Fjern følger?", + "confirmations.revoke_quote.confirm": "Fjern indlæg", + "confirmations.revoke_quote.message": "Denne handling kan ikke fortrydes.", + "confirmations.revoke_quote.title": "Fjern indlæg?", "confirmations.unfollow.confirm": "Følg ikke længere", "confirmations.unfollow.message": "Er du sikker på, at du ikke længere vil følge {name}?", "confirmations.unfollow.title": "Følg ikke længere bruger?", @@ -324,7 +327,7 @@ "empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.", "empty_column.followed_tags": "Ingen hashtags følges endnu. Når det sker, vil de fremgå her.", "empty_column.hashtag": "Der er intet med dette hashtag endnu.", - "empty_column.home": "Din hjemmetidslinje er tom! Følg nogle personer, for at fylde den op.", + "empty_column.home": "Din hjem-tidslinje er tom! Følg nogle personer, for at fylde den op.", "empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af denne liste udgiver nye indlæg, vil de blive vist her.", "empty_column.mutes": "Du har endnu ikke skjult nogle brugere.", "empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jævnfør dine indstillinger.", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Se flere følgere på {domain}", "hints.profiles.see_more_follows": "Se flere fulgte på {domain}", "hints.profiles.see_more_posts": "Se flere indlæg på {domain}", - "hints.threads.replies_may_be_missing": "Der kan mangle svar fra andre servere.", - "hints.threads.see_more": "Se flere svar på {domain}", "home.column_settings.show_quotes": "Vis citater", "home.column_settings.show_reblogs": "Vis fremhævelser", "home.column_settings.show_replies": "Vis svar", @@ -478,7 +479,7 @@ "keyboard_shortcuts.favourites": "Åbn favoritlisten", "keyboard_shortcuts.federated": "Åbn fødereret tidslinje", "keyboard_shortcuts.heading": "Tastaturgenveje", - "keyboard_shortcuts.home": "Åbn hjemmetidslinje", + "keyboard_shortcuts.home": "Åbn hjem-tidslinje", "keyboard_shortcuts.hotkey": "Hurtigtast", "keyboard_shortcuts.legend": "Vis dette symbol", "keyboard_shortcuts.local": "Åbn lokal tidslinje", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "for at oversætte et indlæg", "keyboard_shortcuts.unfocus": "Fjern fokus fra tekstskrivningsområde/søgning", "keyboard_shortcuts.up": "Flyt opad på listen", + "learn_more_link.got_it": "Forstået", + "learn_more_link.learn_more": "Få mere at vide", "lightbox.close": "Luk", "lightbox.next": "Næste", "lightbox.previous": "Forrige", @@ -520,7 +523,7 @@ "lists.done": "Færdig", "lists.edit": "Redigér liste", "lists.exclusive": "Skjul medlemmer i Hjem", - "lists.exclusive_hint": "Er nogen er på denne liste, skjul personen i hjemme-feeds for at undgå at se vedkommendes indlæg to gange.", + "lists.exclusive_hint": "Hvis nogen er på denne liste, så skjul dem i hjem-feed for at undgå at se deres indlæg to gange.", "lists.find_users_to_add": "Find brugere at tilføje", "lists.list_members_count": "{count, plural, one {# medlem} other {# medlemmer}}", "lists.list_name": "Listetitel", @@ -600,6 +603,7 @@ "notification.label.mention": "Omtale", "notification.label.private_mention": "Privat omtale", "notification.label.private_reply": "Privat svar", + "notification.label.quote": "{name} citerede dit indlæg", "notification.label.reply": "Svar", "notification.mention": "Omtale", "notification.mentioned_you": "{name} omtalte dig", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Omtaler:", "notifications.column_settings.poll": "Afstemningsresultater:", "notifications.column_settings.push": "Push-notifikationer", + "notifications.column_settings.quote": "Citater:", "notifications.column_settings.reblog": "Fremhævelser:", "notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.sound": "Afspil lyd", @@ -794,7 +799,7 @@ "report.thanks.title": "Ønsker ikke at se dette?", "report.thanks.title_actionable": "Tak for anmeldelsen, der vil blive set nærmere på dette.", "report.unfollow": "Følg ikke længere @{name}", - "report.unfollow_explanation": "Du følger denne konto. For ikke længere at se vedkommendes indlæg i din hjemmestrøm, kan du stoppe med at følge dem.", + "report.unfollow_explanation": "Du følger denne konto. Hvis du ikke længere vil se vedkommendes indlæg i dit hjem-feed, så stop med at følge dem.", "report_notification.attached_statuses": "{count, plural, one {{count} indlæg} other {{count} indlæg}} vedhæftet", "report_notification.categories.legal": "Juridisk", "report_notification.categories.legal_sentence": "ikke-tilladt indhold", @@ -847,6 +852,8 @@ "status.bookmark": "Bogmærk", "status.cancel_reblog_private": "Fjern fremhævelse", "status.cannot_reblog": "Dette indlæg kan ikke fremhæves", + "status.context.load_new_replies": "Nye svar tilgængelige", + "status.context.loading": "Tjekker for flere svar", "status.continued_thread": "Fortsat tråd", "status.copy": "Kopiér link til indlæg", "status.delete": "Slet", @@ -873,12 +880,11 @@ "status.open": "Udvid dette indlæg", "status.pin": "Fastgør til profil", "status.quote_error.filtered": "Skjult grundet et af filterne", - "status.quote_error.not_found": "Dette indlæg kan ikke vises.", - "status.quote_error.pending_approval": "Dette indlæg afventer godkendelse fra den oprindelige forfatter.", - "status.quote_error.rejected": "Dette indlæg kan ikke vises, da den oprindelige forfatter ikke tillader citering heraf.", - "status.quote_error.removed": "Dette indlæg er fjernet af forfatteren.", - "status.quote_error.unauthorized": "Dette indlæg kan ikke vises, da man ikke har tilladelse til at se det.", - "status.quote_post_author": "Indlæg fra {name}", + "status.quote_error.not_available": "Indlæg utilgængeligt", + "status.quote_error.pending_approval": "Afventende indlæg", + "status.quote_error.pending_approval_popout.body": "Citater delt på tværs af Fediverset kan tage tid at vise, da forskellige servere har forskellige protokoller.", + "status.quote_error.pending_approval_popout.title": "Afventende citat? Tag det roligt", + "status.quote_post_author": "Citerede et indlæg fra @{name}", "status.read_more": "Læs mere", "status.reblog": "Fremhæv", "status.reblog_private": "Fremhæv med oprindelig synlighed", @@ -893,6 +899,7 @@ "status.reply": "Besvar", "status.replyAll": "Svar alle", "status.report": "Anmeld @{name}", + "status.revoke_quote": "Fjern mit indlæg fra @{name}'s indlæg", "status.sensitive_warning": "Følsomt indhold", "status.share": "Del", "status.show_less_all": "Vis mindre for alle", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index f0beb106051..018f66935cd 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -6,7 +6,7 @@ "about.domain_blocks.no_reason_available": "Grund unbekannt", "about.domain_blocks.preamble": "Mastodon erlaubt es dir grundsätzlich, alle Inhalte von allen Nutzer*innen auf allen Servern im Fediverse zu sehen und mit ihnen zu interagieren. Für diesen Server gibt es aber ein paar Ausnahmen.", "about.domain_blocks.silenced.explanation": "Standardmäßig werden von diesem Server keine Inhalte oder Profile angezeigt. Du kannst die Profile und Inhalte aber dennoch sehen, wenn du explizit nach diesen suchst oder diesen folgst.", - "about.domain_blocks.silenced.title": "Stummgeschaltet", + "about.domain_blocks.silenced.title": "Ausgeblendet", "about.domain_blocks.suspended.explanation": "Es werden keine Daten von diesem Server verarbeitet, gespeichert oder ausgetauscht, sodass eine Interaktion oder Kommunikation mit Nutzer*innen dieses Servers nicht möglich ist.", "about.domain_blocks.suspended.title": "Gesperrt", "about.language_label": "Sprache", @@ -63,7 +63,7 @@ "account.mute_short": "Stummschalten", "account.muted": "Stummgeschaltet", "account.muting": "Stummgeschaltet", - "account.mutual": "Ihr folgt einander", + "account.mutual": "Ihr folgt euch", "account.no_bio": "Keine Beschreibung verfügbar.", "account.open_original_page": "Ursprüngliche Seite öffnen", "account.posts": "Beiträge", @@ -225,7 +225,7 @@ "confirmations.discard_draft.edit.title": "Änderungen an diesem Beitrag verwerfen?", "confirmations.discard_draft.post.cancel": "Entwurf fortsetzen", "confirmations.discard_draft.post.message": "Beim Fortfahren wird der gerade verfasste Beitrag verworfen.", - "confirmations.discard_draft.post.title": "Beitragsentwurf verwerfen?", + "confirmations.discard_draft.post.title": "Entwurf verwerfen?", "confirmations.discard_edit_media.confirm": "Verwerfen", "confirmations.discard_edit_media.message": "Du hast Änderungen an der Medienbeschreibung oder -vorschau vorgenommen, die noch nicht gespeichert sind. Trotzdem verwerfen?", "confirmations.follow_to_list.confirm": "Folgen und zur Liste hinzufügen", @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Follower entfernen", "confirmations.remove_from_followers.message": "{name} wird dir nicht länger folgen. Bist du dir sicher?", "confirmations.remove_from_followers.title": "Follower entfernen?", + "confirmations.revoke_quote.confirm": "Beitrag entfernen", + "confirmations.revoke_quote.message": "Diese Aktion kann nicht rückgängig gemacht werden.", + "confirmations.revoke_quote.title": "Beitrag entfernen?", "confirmations.unfollow.confirm": "Entfolgen", "confirmations.unfollow.message": "Möchtest du {name} wirklich entfolgen?", "confirmations.unfollow.title": "Profil entfolgen?", @@ -362,7 +365,7 @@ "filter_modal.select_filter.subtitle": "Einem vorhandenen Filter hinzufügen oder einen neuen erstellen", "filter_modal.select_filter.title": "Diesen Beitrag filtern", "filter_modal.title.status": "Beitrag per Filter ausblenden", - "filter_warning.matches_filter": "Übereinstimmend mit dem Filter „{title}“", + "filter_warning.matches_filter": "Ausgeblendet wegen des Filters „{title}“", "filtered_notifications_banner.pending_requests": "Von {count, plural, =0 {keinem Profil, das dir möglicherweise bekannt ist} one {einem Profil, das dir möglicherweise bekannt ist} other {# Profilen, die dir möglicherweise bekannt sind}}", "filtered_notifications_banner.title": "Gefilterte Benachrichtigungen", "firehose.all": "Alle Server", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Weitere Follower auf {domain} ansehen", "hints.profiles.see_more_follows": "Weitere gefolgte Profile auf {domain} ansehen", "hints.profiles.see_more_posts": "Weitere Beiträge auf {domain} ansehen", - "hints.threads.replies_may_be_missing": "Möglicherweise werden nicht alle Antworten von anderen Servern angezeigt.", - "hints.threads.see_more": "Weitere Antworten auf {domain} ansehen", "home.column_settings.show_quotes": "Zitierte Beiträge anzeigen", "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_replies": "Antworten anzeigen", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "Beitrag übersetzen", "keyboard_shortcuts.unfocus": "Eingabefeld/Suche nicht mehr fokussieren", "keyboard_shortcuts.up": "Ansicht nach oben bewegen", + "learn_more_link.got_it": "Verstanden", + "learn_more_link.learn_more": "Mehr erfahren", "lightbox.close": "Schließen", "lightbox.next": "Vor", "lightbox.previous": "Zurück", @@ -600,6 +603,7 @@ "notification.label.mention": "Erwähnung", "notification.label.private_mention": "Private Erwähnung", "notification.label.private_reply": "Private Antwort", + "notification.label.quote": "{name} zitierte deinen Beitrag", "notification.label.reply": "Antwort", "notification.mention": "Erwähnung", "notification.mentioned_you": "{name} erwähnte dich", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.poll": "Umfrageergebnisse:", "notifications.column_settings.push": "Push-Benachrichtigungen", + "notifications.column_settings.quote": "Zitate:", "notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.show": "In dieser Spalte anzeigen", "notifications.column_settings.sound": "Ton abspielen", @@ -847,6 +852,8 @@ "status.bookmark": "Lesezeichen setzen", "status.cancel_reblog_private": "Beitrag nicht mehr teilen", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden", + "status.context.load_new_replies": "Neue Antworten verfügbar", + "status.context.loading": "Weitere Antworten werden abgerufen", "status.continued_thread": "Fortgeführter Thread", "status.copy": "Link zum Beitrag kopieren", "status.delete": "Beitrag löschen", @@ -873,12 +880,11 @@ "status.open": "Beitrag öffnen", "status.pin": "Im Profil anheften", "status.quote_error.filtered": "Ausgeblendet wegen eines deiner Filter", - "status.quote_error.not_found": "Dieser Beitrag kann nicht angezeigt werden.", - "status.quote_error.pending_approval": "Dieser Beitrag muss noch durch das ursprüngliche Profil genehmigt werden.", - "status.quote_error.rejected": "Dieser Beitrag kann nicht angezeigt werden, weil das ursprüngliche Profil das Zitieren nicht erlaubt.", - "status.quote_error.removed": "Dieser Beitrag wurde durch das Profil entfernt.", - "status.quote_error.unauthorized": "Dieser Beitrag kann nicht angezeigt werden, weil du zum Ansehen nicht berechtigt bist.", - "status.quote_post_author": "Beitrag von {name}", + "status.quote_error.not_available": "Beitrag nicht verfügbar", + "status.quote_error.pending_approval": "Beitragsveröffentlichung ausstehend", + "status.quote_error.pending_approval_popout.body": "Zitierte Beiträge, die im Fediverse geteilt werden, benötigen einige Zeit, bis sie überall angezeigt werden, da die verschiedenen Server unterschiedliche Protokolle nutzen.", + "status.quote_error.pending_approval_popout.title": "Zitierter Beitrag noch nicht freigegeben? Immer mit der Ruhe", + "status.quote_post_author": "Zitierte einen Beitrag von @{name}", "status.read_more": "Gesamten Beitrag anschauen", "status.reblog": "Teilen", "status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen", @@ -893,6 +899,7 @@ "status.reply": "Antworten", "status.replyAll": "Allen antworten", "status.report": "@{name} melden", + "status.revoke_quote": "Meinen zitierten Beitrag aus dem Beitrag von @{name} entfernen", "status.sensitive_warning": "Inhaltswarnung", "status.share": "Teilen", "status.show_less_all": "Alles einklappen", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 864632a6353..58de44bce39 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Δες περισσότερους ακόλουθους στο {domain}", "hints.profiles.see_more_follows": "Δες περισσότερα άτομα που ακολουθούνται στο {domain}", "hints.profiles.see_more_posts": "Δες περισσότερες αναρτήσεις στο {domain}", - "hints.threads.replies_may_be_missing": "Απαντήσεις από άλλους διακομιστές μπορεί να λείπουν.", - "hints.threads.see_more": "Δες περισσότερες απαντήσεις στο {domain}", "home.column_settings.show_quotes": "Εμφάνιση παραθεμάτων", "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", "home.column_settings.show_replies": "Εμφάνιση απαντήσεων", @@ -500,6 +498,8 @@ "keyboard_shortcuts.translate": "για να μεταφραστεί μια ανάρτηση", "keyboard_shortcuts.unfocus": "Αποεστίαση του πεδίου σύνθεσης/αναζήτησης", "keyboard_shortcuts.up": "Μετακίνηση προς τα πάνω στη λίστα", + "learn_more_link.got_it": "Το κατάλαβα", + "learn_more_link.learn_more": "Μάθε περισσότερα", "lightbox.close": "Κλείσιμο", "lightbox.next": "Επόμενο", "lightbox.previous": "Προηγούμενο", @@ -600,6 +600,7 @@ "notification.label.mention": "Επισήμανση", "notification.label.private_mention": "Ιδιωτική επισήμανση", "notification.label.private_reply": "Ιδιωτική απάντηση", + "notification.label.quote": "Ο/Η {name} έκανε παράθεση της ανάρτησής σου", "notification.label.reply": "Απάντηση", "notification.mention": "Επισήμανση", "notification.mentioned_you": "Ο χρήστης {name} σε επισήμανε", @@ -614,7 +615,7 @@ "notification.moderation_warning.action_suspend": "Ο λογαριασμός σου έχει ανασταλεί.", "notification.own_poll": "Η δημοσκόπησή σου έληξε", "notification.poll": "Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει", - "notification.reblog": "Ο/Η {name} ενίσχυσε τη δημοσίευσή σου", + "notification.reblog": "Ο/Η {name} ενίσχυσε την ανάρτηση σου", "notification.reblog.name_and_others_with_link": "{name} και {count, plural, one {# ακόμη} other {# ακόμη}} ενίσχυσαν την ανάρτησή σου", "notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}", "notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Επισημάνσεις:", "notifications.column_settings.poll": "Αποτελέσματα δημοσκόπησης:", "notifications.column_settings.push": "Ειδοποιήσεις Push", + "notifications.column_settings.quote": "Παραθέσεις:", "notifications.column_settings.reblog": "Ενισχύσεις:", "notifications.column_settings.show": "Εμφάνισε σε στήλη", "notifications.column_settings.sound": "Αναπαραγωγή ήχου", @@ -847,6 +849,8 @@ "status.bookmark": "Σελιδοδείκτης", "status.cancel_reblog_private": "Ακύρωση ενίσχυσης", "status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί", + "status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες", + "status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις", "status.continued_thread": "Συνεχιζόμενο νήματος", "status.copy": "Αντιγραφή συνδέσμου ανάρτησης", "status.delete": "Διαγραφή", @@ -873,12 +877,11 @@ "status.open": "Επέκταση ανάρτησης", "status.pin": "Καρφίτσωσε στο προφίλ", "status.quote_error.filtered": "Κρυφό λόγω ενός από τα φίλτρα σου", - "status.quote_error.not_found": "Αυτή η ανάρτηση δεν μπορεί να εμφανιστεί.", - "status.quote_error.pending_approval": "Αυτή η ανάρτηση εκκρεμεί έγκριση από τον αρχικό συντάκτη.", - "status.quote_error.rejected": "Αυτή η ανάρτηση δεν μπορεί να εμφανιστεί καθώς ο αρχικός συντάκτης δεν επιτρέπει τις παραθέσεις.", - "status.quote_error.removed": "Αυτή η ανάρτηση αφαιρέθηκε από τον συντάκτη της.", - "status.quote_error.unauthorized": "Αυτή η ανάρτηση δεν μπορεί να εμφανιστεί καθώς δεν έχεις εξουσιοδότηση για να τη δεις.", - "status.quote_post_author": "Ανάρτηση από {name}", + "status.quote_error.not_available": "Ανάρτηση μη διαθέσιμη", + "status.quote_error.pending_approval": "Ανάρτηση σε αναμονή", + "status.quote_error.pending_approval_popout.body": "Οι παραθέσεις που μοιράζονται στο Fediverse μπορεί να χρειαστούν χρόνο για να εμφανιστούν, καθώς διαφορετικοί διακομιστές έχουν διαφορετικά πρωτόκολλα.", + "status.quote_error.pending_approval_popout.title": "Παράθεση σε εκκρεμότητα; Μείνετε ψύχραιμοι", + "status.quote_post_author": "Παρατίθεται μια ανάρτηση από @{name}", "status.read_more": "Διάβασε περισότερα", "status.reblog": "Ενίσχυση", "status.reblog_private": "Ενίσχυση με αρχική ορατότητα", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 1702076b3fb..1bb482d9888 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "See more followers on {domain}", "hints.profiles.see_more_follows": "See more follows on {domain}", "hints.profiles.see_more_posts": "See more posts on {domain}", - "hints.threads.replies_may_be_missing": "Replies from other servers may be missing.", - "hints.threads.see_more": "See more replies on {domain}", "home.column_settings.show_quotes": "Show quotes", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", @@ -614,7 +612,7 @@ "notification.moderation_warning.action_suspend": "Your account has been suspended.", "notification.own_poll": "Your poll has ended", "notification.poll": "A poll you voted in has ended", - "notification.reblog": "{name} boosted your status", + "notification.reblog": "{name} boosted your post", "notification.reblog.name_and_others_with_link": "{name} and {count, plural, one {# other} other {# others}} boosted your post", "notification.relationships_severance_event": "Lost connections with {name}", "notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.", @@ -873,12 +871,6 @@ "status.open": "Expand this post", "status.pin": "Pin on profile", "status.quote_error.filtered": "Hidden due to one of your filters", - "status.quote_error.not_found": "This post cannot be displayed.", - "status.quote_error.pending_approval": "This post is pending approval from the original author.", - "status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.", - "status.quote_error.removed": "This post was removed by its author.", - "status.quote_error.unauthorized": "This post cannot be displayed as you are not authorised", - "status.quote_post_author": "Post by {name}", "status.read_more": "Read more", "status.reblog": "Boost", "status.reblog_private": "Boost with original visibility", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 13b7aa42121..6b796f9f81d 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Remove follower", "confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?", "confirmations.remove_from_followers.title": "Remove follower?", + "confirmations.revoke_quote.confirm": "Remove post", + "confirmations.revoke_quote.message": "This action cannot be undone.", + "confirmations.revoke_quote.title": "Remove post?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.title": "Unfollow user?", @@ -498,6 +501,8 @@ "keyboard_shortcuts.translate": "to translate a post", "keyboard_shortcuts.unfocus": "Unfocus compose textarea/search", "keyboard_shortcuts.up": "Move up in the list", + "learn_more_link.got_it": "Got it", + "learn_more_link.learn_more": "Learn more", "lightbox.close": "Close", "lightbox.next": "Next", "lightbox.previous": "Previous", @@ -598,6 +603,7 @@ "notification.label.mention": "Mention", "notification.label.private_mention": "Private mention", "notification.label.private_reply": "Private reply", + "notification.label.quote": "{name} quoted your post", "notification.label.reply": "Reply", "notification.mention": "Mention", "notification.mentioned_you": "{name} mentioned you", @@ -655,6 +661,7 @@ "notifications.column_settings.mention": "Mentions:", "notifications.column_settings.poll": "Poll results:", "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.quote": "Quotes:", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", @@ -873,12 +880,11 @@ "status.open": "Expand this post", "status.pin": "Pin on profile", "status.quote_error.filtered": "Hidden due to one of your filters", - "status.quote_error.not_found": "This post cannot be displayed.", - "status.quote_error.pending_approval": "This post is pending approval from the original author.", - "status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.", - "status.quote_error.removed": "This post was removed by its author.", - "status.quote_error.unauthorized": "This post cannot be displayed as you are not authorized to view it.", - "status.quote_post_author": "Post by {name}", + "status.quote_error.not_available": "Post unavailable", + "status.quote_error.pending_approval": "Post pending", + "status.quote_error.pending_approval_popout.body": "Quotes shared across the Fediverse may take time to display, as different servers have different protocols.", + "status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm", + "status.quote_post_author": "Quoted a post by @{name}", "status.read_more": "Read more", "status.reblog": "Boost", "status.reblog_private": "Boost with original visibility", @@ -893,6 +899,7 @@ "status.reply": "Reply", "status.replyAll": "Reply to thread", "status.report": "Report @{name}", + "status.revoke_quote": "Remove my post from @{name}’s post", "status.sensitive_warning": "Sensitive content", "status.share": "Share", "status.show_less_all": "Show less for all", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 3af96c22c45..938e9e706b6 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -1,7 +1,7 @@ { "about.blocks": "Reguligitaj serviloj", "about.contact": "Kontakto:", - "about.default_locale": "기본", + "about.default_locale": "Defaŭlta", "about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Kialo ne disponeblas", "about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.", @@ -40,6 +40,7 @@ "account.followers": "Sekvantoj", "account.followers.empty": "Ankoraŭ neniu sekvas ĉi tiun uzanton.", "account.followers_counter": "{count, plural, one{{counter} sekvanto} other {{counter} sekvantoj}}", + "account.followers_you_know_counter": "Vi scias {counter}", "account.following": "Sekvatoj", "account.following_counter": "{count, plural, one {{counter} sekvato} other {{counter} sekvatoj}}", "account.follows.empty": "La uzanto ankoraŭ ne sekvas iun ajn.", @@ -215,6 +216,11 @@ "confirmations.delete_list.confirm": "Forigi", "confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?", "confirmations.delete_list.title": "Ĉu forigi liston?", + "confirmations.discard_draft.confirm": "Forĵetu kaj daŭrigu", + "confirmations.discard_draft.edit.cancel": "Daŭrigi redaktadon", + "confirmations.discard_draft.edit.title": "Ĉu forĵeti ŝanĝojn al via afiŝo?", + "confirmations.discard_draft.post.cancel": "Daŭrigi malneton", + "confirmations.discard_draft.post.title": "Ĉu forĵeti vian malneton?", "confirmations.discard_edit_media.confirm": "Forĵeti", "confirmations.discard_edit_media.message": "Vi havas nekonservitajn ŝanĝojn de la priskribo aŭ la antaŭvidigo de la vidaŭdaĵo, ĉu vi forĵetu ilin malgraŭe?", "confirmations.follow_to_list.confirm": "Sekvi kaj aldoni al listo", @@ -234,6 +240,9 @@ "confirmations.remove_from_followers.confirm": "Forigi sekvanton", "confirmations.remove_from_followers.message": "{name} ne plu sekvos vin. Ĉu vi certas ke vi volas daŭri?", "confirmations.remove_from_followers.title": "Forigi sekvanton?", + "confirmations.revoke_quote.confirm": "Forigi afiŝon", + "confirmations.revoke_quote.message": "Ĉi tiu ago ne povas esti malfarita.", + "confirmations.revoke_quote.title": "Ĉu forigi afiŝon?", "confirmations.unfollow.confirm": "Ne plu sekvi", "confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?", "confirmations.unfollow.title": "Ĉu ĉesi sekvi uzanton?", @@ -331,6 +340,7 @@ "featured_carousel.next": "Antaŭen", "featured_carousel.post": "Afiŝi", "featured_carousel.previous": "Malantaŭen", + "featured_carousel.slide": "{index} de {total}", "filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.", "filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!", "filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.", @@ -409,8 +419,7 @@ "hints.profiles.see_more_followers": "Vidi pli da sekvantoj sur {domain}", "hints.profiles.see_more_follows": "Vidi pli da sekvatoj sur {domain}", "hints.profiles.see_more_posts": "Vidi pli da afiŝoj sur {domain}", - "hints.threads.replies_may_be_missing": "Respondoj de aliaj serviloj eble mankas.", - "hints.threads.see_more": "Vidi pli da respondoj sur {domain}", + "home.column_settings.show_quotes": "Montri citaĵojn", "home.column_settings.show_reblogs": "Montri diskonigojn", "home.column_settings.show_replies": "Montri respondojn", "home.hide_announcements": "Kaŝi la anoncojn", @@ -484,6 +493,8 @@ "keyboard_shortcuts.translate": "Traduki afiŝon", "keyboard_shortcuts.unfocus": "Senfokusigi verki tekstareon/serĉon", "keyboard_shortcuts.up": "Movu supren en la listo", + "learn_more_link.got_it": "Komprenite", + "learn_more_link.learn_more": "Lernu pli", "lightbox.close": "Fermi", "lightbox.next": "Antaŭen", "lightbox.previous": "Malantaŭen", @@ -533,8 +544,10 @@ "mute_modal.you_wont_see_mentions": "Vi ne vidos afiŝojn, kiuj mencias ilin.", "mute_modal.you_wont_see_posts": "Ili ankoraŭ povas vidi viajn afiŝojn, sed vi ne vidos iliajn.", "navigation_bar.about": "Pri", + "navigation_bar.account_settings": "Pasvorto kaj sekureco", "navigation_bar.administration": "Administrado", "navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon", + "navigation_bar.automated_deletion": "Aŭtomata forigo de afiŝoj", "navigation_bar.blocks": "Blokitaj uzantoj", "navigation_bar.bookmarks": "Legosignoj", "navigation_bar.direct": "Privataj mencioj", @@ -544,6 +557,7 @@ "navigation_bar.follow_requests": "Petoj de sekvado", "navigation_bar.followed_tags": "Sekvataj kradvortoj", "navigation_bar.follows_and_followers": "Sekvatoj kaj sekvantoj", + "navigation_bar.import_export": "Importo kaj eksporto", "navigation_bar.lists": "Listoj", "navigation_bar.logout": "Elsaluti", "navigation_bar.moderation": "Modereco", @@ -551,6 +565,7 @@ "navigation_bar.mutes": "Silentigitaj uzantoj", "navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.", "navigation_bar.preferences": "Preferoj", + "navigation_bar.privacy_and_reach": "Privateco kaj atingo", "navigation_bar.search": "Serĉi", "not_signed_in_indicator.not_signed_in": "Necesas saluti por aliri tiun rimedon.", "notification.admin.report": "{name} raportis {target}", @@ -573,6 +588,7 @@ "notification.label.mention": "Mencii", "notification.label.private_mention": "Privata mencio", "notification.label.private_reply": "Privata respondo", + "notification.label.quote": "{name} citis vian afiŝon", "notification.label.reply": "Respondi", "notification.mention": "Mencii", "notification.mentioned_you": "{name} menciis vin", @@ -630,6 +646,7 @@ "notifications.column_settings.mention": "Mencioj:", "notifications.column_settings.poll": "Balotenketaj rezultoj:", "notifications.column_settings.push": "Puŝsciigoj", + "notifications.column_settings.quote": "Citaĵoj:", "notifications.column_settings.reblog": "Diskonigoj:", "notifications.column_settings.show": "Montri en kolumno", "notifications.column_settings.sound": "Eligi sonon", @@ -787,7 +804,7 @@ "search.quick_action.open_url": "Malfermi URL en Mastodono", "search.quick_action.status_search": "Afiŝoj kiuj konformas kun {x}", "search.search_or_paste": "Serĉu aŭ algluu URL-on", - "search_popout.full_text_search_disabled_message": "Ne havebla sur {domain}.", + "search_popout.full_text_search_disabled_message": "Ne disponebla sur {domain}.", "search_popout.full_text_search_logged_out_message": "Disponebla nur kiam ensalutinte.", "search_popout.language_code": "ISO-lingva kodo", "search_popout.options": "Serĉaj opcioj", @@ -820,6 +837,8 @@ "status.bookmark": "Aldoni al la legosignoj", "status.cancel_reblog_private": "Ne plu diskonigi", "status.cannot_reblog": "Ĉi tiun afiŝon ne eblas diskonigi", + "status.context.load_new_replies": "Disponeblaj novaj respondoj", + "status.context.loading": "Serĉante pliajn respondojn", "status.continued_thread": "Daŭrigis fadenon", "status.copy": "Kopii la ligilon al la afiŝo", "status.delete": "Forigi", @@ -845,6 +864,9 @@ "status.mute_conversation": "Silentigi konversacion", "status.open": "Pligrandigu ĉi tiun afiŝon", "status.pin": "Alpingli al la profilo", + "status.quote_error.not_available": "Afiŝo ne disponebla", + "status.quote_error.pending_approval": "Pritraktata afiŝo", + "status.quote_error.pending_approval_popout.title": "Ĉu pritraktata citaĵo? Restu trankvila", "status.read_more": "Legi pli", "status.reblog": "Diskonigi", "status.reblog_private": "Diskonigi kun la sama videbleco", @@ -876,6 +898,7 @@ "tabs_bar.home": "Hejmo", "tabs_bar.menu": "Menuo", "tabs_bar.notifications": "Sciigoj", + "tabs_bar.publish": "Nova afiŝo", "tabs_bar.search": "Serĉi", "terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}", "terms_of_service.title": "Kondiĉoj de uzado", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 89f3873564a..2b2ce14d5bc 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Quitar seguidor", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que querés continuar?", "confirmations.remove_from_followers.title": "¿Quitar seguidor?", + "confirmations.revoke_quote.confirm": "Eliminar mensaje", + "confirmations.revoke_quote.message": "Esta acción no se puede deshacer.", + "confirmations.revoke_quote.title": "¿Eliminar mensaje?", "confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.message": "¿Estás seguro que querés dejar de seguir a {name}?", "confirmations.unfollow.title": "¿Dejar de seguir al usuario?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_follows": "Ver más seguimientos en {domain}", "hints.profiles.see_more_posts": "Ver más mensajes en {domain}", - "hints.threads.replies_may_be_missing": "Es posible que falten respuestas de otros servidores.", - "hints.threads.see_more": "Ver más respuestas en {domain}", "home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_reblogs": "Mostrar adhesiones", "home.column_settings.show_replies": "Mostrar respuestas", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "para traducir un mensaje", "keyboard_shortcuts.unfocus": "Quitar el foco del área de texto de redacción o de búsqueda", "keyboard_shortcuts.up": "Subir en la lista", + "learn_more_link.got_it": "Entendido", + "learn_more_link.learn_more": "Aprendé más", "lightbox.close": "Cerrar", "lightbox.next": "Siguiente", "lightbox.previous": "Anterior", @@ -600,6 +603,7 @@ "notification.label.mention": "Mención", "notification.label.private_mention": "Mención privada", "notification.label.private_reply": "Respuesta privada", + "notification.label.quote": "{name} citó tu mensaje", "notification.label.reply": "Respuesta", "notification.mention": "Mención", "notification.mentioned_you": "{name} te mencionó", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Menciones:", "notifications.column_settings.poll": "Resultados de la encuesta:", "notifications.column_settings.push": "Notificaciones push", + "notifications.column_settings.quote": "Citas:", "notifications.column_settings.reblog": "Adhesiones:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", @@ -847,6 +852,8 @@ "status.bookmark": "Marcar", "status.cancel_reblog_private": "Quitar adhesión", "status.cannot_reblog": "No se puede adherir a este mensaje", + "status.context.load_new_replies": "Hay nuevas respuestas", + "status.context.loading": "Buscando más respuestas", "status.continued_thread": "Continuación de hilo", "status.copy": "Copiar enlace al mensaje", "status.delete": "Eliminar", @@ -873,12 +880,11 @@ "status.open": "Expandir este mensaje", "status.pin": "Fijar en el perfil", "status.quote_error.filtered": "Oculto debido a uno de tus filtros", - "status.quote_error.not_found": "No se puede mostrar este mensaje.", - "status.quote_error.pending_approval": "Este mensaje está pendiente de aprobación del autor original.", - "status.quote_error.rejected": "No se puede mostrar este mensaje, ya que el autor original no permite que se cite.", - "status.quote_error.removed": "Este mensaje fue eliminado por su autor.", - "status.quote_error.unauthorized": "No se puede mostrar este mensaje, ya que no tenés autorización para verlo.", - "status.quote_post_author": "Mensaje de @{name}", + "status.quote_error.not_available": "Mensaje no disponible", + "status.quote_error.pending_approval": "Mensaje pendiente", + "status.quote_error.pending_approval_popout.body": "Las citas compartidas a través del Fediverso pueden tardar en mostrarse, ya que diferentes servidores tienen diferentes protocolos.", + "status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Esperá un momento", + "status.quote_post_author": "Se citó un mensaje de @{name}", "status.read_more": "Leé más", "status.reblog": "Adherir", "status.reblog_private": "Adherir a la audiencia original", @@ -893,6 +899,7 @@ "status.reply": "Responder", "status.replyAll": "Responder al hilo", "status.report": "Denunciar a @{name}", + "status.revoke_quote": "Eliminar mi mensaje de la cita de @{name}", "status.sensitive_warning": "Contenido sensible", "status.share": "Compartir", "status.show_less_all": "Mostrar menos para todo", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index d764b3dac97..317f5311088 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Eliminar seguidor", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?", "confirmations.remove_from_followers.title": "¿Eliminar seguidor?", + "confirmations.revoke_quote.confirm": "Eliminar publicación", + "confirmations.revoke_quote.message": "Esta acción no se puede deshacer.", + "confirmations.revoke_quote.title": "¿Deseas eliminar la publicación?", "confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", "confirmations.unfollow.title": "¿Dejar de seguir al usuario?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_follows": "Ver más perfiles seguidos en {domain}", "hints.profiles.see_more_posts": "Ver más publicaciones en {domain}", - "hints.threads.replies_may_be_missing": "Puede que no se muestren algunas respuestas de otros servidores.", - "hints.threads.see_more": "Ver más respuestas en {domain}", "home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respuestas", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "para traducir una publicación", "keyboard_shortcuts.unfocus": "Desenfocar área de redacción/búsqueda", "keyboard_shortcuts.up": "Ascender en la lista", + "learn_more_link.got_it": "Entendido", + "learn_more_link.learn_more": "Más información", "lightbox.close": "Cerrar", "lightbox.next": "Siguiente", "lightbox.previous": "Anterior", @@ -600,6 +603,7 @@ "notification.label.mention": "Mención", "notification.label.private_mention": "Mención privada", "notification.label.private_reply": "Respuesta privada", + "notification.label.quote": "{name} citó tu publicación", "notification.label.reply": "Respuesta", "notification.mention": "Mención", "notification.mentioned_you": "{name} te mencionó", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Menciones:", "notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.push": "Notificaciones push", + "notifications.column_settings.quote": "Citas:", "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", @@ -847,6 +852,8 @@ "status.bookmark": "Añadir marcador", "status.cancel_reblog_private": "Deshacer impulso", "status.cannot_reblog": "Esta publicación no puede ser impulsada", + "status.context.load_new_replies": "Nuevas respuestas disponibles", + "status.context.loading": "Comprobando si hay más respuestas", "status.continued_thread": "Hilo continuado", "status.copy": "Copiar enlace al estado", "status.delete": "Borrar", @@ -873,12 +880,11 @@ "status.open": "Expandir estado", "status.pin": "Fijar", "status.quote_error.filtered": "Oculto debido a uno de tus filtros", - "status.quote_error.not_found": "No se puede mostrar esta publicación.", - "status.quote_error.pending_approval": "Esta publicación está pendiente de aprobación del autor original.", - "status.quote_error.rejected": "No se puede mostrar esta publicación, puesto que el autor original no permite que sea citado.", - "status.quote_error.removed": "Esta publicación fue eliminada por su autor.", - "status.quote_error.unauthorized": "No se puede mostrar esta publicación, puesto que no estás autorizado a verla.", - "status.quote_post_author": "Publicado por {name}", + "status.quote_error.not_available": "Publicación no disponible", + "status.quote_error.pending_approval": "Publicación pendiente", + "status.quote_error.pending_approval_popout.body": "Las citas compartidas en el Fediverso pueden tardar en mostrarse, ya que cada servidor tiene un protocolo diferente.", + "status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Mantén la calma", + "status.quote_post_author": "Ha citado una publicación de @{name}", "status.read_more": "Leer más", "status.reblog": "Impulsar", "status.reblog_private": "Implusar a la audiencia original", @@ -893,6 +899,7 @@ "status.reply": "Responder", "status.replyAll": "Responder al hilo", "status.report": "Reportar @{name}", + "status.revoke_quote": "Eliminar mi publicación de la cita de @{name}", "status.sensitive_warning": "Contenido sensible", "status.share": "Compartir", "status.show_less_all": "Mostrar menos para todo", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 47d23e5b40d..d23f1f5c128 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Eliminar seguidor", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?", "confirmations.remove_from_followers.title": "¿Eliminar seguidor?", + "confirmations.revoke_quote.confirm": "Eliminar publicación", + "confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.", + "confirmations.revoke_quote.title": "¿Eliminar la publicación?", "confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.message": "¿Seguro que quieres dejar de seguir a {name}?", "confirmations.unfollow.title": "¿Dejar de seguir al usuario?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_follows": "Ver más perfiles seguidos en {domain}", "hints.profiles.see_more_posts": "Ver más publicaciones en {domain}", - "hints.threads.replies_may_be_missing": "Puede que no se muestren algunas respuestas de otros servidores.", - "hints.threads.see_more": "Ver más respuestas en {domain}", "home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respuestas", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "para traducir una publicación", "keyboard_shortcuts.unfocus": "Quitar el foco de la caja de redacción/búsqueda", "keyboard_shortcuts.up": "Moverse hacia arriba en la lista", + "learn_more_link.got_it": "Entendido", + "learn_more_link.learn_more": "Más información", "lightbox.close": "Cerrar", "lightbox.next": "Siguiente", "lightbox.previous": "Anterior", @@ -600,6 +603,7 @@ "notification.label.mention": "Mención", "notification.label.private_mention": "Mención privada", "notification.label.private_reply": "Respuesta privada", + "notification.label.quote": "{name} citó tu publicación", "notification.label.reply": "Respuesta", "notification.mention": "Mención", "notification.mentioned_you": "{name} te ha mencionado", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Menciones:", "notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.push": "Notificaciones push", + "notifications.column_settings.quote": "Citas:", "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", @@ -847,6 +852,8 @@ "status.bookmark": "Añadir marcador", "status.cancel_reblog_private": "Deshacer impulso", "status.cannot_reblog": "Esta publicación no se puede impulsar", + "status.context.load_new_replies": "Hay nuevas respuestas", + "status.context.loading": "Buscando más respuestas", "status.continued_thread": "Continuó el hilo", "status.copy": "Copiar enlace a la publicación", "status.delete": "Borrar", @@ -873,12 +880,11 @@ "status.open": "Expandir publicación", "status.pin": "Fijar", "status.quote_error.filtered": "Oculto debido a uno de tus filtros", - "status.quote_error.not_found": "No se puede mostrar esta publicación.", - "status.quote_error.pending_approval": "Esta publicación está pendiente de aprobación del autor original.", - "status.quote_error.rejected": "Esta publicación no puede mostrarse porque el autor original no permite que se cite.", - "status.quote_error.removed": "Esta publicación fue eliminada por su autor.", - "status.quote_error.unauthorized": "Esta publicación no puede mostrarse, ya que no estás autorizado a verla.", - "status.quote_post_author": "Publicación de {name}", + "status.quote_error.not_available": "Publicación no disponible", + "status.quote_error.pending_approval": "Publicación pendiente", + "status.quote_error.pending_approval_popout.body": "Las citas compartidas a través del Fediverso pueden tardar en mostrarse, ya que los diferentes servidores tienen diferentes protocolos.", + "status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Mantén la calma", + "status.quote_post_author": "Ha citado una publicación de @{name}", "status.read_more": "Leer más", "status.reblog": "Impulsar", "status.reblog_private": "Impulsar a la audiencia original", @@ -893,6 +899,7 @@ "status.reply": "Responder", "status.replyAll": "Responder al hilo", "status.report": "Reportar a @{name}", + "status.revoke_quote": "Eliminar mi publicación de la cita de {name}", "status.sensitive_warning": "Contenido sensible", "status.share": "Compartir", "status.show_less_all": "Mostrar menos para todo", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index ffb33c7e7a6..e5d717d2270 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Vaata rohkem jälgijaid kohas {domain}", "hints.profiles.see_more_follows": "Vaata rohkem jälgitavaid kohas {domain}", "hints.profiles.see_more_posts": "Vaata rohkem postitusi kohas {domain}", - "hints.threads.replies_may_be_missing": "Vastuseid teistest serveritest võib olla puudu.", - "hints.threads.see_more": "Vaata rohkem vastuseid kohas {domain}", "home.column_settings.show_quotes": "Näita tsiteeritut", "home.column_settings.show_reblogs": "Näita jagamisi", "home.column_settings.show_replies": "Näita vastuseid", @@ -500,6 +498,8 @@ "keyboard_shortcuts.translate": "postituse tõlkimiseks", "keyboard_shortcuts.unfocus": "Fookus tekstialalt/otsingult ära", "keyboard_shortcuts.up": "Liigu loetelus üles", + "learn_more_link.got_it": "Sain aru", + "learn_more_link.learn_more": "Lisateave", "lightbox.close": "Sulge", "lightbox.next": "Järgmine", "lightbox.previous": "Eelmine", @@ -847,6 +847,8 @@ "status.bookmark": "Järjehoidja", "status.cancel_reblog_private": "Lõpeta jagamine", "status.cannot_reblog": "Seda postitust ei saa jagada", + "status.context.load_new_replies": "Leidub uusi vastuseid", + "status.context.loading": "Kontrollin täiendavate vastuste olemasolu", "status.continued_thread": "Jätkatud lõim", "status.copy": "Kopeeri postituse link", "status.delete": "Kustuta", @@ -873,12 +875,11 @@ "status.open": "Laienda postitus", "status.pin": "Kinnita profiilile", "status.quote_error.filtered": "Peidetud mõne kasutatud filtri tõttu", - "status.quote_error.not_found": "Seda postitust ei saa näidata.", - "status.quote_error.pending_approval": "See postitus on algse autori kinnituse ootel.", - "status.quote_error.rejected": "Seda postitust ei saa näidata, kuina algne autor ei luba teda tsiteerida.", - "status.quote_error.removed": "Autor kustutas selle postituse.", - "status.quote_error.unauthorized": "Kuna sul pole luba selle postituse nägemiseks, siis seda ei saa kuvada.", - "status.quote_post_author": "Postitajaks {name}", + "status.quote_error.not_available": "Postitus pole saadaval", + "status.quote_error.pending_approval": "Postitus on ootel", + "status.quote_error.pending_approval_popout.body": "Kuna erinevates serverites on erinevad reeglid, siis üle Födiversumi jagatud tsitaatide kuvamine võib võtta aega.", + "status.quote_error.pending_approval_popout.title": "Tsiteerimine on ootel? Palun jää rahulikuks", + "status.quote_post_author": "Tsiteeris kasutaja @{name} postitust", "status.read_more": "Loe veel", "status.reblog": "Jaga", "status.reblog_private": "Jaga algse nähtavusega", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index b757fb0cdaa..316dc43a4d2 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -30,6 +30,7 @@ "account.edit_profile": "Editatu profila", "account.enable_notifications": "Jakinarazi @{name} erabiltzaileak argitaratzean", "account.endorse": "Nabarmendu profilean", + "account.familiar_followers_many": "Jarraitzaileak: {name1}, {name2} eta beste {othersCount, plural, one {ezagun bat} other {# ezagun}}", "account.familiar_followers_one": "{name1}-k jarraitzen du", "account.familiar_followers_two": "{name1}-k eta {name2}-k jarraitzen dute", "account.featured": "Gailenak", @@ -118,6 +119,8 @@ "annual_report.summary.most_used_hashtag.most_used_hashtag": "traola erabiliena", "annual_report.summary.most_used_hashtag.none": "Bat ere ez", "annual_report.summary.new_posts.new_posts": "bidalketa berriak", + "annual_report.summary.percentile.text": "Horrek jartzen zaitu top (e)an {domain} erabiltzaileen artean ", + "annual_report.summary.percentile.we_wont_tell_bernie": "Bernieri ez diogu ezer esango ;)..", "annual_report.summary.thanks": "Eskerrik asko Mastodonen parte izateagatik!", "attachments_list.unprocessed": "(prozesatu gabe)", "audio.hide": "Ezkutatu audioa", @@ -216,6 +219,7 @@ "confirmations.discard_draft.edit.message": "Jarraitzeak editatzen ari zaren mezuan egindako aldaketak baztertuko ditu.", "confirmations.discard_draft.edit.title": "Baztertu zure argitalpenari egindako aldaketak?", "confirmations.discard_draft.post.cancel": "Zirriborroa berrekin", + "confirmations.discard_draft.post.message": "Jarraituz gero, idazten ari zaren sarrera bertan behera geratuko da.", "confirmations.discard_draft.post.title": "Zure argitalpenaren zirriborroa baztertu nahi duzu?", "confirmations.discard_edit_media.confirm": "Baztertu", "confirmations.discard_edit_media.message": "Multimediaren deskribapen edo aurrebistan gorde gabeko aldaketak daude, baztertu nahi dituzu?", @@ -413,8 +417,6 @@ "hints.profiles.see_more_followers": "Ikusi jarraitzaile gehiago {domain}-(e)n", "hints.profiles.see_more_follows": "Ikusi jarraitzaile gehiago {domain}-(e)n", "hints.profiles.see_more_posts": "Ikusi bidalketa gehiago {domain}-(e)n", - "hints.threads.replies_may_be_missing": "Baliteke beste zerbitzari batzuen erantzun batzuk ez erakustea.", - "hints.threads.see_more": "Ikusi erantzun gehiago {domain}-(e)n", "home.column_settings.show_quotes": "Erakutsi aipamenak", "home.column_settings.show_reblogs": "Erakutsi bultzadak", "home.column_settings.show_replies": "Erakutsi erantzunak", @@ -435,6 +437,7 @@ "ignore_notifications_modal.not_following_title": "Jarraitzen ez dituzun pertsonen jakinarazpenei ez ikusiarena egin?", "ignore_notifications_modal.private_mentions_title": "Eskatu gabeko aipamen pribatuen jakinarazpenei ez ikusiarena egin?", "info_button.label": "Laguntza", + "info_button.what_is_alt_text": "

Zer da Alt testua?

Alt testuak irudiak deskribatzeko aukera ematen du, ikusmen-urritasunak, banda-zabalera txikiko konexioak edo testuinguru gehigarria nahi duten pertsonentzat.

Alt testu argi, zehatz eta objektiboen bidez, guztion irisgarritasuna eta ulermena hobetu ditzakezu.

  • Hartu elementu garrantzitsuenak
  • Laburbildu irudietako testua
  • Erabili esaldien egitura erregularra
  • Baztertu informazio erredundantea.
  • Enfokatu joeretan eta funtsezko elementuetan irudi konplexuetan (diagrametan edo mapetan, adibidez)
", "interaction_modal.action.favourite": "Jarraitzeko, zure kontutik atsegindu behar duzu.", "interaction_modal.action.follow": "Jarraitzeko zure kontutik jarraitu behar duzu.", "interaction_modal.action.reply": "Jarraitzeko zure kontutik erantzun behar duzu.", @@ -841,8 +844,6 @@ "status.mute_conversation": "Mututu elkarrizketa", "status.open": "Hedatu bidalketa hau", "status.pin": "Finkatu profilean", - "status.quote_error.not_found": "Bidalketa hau ezin da erakutsi.", - "status.quote_error.pending_approval": "Bidalketa hau egile originalak onartzeko zain dago.", "status.read_more": "Irakurri gehiago", "status.reblog": "Bultzada", "status.reblog_private": "Bultzada jatorrizko hartzaileei", @@ -903,6 +904,7 @@ "video.hide": "Ezkutatu bideoa", "video.pause": "Pausatu", "video.play": "Jo", + "video.skip_forward": "Jauzi aurrerantz", "video.unmute": "Soinua ezarri", "video.volume_down": "Bolumena jaitsi", "video.volume_up": "Bolumena Igo" diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index fadbb247007..798be24ad65 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -235,7 +235,7 @@ "confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟", "confirmations.logout.title": "خروج؟", "confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید", - "confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.", + "confirmations.missing_alt_text.message": "فرسته‌تان رسانه‌هایی بدون متن جایگزین دارد. افزودن شرح به دسترس‌پذیر شدن محتوایتان برای افراد بیش‌تری کمک می‌کند.", "confirmations.missing_alt_text.secondary": "به هر حال پست کن", "confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟", "confirmations.mute.confirm": "خموش", @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "برداشتن پی‌گیرنده", "confirmations.remove_from_followers.message": "دیگر {name} پیتان نخواهد گرفت. مطمئنید که می‌خواهید ادامه دهید؟", "confirmations.remove_from_followers.title": "برداشتن پی‌گیرنده؟", + "confirmations.revoke_quote.confirm": "حذف فرسته", + "confirmations.revoke_quote.message": "این اقدام قابل بازگشت نیست.", + "confirmations.revoke_quote.title": "آیا فرسته را حذف کنم؟", "confirmations.unfollow.confirm": "پی‌نگرفتن", "confirmations.unfollow.message": "مطمئنید که می‌خواهید به پی‌گیری از {name} پایان دهید؟", "confirmations.unfollow.title": "ناپی‌گیری کاربر؟", @@ -424,9 +427,7 @@ "hints.profiles.see_more_followers": "دیدن پی‌گیرندگان بیش‌تر روی {domain}", "hints.profiles.see_more_follows": "دیدن پی‌گرفته‌های بیش‌تر روی {domain}", "hints.profiles.see_more_posts": "دیدن فرسته‌های بیش‌تر روی {domain}", - "hints.threads.replies_may_be_missing": "شاید پاسخ‌ها از دیگر کارسازها نباشند.", - "hints.threads.see_more": "دیدن پاسخ‌های بیش‌تر روی {domain}", - "home.column_settings.show_quotes": "نمایش نقل‌قول‌ها", + "home.column_settings.show_quotes": "نمایش نقل‌ها", "home.column_settings.show_reblogs": "نمایش تقویت‌ها", "home.column_settings.show_replies": "نمایش پاسخ‌ها", "home.hide_announcements": "نهفتن اعلامیه‌ها", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "برای ترجمه یک پست", "keyboard_shortcuts.unfocus": "برداشتن تمرکز از ناحیهٔ نوشتن یا جست‌وجو", "keyboard_shortcuts.up": "بالا بردن در سیاهه", + "learn_more_link.got_it": "متوجه شدم", + "learn_more_link.learn_more": "دانستن بیش‌تر", "lightbox.close": "بستن", "lightbox.next": "بعدی", "lightbox.previous": "قبلی", @@ -600,6 +603,7 @@ "notification.label.mention": "اشاره", "notification.label.private_mention": "اشارهٔ خصوصی", "notification.label.private_reply": "پاسخ خصوصی", + "notification.label.quote": "{name} فرسته‌تان را نقل کرد", "notification.label.reply": "پاسخ", "notification.mention": "اشاره", "notification.mentioned_you": "‫{name}‬ از شما نام برد", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "اشاره‌ها:", "notifications.column_settings.poll": "نتایج نظرسنجی:", "notifications.column_settings.push": "آگاهی‌های ارسالی", + "notifications.column_settings.quote": "نقل‌قول‌ها:", "notifications.column_settings.reblog": "تقویت‌ها:", "notifications.column_settings.show": "نمایش در ستون", "notifications.column_settings.sound": "پخش صدا", @@ -847,6 +852,8 @@ "status.bookmark": "نشانک", "status.cancel_reblog_private": "ناتقویت", "status.cannot_reblog": "این فرسته قابل تقویت نیست", + "status.context.load_new_replies": "پاسخ‌های جدیدی موجودند", + "status.context.loading": "بررسی کردن برای پاسخ‌های بیش‌تر", "status.continued_thread": "رشتهٔ دنباله دار", "status.copy": "رونوشت از پیوند فرسته", "status.delete": "حذف", @@ -873,12 +880,9 @@ "status.open": "گسترش این فرسته", "status.pin": "سنجاق به نمایه", "status.quote_error.filtered": "نهفته بنا بر یکی از پالایه‌هایتان", - "status.quote_error.not_found": "این فرسته قابل نمایش نیست.", - "status.quote_error.pending_approval": "این فرسته منظر تأیید نگارندهٔ اصلی است.", - "status.quote_error.rejected": "از آن‌جا که نگارندهٔ اصلی فرسته اجازهٔ نقل قولش را نمی‌دهد این فرسته قابل نمایش نیست.", - "status.quote_error.removed": "این فرسته به دست نگارنده‌اش برداشته شده.", - "status.quote_error.unauthorized": "از آن‌جا که اجازهٔ دیدن این فرسته را ندارید قابل نمایش نیست.", - "status.quote_post_author": "فرسته توسط {name}", + "status.quote_error.not_available": "فرسته در دسترس نیست", + "status.quote_error.pending_approval_popout.body": "نقل‌قول‌هایی که در سراسر فدیورس هم‌رسانی می‌شوند ممکن است زمان‌بر باشند تا نمایش داده شوند، چون کارسازهای مختلف از شیوه‌نامه‌های متفاوتی استفاده می‌کنند.", + "status.quote_post_author": "فرسته‌ای از @{name} نقل شد", "status.read_more": "بیشتر بخوانید", "status.reblog": "تقویت", "status.reblog_private": "تقویت برای مخاطبان نخستین", @@ -893,6 +897,7 @@ "status.reply": "پاسخ", "status.replyAll": "پاسخ به رشته", "status.report": "گزارش ‎@{name}", + "status.revoke_quote": "حذف فرسته‌ام از فرسته @{name}", "status.sensitive_warning": "محتوای حساس", "status.share": "هم‌رسانی", "status.show_less_all": "نمایش کمتر همه", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 7de3fc07ea9..2f7c13bfaf8 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -311,7 +311,7 @@ "empty_column.account_featured_other.unknown": "Tämä tili ei suosittele vielä mitään.", "empty_column.account_hides_collections": "Käyttäjä on päättänyt pitää nämä tiedot yksityisinä", "empty_column.account_suspended": "Tili jäädytetty", - "empty_column.account_timeline": "Ei viestejä täällä.", + "empty_column.account_timeline": "Ei julkaisuja täällä!", "empty_column.account_unavailable": "Profiilia ei ole saatavilla", "empty_column.blocks": "Et ole vielä estänyt käyttäjiä.", "empty_column.bookmarked_statuses": "Et ole vielä lisännyt julkaisuja kirjanmerkkeihisi. Kun lisäät yhden, se näkyy tässä.", @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Näytä lisää seuraajia palvelimella {domain}", "hints.profiles.see_more_follows": "Näytä lisää seurattavia palvelimella {domain}", "hints.profiles.see_more_posts": "Näytä lisää julkaisuja palvelimella {domain}", - "hints.threads.replies_may_be_missing": "Muiden palvelinten vastauksia saattaa puuttua.", - "hints.threads.see_more": "Näytä lisää vastauksia palvelimella {domain}", "home.column_settings.show_quotes": "Näytä lainaukset", "home.column_settings.show_reblogs": "Näytä tehostukset", "home.column_settings.show_replies": "Näytä vastaukset", @@ -500,6 +498,8 @@ "keyboard_shortcuts.translate": "Käännä julkaisu", "keyboard_shortcuts.unfocus": "Poistu kirjoitus- tai hakukentästä", "keyboard_shortcuts.up": "Siirry luettelossa taaksepäin", + "learn_more_link.got_it": "Selvä", + "learn_more_link.learn_more": "Lue lisää", "lightbox.close": "Sulje", "lightbox.next": "Seuraava", "lightbox.previous": "Edellinen", @@ -600,6 +600,7 @@ "notification.label.mention": "Maininta", "notification.label.private_mention": "Yksityismaininta", "notification.label.private_reply": "Yksityinen vastaus", + "notification.label.quote": "{name} lainasi julkaisuasi", "notification.label.reply": "Vastaus", "notification.mention": "Maininta", "notification.mentioned_you": "{name} mainitsi sinut", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Maininnat:", "notifications.column_settings.poll": "Äänestystulokset:", "notifications.column_settings.push": "Puskuilmoitukset", + "notifications.column_settings.quote": "Lainaukset:", "notifications.column_settings.reblog": "Tehostukset:", "notifications.column_settings.show": "Näytä sarakkeessa", "notifications.column_settings.sound": "Äänimerkki", @@ -756,7 +758,7 @@ "reply_indicator.cancel": "Peruuta", "reply_indicator.poll": "Äänestys", "report.block": "Estä", - "report.block_explanation": "Et näe hänen viestejään, eikä hän voi nähdä viestejäsi tai seurata sinua. Hän näkee, että olet estänyt hänet.", + "report.block_explanation": "Et näe hänen julkaisujaan. Hän ei voi nähdä julkaisujasi eikä seurata sinua. Hän näkee, että olet estänyt hänet.", "report.categories.legal": "Lakiseikat", "report.categories.other": "Muu", "report.categories.spam": "Roskaposti", @@ -847,6 +849,8 @@ "status.bookmark": "Lisää kirjanmerkki", "status.cancel_reblog_private": "Peru tehostus", "status.cannot_reblog": "Tätä julkaisua ei voi tehostaa", + "status.context.load_new_replies": "Uusia vastauksia saatavilla", + "status.context.loading": "Tarkistetaan lisävastauksia", "status.continued_thread": "Jatkoi ketjua", "status.copy": "Kopioi linkki julkaisuun", "status.delete": "Poista", @@ -873,12 +877,11 @@ "status.open": "Laajenna julkaisu", "status.pin": "Kiinnitä profiiliin", "status.quote_error.filtered": "Piilotettu jonkin asettamasi suodattimen takia", - "status.quote_error.not_found": "Tätä julkaisua ei voi näyttää.", - "status.quote_error.pending_approval": "Tämä julkaisu odottaa alkuperäisen tekijänsä hyväksyntää.", - "status.quote_error.rejected": "Tätä julkaisua ei voi näyttää, sillä sen alkuperäinen tekijä ei salli lainattavan julkaisua.", - "status.quote_error.removed": "Tekijä on poistanut julkaisun.", - "status.quote_error.unauthorized": "Tätä julkaisua ei voi näyttää, koska sinulla ei ole oikeutta tarkastella sitä.", - "status.quote_post_author": "Julkaisu käyttäjältä {name}", + "status.quote_error.not_available": "Julkaisu ei saatavilla", + "status.quote_error.pending_approval": "Julkaisu odottaa", + "status.quote_error.pending_approval_popout.body": "Saattaa viedä jonkin ainaa ennen kuin fediversumin kautta jaetut julkaisut tulevat näkyviin, sillä eri palvelimet käyttävät eri protokollia.", + "status.quote_error.pending_approval_popout.title": "Odottava lainaus? Pysy rauhallisena", + "status.quote_post_author": "Lainaa käyttäjän @{name} julkaisua", "status.read_more": "Näytä enemmän", "status.reblog": "Tehosta", "status.reblog_private": "Tehosta alkuperäiselle yleisölle", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index afd75afc42b..7caaf0c2305 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Strika fylgjara", "confirmations.remove_from_followers.message": "{name} fer ikki longur at fylgja tær. Er tú vís/ur í at tú vilt halda fram?", "confirmations.remove_from_followers.title": "Strika fylgjara?", + "confirmations.revoke_quote.confirm": "Strika post", + "confirmations.revoke_quote.message": "Hendan atgerðin kann ikki angrast.", + "confirmations.revoke_quote.title": "Strika post?", "confirmations.unfollow.confirm": "Fylg ikki", "confirmations.unfollow.message": "Ert tú vís/ur í, at tú vil steðga við at fylgja {name}?", "confirmations.unfollow.title": "Gevst at fylgja brúkara?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Sí fleiri fylgjarar á {domain}", "hints.profiles.see_more_follows": "Sí fleiri, ið viðkomandi fylgir, á {domain}", "hints.profiles.see_more_posts": "Sí fleiri postar á {domain}", - "hints.threads.replies_may_be_missing": "Svar frá øðrum ambætarum mangla møguliga.", - "hints.threads.see_more": "Sí fleiri svar á {domain}", "home.column_settings.show_quotes": "Vís siteringar", "home.column_settings.show_reblogs": "Vís lyft", "home.column_settings.show_replies": "Vís svar", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "at umseta ein post", "keyboard_shortcuts.unfocus": "Tak skrivi-/leiti-økið úr miðdeplinum", "keyboard_shortcuts.up": "Flyt upp á listanum", + "learn_more_link.got_it": "Eg skilji", + "learn_more_link.learn_more": "Lær meira", "lightbox.close": "Lat aftur", "lightbox.next": "Fram", "lightbox.previous": "Aftur", @@ -600,6 +603,7 @@ "notification.label.mention": "Umrøða", "notification.label.private_mention": "Privat umrøða", "notification.label.private_reply": "Privat svar", + "notification.label.quote": "{name} siteraði postin hjá tær", "notification.label.reply": "Svara", "notification.mention": "Umrøð", "notification.mentioned_you": "{name} nevndi teg", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Umrøður:", "notifications.column_settings.poll": "Úrslit frá atkvøðugreiðslu:", "notifications.column_settings.push": "Trýstifráboðanir", + "notifications.column_settings.quote": "Sitatir:", "notifications.column_settings.reblog": "Stimbranir:", "notifications.column_settings.show": "Vís í teigi", "notifications.column_settings.sound": "Spæl ljóð", @@ -847,6 +852,8 @@ "status.bookmark": "Goym", "status.cancel_reblog_private": "Strika stimbran", "status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin", + "status.context.load_new_replies": "Nýggj svar tøk", + "status.context.loading": "Kanni um tað eru fleiri svar", "status.continued_thread": "Framhaldandi tráður", "status.copy": "Kopiera leinki til postin", "status.delete": "Strika", @@ -873,12 +880,11 @@ "status.open": "Víðka henda postin", "status.pin": "Ger fastan í vangan", "status.quote_error.filtered": "Eitt av tínum filtrum fjalir hetta", - "status.quote_error.not_found": "Tað ber ikki til at vísa hendan postin.", - "status.quote_error.pending_approval": "Hesin posturin bíðar eftir góðkenning frá upprunahøvundinum.", - "status.quote_error.rejected": "Hesin posturin kann ikki vísast, tí upprunahøvundurin loyvir ikki at posturin verður siteraður.", - "status.quote_error.removed": "Hesin posturin var strikaður av høvundinum.", - "status.quote_error.unauthorized": "Hesin posturin kann ikki vísast, tí tú hevur ikki rættindi at síggja hann.", - "status.quote_post_author": "Postur hjá @{name}", + "status.quote_error.not_available": "Postur ikki tøkur", + "status.quote_error.pending_approval": "Postur bíðar", + "status.quote_error.pending_approval_popout.body": "Sitatir, sum eru deild tvørtur um fediversið, kunnu taka nakað av tíð at vísast, tí ymiskir ambætarar hava ymiskar protokollir.", + "status.quote_error.pending_approval_popout.title": "Bíðar eftir sitati? Tak tað róligt", + "status.quote_post_author": "Siteraði ein post hjá @{name}", "status.read_more": "Les meira", "status.reblog": "Stimbra", "status.reblog_private": "Stimbra við upprunasýni", @@ -893,6 +899,7 @@ "status.reply": "Svara", "status.replyAll": "Svara tráðnum", "status.report": "Melda @{name}", + "status.revoke_quote": "Strika postin hjá mær frá postinum hjá @{name}", "status.sensitive_warning": "Viðkvæmt tilfar", "status.share": "Deil", "status.show_less_all": "Vís øllum minni", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 0fdd593859a..7803004869e 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -423,8 +423,6 @@ "hints.profiles.see_more_followers": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_follows": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_posts": "Voir plus de messages sur {domain}", - "hints.threads.replies_may_be_missing": "Les réponses provenant des autres serveurs pourraient être manquantes.", - "hints.threads.see_more": "Afficher plus de réponses sur {domain}", "home.column_settings.show_quotes": "Afficher les citations", "home.column_settings.show_reblogs": "Afficher boosts", "home.column_settings.show_replies": "Afficher réponses", @@ -866,9 +864,6 @@ "status.mute_conversation": "Masquer la conversation", "status.open": "Afficher la publication entière", "status.pin": "Épingler sur profil", - "status.quote_error.removed": "Ce message a été retiré par son auteur·ice.", - "status.quote_error.unauthorized": "Ce message ne peut pas être affiché car vous n'êtes pas autorisé·e à le voir.", - "status.quote_post_author": "Message par {name}", "status.read_more": "En savoir plus", "status.reblog": "Booster", "status.reblog_private": "Booster avec visibilité originale", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 6f1bbae61c1..65b97b498b3 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -423,8 +423,6 @@ "hints.profiles.see_more_followers": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_follows": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_posts": "Voir plus de messages sur {domain}", - "hints.threads.replies_may_be_missing": "Les réponses provenant des autres serveurs pourraient être manquantes.", - "hints.threads.see_more": "Afficher plus de réponses sur {domain}", "home.column_settings.show_quotes": "Afficher les citations", "home.column_settings.show_reblogs": "Afficher les partages", "home.column_settings.show_replies": "Afficher les réponses", @@ -866,9 +864,6 @@ "status.mute_conversation": "Masquer la conversation", "status.open": "Afficher le message entier", "status.pin": "Épingler sur le profil", - "status.quote_error.removed": "Ce message a été retiré par son auteur·ice.", - "status.quote_error.unauthorized": "Ce message ne peut pas être affiché car vous n'êtes pas autorisé·e à le voir.", - "status.quote_post_author": "Message par {name}", "status.read_more": "En savoir plus", "status.reblog": "Partager", "status.reblog_private": "Partager à l’audience originale", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index 33c6d89d195..f28fbb2ff86 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Besjoch mear folgers op {domain}", "hints.profiles.see_more_follows": "Besjoch mear folge accounts op {domain}", "hints.profiles.see_more_posts": "Besjoch mear berjochten op {domain}", - "hints.threads.replies_may_be_missing": "Antwurden fan oare servers kinne ûntbrekke.", - "hints.threads.see_more": "Besjoch mear reaksjes op {domain}", "home.column_settings.show_quotes": "Sitaten toane", "home.column_settings.show_reblogs": "Boosts toane", "home.column_settings.show_replies": "Reaksjes toane", @@ -847,6 +845,8 @@ "status.bookmark": "Blêdwizer tafoegje", "status.cancel_reblog_private": "Net langer booste", "status.cannot_reblog": "Dit berjocht kin net boost wurde", + "status.context.load_new_replies": "Nije reaksjes beskikber", + "status.context.loading": "Op nije reaksjes oan it kontrolearjen", "status.continued_thread": "Ferfolgje it petear", "status.copy": "Copy link to status", "status.delete": "Fuortsmite", @@ -873,12 +873,6 @@ "status.open": "Dit berjocht útklappe", "status.pin": "Op profylside fêstsette", "status.quote_error.filtered": "Ferburgen troch ien fan jo filters", - "status.quote_error.not_found": "Dit berjocht kin net toand wurde.", - "status.quote_error.pending_approval": "Dit berjocht is yn ôfwachting fan goedkarring troch de oarspronklike auteur.", - "status.quote_error.rejected": "Dit berjocht kin net toand wurde, omdat de oarspronklike auteur net tastiet dat it sitearre wurdt.", - "status.quote_error.removed": "Dit berjocht is fuotsmiten troch de auteur.", - "status.quote_error.unauthorized": "Dit berjocht kin net toand wurde, omdat jo net it foech hawwe om it te besjen.", - "status.quote_post_author": "Berjocht fan {name}", "status.read_more": "Mear ynfo", "status.reblog": "Booste", "status.reblog_private": "Boost nei oarspronklike ûntfangers", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 92d06a05cf1..1f7c3845e1c 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Féach ar a thuilleadh leantóirí ar {domain}", "hints.profiles.see_more_follows": "Féach tuilleadh seo a leanas ar {domain}", "hints.profiles.see_more_posts": "Féach ar a thuilleadh postálacha ar {domain}", - "hints.threads.replies_may_be_missing": "Seans go bhfuil freagraí ó fhreastalaithe eile in easnamh.", - "hints.threads.see_more": "Féach ar a thuilleadh freagraí ar {domain}", "home.column_settings.show_quotes": "Taispeáin Sleachta", "home.column_settings.show_reblogs": "Taispeáin moltaí", "home.column_settings.show_replies": "Taispeán freagraí", @@ -500,6 +498,8 @@ "keyboard_shortcuts.translate": "post a aistriú", "keyboard_shortcuts.unfocus": "Unfocus cum textarea/search", "keyboard_shortcuts.up": "Bog suas ar an liosta", + "learn_more_link.got_it": "Tuigim é", + "learn_more_link.learn_more": "Foghlaim níos mó", "lightbox.close": "Dún", "lightbox.next": "An céad eile", "lightbox.previous": "Roimhe seo", @@ -600,6 +600,7 @@ "notification.label.mention": "Luaigh", "notification.label.private_mention": "Lua príobháideach", "notification.label.private_reply": "Freagra príobháideach", + "notification.label.quote": "Luaigh {name} do phost", "notification.label.reply": "Freagra", "notification.mention": "Luaigh", "notification.mentioned_you": "Luaigh {name} tú", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Tráchtanna:", "notifications.column_settings.poll": "Torthaí suirbhéanna:", "notifications.column_settings.push": "Brúfhógraí", + "notifications.column_settings.quote": "Luachain:", "notifications.column_settings.reblog": "Moltaí:", "notifications.column_settings.show": "Taispeáin i gcolún", "notifications.column_settings.sound": "Seinn an fhuaim", @@ -847,6 +849,8 @@ "status.bookmark": "Leabharmharcanna", "status.cancel_reblog_private": "Dímhol", "status.cannot_reblog": "Ní féidir an phostáil seo a mholadh", + "status.context.load_new_replies": "Freagraí nua ar fáil", + "status.context.loading": "Ag seiceáil le haghaidh tuilleadh freagraí", "status.continued_thread": "Snáithe ar lean", "status.copy": "Cóipeáil an nasc chuig an bpostáil", "status.delete": "Scrios", @@ -873,12 +877,11 @@ "status.open": "Leathnaigh an post seo", "status.pin": "Pionnáil ar do phróifíl", "status.quote_error.filtered": "I bhfolach mar gheall ar cheann de do scagairí", - "status.quote_error.not_found": "Ní féidir an post seo a thaispeáint.", - "status.quote_error.pending_approval": "Tá an post seo ag feitheamh ar cheadú ón údar bunaidh.", - "status.quote_error.rejected": "Ní féidir an post seo a thaispeáint mar ní cheadaíonn an t-údar bunaidh é a lua.", - "status.quote_error.removed": "Baineadh an post seo ag a údar.", - "status.quote_error.unauthorized": "Ní féidir an post seo a thaispeáint mar níl údarú agat é a fheiceáil.", - "status.quote_post_author": "Postáil le {name}", + "status.quote_error.not_available": "Níl an postáil ar fáil", + "status.quote_error.pending_approval": "Post ar feitheamh", + "status.quote_error.pending_approval_popout.body": "D’fhéadfadh sé go dtógfadh sé tamall le Sleachta a roinntear ar fud Fediverse a thaispeáint, toisc go mbíonn prótacail éagsúla ag freastalaithe éagsúla.", + "status.quote_error.pending_approval_popout.title": "Ag fanacht le luachan? Fan socair", + "status.quote_post_author": "Luaigh mé post le @{name}", "status.read_more": "Léan a thuilleadh", "status.reblog": "Treisiú", "status.reblog_private": "Mol le léargas bunúsach", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index 208e117037b..be02d0a67a7 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Faic barrachd luchd-leantainn air {domain}", "hints.profiles.see_more_follows": "Faic barrachd a tha 'gan leantainn air {domain}", "hints.profiles.see_more_posts": "Faic barrachd phostaichean air {domain}", - "hints.threads.replies_may_be_missing": "Dh’fhaoidte gu bheil freagairtean o fhrithealaichean eile a dhìth.", - "hints.threads.see_more": "Faic barrachd fhreagairtean air {domain}", "home.column_settings.show_quotes": "Seall luaidhean", "home.column_settings.show_reblogs": "Seall na brosnachaidhean", "home.column_settings.show_replies": "Seall na freagairtean", @@ -873,12 +871,6 @@ "status.open": "Leudaich am post seo", "status.pin": "Prìnich ris a’ phròifil", "status.quote_error.filtered": "Falaichte le criathrag a th’ agad", - "status.quote_error.not_found": "Chan urrainn dhuinn am post seo a shealltainn.", - "status.quote_error.pending_approval": "Tha am post seo a’ feitheamh air aontachadh leis an ùghdar tùsail.", - "status.quote_error.rejected": "Chan urrainn dhuinn am post seo a shealltainn air sgàth ’s nach ceadaich an t-ùghdar tùsail aige gun dèid a luaidh.", - "status.quote_error.removed": "Chaidh am post seo a thoirt air falbh le ùghdar.", - "status.quote_error.unauthorized": "Chan urrainn dhuinn am post seo a shealltainn air sgàth ’s nach eil cead agad fhaicinn.", - "status.quote_post_author": "Post le {name}", "status.read_more": "Leugh an còrr", "status.reblog": "Brosnaich", "status.reblog_private": "Brosnaich leis an t-so-fhaicsinneachd tùsail", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 7cb227215e4..c6d0fbd8066 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Quitar seguidora", "confirmations.remove_from_followers.message": "{name} vai deixar de seguirte. É isto o que queres?", "confirmations.remove_from_followers.title": "Quitar seguidora?", + "confirmations.revoke_quote.confirm": "Eliminar publicación", + "confirmations.revoke_quote.message": "Esta acción non se pode desfacer.", + "confirmations.revoke_quote.title": "Eliminar publicación?", "confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.message": "Tes certeza de querer deixar de seguir a {name}?", "confirmations.unfollow.title": "Deixar de seguir á usuaria?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Mira máis seguidoras en {domain}", "hints.profiles.see_more_follows": "Mira máis seguimentos en {domain}", "hints.profiles.see_more_posts": "Mira máis publicacións en {domain}", - "hints.threads.replies_may_be_missing": "Poderían faltar respostas desde outros servidores.", - "hints.threads.see_more": "Mira máis respostas en {domain}", "home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_reblogs": "Amosar compartidos", "home.column_settings.show_replies": "Amosar respostas", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "para traducir unha publicación", "keyboard_shortcuts.unfocus": "Para deixar de destacar a área de escritura/procura", "keyboard_shortcuts.up": "Para mover cara arriba na listaxe", + "learn_more_link.got_it": "Entendo", + "learn_more_link.learn_more": "Saber máis", "lightbox.close": "Fechar", "lightbox.next": "Seguinte", "lightbox.previous": "Anterior", @@ -600,6 +603,7 @@ "notification.label.mention": "Mención", "notification.label.private_mention": "Mención privada", "notification.label.private_reply": "Resposta privada", + "notification.label.quote": "{name} citou a túa publicación", "notification.label.reply": "Resposta", "notification.mention": "Mención", "notification.mentioned_you": "{name} mencionoute", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Mencións:", "notifications.column_settings.poll": "Resultados da enquisa:", "notifications.column_settings.push": "Notificacións emerxentes", + "notifications.column_settings.quote": "Citacións:", "notifications.column_settings.reblog": "Promocións:", "notifications.column_settings.show": "Amosar en columna", "notifications.column_settings.sound": "Reproducir son", @@ -847,6 +852,8 @@ "status.bookmark": "Marcar", "status.cancel_reblog_private": "Desfacer compartido", "status.cannot_reblog": "Esta publicación non pode ser promovida", + "status.context.load_new_replies": "Non hai respostas dispoñibles", + "status.context.loading": "Mirando se hai máis respostas", "status.continued_thread": "Continua co fío", "status.copy": "Copiar ligazón á publicación", "status.delete": "Eliminar", @@ -873,12 +880,11 @@ "status.open": "Estender esta publicación", "status.pin": "Fixar no perfil", "status.quote_error.filtered": "Oculto debido a un dos teus filtros", - "status.quote_error.not_found": "Non se pode mostrar a publicación.", - "status.quote_error.pending_approval": "A publicación está pendente da aprobación pola autora orixinal.", - "status.quote_error.rejected": "Non se pode mostrar esta publicación xa que a autora orixinal non permite que se cite.", - "status.quote_error.removed": "Publicación eliminada pola autora.", - "status.quote_error.unauthorized": "Non se pode mostrar esta publicación porque non tes permiso para vela.", - "status.quote_post_author": "Publicación de {name}", + "status.quote_error.not_available": "Publicación non dispoñible", + "status.quote_error.pending_approval": "Publicación pendente", + "status.quote_error.pending_approval_popout.body": "As citas compartidas no Fediverso poderían tardar en mostrarse, xa que os diferentes servidores teñen diferentes protocolos.", + "status.quote_error.pending_approval_popout.title": "Cita pendente? Non te apures", + "status.quote_post_author": "Citou unha publicación de @{name}", "status.read_more": "Ler máis", "status.reblog": "Promover", "status.reblog_private": "Compartir coa audiencia orixinal", @@ -893,6 +899,7 @@ "status.reply": "Responder", "status.replyAll": "Responder ao tema", "status.report": "Denunciar @{name}", + "status.revoke_quote": "Retirar a miña publicación da cita de @{name}", "status.sensitive_warning": "Contido sensíbel", "status.share": "Compartir", "status.show_less_all": "Amosar menos para todos", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index cb5ffb52db4..bdb23bc4cbe 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "הסרת עוקב", "confirmations.remove_from_followers.message": "{name} יוסר/תוסר ממעקב אחריך. האם להמשיך?", "confirmations.remove_from_followers.title": "להסיר עוקב/עוקבת?", + "confirmations.revoke_quote.confirm": "הסרת הודעה", + "confirmations.revoke_quote.message": "פעולה זו אינה הפיכה.", + "confirmations.revoke_quote.title": "הסרת הודעה?", "confirmations.unfollow.confirm": "הפסקת מעקב", "confirmations.unfollow.message": "להפסיק מעקב אחרי {name}?", "confirmations.unfollow.title": "לבטל מעקב אחר המשתמש.ת?", @@ -346,7 +349,7 @@ "featured_carousel.post": "הודעה", "featured_carousel.previous": "הקודם", "featured_carousel.slide": "{index} מתוך {total}", - "filter_modal.added.context_mismatch_explanation": "קטגוריית המסנן הזאת לא חלה על ההקשר שממנו הגעת אל ההודעה הזו. אם תרצה/י שההודעה תסונן גם בהקשר זה, תצטרך/י לערוך את הסנן.", + "filter_modal.added.context_mismatch_explanation": "קטגוריית הסנן הזאת לא חלה על ההקשר שממנו הגעת אל ההודעה הזו. אם תרצה/י שההודעה תסונן גם בהקשר זה, תצטרך/י לערוך את הסנן.", "filter_modal.added.context_mismatch_title": "אין התאמה להקשר!", "filter_modal.added.expired_explanation": "פג תוקפה של קטגוריית הסינון הזו, יש צורך לשנות את תאריך התפוגה כדי שהסינון יוחל.", "filter_modal.added.expired_title": "פג תוקף המסנן!", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "צפיה בעוד עוקבים משרת {domain}", "hints.profiles.see_more_follows": "צפיה בעוד נעקבים בשרת {domain}", "hints.profiles.see_more_posts": "צפיה בעוד פרסומים בשרת {domain}", - "hints.threads.replies_may_be_missing": "תגובות משרתים אחרים עלולות להיות חסרות.", - "hints.threads.see_more": "צפיה בעוד תגובות משרת {domain}", "home.column_settings.show_quotes": "הצגת ציטוטים", "home.column_settings.show_reblogs": "הצגת הדהודים", "home.column_settings.show_replies": "הצגת תגובות", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "לתרגם הודעה", "keyboard_shortcuts.unfocus": "לצאת מתיבת חיבור/חיפוש", "keyboard_shortcuts.up": "לנוע במעלה הרשימה", + "learn_more_link.got_it": "הבנתי", + "learn_more_link.learn_more": "למידע נוסף", "lightbox.close": "סגירה", "lightbox.next": "הבא", "lightbox.previous": "הקודם", @@ -600,6 +603,7 @@ "notification.label.mention": "אזכור", "notification.label.private_mention": "אזכור פרטי", "notification.label.private_reply": "תשובה בפרטי", + "notification.label.quote": "{name} ציטט.ה את הודעתך", "notification.label.reply": "תשובה", "notification.mention": "אזכור", "notification.mentioned_you": "אוזכרת על ידי {name}", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "פניות:", "notifications.column_settings.poll": "תוצאות סקר:", "notifications.column_settings.push": "התראות בדחיפה", + "notifications.column_settings.quote": "ציטוטים:", "notifications.column_settings.reblog": "הדהודים:", "notifications.column_settings.show": "הצגה בטור", "notifications.column_settings.sound": "שמע מופעל", @@ -847,6 +852,8 @@ "status.bookmark": "סימניה", "status.cancel_reblog_private": "הסרת הדהוד", "status.cannot_reblog": "לא ניתן להדהד חצרוץ זה", + "status.context.load_new_replies": "הגיעו תגובות חדשות", + "status.context.loading": "מחפש תגובות חדשות", "status.continued_thread": "שרשור מתמשך", "status.copy": "העתק/י קישור להודעה זו", "status.delete": "מחיקה", @@ -873,12 +880,11 @@ "status.open": "הרחבת הודעה זו", "status.pin": "הצמדה לפרופיל שלי", "status.quote_error.filtered": "מוסתר בהתאם לסננים שלך", - "status.quote_error.not_found": "לא ניתן להציג הודעה זו.", - "status.quote_error.pending_approval": "הודעה זו מחכה לאישור מידי היוצר המקורי.", - "status.quote_error.rejected": "לא ניתן להציג הודעה זו שכן המחבר.ת המקוריים לא הרשו לצטט אותה.", - "status.quote_error.removed": "הודעה זו הוסרה על ידי השולחים המקוריים.", - "status.quote_error.unauthorized": "הודעה זו לא מוצגת כיוון שאין לך רשות לראותה.", - "status.quote_post_author": "פרסום מאת {name}", + "status.quote_error.not_available": "ההודעה לא זמינה", + "status.quote_error.pending_approval": "ההודעה בהמתנה לאישור", + "status.quote_error.pending_approval_popout.body": "ציטוטים ששותפו בפדיוורס עשויים להתפרסם אחרי עיכוב קל, כיוון ששרתים שונים משתמשים בפרוטוקולים שונים.", + "status.quote_error.pending_approval_popout.title": "ההודעה בהמתנה? המתינו ברוגע", + "status.quote_post_author": "ההודעה צוטטה על ידי @{name}", "status.read_more": "לקרוא עוד", "status.reblog": "הדהוד", "status.reblog_private": "להדהד ברמת הנראות המקורית", @@ -893,6 +899,7 @@ "status.reply": "תגובה", "status.replyAll": "תגובה לשרשור", "status.report": "דיווח על @{name}", + "status.revoke_quote": "הסירו את הודעתי מההודעה של @{name}", "status.sensitive_warning": "תוכן רגיש", "status.share": "שיתוף", "status.show_less_all": "להציג פחות מהכל", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index abd9b8ad116..60740438e18 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "További követők megtekintése itt: {domain}", "hints.profiles.see_more_follows": "További követések megtekintése itt: {domain}", "hints.profiles.see_more_posts": "További bejegyzések megtekintése itt: {domain}", - "hints.threads.replies_may_be_missing": "A más kiszolgálókról érkező válaszok lehet, hogy hiányoznak.", - "hints.threads.see_more": "További válaszok megtekintése itt: {domain}", "home.column_settings.show_quotes": "Idézetek megjelenítése", "home.column_settings.show_reblogs": "Megtolások megjelenítése", "home.column_settings.show_replies": "Válaszok megjelenítése", @@ -500,6 +498,8 @@ "keyboard_shortcuts.translate": "Bejegyzés lefordítása", "keyboard_shortcuts.unfocus": "Szerkesztés/keresés fókuszból való kivétele", "keyboard_shortcuts.up": "Mozgás felfelé a listában", + "learn_more_link.got_it": "Rendben", + "learn_more_link.learn_more": "További tudnivalók", "lightbox.close": "Bezárás", "lightbox.next": "Következő", "lightbox.previous": "Előző", @@ -600,6 +600,7 @@ "notification.label.mention": "Említés", "notification.label.private_mention": "Privát említés", "notification.label.private_reply": "Privát válasz", + "notification.label.quote": "{name} idézte a bejegyzésedet", "notification.label.reply": "Válasz", "notification.mention": "Említés", "notification.mentioned_you": "{name} megemlített", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Megemlítések:", "notifications.column_settings.poll": "Szavazási eredmények:", "notifications.column_settings.push": "Leküldéses értesítések", + "notifications.column_settings.quote": "Idézetek:", "notifications.column_settings.reblog": "Megtolások:", "notifications.column_settings.show": "Megjelenítés az oszlopban", "notifications.column_settings.sound": "Hang lejátszása", @@ -847,6 +849,8 @@ "status.bookmark": "Könyvjelzőzés", "status.cancel_reblog_private": "Megtolás visszavonása", "status.cannot_reblog": "Ezt a bejegyzést nem lehet megtolni", + "status.context.load_new_replies": "Új válaszok érhetőek el", + "status.context.loading": "További válaszok keresése", "status.continued_thread": "Folytatott szál", "status.copy": "Link másolása bejegyzésbe", "status.delete": "Törlés", @@ -873,12 +877,11 @@ "status.open": "Bejegyzés kibontása", "status.pin": "Kitűzés a profilodra", "status.quote_error.filtered": "A szűrőid miatt rejtett", - "status.quote_error.not_found": "Ez a bejegyzés nem jeleníthető meg.", - "status.quote_error.pending_approval": "Ez a bejegyzés az eredeti szerző jóváhagyására vár.", - "status.quote_error.rejected": "Ez a bejegyzés nem jeleníthető meg, mert az eredeti szerzője nem engedélyezi az idézését.", - "status.quote_error.removed": "Ezt a bejegyzés eltávolította a szerzője.", - "status.quote_error.unauthorized": "Ez a bejegyzés nem jeleníthető meg, mert nem jogosult a megtekintésére.", - "status.quote_post_author": "Szerző: {name}", + "status.quote_error.not_available": "A bejegyzés nem érhető el", + "status.quote_error.pending_approval": "A bejegyzés függőben van", + "status.quote_error.pending_approval_popout.body": "A Födiverzumon keresztül megosztott idézetek megjelenítése eltarthat egy darabig, mivel a különböző kiszolgálók különböző protokollokat használnak.", + "status.quote_error.pending_approval_popout.title": "Függőben lévő idézet? Maradj nyugodt.", + "status.quote_post_author": "Idézte @{name} bejegyzését", "status.read_more": "Bővebben", "status.reblog": "Megtolás", "status.reblog_private": "Megtolás az eredeti közönségnek", diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index f9deb2f859d..2edb77b7f39 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -404,8 +404,6 @@ "hints.profiles.see_more_followers": "Vider plus de sequitores sur {domain}", "hints.profiles.see_more_follows": "Vider plus de sequites sur {domain}", "hints.profiles.see_more_posts": "Vider plus de messages sur {domain}", - "hints.threads.replies_may_be_missing": "Responsas de altere servitores pote mancar.", - "hints.threads.see_more": "Vider plus de responsas sur {domain}", "home.column_settings.show_reblogs": "Monstrar impulsos", "home.column_settings.show_replies": "Monstrar responsas", "home.hide_announcements": "Celar annuncios", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 69e549ea884..deb1cab89a4 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -383,8 +383,6 @@ "hints.profiles.see_more_followers": "Vidar plu multa sequanti sur {domain}", "hints.profiles.see_more_follows": "Vidar plu multa sequati sur {domain}", "hints.profiles.see_more_posts": "Vidar plu multa posti sur {domain}", - "hints.threads.replies_may_be_missing": "Respondi de altra servili forsan ne esas hike.", - "hints.threads.see_more": "Vidar plu multa demandi sur {domain}", "home.column_settings.show_reblogs": "Montrar repeti", "home.column_settings.show_replies": "Montrar respondi", "home.hide_announcements": "Celez anunci", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 52b3f5d97dd..a3c9e8733cc 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Fjarlægja fylgjanda", "confirmations.remove_from_followers.message": "{name} mun hætta að fylgjast með þér. Ertu viss um að þú viljir halda áfram?", "confirmations.remove_from_followers.title": "Fjarlægja fylgjanda?", + "confirmations.revoke_quote.confirm": "Fjarlægja færslu", + "confirmations.revoke_quote.message": "Þessa aðgerð er ekki hægt að afturkalla.", + "confirmations.revoke_quote.title": "Fjarlægja færslu?", "confirmations.unfollow.confirm": "Hætta að fylgja", "confirmations.unfollow.message": "Ertu viss um að þú viljir hætta að fylgjast með {name}?", "confirmations.unfollow.title": "Hætta að fylgjast með viðkomandi?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Sjá fleiri fylgjendur á {domain}", "hints.profiles.see_more_follows": "Sjá fleiri sem þú fylgist með á {domain}", "hints.profiles.see_more_posts": "Sjá fleiri færslur á {domain}", - "hints.threads.replies_may_be_missing": "Svör af öðrum netþjónum gæti vantað.", - "hints.threads.see_more": "Sjá fleiri svör á {domain}", "home.column_settings.show_quotes": "Birta tilvitnanir", "home.column_settings.show_reblogs": "Sýna endurbirtingar", "home.column_settings.show_replies": "Birta svör", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "að þýða færslu", "keyboard_shortcuts.unfocus": "Taka virkni úr textainnsetningarreit eða leit", "keyboard_shortcuts.up": "Fara ofar í listanum", + "learn_more_link.got_it": "Náði því", + "learn_more_link.learn_more": "Kanna nánar", "lightbox.close": "Loka", "lightbox.next": "Næsta", "lightbox.previous": "Fyrra", @@ -600,6 +603,7 @@ "notification.label.mention": "Minnst á", "notification.label.private_mention": "Einkaspjall", "notification.label.private_reply": "Einkasvar", + "notification.label.quote": "{name} vitnaði í færsluna þína", "notification.label.reply": "Svara", "notification.mention": "Minnst á", "notification.mentioned_you": "{name} minntist á þig", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Tilvísanir:", "notifications.column_settings.poll": "Niðurstöður könnunar:", "notifications.column_settings.push": "Ýti-tilkynningar", + "notifications.column_settings.quote": "Tilvitnanir:", "notifications.column_settings.reblog": "Endurbirtingar:", "notifications.column_settings.show": "Sýna í dálki", "notifications.column_settings.sound": "Spila hljóð", @@ -847,6 +852,8 @@ "status.bookmark": "Bókamerki", "status.cancel_reblog_private": "Taka úr endurbirtingu", "status.cannot_reblog": "Þessa færslu er ekki hægt að endurbirta", + "status.context.load_new_replies": "Ný svör hafa borist", + "status.context.loading": "Athuga með fleiri svör", "status.continued_thread": "Hélt samtali áfram", "status.copy": "Afrita tengil í færslu", "status.delete": "Eyða", @@ -873,12 +880,11 @@ "status.open": "Opna þessa færslu", "status.pin": "Festa á notandasnið", "status.quote_error.filtered": "Falið vegna einnar síu sem er virk", - "status.quote_error.not_found": "Þessa færslu er ekki hægt að birta.", - "status.quote_error.pending_approval": "Þessi færsla bíður eftir samþykki frá upprunalegum höfundi hennar.", - "status.quote_error.rejected": "Þessa færslu er ekki hægt að birta þar sem upphaflegur höfundur hennar leyfir ekki að vitnað sé til hennar.", - "status.quote_error.removed": "Þessi færsla var fjarlægð af höfundi hennar.", - "status.quote_error.unauthorized": "Þessa færslu er ekki hægt að birta þar sem þú hefur ekki heimild til að skoða hana.", - "status.quote_post_author": "Færsla frá {name}", + "status.quote_error.not_available": "Færsla ekki tiltæk", + "status.quote_error.pending_approval": "Færsla í bið", + "status.quote_error.pending_approval_popout.body": "Tilvitnanir sem deilt er út um samfélagsnetið geta þurft nokkurn tíma áður en þær birtast, því mismunandi netþjónar geta haft mismunandi samskiptareglur.", + "status.quote_error.pending_approval_popout.title": "Færsla í bið? Verum róleg", + "status.quote_post_author": "Vitnaði í færslu frá @{name}", "status.read_more": "Lesa meira", "status.reblog": "Endurbirting", "status.reblog_private": "Endurbirta til upphaflegra lesenda", @@ -893,6 +899,7 @@ "status.reply": "Svara", "status.replyAll": "Svara þræði", "status.report": "Kæra @{name}", + "status.revoke_quote": "Fjarlægja færsluna mína úr færslu frá @{name}", "status.sensitive_warning": "Viðkvæmt efni", "status.share": "Deila", "status.show_less_all": "Sýna minna fyrir allt", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index f39862e7cc5..072ee445b5f 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Vedi altri seguaci su {domain}", "hints.profiles.see_more_follows": "Vedi altri profili seguiti su {domain}", "hints.profiles.see_more_posts": "Vedi altri post su {domain}", - "hints.threads.replies_may_be_missing": "Le risposte da altri server potrebbero essere mancanti.", - "hints.threads.see_more": "Vedi altre risposte su {domain}", "home.column_settings.show_quotes": "Mostra le citazioni", "home.column_settings.show_reblogs": "Mostra reblog", "home.column_settings.show_replies": "Mostra risposte", @@ -500,6 +498,8 @@ "keyboard_shortcuts.translate": "Traduce un post", "keyboard_shortcuts.unfocus": "Rimuove il focus sull'area di composizione testuale/ricerca", "keyboard_shortcuts.up": "Scorre in su nell'elenco", + "learn_more_link.got_it": "Ho capito", + "learn_more_link.learn_more": "Scopri di più", "lightbox.close": "Chiudi", "lightbox.next": "Successivo", "lightbox.previous": "Precedente", @@ -600,6 +600,7 @@ "notification.label.mention": "Menziona", "notification.label.private_mention": "Menzione privata", "notification.label.private_reply": "Rispondi in privato", + "notification.label.quote": "{name} ha citato il tuo post", "notification.label.reply": "Rispondi", "notification.mention": "Menziona", "notification.mentioned_you": "{name} ti ha menzionato", @@ -657,6 +658,7 @@ "notifications.column_settings.mention": "Menzioni:", "notifications.column_settings.poll": "Risultati del sondaggio:", "notifications.column_settings.push": "Notifiche push", + "notifications.column_settings.quote": "Citazioni:", "notifications.column_settings.reblog": "Reblog:", "notifications.column_settings.show": "Mostra nella colonna", "notifications.column_settings.sound": "Riproduci suono", @@ -847,6 +849,8 @@ "status.bookmark": "Aggiungi segnalibro", "status.cancel_reblog_private": "Annulla reblog", "status.cannot_reblog": "Questo post non può essere condiviso", + "status.context.load_new_replies": "Nuove risposte disponibili", + "status.context.loading": "Controllo per altre risposte", "status.continued_thread": "Discussione continua", "status.copy": "Copia link al post", "status.delete": "Elimina", @@ -873,12 +877,11 @@ "status.open": "Espandi questo post", "status.pin": "Fissa in cima sul profilo", "status.quote_error.filtered": "Nascosto a causa di uno dei tuoi filtri", - "status.quote_error.not_found": "Questo post non può essere visualizzato.", - "status.quote_error.pending_approval": "Questo post è in attesa di approvazione dell'autore originale.", - "status.quote_error.rejected": "Questo post non può essere visualizzato perché l'autore originale non consente che venga citato.", - "status.quote_error.removed": "Questo post è stato rimosso dal suo autore.", - "status.quote_error.unauthorized": "Questo post non può essere visualizzato in quanto non sei autorizzato a visualizzarlo.", - "status.quote_post_author": "Post di @{name}", + "status.quote_error.not_available": "Post non disponibile", + "status.quote_error.pending_approval": "Post in attesa", + "status.quote_error.pending_approval_popout.body": "Le citazioni condivise in tutto il Fediverso possono richiedere del tempo per la visualizzazione, poiché server diversi hanno protocolli diversi.", + "status.quote_error.pending_approval_popout.title": "Citazione in attesa? Resta calmo", + "status.quote_post_author": "Citato un post di @{name}", "status.read_more": "Leggi di più", "status.reblog": "Reblog", "status.reblog_private": "Reblog con visibilità originale", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index ff12bf1dd18..b7af03c1099 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -423,8 +423,6 @@ "hints.profiles.see_more_followers": "{domain} で正確な情報を見る", "hints.profiles.see_more_follows": "{domain} で正確な情報を見る", "hints.profiles.see_more_posts": "{domain} でその他の投稿を見る", - "hints.threads.replies_may_be_missing": "リモートの返信は表示されない場合があります。", - "hints.threads.see_more": "{domain} でその他の返信を見る", "home.column_settings.show_quotes": "引用を表示", "home.column_settings.show_reblogs": "ブースト表示", "home.column_settings.show_replies": "返信表示", @@ -870,12 +868,6 @@ "status.open": "詳細を表示", "status.pin": "プロフィールに固定表示", "status.quote_error.filtered": "あなたのフィルター設定によって非表示になっています", - "status.quote_error.not_found": "この投稿は表示できません。", - "status.quote_error.pending_approval": "この投稿は投稿者の承認待ちです。", - "status.quote_error.rejected": "この投稿は、オリジナルの投稿者が引用することを許可していないため、表示できません。", - "status.quote_error.removed": "この投稿は投稿者によって削除されました。", - "status.quote_error.unauthorized": "この投稿を表示する権限がないため、表示できません。", - "status.quote_post_author": "{name} の投稿", "status.read_more": "もっと見る", "status.reblog": "ブースト", "status.reblog_private": "ブースト", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 0c2faa120e1..b0bee442cf3 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -34,6 +34,7 @@ "account.followers": "Imeḍfaren", "account.followers.empty": "Ar tura, ulac yiwen i yeṭṭafaṛen amseqdac-agi.", "account.followers_counter": "{count, plural, one {{counter} n umḍfar} other {{counter} n yimeḍfaren}}", + "account.followers_you_know_counter": "Tessneḍ {counter}", "account.following": "Yeṭṭafaṛ", "account.following_counter": "{count, plural, one {{counter} yettwaḍfaren} other {{counter} yettwaḍfaren}}", "account.follows.empty": "Ar tura, amseqdac-agi ur yeṭṭafaṛ yiwen.", @@ -51,10 +52,12 @@ "account.mute_notifications_short": "Susem ilɣa", "account.mute_short": "Sgugem", "account.muted": "Yettwasgugem", + "account.mutual": "Temmeḍfaṛem", "account.no_bio": "Ulac aglam i d-yettunefken.", "account.open_original_page": "Ldi asebter anasli", "account.posts": "Tisuffaɣ", "account.posts_with_replies": "Tisuffaɣ d tririyin", + "account.remove_from_followers": "Kkes {name} seg ineḍfaren", "account.report": "Cetki ɣef @{name}", "account.requested": "Di laɛḍil ad yettwaqbel. Ssit i wakken ad yefsex usuter n uḍfar", "account.requested_follow": "{name} yessuter ad k·m-yeḍfer", @@ -85,6 +88,7 @@ "annual_report.summary.most_used_app.most_used_app": "asnas yettwasqedcen s waṭas", "annual_report.summary.most_used_hashtag.none": "Ula yiwen", "annual_report.summary.new_posts.new_posts": "tisuffaɣ timaynutin", + "annual_report.summary.thanks": "Tanemmirt imi i tettekkiḍ deg Mastodon!", "audio.hide": "Ffer amesli", "block_modal.show_less": "Ssken-d drus", "block_modal.show_more": "Ssken-d ugar", @@ -297,8 +301,6 @@ "hashtag.follow": "Ḍfeṛ ahacṭag", "hashtag.mute": "Sgugem #{hashtag}", "hashtags.and_other": "…d {count, plural, one {}other {# nniḍen}}", - "hints.threads.replies_may_be_missing": "Tiririyin d-yusan deg iqeddacen nniḍen, yezmer ur d-ddant ara.", - "hints.threads.see_more": "Wali ugar n tririt deg {domain}", "home.column_settings.show_reblogs": "Ssken-d beṭṭu", "home.column_settings.show_replies": "Ssken-d tiririyin", "home.hide_announcements": "Ffer ulɣuyen", @@ -354,6 +356,8 @@ "keyboard_shortcuts.toot": "i wakken attebdud tajewwaqt tamaynut", "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.up": "i tulin ɣer d asawen n tebdart", + "learn_more_link.got_it": "Gziɣ-t", + "learn_more_link.learn_more": "Issin ugar", "lightbox.close": "Mdel", "lightbox.next": "Ɣer zdat", "lightbox.previous": "Ɣer deffir", @@ -419,6 +423,8 @@ "notification.admin.sign_up": "Ijerred {name}", "notification.annual_report.view": "Wali #Wrapstodon", "notification.favourite": "{name} yesmenyaf addad-ik·im", + "notification.favourite_pm": "{name} yesmenyef abdar-ik·im uslig", + "notification.favourite_pm.name_and_others_with_link": "{name} akked {count, plural, one {# nnayeḍ} other {# nniḍen}} rnan abdar-ik·im uslig ar ismenyafen-nsen", "notification.follow": "iṭṭafar-ik·em-id {name}", "notification.follow.name_and_others": "{name} akked {count, plural, one {# nniḍen} other {# nniḍen}} iḍfeṛ-k·m-id", "notification.follow_request": "{name} yessuter-d ad k·m-yeḍfeṛ", @@ -625,7 +631,6 @@ "status.mute_conversation": "Sgugem adiwenni", "status.open": "Semɣeṛ tasuffeɣt-ayi", "status.pin": "Senteḍ-itt deg umaɣnu", - "status.quote_post_author": "Izen sɣur {name}", "status.read_more": "Issin ugar", "status.reblog": "Bḍu", "status.reblogged_by": "Yebḍa-tt {name}", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index f0e981adb35..be0025fdb2f 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -1,6 +1,7 @@ { "about.blocks": "Модерацияланған серверлер", "about.contact": "Байланыс:", + "about.default_locale": "Әдепкі", "about.disclaimer": "Mastodon деген тегін, бастапқы коды ашық бағдарламалық жасақтама және Mastodon gGmbH-тің сауда маркасы.", "about.domain_blocks.no_reason_available": "Себеп қолжетімсіз", "about.domain_blocks.preamble": "Mastodon әдетте сізге Fediverse'тің кез келген серверінің қолданушыларының контентін көріп, олармен байланысуға мүмкіндік береді. Осы белгілі серверде жасалған ережеден тыс жағдайлар міне.", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 6c2cc7ea142..e6cdc4e7800 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "{domain}에서 더 많은 팔로워 보기", "hints.profiles.see_more_follows": "{domain}에서 더 많은 팔로우 보기", "hints.profiles.see_more_posts": "{domain}에서 더 많은 게시물 보기", - "hints.threads.replies_may_be_missing": "다른 서버의 답글은 일부 누락되었을 수 있습니다.", - "hints.threads.see_more": "{domain}에서 더 많은 답글 보기", "home.column_settings.show_quotes": "인용 보기", "home.column_settings.show_reblogs": "부스트 표시", "home.column_settings.show_replies": "답글 표시", @@ -515,7 +513,7 @@ "lists.add_to_lists": "리스트에 {name} 추가", "lists.create": "생성", "lists.create_a_list_to_organize": "새 리스트를 만들어 홈 피드를 정리하세요", - "lists.create_list": "리스트 생성", + "lists.create_list": "리스트 만들기", "lists.delete": "리스트 삭제", "lists.done": "완료", "lists.edit": "리스트 편집", @@ -873,12 +871,6 @@ "status.open": "상세 정보 표시", "status.pin": "고정", "status.quote_error.filtered": "필터에 의해 가려짐", - "status.quote_error.not_found": "이 게시물은 표시할 수 없습니다.", - "status.quote_error.pending_approval": "이 게시물은 원작자의 승인을 기다리고 있습니다.", - "status.quote_error.rejected": "이 게시물은 원작자가 인용을 허용하지 않았기 때문에 표시할 수 없습니다.", - "status.quote_error.removed": "이 게시물은 작성자에 의해 삭제되었습니다.", - "status.quote_error.unauthorized": "이 게시물은 권한이 없기 때문에 볼 수 없습니다.", - "status.quote_post_author": "{name} 님의 게시물", "status.read_more": "더 보기", "status.reblog": "부스트", "status.reblog_private": "원래의 수신자들에게 부스트", diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json index 63110fda875..f628721f479 100644 --- a/app/javascript/mastodon/locales/ku.json +++ b/app/javascript/mastodon/locales/ku.json @@ -272,7 +272,6 @@ "hashtag.column_settings.tag_toggle": "Ji bo vê stûnê hin pêvekan tevlî bike", "hashtag.follow": "Hashtagê bişopîne", "hashtag.unfollow": "Hashtagê neşopîne", - "hints.threads.replies_may_be_missing": "Beriv ji rajekarên din dibe ku wendayî bin.", "home.column_settings.show_reblogs": "Bilindkirinan nîşan bike", "home.column_settings.show_replies": "Bersivan nîşan bide", "home.hide_announcements": "Reklaman veşêre", diff --git a/app/javascript/mastodon/locales/la.json b/app/javascript/mastodon/locales/la.json index 5d34aafafce..720e940996f 100644 --- a/app/javascript/mastodon/locales/la.json +++ b/app/javascript/mastodon/locales/la.json @@ -1,6 +1,7 @@ { "about.blocks": "Servī moderātī", "about.contact": "Ratio:", + "about.default_locale": "Default", "about.disclaimer": "Mastodon est software līberum, apertum fontem, et nōtam commercium Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Ratio abdere est", "about.domain_blocks.preamble": "Mastodon genērāliter sinit tē contentum ex aliīs servientibus in fedīversō vidēre et cum usoribus ab iīs interāgere. Haē sunt exceptionēs quae in hōc particulārī servientē factae sunt.", @@ -8,6 +9,7 @@ "about.domain_blocks.silenced.title": "Limitātus", "about.domain_blocks.suspended.explanation": "Nulla data ab hōc servientē processābuntur, servābuntur aut commūtābuntur, faciendumque omnem interactionem aut communicātiōnem cum usoribus ab hōc servientē impossibilem.", "about.domain_blocks.suspended.title": "suspensus", + "about.language_label": "Linguae", "about.not_available": "Haec informātiō in hōc servientē nōn praebita est.", "about.powered_by": "Nuntii socīālēs decentralizātī ā {mastodon} sustentātī.", "about.rules": "Servo praecepta", @@ -41,7 +43,12 @@ "account.followers": "Sectatores", "account.followers.empty": "Nemo hunc usorem adhuc sequitur.", "account.followers_counter": "{count, plural, one {{counter} sectator} other {{counter} sectatores}}", + "account.followers_you_know_counter": "{counter} scis", + "account.following": "Sequentia", "account.following_counter": "{count, plural, one {{counter} sectans} other {{counter} sectans}}", + "account.follows.empty": "Hic usor adhuc neminem sequitur.", + "account.follows_you": "Sequitur te", + "account.go_to_profile": "Vade ad profile", "account.moved_to": "{name} significavit eum suam rationem novam nunc esse:", "account.muted": "Confutatus", "account.requested_follow": "{name} postulavit ut te sequeretur", diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json index 83ffe1834be..7c6ce24a8ff 100644 --- a/app/javascript/mastodon/locales/lad.json +++ b/app/javascript/mastodon/locales/lad.json @@ -368,8 +368,6 @@ "hints.profiles.see_more_followers": "Ve mas suivantes en {domain}", "hints.profiles.see_more_follows": "Ve mas segidos en {domain}", "hints.profiles.see_more_posts": "Ve mas puvlikasyones en {domain}", - "hints.threads.replies_may_be_missing": "Puede mankar repuestas de otros sirvidores.", - "hints.threads.see_more": "Ve mas repuestas en {domain}", "home.column_settings.show_reblogs": "Amostra repartajasyones", "home.column_settings.show_replies": "Amostra repuestas", "home.hide_announcements": "Eskonde pregones", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 944ab8d9649..6060cfeda3e 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -292,6 +292,7 @@ "emoji_button.search_results": "Paieškos rezultatai", "emoji_button.symbols": "Simboliai", "emoji_button.travel": "Kelionės ir vietos", + "empty_column.account_featured_other.unknown": "Ši paskyra dar nieko neparodė.", "empty_column.account_hides_collections": "Šis (-i) naudotojas (-a) pasirinko nepadaryti šią informaciją prieinamą.", "empty_column.account_suspended": "Paskyra pristabdyta.", "empty_column.account_timeline": "Nėra čia įrašų.", @@ -396,8 +397,6 @@ "hints.profiles.see_more_followers": "Žiūrėti daugiau sekėjų serveryje {domain}", "hints.profiles.see_more_follows": "Žiūrėti daugiau sekimų serveryje {domain}", "hints.profiles.see_more_posts": "Žiūrėti daugiau įrašų serveryje {domain}", - "hints.threads.replies_may_be_missing": "Atsakymai iš kitų serverių gali būti nepateikti.", - "hints.threads.see_more": "Žiūrėti daugiau atsakymų serveryje {domain}", "home.column_settings.show_reblogs": "Rodyti pakėlimus", "home.column_settings.show_replies": "Rodyti atsakymus", "home.hide_announcements": "Slėpti skelbimus", @@ -796,6 +795,8 @@ "status.bookmark": "Pridėti į žymės", "status.cancel_reblog_private": "Nebepasidalinti", "status.cannot_reblog": "Šis įrašas negali būti pakeltas.", + "status.context.load_new_replies": "Yra naujų atsakymų", + "status.context.loading": "Tikrinama dėl daugiau atsakymų", "status.continued_thread": "Tęsiama gijoje", "status.copy": "Kopijuoti nuorodą į įrašą", "status.delete": "Ištrinti", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 104528c0e13..cb77337a658 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -386,8 +386,6 @@ "hints.profiles.see_more_followers": "Skatīt vairāk sekotāju {domain}", "hints.profiles.see_more_follows": "Skatīt vairāk sekojumu {domain}", "hints.profiles.see_more_posts": "Skatīt vairāk ierakstu {domain}", - "hints.threads.replies_may_be_missing": "Var trūkt atbilžu no citiem serveriem.", - "hints.threads.see_more": "Skatīt vairāk atbilžu {domain}", "home.column_settings.show_quotes": "Rādīt citātus", "home.column_settings.show_reblogs": "Rādīt pastiprinātos ierakstus", "home.column_settings.show_replies": "Rādīt atbildes", @@ -740,12 +738,6 @@ "status.mute_conversation": "Apklusināt sarunu", "status.open": "Izvērst šo ierakstu", "status.pin": "Piespraust profilam", - "status.quote_error.not_found": "Šo ierakstu nevar parādīt.", - "status.quote_error.pending_approval": "Šis ieraksts gaida apstiprinājumu no tā autora.", - "status.quote_error.rejected": "Šo ierakstu nevar parādīt, jo tā autors neļauj to citēt.", - "status.quote_error.removed": "Šo ierakstu noņēma tā autors.", - "status.quote_error.unauthorized": "Šo ierakstu nevar parādīt, jo jums nav atļaujas to skatīt.", - "status.quote_post_author": "Publicēja {name}", "status.read_more": "Lasīt vairāk", "status.reblog": "Pastiprināt", "status.reblog_private": "Pastiprināt ar sākotnējo redzamību", diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json index 48e1b75c2b0..40b3c06281c 100644 --- a/app/javascript/mastodon/locales/nan.json +++ b/app/javascript/mastodon/locales/nan.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "佇 {domain} 看koh khah tsē跟tuè lí ê", "hints.profiles.see_more_follows": "佇 {domain} 看koh khah tsē lí跟tuè ê", "hints.profiles.see_more_posts": "佇 {domain} 看koh khah tsē ê PO文", - "hints.threads.replies_may_be_missing": "Tuì其他ê服侍器來ê回應可能有phah m̄見。", - "hints.threads.see_more": "佇 {domain} 看koh khah tsē ê回應", "home.column_settings.show_quotes": "顯示引用", "home.column_settings.show_reblogs": "顯示轉PO", "home.column_settings.show_replies": "顯示回應", @@ -500,6 +498,8 @@ "keyboard_shortcuts.translate": "kā PO文翻譯", "keyboard_shortcuts.unfocus": "離開輸入框仔/tshiau-tshuē格仔", "keyboard_shortcuts.up": "佇列單內kā suá khah面頂", + "learn_more_link.got_it": "知矣", + "learn_more_link.learn_more": "看詳細", "lightbox.close": "關", "lightbox.next": "下tsi̍t ê", "lightbox.previous": "頂tsi̍t ê", @@ -847,6 +847,8 @@ "status.bookmark": "冊籤", "status.cancel_reblog_private": "取消轉送", "status.cannot_reblog": "Tsit篇PO文bē當轉送", + "status.context.load_new_replies": "有新ê回應", + "status.context.loading": "Leh檢查其他ê回應", "status.continued_thread": "接續ê討論線", "status.copy": "Khóo-pih PO文ê連結", "status.delete": "Thâi掉", @@ -872,12 +874,11 @@ "status.mute_conversation": "Kā對話消音", "status.open": "Kā PO文展開", "status.quote_error.filtered": "Lí所設定ê過濾器kā tse khàm起來", - "status.quote_error.not_found": "Tsit篇PO文bē當顯示。", - "status.quote_error.pending_approval": "Tsit篇PO文teh等原作者審查。", - "status.quote_error.rejected": "因為原作者無允准引用,tsit篇PO文bē當顯示。", - "status.quote_error.removed": "Tsit篇hōo作者thâi掉ah。", - "status.quote_error.unauthorized": "因為lí無得著讀tse ê權限,tsit篇PO文bē當顯示。", - "status.quote_post_author": "{name} 所PO ê", + "status.quote_error.not_available": "鋪文bē當看", + "status.quote_error.pending_approval": "鋪文當咧送", + "status.quote_error.pending_approval_popout.body": "因為無kâng ê服侍器有無kâng ê協定,佇聯邦宇宙分享ê引文可能愛開時間來顯示。", + "status.quote_error.pending_approval_popout.title": "Leh送引文?請sió等leh", + "status.quote_post_author": "引用 @{name} ê PO文ah", "status.read_more": "讀詳細", "status.reblog": "轉送", "status.reblog_private": "照原PO ê通看見ê範圍轉送", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 90593ac2058..b5f4871078d 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Volger verwijderen", "confirmations.remove_from_followers.message": "{name} zal je niet meer volgen. Weet je zeker dat je wilt doorgaan?", "confirmations.remove_from_followers.title": "Volger verwijderen?", + "confirmations.revoke_quote.confirm": "Bericht verwijderen", + "confirmations.revoke_quote.message": "Deze actie kan niet ongedaan worden gemaakt.", + "confirmations.revoke_quote.title": "Bericht verwijderen?", "confirmations.unfollow.confirm": "Ontvolgen", "confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?", "confirmations.unfollow.title": "Gebruiker ontvolgen?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Bekijk meer volgers op {domain}", "hints.profiles.see_more_follows": "Bekijk meer gevolgde accounts op {domain}", "hints.profiles.see_more_posts": "Bekijk meer berichten op {domain}", - "hints.threads.replies_may_be_missing": "Antwoorden van andere servers kunnen ontbreken.", - "hints.threads.see_more": "Bekijk meer reacties op {domain}", "home.column_settings.show_quotes": "Citaten tonen", "home.column_settings.show_reblogs": "Boosts tonen", "home.column_settings.show_replies": "Reacties tonen", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "om een bericht te vertalen", "keyboard_shortcuts.unfocus": "Tekst- en zoekveld ontfocussen", "keyboard_shortcuts.up": "Naar boven in de lijst bewegen", + "learn_more_link.got_it": "Begrepen", + "learn_more_link.learn_more": "Meer informatie", "lightbox.close": "Sluiten", "lightbox.next": "Volgende", "lightbox.previous": "Vorige", @@ -600,6 +603,7 @@ "notification.label.mention": "Vermelding", "notification.label.private_mention": "Privébericht", "notification.label.private_reply": "Privéreactie", + "notification.label.quote": "{name} heeft jouw bericht geciteerd", "notification.label.reply": "Reactie", "notification.mention": "Vermelding", "notification.mentioned_you": "Je bent vermeld door {name}", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Vermeldingen:", "notifications.column_settings.poll": "Peilingresultaten:", "notifications.column_settings.push": "Pushmeldingen", + "notifications.column_settings.quote": "Citaten:", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "In kolom tonen", "notifications.column_settings.sound": "Geluid afspelen", @@ -847,6 +852,8 @@ "status.bookmark": "Bladwijzer toevoegen", "status.cancel_reblog_private": "Niet langer boosten", "status.cannot_reblog": "Dit bericht kan niet geboost worden", + "status.context.load_new_replies": "Nieuwe reacties beschikbaar", + "status.context.loading": "Op nieuwe reacties aan het controleren", "status.continued_thread": "Vervolg van gesprek", "status.copy": "Link naar bericht kopiëren", "status.delete": "Verwijderen", @@ -873,12 +880,11 @@ "status.open": "Volledig bericht tonen", "status.pin": "Aan profielpagina vastmaken", "status.quote_error.filtered": "Verborgen door een van je filters", - "status.quote_error.not_found": "Dit bericht kan niet worden weergegeven.", - "status.quote_error.pending_approval": "Dit bericht is in afwachting van goedkeuring door de oorspronkelijke auteur.", - "status.quote_error.rejected": "Dit bericht kan niet worden weergegeven omdat de oorspronkelijke auteur niet toestaat dat het wordt geciteerd.", - "status.quote_error.removed": "Dit bericht is verwijderd door de auteur.", - "status.quote_error.unauthorized": "Dit bericht kan niet worden weergegeven omdat je niet bevoegd bent om het te bekijken.", - "status.quote_post_author": "Bericht van {name}", + "status.quote_error.not_available": "Bericht niet beschikbaar", + "status.quote_error.pending_approval": "Bericht in afwachting", + "status.quote_error.pending_approval_popout.body": "Het kan even duren voordat citaten die in de Fediverse gedeeld worden, worden weergegeven. Omdat verschillende servers niet allemaal hetzelfde protocol gebruiken.", + "status.quote_error.pending_approval_popout.title": "Even geduld wanneer het citaat nog moet worden goedgekeurd.", + "status.quote_post_author": "Citeerde een bericht van @{name}", "status.read_more": "Meer lezen", "status.reblog": "Boosten", "status.reblog_private": "Boost naar oorspronkelijke ontvangers", @@ -893,6 +899,7 @@ "status.reply": "Reageren", "status.replyAll": "Op iedereen reageren", "status.report": "@{name} rapporteren", + "status.revoke_quote": "Mijn bericht uit het bericht van @{name} verwijderen", "status.sensitive_warning": "Gevoelige inhoud", "status.share": "Delen", "status.show_less_all": "Alles minder tonen", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index b739208ab37..4d7b6d401f0 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Sjå fleire fylgjarar på {domain}", "hints.profiles.see_more_follows": "Sjå fleire fylgjer på {domain}", "hints.profiles.see_more_posts": "Sjå fleire innlegg på {domain}", - "hints.threads.replies_may_be_missing": "Svar frå andre tenarar manglar kanskje.", - "hints.threads.see_more": "Sjå fleire svar på {domain}", "home.column_settings.show_quotes": "Vis sitat", "home.column_settings.show_reblogs": "Vis framhevingar", "home.column_settings.show_replies": "Vis svar", @@ -873,12 +871,6 @@ "status.open": "Utvid denne statusen", "status.pin": "Fest på profil", "status.quote_error.filtered": "Gøymt på grunn av eitt av filtra dine", - "status.quote_error.not_found": "Du kan ikkje visa dette innlegget.", - "status.quote_error.pending_approval": "Dette innlegget ventar på at skribenten skal godkjenna det.", - "status.quote_error.rejected": "Du kan ikkje visa dette innlegget fordi skribenten ikkje vil at det skal siterast.", - "status.quote_error.removed": "Skribenten sletta dette innlegget.", - "status.quote_error.unauthorized": "Du kan ikkje visa dette innlegget fordi du ikkje har løyve til det.", - "status.quote_post_author": "Innlegg av {name}", "status.read_more": "Les meir", "status.reblog": "Framhev", "status.reblog_private": "Framhev til dei originale mottakarane", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index ca7c43e1e92..f96377caa28 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -417,8 +417,6 @@ "hints.profiles.see_more_followers": "Se flere følgere på {domain}", "hints.profiles.see_more_follows": "Se flere som følger på {domain}", "hints.profiles.see_more_posts": "Se flere innlegg på {domain}", - "hints.threads.replies_may_be_missing": "Svar fra andre servere mangler kanskje.", - "hints.threads.see_more": "Se flere svar på {domain}", "home.column_settings.show_quotes": "Vis sitater", "home.column_settings.show_reblogs": "Vis fremhevinger", "home.column_settings.show_replies": "Vis svar", @@ -841,9 +839,6 @@ "status.open": "Utvid dette innlegget", "status.pin": "Fest på profilen", "status.quote_error.filtered": "Skjult på grunn av et av filterne dine", - "status.quote_error.not_found": "Dette innlegget kan ikke vises.", - "status.quote_error.pending_approval": "Dette innlegget venter på godkjenning fra den opprinnelige forfatteren.", - "status.quote_error.rejected": "Dette innlegget kan ikke vises fordi den opprinnelige forfatteren ikke har tillatt at det blir sitert.", "status.read_more": "Les mer", "status.reblog": "Fremhev", "status.reblog_private": "Fremhev til det opprinnelige publikummet", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index 5236d246c0f..50756eebbf0 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -276,7 +276,6 @@ "hints.profiles.see_more_followers": "{domain} ਉੱਤੇ ਹੋਰ ਫ਼ਾਲੋਅਰ ਵੇਖੋ", "hints.profiles.see_more_follows": "{domain} ਉੱਤੇ ਹੋਰ ਫ਼ਾਲੋ ਨੂੰ ਵੇਖੋ", "hints.profiles.see_more_posts": "{domain} ਉੱਤੇ ਹੋਰ ਪੋਸਟਾਂ ਨੂੰ ਵੇਖੋ", - "hints.threads.see_more": "{domain} ਤੋਂ ਹੋਰ ਜਵਾਬਾਂ ਨੂੰ ਵੇਖੋ", "home.column_settings.show_reblogs": "ਬੂਸਟਾਂ ਨੂੰ ਵੇਖੋ", "home.column_settings.show_replies": "ਜਵਾਬਾਂ ਨੂੰ ਵੇਖੋ", "home.hide_announcements": "ਐਲਾਨਾਂ ਨੂੰ ਓਹਲੇ ਕਰੋ", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 1967a2333c0..58c3f1fa574 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -1,6 +1,7 @@ { "about.blocks": "Serwery moderowane", "about.contact": "Kontakt:", + "about.default_locale": "Domyślny", "about.disclaimer": "Mastodon jest darmowym, otwartym oprogramowaniem i znakiem towarowym Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Powód niedostępny", "about.domain_blocks.preamble": "Domyślnie Mastodon pozwala ci przeglądać i reagować na treści od innych użytkowników z jakiegokolwiek serwera w fediwersum. Poniżej znajduje się lista wyjątków, które zostały stworzone na tym konkretnym serwerze.", @@ -8,6 +9,7 @@ "about.domain_blocks.silenced.title": "Ograniczone", "about.domain_blocks.suspended.explanation": "Żadne dane z tego serwera nie będą przetwarzane, przechowywane lub wymieniane, co uniemożliwia jakąkolwiek interakcję lub komunikację z użytkownikami z tego serwera.", "about.domain_blocks.suspended.title": "Zawieszono", + "about.language_label": "Język", "about.not_available": "Ta informacja nie została udostępniona na tym serwerze.", "about.powered_by": "Zdecentralizowane media społecznościowe napędzane przez {mastodon}", "about.rules": "Regulamin serwera", @@ -19,13 +21,21 @@ "account.block_domain": "Blokuj wszystko z {domain}", "account.block_short": "Zablokuj", "account.blocked": "Zablokowany(-a)", + "account.blocking": "Blokowanie", "account.cancel_follow_request": "Nie obserwuj", "account.copy": "Skopiuj link do profilu", "account.direct": "Napisz bezpośrednio do @{name}", "account.disable_notifications": "Przestań powiadamiać mnie o wpisach @{name}", + "account.domain_blocking": "Blokowanie domeny", "account.edit_profile": "Edytuj profil", "account.enable_notifications": "Powiadamiaj mnie o wpisach @{name}", "account.endorse": "Wyróżnij na profilu", + "account.familiar_followers_many": "Obserwowane przez: {name1}, {name2} i {othersCount, plural, one {jeszcze jedną osobę, którą znasz} few {# inne osoby, które znasz} many {# innych osób, które znasz} other {# innych osób, które znasz}}", + "account.familiar_followers_one": "Obserwowane przez {name1}", + "account.familiar_followers_two": "Obserwowane przez {name1} i {name2}", + "account.featured": "Wyróżnione", + "account.featured.accounts": "Profile", + "account.featured.hashtags": "Tagi", "account.featured_tags.last_status_at": "Ostatni post {date}", "account.featured_tags.last_status_never": "Brak postów", "account.follow": "Obserwuj", @@ -33,9 +43,11 @@ "account.followers": "Obserwujący", "account.followers.empty": "Nikt jeszcze nie obserwuje tego użytkownika.", "account.followers_counter": "{count, plural, one {{counter} obserwujący} few {{counter} obserwujących} many {{counter} obserwujących} other {{counter} obserwujących}}", + "account.followers_you_know_counter": "{counter} które znasz", "account.following": "Obserwowani", "account.following_counter": "{count, plural, one {{counter} obserwowany} few {{counter} obserwowanych} many {{counter} obserwowanych} other {{counter} obserwowanych}}", "account.follows.empty": "Ten użytkownik nie obserwuje jeszcze nikogo.", + "account.follows_you": "Obserwuje cię", "account.go_to_profile": "Przejdź do profilu", "account.hide_reblogs": "Ukryj podbicia od @{name}", "account.in_memoriam": "Ku pamięci.", @@ -50,18 +62,23 @@ "account.mute_notifications_short": "Wycisz powiadomienia", "account.mute_short": "Wycisz", "account.muted": "Wyciszony", + "account.muting": "Wyciszenie", + "account.mutual": "Obserwujecie siebie nazwajem", "account.no_bio": "Brak opisu.", "account.open_original_page": "Otwórz stronę oryginalną", "account.posts": "Wpisy", "account.posts_with_replies": "Wpisy i odpowiedzi", + "account.remove_from_followers": "Usuń {name} z obserwujących", "account.report": "Zgłoś @{name}", "account.requested": "Oczekująca prośba, kliknij aby anulować", "account.requested_follow": "{name} chce cię zaobserwować", + "account.requests_to_follow_you": "Prośby o obserwowanie", "account.share": "Udostępnij profil @{name}", "account.show_reblogs": "Pokazuj podbicia od @{name}", "account.statuses_counter": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}", "account.unblock": "Odblokuj @{name}", "account.unblock_domain": "Odblokuj domenę {domain}", + "account.unblock_domain_short": "Odblokuj", "account.unblock_short": "Odblokuj", "account.unendorse": "Nie wyświetlaj w profilu", "account.unfollow": "Nie obserwuj", @@ -202,6 +219,12 @@ "confirmations.delete_list.confirm": "Usuń", "confirmations.delete_list.message": "Czy na pewno chcesz trwale usunąć tę listę?", "confirmations.delete_list.title": "Usunąć listę?", + "confirmations.discard_draft.confirm": "Odrzuć i kontynuuj", + "confirmations.discard_draft.edit.cancel": "Wznów edytowanie", + "confirmations.discard_draft.edit.message": "Kontynuowanie spowoduje utratę wszystkich zmian wprowadzonych przez Ciebie w aktualnie edytowanym poście.", + "confirmations.discard_draft.edit.title": "Odrzucić zmiany w poście?", + "confirmations.discard_draft.post.cancel": "Wznów wersję roboczą", + "confirmations.discard_draft.post.title": "Anulować wersję roboczą?", "confirmations.discard_edit_media.confirm": "Odrzuć", "confirmations.discard_edit_media.message": "Masz niezapisane zmiany w opisie lub podglądzie, odrzucić je mimo to?", "confirmations.follow_to_list.confirm": "Zaobserwuj i dodaj do listy", @@ -218,6 +241,9 @@ "confirmations.redraft.confirm": "Usuń i popraw", "confirmations.redraft.message": "Czy na pewno chcesz usunąć i poprawić ten wpis? Polubienia, podbicia i komentarze pierwotnego wpisu zostaną utracone.", "confirmations.redraft.title": "Usunąć i poprawić wpis?", + "confirmations.remove_from_followers.confirm": "Usuń obserwującego", + "confirmations.remove_from_followers.message": "{name} przestanie Cię obserwować. Czy na pewno chcesz kontynuować?", + "confirmations.remove_from_followers.title": "Usunąć obserwującego?", "confirmations.unfollow.confirm": "Nie obserwuj", "confirmations.unfollow.message": "Czy na pewno nie chcesz obserwować {name}?", "confirmations.unfollow.title": "Cofnąć obserwację?", @@ -307,9 +333,15 @@ "errors.unexpected_crash.copy_stacktrace": "Skopiuj stacktrace do schowka", "errors.unexpected_crash.report_issue": "Zgłoś problem", "explore.suggested_follows": "Ludzie", + "explore.title": "Na czasie", "explore.trending_links": "Aktualności", "explore.trending_statuses": "Wpisy", "explore.trending_tags": "Hasztagi", + "featured_carousel.header": "{count, plural, one {Przypięty post} other {Przypięte posty}}", + "featured_carousel.next": "Następny", + "featured_carousel.post": "Opublikuj", + "featured_carousel.previous": "Poprzedni", + "featured_carousel.slide": "{index} z {total}", "filter_modal.added.context_mismatch_explanation": "To filtrowanie nie dotyczy kategorii, w której pojawił się ten wpis. Jeśli chcesz, aby wpis był filtrowany również w tym kontekście, musisz edytować ustawienia filtrowania.", "filter_modal.added.context_mismatch_title": "Niewłaściwy kontekst!", "filter_modal.added.expired_explanation": "Ta kategoria filtrowania wygasła, aby ją zastosować, należy zmienić datę wygaśnięcia.", @@ -362,6 +394,8 @@ "generic.saved": "Zapisano", "getting_started.heading": "Pierwsze kroki", "hashtag.admin_moderation": "Otwórz interfejs moderacji #{name}", + "hashtag.browse": "Przeglądaj posty z #{hashtag}", + "hashtag.browse_from_account": "Przeglądaj posty od @{name} z #{hashtag}", "hashtag.column_header.tag_mode.all": "i {additional}", "hashtag.column_header.tag_mode.any": "lub {additional}", "hashtag.column_header.tag_mode.none": "bez {additional}", @@ -374,7 +408,10 @@ "hashtag.counter_by_accounts": "{count, plural, one {{counter} osoba} few {{counter} osoby} many {{counter} osób} other {{counter} osób}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}} dzisiaj", + "hashtag.feature": "Wyróżnij w profilu", "hashtag.follow": "Obserwuj hasztag", + "hashtag.mute": "Wycisz #{hashtag}", + "hashtag.unfeature": "Nie wyróżniaj w profilu", "hashtag.unfollow": "Przestań obserwować hashtag", "hashtags.and_other": "…i {count, plural, other {jeszcze #}}", "hints.profiles.followers_may_be_missing": "Niektórzy obserwujący ten profil mogą być niewidoczni.", @@ -383,8 +420,7 @@ "hints.profiles.see_more_followers": "Zobacz więcej obserwujących na {domain}", "hints.profiles.see_more_follows": "Zobacz więcej obserwowanych na {domain}", "hints.profiles.see_more_posts": "Zobacz więcej wpisów na {domain}", - "hints.threads.replies_may_be_missing": "Komentarze z innych serwerów mogą być niewidoczne.", - "hints.threads.see_more": "Zobacz więcej komentarzy na {domain}", + "home.column_settings.show_quotes": "Pokaż cytaty", "home.column_settings.show_reblogs": "Pokazuj podbicia", "home.column_settings.show_replies": "Pokazuj odpowiedzi", "home.hide_announcements": "Ukryj ogłoszenia", @@ -458,6 +494,8 @@ "keyboard_shortcuts.translate": "aby przetłumaczyć wpis", "keyboard_shortcuts.unfocus": "Opuść pole tekstowe", "keyboard_shortcuts.up": "Przesuń w górę na liście", + "learn_more_link.got_it": "Rozumiem", + "learn_more_link.learn_more": "Dowiedz się więcej", "lightbox.close": "Zamknij", "lightbox.next": "Następne", "lightbox.previous": "Poprzednie", @@ -507,8 +545,10 @@ "mute_modal.you_wont_see_mentions": "Nie zobaczysz wpisów wzmiankujących tę osobę.", "mute_modal.you_wont_see_posts": "Nie zobaczysz wpisów tej osoby, ale ona może widzieć twoje.", "navigation_bar.about": "O serwerze", + "navigation_bar.account_settings": "Hasło i zabezpieczenia", "navigation_bar.administration": "Administracja", "navigation_bar.advanced_interface": "Otwórz w widoku zaawansowanym", + "navigation_bar.automated_deletion": "Automatyczne usuwanie postów", "navigation_bar.blocks": "Zablokowani", "navigation_bar.bookmarks": "Zakładki", "navigation_bar.direct": "Wzmianki bezpośrednie", @@ -518,13 +558,21 @@ "navigation_bar.follow_requests": "Prośby o obserwowanie", "navigation_bar.followed_tags": "Obserwowane hasztagi", "navigation_bar.follows_and_followers": "Obserwowani i obserwujący", + "navigation_bar.import_export": "Import i eksport", "navigation_bar.lists": "Listy", "navigation_bar.logout": "Wyloguj", "navigation_bar.moderation": "Moderacja", + "navigation_bar.more": "Więcej", "navigation_bar.mutes": "Wyciszeni", "navigation_bar.opened_in_classic_interface": "Wpisy, konta i inne określone strony są domyślnie otwierane w widoku klasycznym.", "navigation_bar.preferences": "Ustawienia", + "navigation_bar.privacy_and_reach": "Prywatność i zasięg", "navigation_bar.search": "Szukaj", + "navigation_bar.search_trends": "Szukaj / Na czasie", + "navigation_panel.collapse_followed_tags": "Zwiń menu obserwowanych hashtagów", + "navigation_panel.collapse_lists": "Zwiń menu listy", + "navigation_panel.expand_followed_tags": "Rozwiń menu obserwowanych hashtagów", + "navigation_panel.expand_lists": "Rozwiń menu listy", "not_signed_in_indicator.not_signed_in": "Zaloguj się, aby uzyskać dostęp.", "notification.admin.report": "{name} zgłosił {target}", "notification.admin.report_account": "{name} zgłosił(a) {count, plural, one {1 wpis} few {# wpisy} other {# wpisów}} z {target} w kategorii {category}", @@ -751,6 +799,7 @@ "report_notification.categories.violation": "Naruszenie zasad", "report_notification.categories.violation_sentence": "naruszenie zasad", "report_notification.open": "Otwórz zgłoszenie", + "search.clear": "Wyczyść wyszukiwanie", "search.no_recent_searches": "Brak ostatnich wyszukiwań", "search.placeholder": "Szukaj", "search.quick_action.account_search": "Profile pasujące do {x}", @@ -792,6 +841,8 @@ "status.bookmark": "Dodaj zakładkę", "status.cancel_reblog_private": "Cofnij podbicie", "status.cannot_reblog": "Ten wpis nie może zostać podbity", + "status.context.load_new_replies": "Dostępne są nowe odpowiedzi", + "status.context.loading": "Sprawdzanie kolejnych odpowiedzi", "status.continued_thread": "Ciąg dalszy wątku", "status.copy": "Skopiuj odnośnik do wpisu", "status.delete": "Usuń", @@ -817,6 +868,10 @@ "status.mute_conversation": "Wycisz konwersację", "status.open": "Rozszerz ten wpis", "status.pin": "Przypnij do profilu", + "status.quote_error.filtered": "Ukryte z powodu jednego z Twoich filtrów", + "status.quote_error.not_available": "Post niedostępny", + "status.quote_error.pending_approval": "Post oczekujący", + "status.quote_post_author": "Zacytowano post @{name}", "status.read_more": "Czytaj dalej", "status.reblog": "Podbij", "status.reblog_private": "Podbij dla odbiorców oryginalnego wpisu", @@ -846,8 +901,13 @@ "subscribed_languages.save": "Zapisz zmiany", "subscribed_languages.target": "Zmień subskrybowane języki dla {target}", "tabs_bar.home": "Strona główna", + "tabs_bar.menu": "Menu", "tabs_bar.notifications": "Powiadomienia", + "tabs_bar.publish": "Nowy post", + "tabs_bar.search": "Szukaj", + "terms_of_service.effective_as_of": "Obowiązuje od {date}", "terms_of_service.title": "Warunki korzystania z usługi", + "terms_of_service.upcoming_changes_on": "Nadchodzące zmiany od {date}", "time_remaining.days": "{number, plural, one {Pozostał # dzień} few {Pozostały # dni} many {Pozostało # dni} other {Pozostało # dni}}", "time_remaining.hours": "{number, plural, one {Pozostała # godzina} few {Pozostały # godziny} many {Pozostało # godzin} other {Pozostało # godzin}}", "time_remaining.minutes": "{number, plural, one {Pozostała # minuta} few {Pozostały # minuty} many {Pozostało # minut} other {Pozostało # minut}}", @@ -878,6 +938,12 @@ "video.expand": "Rozszerz film", "video.fullscreen": "Pełny ekran", "video.hide": "Ukryj film", + "video.mute": "Wycisz", "video.pause": "Pauzuj", - "video.play": "Odtwórz" + "video.play": "Odtwórz", + "video.skip_backward": "Skocz do tyłu", + "video.skip_forward": "Skocz do przodu", + "video.unmute": "Wyłącz wyciszenie", + "video.volume_down": "Zmniejsz głośność", + "video.volume_up": "Zwiększ głośność" } diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index b437786b9b6..7844de56339 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -224,6 +224,8 @@ "confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas ao post sendo editado.", "confirmations.discard_draft.edit.title": "Descartar mudanças no seu post?", "confirmations.discard_draft.post.cancel": "Continuar rascunho", + "confirmations.discard_draft.post.message": "Continuar eliminará a publicação que está sendo elaborada no momento.", + "confirmations.discard_draft.post.title": "Eliminar seu esboço de publicação?", "confirmations.discard_edit_media.confirm": "Descartar", "confirmations.discard_edit_media.message": "Há mudanças não salvas na descrição ou pré-visualização da mídia. Descartar assim mesmo?", "confirmations.follow_to_list.confirm": "Seguir e adicionar à lista", @@ -333,9 +335,13 @@ "errors.unexpected_crash.copy_stacktrace": "Copiar dados do erro para área de transferência", "errors.unexpected_crash.report_issue": "Reportar problema", "explore.suggested_follows": "Pessoas", + "explore.title": "Em alta", "explore.trending_links": "Notícias", "explore.trending_statuses": "Publicações", "explore.trending_tags": "Hashtags", + "featured_carousel.header": "{count, plural, one {Postagem fixada} other {Postagens fixadas}}", + "featured_carousel.next": "Próximo", + "featured_carousel.previous": "Anterior", "filter_modal.added.context_mismatch_explanation": "Esta categoria de filtro não se aplica ao contexto no qual você acessou esta publicação. Se quiser que a publicação seja filtrada nesse contexto também, você terá que editar o filtro.", "filter_modal.added.context_mismatch_title": "Incompatibilidade de contexto!", "filter_modal.added.expired_explanation": "Esta categoria de filtro expirou, você precisará alterar a data de expiração para aplicar.", @@ -414,8 +420,6 @@ "hints.profiles.see_more_followers": "Ver mais seguidores no {domain}", "hints.profiles.see_more_follows": "Ver mais seguidores no {domain}", "hints.profiles.see_more_posts": "Ver mais publicações em {domain}", - "hints.threads.replies_may_be_missing": "Respostas de outros servidores podem estar faltando.", - "hints.threads.see_more": "Ver mais respostas no {domain}", "home.column_settings.show_reblogs": "Mostrar boosts", "home.column_settings.show_replies": "Mostrar respostas", "home.hide_announcements": "Ocultar comunicados", @@ -552,6 +556,7 @@ "navigation_bar.lists": "Listas", "navigation_bar.logout": "Sair", "navigation_bar.moderation": "Moderação", + "navigation_bar.more": "Mais", "navigation_bar.mutes": "Usuários silenciados", "navigation_bar.opened_in_classic_interface": "Publicações, contas e outras páginas específicas são abertas por padrão na interface 'web' clássica.", "navigation_bar.preferences": "Preferências", @@ -849,12 +854,6 @@ "status.open": "Abrir toot", "status.pin": "Fixar", "status.quote_error.filtered": "Oculto devido a um dos seus filtros", - "status.quote_error.not_found": "Esta postagem não pode ser exibida.", - "status.quote_error.pending_approval": "Esta postagem está pendente de aprovação do autor original.", - "status.quote_error.rejected": "Esta publicação não pode ser exibida porque o autor original não permite que seja citada.", - "status.quote_error.removed": "Esta postagem foi removida pelo autor.", - "status.quote_error.unauthorized": "Esta publicação não pode ser exibida, pois, você não está autorizado a vê-la.", - "status.quote_post_author": "Publicação por {name}", "status.read_more": "Ler mais", "status.reblog": "Dar boost", "status.reblog_private": "Dar boost para o mesmo público", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 424e48ef2e3..fd9d259751f 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Remover seguidor", "confirmations.remove_from_followers.message": "{name} vai parar de seguir-te. Tens a certeza que prentedes continuar?", "confirmations.remove_from_followers.title": "Remover seguidor?", + "confirmations.revoke_quote.confirm": "Remover publicação", + "confirmations.revoke_quote.message": "Esta ação é irreversível.", + "confirmations.revoke_quote.title": "Remover publicação?", "confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?", "confirmations.unfollow.title": "Deixar de seguir o utilizador?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Ver mais seguidores em {domain}", "hints.profiles.see_more_follows": "Ver mais perfis seguidos em {domain}", "hints.profiles.see_more_posts": "Ver mais publicações em {domain}", - "hints.threads.replies_may_be_missing": "É possível que não estejam a ser mostradas todas as respostas de outros servidores.", - "hints.threads.see_more": "Ver mais respostas em {domain}", "home.column_settings.show_quotes": "Mostrar citações", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respostas", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "traduzir uma publicação", "keyboard_shortcuts.unfocus": "remover o foco da área de texto / pesquisa", "keyboard_shortcuts.up": "mover para cima na lista", + "learn_more_link.got_it": "Entendido", + "learn_more_link.learn_more": "Saber mais", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", @@ -600,6 +603,7 @@ "notification.label.mention": "Menção", "notification.label.private_mention": "Menção privada", "notification.label.private_reply": "Resposta privada", + "notification.label.quote": "{name} citou a sua publicação", "notification.label.reply": "Resposta", "notification.mention": "Menção", "notification.mentioned_you": "{name} mencionou-te", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Menções:", "notifications.column_settings.poll": "Resultados da sondagem:", "notifications.column_settings.push": "Notificações \"push\"", + "notifications.column_settings.quote": "Citações:", "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostrar na coluna", "notifications.column_settings.sound": "Reproduzir som", @@ -847,6 +852,8 @@ "status.bookmark": "Guardar nos marcadores", "status.cancel_reblog_private": "Retirar impulso", "status.cannot_reblog": "Esta publicação não pode ser impulsionada", + "status.context.load_new_replies": "Novas respostas disponíveis", + "status.context.loading": "A verificar por mais respostas", "status.continued_thread": "Continuação da conversa", "status.copy": "Copiar hiperligação da publicação", "status.delete": "Eliminar", @@ -873,12 +880,11 @@ "status.open": "Expandir esta publicação", "status.pin": "Afixar no perfil", "status.quote_error.filtered": "Oculto devido a um dos seus filtros", - "status.quote_error.not_found": "Esta publicação não pode ser exibida.", - "status.quote_error.pending_approval": "Esta publicação está a aguardar a aprovação do autor original.", - "status.quote_error.rejected": "Esta publicação não pode ser exibida porque o autor original não permite que seja citada.", - "status.quote_error.removed": "Esta publicação foi removida pelo seu autor.", - "status.quote_error.unauthorized": "Esta publicação não pode ser exibida porque o utilizador não está autorizado a visualizá-la.", - "status.quote_post_author": "Publicação de {name}", + "status.quote_error.not_available": "Publicação indisponível", + "status.quote_error.pending_approval": "Publicação pendente", + "status.quote_error.pending_approval_popout.body": "As citações partilhadas no Fediverso podem demorar algum tempo a ser exibidas, uma vez que diferentes servidores têm protocolos diferentes.", + "status.quote_error.pending_approval_popout.title": "Citação pendente? Mantenha a calma", + "status.quote_post_author": "Citou uma publicação de @{name}", "status.read_more": "Ler mais", "status.reblog": "Impulsionar", "status.reblog_private": "Impulsionar com a visibilidade original", @@ -893,6 +899,7 @@ "status.reply": "Responder", "status.replyAll": "Responder à conversa", "status.report": "Denunciar @{name}", + "status.revoke_quote": "Remover a minha publicação da publicação de @{name}", "status.sensitive_warning": "Conteúdo sensível", "status.share": "Partilhar", "status.show_less_all": "Ocultar conteúdo sensível em todas", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index e2022b52d94..ada27cea5fd 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -45,7 +45,7 @@ "account.followers_counter": "{count, plural, one {{counter} подписчик} few {{counter} подписчика} other {{counter} подписчиков}}", "account.followers_you_know_counter": "{count, plural, one {{counter} ваш знакомый} other {{counter} ваших знакомых}}", "account.following": "Подписки", - "account.following_counter": "{count, plural, one {# подписка} many {# подписок} other {# подписки}}", + "account.following_counter": "{count, plural, one {{counter} подписка} few {{counter} подписки} many {{counter} подписок} other {{counter} подписок}}", "account.follows.empty": "Этот пользователь пока ни на кого не подписался.", "account.follows_you": "Подписан(а) на вас", "account.go_to_profile": "Перейти к профилю", @@ -273,7 +273,7 @@ "domain_block_modal.they_cant_follow": "Пользователи с этого сервера не смогут подписаться на вас.", "domain_block_modal.they_wont_know": "Пользователи с этого сервера не будут знать, что вы их блокируете.", "domain_block_modal.title": "Заблокировать домен?", - "domain_block_modal.you_will_lose_num_followers": "Вы потеряете {followersCount, plural, one {{followersCountDisplay} подписчика} few {{followersCountDisplay} подписчика} other {{followersCountDisplay} подписчиков}} и {followingCount, plural, one {{followingCountDisplay} подписку} few {{followingCountDisplay} подписки} other {{followingCountDisplay} подписок}}.", + "domain_block_modal.you_will_lose_num_followers": "Вы потеряете {followersCount, plural, one {{followersCountDisplay} подписчика} few {{followersCountDisplay} подписчиков} other {{followersCountDisplay} подписчиков}} и {followingCount, plural, one {{followingCountDisplay} подписку} few {{followingCountDisplay} подписки} other {{followingCountDisplay} подписок}}.", "domain_block_modal.you_will_lose_relationships": "Вы потеряете все подписки и всех подписчиков с этого сервера.", "domain_block_modal.you_wont_see_posts": "Вы не будете видеть посты и уведомления от пользователей с этого сервера.", "domain_pill.activitypub_lets_connect": "Благодаря ему вы можете связываться и взаимодействовать не только с пользователями Mastodon, но и с пользователями других платформ.", @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Перейдите на {domain}, чтобы увидеть всех подписчиков", "hints.profiles.see_more_follows": "Перейдите на {domain}, чтобы увидеть все подписки", "hints.profiles.see_more_posts": "Перейдите на {domain}, чтобы увидеть все посты", - "hints.threads.replies_may_be_missing": "Некоторые ответы с других серверов могут здесь отсутствовать.", - "hints.threads.see_more": "Перейдите на {domain}, чтобы увидеть все ответы", "home.column_settings.show_quotes": "Показывать цитирования", "home.column_settings.show_reblogs": "Показывать продвижения", "home.column_settings.show_replies": "Показывать ответы", @@ -873,12 +871,6 @@ "status.open": "Открыть пост", "status.pin": "Закрепить в профиле", "status.quote_error.filtered": "Скрыто одним из ваших фильтров", - "status.quote_error.not_found": "Пост не может быть показан.", - "status.quote_error.pending_approval": "Разрешение на цитирование от автора оригинального поста пока не получено.", - "status.quote_error.rejected": "Автор оригинального поста запретил его цитировать.", - "status.quote_error.removed": "Пост был удалён его автором.", - "status.quote_error.unauthorized": "Этот пост для вас недоступен.", - "status.quote_post_author": "Пост пользователя {name}", "status.read_more": "Читать далее", "status.reblog": "Продвинуть", "status.reblog_private": "Продвинуть для своей аудитории", diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json index d893bc2a0d0..8dc3e597c11 100644 --- a/app/javascript/mastodon/locales/sc.json +++ b/app/javascript/mastodon/locales/sc.json @@ -1,6 +1,7 @@ { "about.blocks": "Serbidores moderados", "about.contact": "Cuntatu:", + "about.default_locale": "Predefinidu", "about.disclaimer": "Mastodon est software de còdighe lìberu e unu màrchiu de Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Peruna resone a disponimentu", "about.domain_blocks.preamble": "Mastodon ti permitit de bìdere su cuntenutu de utentes de cale si siat àteru serbidore de su fediversu. Custas sunt etzetziones fatas in custu serbidore ispetzìficu.", @@ -8,6 +9,7 @@ "about.domain_blocks.silenced.title": "Limitadu", "about.domain_blocks.suspended.explanation": "Perunu datu de custu serbidore at a èssere protzessadu, immagasinadu o cuncambiadu; est impossìbile duncas cale si siat interatzione o comunicatzione cun is utentes de custu serbidore.", "about.domain_blocks.suspended.title": "Suspèndidu", + "about.language_label": "Idioma", "about.not_available": "Custa informatzione no est istada posta a disponimentu in custu serbidore.", "about.powered_by": "Rete sotziale detzentralizada impulsada dae {mastodon}", "about.rules": "Règulas de su serbidore", @@ -19,13 +21,21 @@ "account.block_domain": "Bloca su domìniu {domain}", "account.block_short": "Bloca", "account.blocked": "Blocadu", + "account.blocking": "Blocadu", "account.cancel_follow_request": "Annulla sa sighidura", "account.copy": "Còpia su ligòngiu a su profilu", "account.direct": "Mèntova a @{name} in privadu", "account.disable_notifications": "Non mi notìfiches prus cando @{name} pùblichet messàgios", + "account.domain_blocking": "Blocamus su domìniu", "account.edit_profile": "Modìfica profilu", "account.enable_notifications": "Notìfica·mi cando @{name} pùblicat messàgios", "account.endorse": "Cussìgia in su profilu tuo", + "account.familiar_followers_many": "Sighidu dae {name1}, {name2} e {othersCount, plural,one {un'àtera persone chi connosches} other {àteras # persones chi connosches}}", + "account.familiar_followers_one": "Sighidu dae {name1}", + "account.familiar_followers_two": "Sighidu dae {name1} e {name2}", + "account.featured": "In evidèntzia", + "account.featured.accounts": "Profilos", + "account.featured.hashtags": "Etichetas", "account.featured_tags.last_status_at": "Ùrtima publicatzione in su {date}", "account.featured_tags.last_status_never": "Peruna publicatzione", "account.follow": "Sighi", @@ -33,9 +43,11 @@ "account.followers": "Sighiduras", "account.followers.empty": "Nemos sighit ancora custa persone.", "account.followers_counter": "{count, plural, one {{counter} sighidura} other {{counter} sighiduras}}", + "account.followers_you_know_counter": "{counter} chi connosches", "account.following": "Sighende", "account.following_counter": "{count, plural, one {sighende a {counter}} other {sighende a {counter}}}", "account.follows.empty": "Custa persone non sighit ancora a nemos.", + "account.follows_you": "Ti sighit", "account.go_to_profile": "Bae a su profilu", "account.hide_reblogs": "Cua is cumpartziduras de @{name}", "account.in_memoriam": "In memoriam.", @@ -50,18 +62,22 @@ "account.mute_notifications_short": "Pone is notìficas a sa muda", "account.mute_short": "A sa muda", "account.muted": "A sa muda", + "account.muting": "A sa muda", "account.no_bio": "Peruna descritzione frunida.", "account.open_original_page": "Aberi sa pàgina originale", "account.posts": "Publicatziones", "account.posts_with_replies": "Publicatziones e rispostas", + "account.remove_from_followers": "Cantzella a {name} dae is sighiduras", "account.report": "Signala @{name}", "account.requested": "Abetende s'aprovatzione. Incarca pro annullare sa rechesta de sighidura", "account.requested_follow": "{name} at dimandadu de ti sighire", + "account.requests_to_follow_you": "Rechestas de sighidura", "account.share": "Cumpartzi su profilu de @{name}", "account.show_reblogs": "Ammustra is cumpartziduras de @{name}", "account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "account.unblock": "Isbloca a @{name}", "account.unblock_domain": "Isbloca su domìniu {domain}", + "account.unblock_domain_short": "Isbloca", "account.unblock_short": "Isbloca", "account.unendorse": "Non cussiges in su profilu", "account.unfollow": "Non sigas prus", @@ -83,7 +99,22 @@ "alert.unexpected.message": "Ddoe est istada una faddina.", "alert.unexpected.title": "Oh!", "alt_text_badge.title": "Testu alternativu", + "alt_text_modal.add_alt_text": "Agiunghe testu alternativu", + "alt_text_modal.add_text_from_image": "Agiunghe testu dae un'immàgine", + "alt_text_modal.cancel": "Annulla", + "alt_text_modal.change_thumbnail": "Càmbia sa miniadura", + "alt_text_modal.done": "Fatu", "announcement.announcement": "Annùntziu", + "annual_report.summary.archetype.booster": "Semper a s'ùrtima", + "annual_report.summary.followers.followers": "sighiduras", + "annual_report.summary.followers.total": "{count} totale", + "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.summary.most_used_app.most_used_app": "aplicatzione prus impreada", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "eticheta prus impreada", + "annual_report.summary.most_used_hashtag.none": "Peruna", + "annual_report.summary.new_posts.new_posts": "publicatziones noas", + "annual_report.summary.percentile.we_wont_tell_bernie": "No dd'amus a nàrrere a Bernie.", + "annual_report.summary.thanks": "Gràtzias de èssere parte de Mastodon!", "attachments_list.unprocessed": "(non protzessadu)", "audio.hide": "Cua s'àudio", "block_modal.remote_users_caveat": "Amus a pedire a su serbidore {domain} de rispetare sa detzisione tua. Nointames custu, su rispetu no est garantidu ca unos cantos serbidores diant pòdere gestire is blocos de manera diferente. Is publicatzione pùblicas diant pòdere ancora èssere visìbiles a is utentes chi no ant fatu s'atzessu.", @@ -107,6 +138,7 @@ "bundle_column_error.routing.body": "Impossìbile agatare sa pàgina rechesta. Seguru chi s'URL in sa barra de indiritzos est curretu?", "bundle_column_error.routing.title": "404", "bundle_modal_error.close": "Serra", + "bundle_modal_error.message": "Faddina in su carrigamentu de custu ischermu.", "bundle_modal_error.retry": "Torra·bi a proare", "closed_registrations.other_server_instructions": "Dae chi Mastodon est detzentralizadu, podes creare unu contu in un'àteru serbidore e interagire cun custu.", "closed_registrations_modal.description": "Sa creatzione de contos in {domain} no est possìbile in custu momentu, però tene in cunsideru chi non tenes bisòngiu de unu contu ispetzìficu in {domain} pro impreare Mastodon.", @@ -116,13 +148,16 @@ "column.blocks": "Persones blocadas", "column.bookmarks": "Sinnalibros", "column.community": "Lìnia de tempus locale", + "column.create_list": "Crea una lista", "column.direct": "Mentziones privadas", "column.directory": "Nàviga in is profilos", "column.domain_blocks": "Domìnios blocados", + "column.edit_list": "Modifica sa lista", "column.favourites": "Preferidos", "column.firehose": "Publicatziones in direta", "column.follow_requests": "Rechestas de sighidura", "column.home": "Printzipale", + "column.list_members": "Gesti is persones de sa lista", "column.lists": "Listas", "column.mutes": "Persones a sa muda", "column.notifications": "Notìficas", @@ -135,6 +170,7 @@ "column_header.pin": "Apica", "column_header.show_settings": "Ammustra is cunfiguratziones", "column_header.unpin": "Boga dae pitzu", + "column_search.cancel": "Annulla", "community.column_settings.local_only": "Isceti locale", "community.column_settings.media_only": "Isceti multimediale", "community.column_settings.remote_only": "Isceti remotu", @@ -152,6 +188,7 @@ "compose_form.poll.duration": "Longària de su sondàgiu", "compose_form.poll.multiple": "Sèberu mùltiplu", "compose_form.poll.option_placeholder": "Optzione {number}", + "compose_form.poll.single": "Sèberu ùnicu", "compose_form.poll.switch_to_multiple": "Muda su sondàgiu pro permìtere multi-optziones", "compose_form.poll.switch_to_single": "Muda su sondàgiu pro permìtere un'optzione isceti", "compose_form.poll.type": "Istile", @@ -169,6 +206,8 @@ "confirmations.delete_list.confirm": "Cantzella", "confirmations.delete_list.message": "Seguru chi boles cantzellare custa lista in manera permanente?", "confirmations.delete_list.title": "Cantzellare sa lista?", + "confirmations.discard_draft.confirm": "Iscarta e sighi", + "confirmations.discard_draft.edit.cancel": "Sighi cun s'editzione", "confirmations.discard_edit_media.confirm": "Iscarta", "confirmations.discard_edit_media.message": "Tenes modìficas non sarvadas a is descritziones o a is anteprimas de is cuntenutos, ddas boles iscartare su matessi?", "confirmations.logout.confirm": "Essi·nche", @@ -256,6 +295,7 @@ "explore.trending_links": "Noas", "explore.trending_statuses": "Publicatziones", "explore.trending_tags": "Etichetas", + "featured_carousel.slide": "{index} de {total}", "filter_modal.added.context_mismatch_title": "Su cuntestu non currispondet.", "filter_modal.added.expired_title": "Filtru iscadidu.", "filter_modal.added.review_and_configure_title": "Cunfiguratziones de filtru", @@ -294,8 +334,12 @@ "footer.privacy_policy": "Polìtica de riservadesa", "footer.source_code": "Ammustra su còdighe de orìgine", "footer.status": "Istadu", + "footer.terms_of_service": "Cunditziones de su servìtziu", "generic.saved": "Sarvadu", "getting_started.heading": "Comente cumintzare", + "hashtag.admin_moderation": "Aberi s'interfache de moderatzione pro #{name}", + "hashtag.browse": "Nàviga in is publicatziones de #{hashtag}", + "hashtag.browse_from_account": "Nàviga in is publicatziones de @{name} in #{hashtag}", "hashtag.column_header.tag_mode.all": "e {additional}", "hashtag.column_header.tag_mode.any": "o {additional}", "hashtag.column_header.tag_mode.none": "sena {additional}", @@ -308,13 +352,14 @@ "hashtag.counter_by_accounts": "{count, plural, one {{counter} partetzipante} other {{counter} partetzipantes}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}} oe", + "hashtag.feature": "In evidèntzia in su profilu", "hashtag.follow": "Sighi su hashtag", + "hashtag.mute": "Pone #{hashtag} a sa muda", + "hashtag.unfeature": "No ddu pòngias in evidèntzia in su profilu", "hashtag.unfollow": "Non sigas prus s'eticheta", "hashtags.and_other": "… e {count, plural, one {un'àteru} other {àteros #}}", "hints.profiles.posts_may_be_missing": "Podet èssere chi ammanchent tzertas publicatziones de custu profilu.", "hints.profiles.see_more_posts": "Bide prus publicatziones a {domain}", - "hints.threads.replies_may_be_missing": "Podet èssere chi ammanchent rispostas dae àteros serbidores.", - "hints.threads.see_more": "Bide prus rispostas a {domain}", "home.column_settings.show_reblogs": "Ammustra is cumpartziduras", "home.column_settings.show_replies": "Ammustra rispostas", "home.hide_announcements": "Cua annùntzios", @@ -326,9 +371,14 @@ "ignore_notifications_modal.filter_instead": "Opuru filtra", "ignore_notifications_modal.filter_to_act_users": "As a pòdere ancora atzetare, refudare o sinnalare a utentes", "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrare agiudat a evitare possìbiles confusiones", + "info_button.label": "Agiudu", + "interaction_modal.go": "Sighi", + "interaction_modal.no_account_yet": "Non tenes galu perunu contu?", + "interaction_modal.on_another_server": "In un'àteru serbidore", "interaction_modal.on_this_server": "In custu serbidore", "interaction_modal.title.follow": "Sighi a {name}", "interaction_modal.title.reply": "Risponde a sa publicatzione de {name}", + "interaction_modal.username_prompt": "Pro es., {example}", "intervals.full.days": "{number, plural, one {# die} other {# dies}}", "intervals.full.hours": "{number, plural, one {# ora} other {# oras}}", "intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}", @@ -341,6 +391,7 @@ "keyboard_shortcuts.direct": "pro abèrrere sa colunna de mèntovos privados", "keyboard_shortcuts.down": "Move in bàsciu in sa lista", "keyboard_shortcuts.enter": "Aberi una publicatzione", + "keyboard_shortcuts.favourite": "Publicatzione preferida", "keyboard_shortcuts.favourites": "Aberi sa lista de preferidos", "keyboard_shortcuts.federated": "Aberi sa lìnia de tempus federada", "keyboard_shortcuts.heading": "Incurtzaduras de tecladu", @@ -363,18 +414,25 @@ "keyboard_shortcuts.toggle_hidden": "Ammustra o cua su testu de is AC", "keyboard_shortcuts.toggle_sensitivity": "Ammustra/cua elementos multimediales", "keyboard_shortcuts.toot": "Cumintza a iscrìere una publicatzione noa", + "keyboard_shortcuts.translate": "pro tradùere una publicatzione", "keyboard_shortcuts.unfocus": "Essi de s'àrea de cumpositzione de testu o de chirca", "keyboard_shortcuts.up": "Move in susu in sa lista", "lightbox.close": "Serra", "lightbox.next": "Imbeniente", "lightbox.previous": "Pretzedente", + "lightbox.zoom_in": "Ismànnia finas a sa mannària atuale", "limited_account_hint.title": "Custu profilu est istadu cuadu dae sa moderatzione de {domain}.", + "link_preview.author": "Dae {name}", "link_preview.shares": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "lists.delete": "Cantzella sa lista", "lists.edit": "Modìfica sa lista", + "lists.remove_member": "Cantzella", "lists.replies_policy.followed": "Cale si siat persone chi sighis", "lists.replies_policy.list": "Persones de sa lista", "lists.replies_policy.none": "Nemos", + "lists.save": "Sarva", + "lists.search": "Chirca", + "lists.show_replies_to": "Include rispostas dae gente de sa lista a", "load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}", "loading_indicator.label": "Carrighende…", "media_gallery.hide": "Cua", @@ -389,8 +447,10 @@ "mute_modal.you_wont_see_mentions": "No as a bìdere is publicatziones chi mèntovent a custa persone.", "mute_modal.you_wont_see_posts": "At a pòdere bìdere is publicatziones tuas, però tue no as a bìdere cussas suas.", "navigation_bar.about": "Informatziones", + "navigation_bar.account_settings": "Crae e seguresa", "navigation_bar.administration": "Amministratzione", "navigation_bar.advanced_interface": "Aberi s'interfache web avantzada", + "navigation_bar.automated_deletion": "Cantzelladura automàtica de publicatziones", "navigation_bar.blocks": "Persones blocadas", "navigation_bar.bookmarks": "Sinnalibros", "navigation_bar.direct": "Mentziones privadas", @@ -400,13 +460,18 @@ "navigation_bar.follow_requests": "Rechestas de sighidura", "navigation_bar.followed_tags": "Etichetas sighidas", "navigation_bar.follows_and_followers": "Gente chi sighis e sighiduras", + "navigation_bar.import_export": "Importatzione e esportatzione", "navigation_bar.lists": "Listas", + "navigation_bar.live_feed_local": "Canale in direta (locale)", + "navigation_bar.live_feed_public": "Canale in direta (pùblicu)", "navigation_bar.logout": "Essi", "navigation_bar.moderation": "Moderatzione", + "navigation_bar.more": "Àteru", "navigation_bar.mutes": "Persones a sa muda", "navigation_bar.opened_in_classic_interface": "Publicatziones, contos e àteras pàginas ispetzìficas sunt abertas in manera predefinida in s'interfache web clàssica.", "navigation_bar.preferences": "Preferèntzias", "navigation_bar.search": "Chirca", + "navigation_bar.search_trends": "Chirca / in tendèntzia", "not_signed_in_indicator.not_signed_in": "Ti depes identificare pro atzèdere a custa resursa.", "notification.admin.report": "{name} at sinnaladu a {target}", "notification.admin.report_account": "{name} at sinnaladu {count, plural, one {una publicatzione} other {# publicatziones}} dae {target} pro {category}", @@ -463,15 +528,19 @@ "notification_requests.minimize_banner": "Mìnima su bànner de notìficas filtradas", "notification_requests.notifications_from": "Notìficas dae {name}", "notification_requests.title": "Notìficas filtradas", + "notification_requests.view": "Mustra notìficas", "notifications.clear": "Lìmpia notìficas", "notifications.clear_confirmation": "Seguru chi boles isboidare in manera permanente totu is notìficas tuas?", + "notifications.clear_title": "Boles cantzellare is notìficas?", "notifications.column_settings.admin.report": "Informes noos:", + "notifications.column_settings.admin.sign_up": "Registros noos:", "notifications.column_settings.alert": "Notìficas de iscrivania", "notifications.column_settings.favourite": "Preferidos:", "notifications.column_settings.filter_bar.advanced": "Ammustra totu is categorias", "notifications.column_settings.filter_bar.category": "Barra de filtru lestru", "notifications.column_settings.follow": "Sighiduras noas:", "notifications.column_settings.follow_request": "Rechestas noas de sighidura:", + "notifications.column_settings.group": "Grupu", "notifications.column_settings.mention": "Mèntovos:", "notifications.column_settings.poll": "Resurtados de su sondàgiu:", "notifications.column_settings.push": "Notìficas push", @@ -495,6 +564,8 @@ "notifications.permission_denied": "Is notìficas de iscrivania non sunt a disponimentu pro neghe de rechestas de permissu chi sunt istadas dennegadas in antis", "notifications.permission_denied_alert": "Is notìficas de iscrivania non podent èssere abilitadas, ca su permissu de su navigadore est istadu dennegadu in antis", "notifications.permission_required": "Is notìficas de iscrivania no sunt a disponimentu ca ammancat su permissu rechèdidu.", + "notifications.policy.accept": "Atzeta", + "notifications.policy.accept_hint": "Mustra in is notìficas", "notifications.policy.filter_new_accounts.hint": "Creadu {days, plural, one {erisero} other {in is ùrtimas # dies}}", "notifications.policy.filter_new_accounts_title": "Contos noos", "notifications.policy.filter_not_followers_title": "Gente chi non ti sighit", @@ -503,8 +574,15 @@ "notifications_permission_banner.enable": "Abilita is notìficas de iscrivania", "notifications_permission_banner.how_to_control": "Pro retzire notìficas cando Mastodon no est abertu, abilita is notìficas de iscrivania. Podes controllare cun pretzisione is castas de interatziones chi ingendrant notìficas de iscrivania pro mèdiu de su butone {icon} in subra, cando sunt abilitadas.", "notifications_permission_banner.title": "Non ti perdas mai nudda", + "onboarding.follows.back": "A coa", + "onboarding.follows.done": "Fatu", + "onboarding.follows.search": "Chirca", + "onboarding.follows.title": "Sighi a gente pro cumintzare", "onboarding.profile.display_name": "Nòmine visìbile", "onboarding.profile.note": "Biografia", + "onboarding.profile.save_and_continue": "Sarva e sighi", + "onboarding.profile.title": "Cunfiguratzione de profilu", + "onboarding.profile.upload_avatar": "Càrriga una fotografia de profilu", "picture_in_picture.restore": "Torra·ddu a ue fiat", "poll.closed": "Serradu", "poll.refresh": "Atualiza", @@ -599,6 +677,7 @@ "search_results.hashtags": "Etichetas", "search_results.see_all": "Bide totu", "search_results.statuses": "Publicatziones", + "search_results.title": "Chirca \"{q}\"", "server_banner.about_active_users": "Gente chi at impreadu custu serbidore is ùrtimas 30 dies (Utentes cun Atividade a su Mese)", "server_banner.active_users": "utentes ativos", "server_banner.administered_by": "Amministradu dae:", diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json index 9f9bd1590fe..438ca0b735a 100644 --- a/app/javascript/mastodon/locales/si.json +++ b/app/javascript/mastodon/locales/si.json @@ -406,8 +406,6 @@ "hints.profiles.see_more_followers": "{domain}හි තවත් අනුගාමිකයින් බලන්න", "hints.profiles.see_more_follows": "{domain}හි තවත් පහත ඒවා බලන්න.", "hints.profiles.see_more_posts": "{domain}හි තවත් සටහන් බලන්න", - "hints.threads.replies_may_be_missing": "අනෙකුත් සේවාදායකයන්ගෙන් ලැබෙන පිළිතුරු අස්ථානගත වී තිබිය හැක.", - "hints.threads.see_more": "{domain}හි තවත් පිළිතුරු බලන්න.", "home.column_settings.show_reblogs": "බූස්ට් පෙන්වන්න", "home.column_settings.show_replies": "පිළිතුරු පෙන්වන්න", "home.hide_announcements": "නිවේදන සඟවන්න", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 63fd556f79b..723207a4be4 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -27,6 +27,8 @@ "account.edit_profile": "Upraviť profil", "account.enable_notifications": "Zapnúť upozornenia na príspevky od @{name}", "account.endorse": "Zobraziť na vlastnom profile", + "account.familiar_followers_one": "Nasledovanie od {name1}", + "account.familiar_followers_two": "Nasledovanie od {name1} a {name2}", "account.featured": "Zviditeľnené", "account.featured.accounts": "Profily", "account.featured.hashtags": "Hashtagy", @@ -365,8 +367,6 @@ "hints.profiles.see_more_followers": "Pozri viac nasledovateľov na {domain}", "hints.profiles.see_more_follows": "Pozri viac nasledovateľov na {domain}", "hints.profiles.see_more_posts": "Pozri viac príspevkov na {domain}", - "hints.threads.replies_may_be_missing": "Odpovede z ostatných serverov môžu chýbať.", - "hints.threads.see_more": "Pozri viac odpovedí na {domain}", "home.column_settings.show_reblogs": "Zobraziť zdieľania", "home.column_settings.show_replies": "Zobraziť odpovede", "home.hide_announcements": "Skryť oznámenia", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 7213af36662..c256318fee9 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -383,8 +383,6 @@ "hints.profiles.see_more_followers": "Pokaži več sledilcev na {domain}", "hints.profiles.see_more_follows": "Pokaži več sledenih ljudi na zbirališču {domain}", "hints.profiles.see_more_posts": "Pokaži več objav na {domain}", - "hints.threads.replies_may_be_missing": "Odgovori z drugih strežnikov morda manjkajo.", - "hints.threads.see_more": "Pokaži več odgovorov na {domain}", "home.column_settings.show_reblogs": "Pokaži izpostavitve", "home.column_settings.show_replies": "Pokaži odgovore", "home.hide_announcements": "Skrij obvestila", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index b5a7cb0e886..d871d1bb278 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -419,8 +419,6 @@ "hints.profiles.see_more_followers": "Shihni më tepër ndjekës në {domain}", "hints.profiles.see_more_follows": "Shihni më tepër ndjekje në {domain}", "hints.profiles.see_more_posts": "Shihni më tepër postime në {domain}", - "hints.threads.replies_may_be_missing": "Mund të mungojnë përgjigje nga shërbyes të tjerë.", - "hints.threads.see_more": "Shihni më tepër përgjigje në {domain}", "home.column_settings.show_quotes": "Shfaq thonjëza", "home.column_settings.show_reblogs": "Shfaq përforcime", "home.column_settings.show_replies": "Shfaq përgjigje", @@ -866,12 +864,6 @@ "status.open": "Zgjeroje këtë mesazh", "status.pin": "Fiksoje në profil", "status.quote_error.filtered": "Fshehur për shkak të njërit nga filtrat tuaj", - "status.quote_error.not_found": "Ky postim s’mund të shfaqet.", - "status.quote_error.pending_approval": "Ky postim është në pritje të miratimit nga autori origjinal.", - "status.quote_error.rejected": "Ky postim s’mund të shfaqet, ngaqë autori origjinal nuk lejon citim të tij.", - "status.quote_error.removed": "Ky postim u hoq nga autori i tij.", - "status.quote_error.unauthorized": "Ky postim s’mund të shfaqet, ngaqë s’jeni i autorizuar ta shihni.", - "status.quote_post_author": "Postim nga {name}", "status.read_more": "Lexoni më tepër", "status.reblog": "Përforcojeni", "status.reblog_private": "Përforcim për publikun origjinal", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 0808963e5ff..45ca92bebeb 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -424,8 +424,6 @@ "hints.profiles.see_more_followers": "Se fler följare på {domain}", "hints.profiles.see_more_follows": "Se fler följare på {domain}", "hints.profiles.see_more_posts": "Se fler inlägg på {domain}", - "hints.threads.replies_may_be_missing": "Det kan saknas svar från andra servrar.", - "hints.threads.see_more": "Se fler svar på {domain}", "home.column_settings.show_quotes": "Visa citat", "home.column_settings.show_reblogs": "Visa boostar", "home.column_settings.show_replies": "Visa svar", @@ -847,6 +845,8 @@ "status.bookmark": "Bokmärk", "status.cancel_reblog_private": "Sluta boosta", "status.cannot_reblog": "Detta inlägg kan inte boostas", + "status.context.load_new_replies": "Nya svar finns", + "status.context.loading": "Letar efter fler svar", "status.continued_thread": "Fortsatt tråd", "status.copy": "Kopiera inläggslänk", "status.delete": "Radera", @@ -873,12 +873,6 @@ "status.open": "Utvidga detta inlägg", "status.pin": "Fäst i profil", "status.quote_error.filtered": "Dolt på grund av ett av dina filter", - "status.quote_error.not_found": "Detta inlägg kan inte boostas.", - "status.quote_error.pending_approval": "Det här inlägget väntar på godkännande från originalförfattaren.", - "status.quote_error.rejected": "Det här inlägget kan inte visas eftersom originalförfattaren inte tillåter att det citeras.", - "status.quote_error.removed": "Detta inlägg har tagits bort av författaren.", - "status.quote_error.unauthorized": "Det här inlägget kan inte visas eftersom du inte har behörighet att se det.", - "status.quote_post_author": "Inlägg av @{name}", "status.read_more": "Läs mer", "status.reblog": "Boosta", "status.reblog_private": "Boosta med ursprunglig synlighet", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 603c1ab61ff..545371e56fb 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -397,8 +397,6 @@ "hints.profiles.see_more_followers": "ดูผู้ติดตามเพิ่มเติมใน {domain}", "hints.profiles.see_more_follows": "ดูการติดตามเพิ่มเติมใน {domain}", "hints.profiles.see_more_posts": "ดูโพสต์เพิ่มเติมใน {domain}", - "hints.threads.replies_may_be_missing": "การตอบกลับจากเซิร์ฟเวอร์อื่น ๆ อาจขาดหายไป", - "hints.threads.see_more": "ดูการตอบกลับเพิ่มเติมใน {domain}", "home.column_settings.show_reblogs": "แสดงการดัน", "home.column_settings.show_replies": "แสดงการตอบกลับ", "home.hide_announcements": "ซ่อนประกาศ", @@ -834,7 +832,6 @@ "status.mute_conversation": "ซ่อนการสนทนา", "status.open": "ขยายโพสต์นี้", "status.pin": "ปักหมุดในโปรไฟล์", - "status.quote_post_author": "โพสต์โดย {name}", "status.read_more": "อ่านเพิ่มเติม", "status.reblog": "ดัน", "status.reblog_private": "ดันด้วยการมองเห็นดั้งเดิม", diff --git a/app/javascript/mastodon/locales/tok.json b/app/javascript/mastodon/locales/tok.json index c48ffa5fe24..cabee093148 100644 --- a/app/javascript/mastodon/locales/tok.json +++ b/app/javascript/mastodon/locales/tok.json @@ -371,8 +371,6 @@ "hints.profiles.see_more_followers": "o lukin e jan ni lon ma {domain}: ona li kute e jan ni.", "hints.profiles.see_more_follows": "o lukin e jan ni lon ma {domain}: jan ni li kute e ona.", "hints.profiles.see_more_posts": "o lukin e toki ante lon ma {domain}", - "hints.threads.replies_may_be_missing": "toki pi ma ante li weka lon ken.", - "hints.threads.see_more": "o lukin e toki ante lon ma {domain}", "home.column_settings.show_reblogs": "o lukin e pana toki", "home.hide_announcements": "o lukin ala e toki lawa suli", "home.pending_critical_update.body": "o sin e ilo Mastodon lon tenpo lili a!", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index a45acff11f5..4043695a160 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Takipçi kaldır", "confirmations.remove_from_followers.message": "{name} sizi takip etmeyi bırakacaktır. Devam etmek istediğinize emin misiniz?", "confirmations.remove_from_followers.title": "Takipçiyi kaldır?", + "confirmations.revoke_quote.confirm": "Gönderiyi kaldır", + "confirmations.revoke_quote.message": "Bu işlem geri alınamaz.", + "confirmations.revoke_quote.title": "Gönderiyi silmek ister misiniz?", "confirmations.unfollow.confirm": "Takibi bırak", "confirmations.unfollow.message": "{name} adlı kullanıcıyı takibi bırakmak istediğinden emin misin?", "confirmations.unfollow.title": "Kullanıcıyı takipten çık?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "{domain} adresinde daha fazla takipçi gör", "hints.profiles.see_more_follows": "{domain} adresinde daha fazla takip edilen gör", "hints.profiles.see_more_posts": "{domain} adresinde daha fazla gönderi gör", - "hints.threads.replies_may_be_missing": "Diğer sunuculardan yanıtlar eksik olabilir.", - "hints.threads.see_more": "{domain} adresinde daha fazla yanıt gör", "home.column_settings.show_quotes": "Alıntıları göster", "home.column_settings.show_reblogs": "Yeniden paylaşımları göster", "home.column_settings.show_replies": "Yanıtları göster", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "bir gönderiyi çevirmek için", "keyboard_shortcuts.unfocus": "Aramada bir gönderiye odaklanmamak için", "keyboard_shortcuts.up": "Listede yukarıya çıkmak için", + "learn_more_link.got_it": "Anladım", + "learn_more_link.learn_more": "Daha fazla bilgi edin", "lightbox.close": "Kapat", "lightbox.next": "Sonraki", "lightbox.previous": "Önceki", @@ -600,6 +603,7 @@ "notification.label.mention": "Bahsetme", "notification.label.private_mention": "Özel bahsetme", "notification.label.private_reply": "Özel yanıt", + "notification.label.quote": "{name} gönderini yeniden paylaştı", "notification.label.reply": "Yanıt", "notification.mention": "Bahsetme", "notification.mentioned_you": "{name} sizden söz etti", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Bahsetmeler:", "notifications.column_settings.poll": "Anket sonuçları:", "notifications.column_settings.push": "Anlık bildirimler", + "notifications.column_settings.quote": "Alıntılar:", "notifications.column_settings.reblog": "Yeniden paylaşanlar:", "notifications.column_settings.show": "Sütunda göster", "notifications.column_settings.sound": "Ses çal", @@ -847,6 +852,8 @@ "status.bookmark": "Yer işareti ekle", "status.cancel_reblog_private": "Yeniden paylaşımı geri al", "status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz", + "status.context.load_new_replies": "Yeni yanıtlar mevcut", + "status.context.loading": "Daha fazla yanıt için kontrol ediliyor", "status.continued_thread": "Devam eden akış", "status.copy": "Gönderi bağlantısını kopyala", "status.delete": "Sil", @@ -873,12 +880,11 @@ "status.open": "Bu gönderiyi genişlet", "status.pin": "Profile sabitle", "status.quote_error.filtered": "Bazı filtrelerinizden dolayı gizlenmiştir", - "status.quote_error.not_found": "Bu gönderi görüntülenemez.", - "status.quote_error.pending_approval": "Bu gönderi özgün yazarın onayını bekliyor.", - "status.quote_error.rejected": "Bu gönderi, özgün yazar alıntılanmasına izin vermediği için görüntülenemez.", - "status.quote_error.removed": "Bu gönderi yazarı tarafından kaldırıldı.", - "status.quote_error.unauthorized": "Bu gönderiyi, yetkiniz olmadığı için görüntüleyemiyorsunuz.", - "status.quote_post_author": "{name} gönderisi", + "status.quote_error.not_available": "Gönderi kullanılamıyor", + "status.quote_error.pending_approval": "Gönderi beklemede", + "status.quote_error.pending_approval_popout.body": "Fediverse genelinde paylaşılan alıntıların görüntülenmesi zaman alabilir, çünkü farklı sunucuların farklı protokolleri vardır.", + "status.quote_error.pending_approval_popout.title": "Bekleyen bir teklif mi var? Sakin olun.", + "status.quote_post_author": "@{name} adlı kullanıcının bir gönderisini alıntıladı", "status.read_more": "Devamını okuyun", "status.reblog": "Yeniden paylaş", "status.reblog_private": "Özgün görünürlük ile yeniden paylaş", @@ -893,6 +899,7 @@ "status.reply": "Yanıtla", "status.replyAll": "Konuyu yanıtla", "status.report": "@{name} adlı kişiyi bildir", + "status.revoke_quote": "@{name}'nin gönderisinden benim gönderimi kaldır", "status.sensitive_warning": "Hassas içerik", "status.share": "Paylaş", "status.show_less_all": "Hepsi için daha az göster", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 56ff3444f45..bee00c0f06f 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -227,6 +227,9 @@ "confirmations.redraft.confirm": "Видалити та виправити", "confirmations.redraft.message": "Ви впевнені, що хочете видалити цей допис та переписати його? Додавання у вибране та поширення буде втрачено, а відповіді на оригінальний допис залишаться без першоджерела.", "confirmations.redraft.title": "Видалити та переробити допис?", + "confirmations.revoke_quote.confirm": "Видалити публікацію", + "confirmations.revoke_quote.message": "Цю дію не можна скасувати.", + "confirmations.revoke_quote.title": "Видалити публікацію?", "confirmations.unfollow.confirm": "Відписатися", "confirmations.unfollow.message": "Ви впевнені, що хочете відписатися від {name}?", "confirmations.unfollow.title": "Відписатися від користувача?", @@ -321,6 +324,7 @@ "explore.trending_links": "Новини", "explore.trending_statuses": "Дописи", "explore.trending_tags": "Хештеґи", + "featured_carousel.next": "Далі", "filter_modal.added.context_mismatch_explanation": "Ця категорія фільтра не застосовується до контексту, в якому ви отримали доступ до цього допису. Якщо ви хочете, щоб дописи також фільтрувалися за цим контекстом, вам доведеться редагувати фільтр.", "filter_modal.added.context_mismatch_title": "Невідповідність контексту!", "filter_modal.added.expired_explanation": "Категорія цього фільтра застаріла, Вам потрібно змінити дату закінчення терміну дії, щоб застосувати її.", @@ -395,8 +399,6 @@ "hints.profiles.see_more_followers": "Переглянути більше підписників на {domain}", "hints.profiles.see_more_follows": "Переглянути більше підписок на {domain}", "hints.profiles.see_more_posts": "Переглянути більше дописів на {domain}", - "hints.threads.replies_may_be_missing": "Відповіді з інших серверів можуть бути не показані.", - "hints.threads.see_more": "Переглянути більше відповідей на {domain}", "home.column_settings.show_reblogs": "Показувати поширення", "home.column_settings.show_replies": "Показувати відповіді", "home.hide_announcements": "Приховати оголошення", @@ -470,6 +472,8 @@ "keyboard_shortcuts.translate": "перекласти допис", "keyboard_shortcuts.unfocus": "Розфокусуватися з нового допису чи пошуку", "keyboard_shortcuts.up": "Рухатися вгору списком", + "learn_more_link.got_it": "Зрозуміло", + "learn_more_link.learn_more": "Докладніше", "lightbox.close": "Закрити", "lightbox.next": "Далі", "lightbox.previous": "Назад", @@ -817,6 +821,8 @@ "status.bookmark": "Додати до закладок", "status.cancel_reblog_private": "Скасувати поширення", "status.cannot_reblog": "Цей допис не може бути поширений", + "status.context.load_new_replies": "Доступні нові відповіді", + "status.context.loading": "Перевірка додаткових відповідей", "status.continued_thread": "Продовження у потоці", "status.copy": "Копіювати посилання на допис", "status.delete": "Видалити", @@ -843,7 +849,8 @@ "status.open": "Розгорнути допис", "status.pin": "Закріпити у профілі", "status.quote_error.filtered": "Приховано через один з ваших фільтрів", - "status.quote_post_author": "@{name} опублікував допис", + "status.quote_error.not_available": "Пост недоступний", + "status.quote_post_author": "Цитований допис @{name}", "status.read_more": "Дізнатися більше", "status.reblog": "Поширити", "status.reblog_private": "Поширити для початкової аудиторії", @@ -858,6 +865,7 @@ "status.reply": "Відповісти", "status.replyAll": "Відповісти на ланцюжок", "status.report": "Поскаржитися на @{name}", + "status.revoke_quote": "Видалити мою публікацію з допису @{name}", "status.sensitive_warning": "Делікатний вміст", "status.share": "Поділитися", "status.show_less_all": "Згорнути для всіх", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index ec6e7188f22..37e8ecee8d4 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Xóa người theo dõi", "confirmations.remove_from_followers.message": "{name} sẽ không còn theo dõi bạn.Bạn có chắc tiếp tục?", "confirmations.remove_from_followers.title": "Xóa người theo dõi?", + "confirmations.revoke_quote.confirm": "Gỡ tút", + "confirmations.revoke_quote.message": "Hành động này không thể hoàn tác.", + "confirmations.revoke_quote.title": "Gỡ tút?", "confirmations.unfollow.confirm": "Bỏ theo dõi", "confirmations.unfollow.message": "Bạn có chắc muốn bỏ theo dõi {name}?", "confirmations.unfollow.title": "Bỏ theo dõi", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "Xem thêm người theo dõi ở {domain}", "hints.profiles.see_more_follows": "Xem thêm người mà người này theo dõi ở {domain}", "hints.profiles.see_more_posts": "Xem thêm tút ở {domain}", - "hints.threads.replies_may_be_missing": "Những trả lời từ máy chủ khác có thể không đầy đủ.", - "hints.threads.see_more": "Xem thêm ở {domain}", "home.column_settings.show_quotes": "Hiện những trích dẫn", "home.column_settings.show_reblogs": "Hiện những lượt đăng lại", "home.column_settings.show_replies": "Hiện những tút dạng trả lời", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "dịch tút", "keyboard_shortcuts.unfocus": "đưa con trỏ ra khỏi ô soạn thảo hoặc ô tìm kiếm", "keyboard_shortcuts.up": "di chuyển lên trên danh sách", + "learn_more_link.got_it": "Đã hiểu", + "learn_more_link.learn_more": "Tìm hiểu thêm", "lightbox.close": "Đóng", "lightbox.next": "Tiếp", "lightbox.previous": "Trước", @@ -600,6 +603,7 @@ "notification.label.mention": "Lượt nhắc", "notification.label.private_mention": "Nhắn riêng", "notification.label.private_reply": "Trả lời riêng", + "notification.label.quote": "{name} đã trích dẫn tút của bạn", "notification.label.reply": "Trả lời", "notification.mention": "Nhắc đến bạn", "notification.mentioned_you": "{name} nhắc đến bạn", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "Lượt nhắc đến:", "notifications.column_settings.poll": "Kết quả vốt:", "notifications.column_settings.push": "Thông báo đẩy", + "notifications.column_settings.quote": "Trích dẫn:", "notifications.column_settings.reblog": "Lượt đăng lại:", "notifications.column_settings.show": "Báo trên thanh bên", "notifications.column_settings.sound": "Kèm âm báo", @@ -847,6 +852,8 @@ "status.bookmark": "Lưu", "status.cancel_reblog_private": "Bỏ đăng lại", "status.cannot_reblog": "Không thể đăng lại tút này", + "status.context.load_new_replies": "Có những trả lời mới", + "status.context.loading": "Kiểm tra nhiều trả lời hơn", "status.continued_thread": "Tiếp tục chủ đề", "status.copy": "Sao chép URL", "status.delete": "Xóa", @@ -873,12 +880,11 @@ "status.open": "Mở tút", "status.pin": "Ghim lên hồ sơ", "status.quote_error.filtered": "Bị ẩn vì một bộ lọc của bạn", - "status.quote_error.not_found": "Tút này không thể hiển thị.", - "status.quote_error.pending_approval": "Tút này cần chờ cho phép từ người đăng.", - "status.quote_error.rejected": "Tút này không thể hiển thị vì người đăng không cho phép trích dẫn nó.", - "status.quote_error.removed": "Tút này đã bị người đăng xóa.", - "status.quote_error.unauthorized": "Tút này không thể hiển thị vì bạn không được cấp quyền truy cập nó.", - "status.quote_post_author": "Tút của {name}", + "status.quote_error.not_available": "Tút không khả dụng", + "status.quote_error.pending_approval": "Tút đang chờ duyệt", + "status.quote_error.pending_approval_popout.body": "Các trích dẫn được chia sẻ trên Fediverse có thể mất thời gian để hiển thị vì các máy chủ khác nhau có giao thức khác nhau.", + "status.quote_error.pending_approval_popout.title": "Đang chờ trích dẫn? Hãy bình tĩnh", + "status.quote_post_author": "Trích dẫn từ tút của @{name}", "status.read_more": "Đọc tiếp", "status.reblog": "Đăng lại", "status.reblog_private": "Đăng lại (Riêng tư)", @@ -893,6 +899,7 @@ "status.reply": "Trả lời", "status.replyAll": "Trả lời", "status.report": "Báo cáo @{name}", + "status.revoke_quote": "Gỡ tút của tôi khỏi trích dẫn của @{name}", "status.sensitive_warning": "Nhạy cảm", "status.share": "Chia sẻ", "status.show_less_all": "Thu gọn", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 8928f253b16..43a7cf0ff84 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -301,6 +301,9 @@ "emoji_button.search_results": "搜索结果", "emoji_button.symbols": "符号", "emoji_button.travel": "旅行与地点", + "empty_column.account_featured.me": "你尚未设置任何精选。你知道吗?你也可以将自己最常使用的话题标签,甚至是好友的帐号,在你的个人主页上设为精选。", + "empty_column.account_featured.other": "{acct} 尚未设置任何精选。你知道吗?你也可以将自己最常使用的话题标签,甚至是好友的帐号,在你的个人主页上设为精选。", + "empty_column.account_featured_other.unknown": "该用户尚未设置任何精选。", "empty_column.account_hides_collections": "该用户选择不公开此信息", "empty_column.account_suspended": "账号已被停用", "empty_column.account_timeline": "这里没有嘟文!", @@ -402,8 +405,10 @@ "hashtag.counter_by_accounts": "{count, plural,other {{counter} 人讨论}}", "hashtag.counter_by_uses": "{count, plural, other {{counter} 条嘟文}}", "hashtag.counter_by_uses_today": "今日 {count, plural, other {{counter} 条嘟文}}", + "hashtag.feature": "设为精选", "hashtag.follow": "关注话题", "hashtag.mute": "停止提醒 #{hashtag}", + "hashtag.unfeature": "取消精选", "hashtag.unfollow": "取消关注话题", "hashtags.and_other": "… 和另外 {count, plural, other {# 个话题}}", "hints.profiles.followers_may_be_missing": "该账号的关注者列表可能没有完全显示。", @@ -412,8 +417,6 @@ "hints.profiles.see_more_followers": "在 {domain} 查看更多关注者", "hints.profiles.see_more_follows": "在 {domain} 查看更多关注", "hints.profiles.see_more_posts": "在 {domain} 查看更多嘟文", - "hints.threads.replies_may_be_missing": "来自其它实例的回复可能没有完全显示。", - "hints.threads.see_more": "在 {domain} 查看更多回复", "home.column_settings.show_quotes": "显示引用", "home.column_settings.show_reblogs": "显示转嘟", "home.column_settings.show_replies": "显示回复", @@ -560,6 +563,7 @@ "navigation_bar.preferences": "偏好设置", "navigation_bar.privacy_and_reach": "隐私与可达性", "navigation_bar.search": "搜索", + "navigation_bar.search_trends": "搜索/热门趋势", "navigation_panel.collapse_lists": "收起菜单列表", "navigation_panel.expand_lists": "展开菜单列表", "not_signed_in_indicator.not_signed_in": "你需要登录才能访问此资源。", @@ -788,6 +792,7 @@ "report_notification.categories.violation": "违反规则", "report_notification.categories.violation_sentence": "违反规则", "report_notification.open": "打开举报", + "search.clear": "清空搜索内容", "search.no_recent_searches": "无最近搜索", "search.placeholder": "搜索", "search.quick_action.account_search": "包含 {x} 的账号", @@ -829,6 +834,8 @@ "status.bookmark": "添加到书签", "status.cancel_reblog_private": "取消转嘟", "status.cannot_reblog": "不能转嘟这条嘟文", + "status.context.load_new_replies": "有新回复", + "status.context.loading": "正在检查更多回复", "status.continued_thread": "上接嘟文串", "status.copy": "复制嘟文链接", "status.delete": "删除", @@ -855,10 +862,6 @@ "status.open": "展开嘟文", "status.pin": "在个人资料页面置顶", "status.quote_error.filtered": "已根据你的筛选器过滤", - "status.quote_error.not_found": "无法显示这篇贴文。", - "status.quote_error.rejected": "由于原作者不允许引用转发,无法显示这篇贴文。", - "status.quote_error.removed": "该帖子已被作者删除。", - "status.quote_post_author": "{name} 的嘟文", "status.read_more": "查看更多", "status.reblog": "转嘟", "status.reblog_private": "以相同可见性转嘟", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 8fe9f71a69f..9dc2998504e 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "移除跟隨者", "confirmations.remove_from_followers.message": "{name} 將會停止跟隨您。您確定要繼續嗎?", "confirmations.remove_from_followers.title": "是否移除該跟隨者?", + "confirmations.revoke_quote.confirm": "移除嘟文", + "confirmations.revoke_quote.message": "此動作無法復原。", + "confirmations.revoke_quote.title": "您是否確定移除嘟文?", "confirmations.unfollow.confirm": "取消跟隨", "confirmations.unfollow.message": "您確定要取消跟隨 {name} 嗎?", "confirmations.unfollow.title": "是否取消跟隨該使用者?", @@ -424,8 +427,6 @@ "hints.profiles.see_more_followers": "於 {domain} 檢視更多跟隨者", "hints.profiles.see_more_follows": "於 {domain} 檢視更多正在跟隨", "hints.profiles.see_more_posts": "於 {domain} 檢視更多嘟文", - "hints.threads.replies_may_be_missing": "來自其他站點之回覆或有缺失。", - "hints.threads.see_more": "於 {domain} 檢視更多回覆", "home.column_settings.show_quotes": "顯示引用嘟文", "home.column_settings.show_reblogs": "顯示轉嘟", "home.column_settings.show_replies": "顯示回覆", @@ -500,6 +501,8 @@ "keyboard_shortcuts.translate": "翻譯嘟文", "keyboard_shortcuts.unfocus": "跳離文字撰寫區塊或搜尋框", "keyboard_shortcuts.up": "向上移動", + "learn_more_link.got_it": "了解", + "learn_more_link.learn_more": "了解更多", "lightbox.close": "關閉", "lightbox.next": "下一步", "lightbox.previous": "上一步", @@ -600,6 +603,7 @@ "notification.label.mention": "提及", "notification.label.private_mention": "私訊", "notification.label.private_reply": "私訊回嘟", + "notification.label.quote": "{name} 已引用您的嘟文", "notification.label.reply": "回嘟", "notification.mention": "提及", "notification.mentioned_you": "{name} 已提及您", @@ -657,6 +661,7 @@ "notifications.column_settings.mention": "提及:", "notifications.column_settings.poll": "投票結果:", "notifications.column_settings.push": "推播通知", + "notifications.column_settings.quote": "引用嘟文:", "notifications.column_settings.reblog": "轉嘟:", "notifications.column_settings.show": "於欄位中顯示", "notifications.column_settings.sound": "播放音效", @@ -847,6 +852,8 @@ "status.bookmark": "書籤", "status.cancel_reblog_private": "取消轉嘟", "status.cannot_reblog": "這則嘟文無法被轉嘟", + "status.context.load_new_replies": "有新回嘟", + "status.context.loading": "正在檢查更多回嘟", "status.continued_thread": "接續討論串", "status.copy": "複製嘟文連結", "status.delete": "刪除", @@ -873,12 +880,11 @@ "status.open": "展開此嘟文", "status.pin": "釘選至個人檔案頁面", "status.quote_error.filtered": "由於您的過濾器,該嘟文被隱藏", - "status.quote_error.not_found": "這則嘟文無法被顯示。", - "status.quote_error.pending_approval": "此嘟文正在等待原作者審核。", - "status.quote_error.rejected": "由於原作者不允許引用,此嘟文無法被顯示。", - "status.quote_error.removed": "此嘟文已被其作者移除。", - "status.quote_error.unauthorized": "由於您未被授權檢視,此嘟文無法被顯示。", - "status.quote_post_author": "由 {name} 發嘟", + "status.quote_error.not_available": "無法取得該嘟文", + "status.quote_error.pending_approval": "嘟文正在發送中", + "status.quote_error.pending_approval_popout.body": "因為伺服器間可能運行不同協定,顯示聯邦宇宙間之引用嘟文會有些許延遲。", + "status.quote_error.pending_approval_popout.title": "引用嘟文正在發送中?別著急,請稍候片刻", + "status.quote_post_author": "已引用 @{name} 之嘟文", "status.read_more": "閱讀更多", "status.reblog": "轉嘟", "status.reblog_private": "依照原嘟可見性轉嘟", @@ -893,6 +899,7 @@ "status.reply": "回覆", "status.replyAll": "回覆討論串", "status.report": "檢舉 @{name}", + "status.revoke_quote": "將我的嘟文自 @{name} 之嘟文中移除", "status.sensitive_warning": "敏感內容", "status.share": "分享", "status.show_less_all": "隱藏所有內容警告與額外標籤", diff --git a/app/javascript/mastodon/main.tsx b/app/javascript/mastodon/main.tsx index e840429c41e..456cc21c318 100644 --- a/app/javascript/mastodon/main.tsx +++ b/app/javascript/mastodon/main.tsx @@ -2,14 +2,18 @@ import { createRoot } from 'react-dom/client'; import { Globals } from '@react-spring/web'; +import * as perf from '@/mastodon/utils/performance'; import { setupBrowserNotifications } from 'mastodon/actions/notifications'; import Mastodon from 'mastodon/containers/mastodon'; -import { isFeatureEnabled, me, reduceMotion } from 'mastodon/initial_state'; -import * as perf from 'mastodon/performance'; +import { me, reduceMotion } from 'mastodon/initial_state'; import ready from 'mastodon/ready'; import { store } from 'mastodon/store'; -import { isProduction, isDevelopment } from './utils/environment'; +import { + isProduction, + isDevelopment, + isModernEmojiEnabled, +} from './utils/environment'; function main() { perf.start('main()'); @@ -29,9 +33,9 @@ function main() { }); } - if (isFeatureEnabled('modern_emojis')) { + if (isModernEmojiEnabled()) { const { initializeEmoji } = await import('@/mastodon/features/emoji'); - await initializeEmoji(); + initializeEmoji(); } const root = createRoot(mountNode); diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index 75a5c09b9d8..3b0c41be818 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -45,7 +45,7 @@ const AccountRoleFactory = ImmutableRecord({ // Account export interface AccountShape extends Required< - Omit + Omit > { emojis: ImmutableList; fields: ImmutableList; @@ -55,6 +55,7 @@ export interface AccountShape note_plain: string | null; hidden: boolean; moved: string | null; + url: string; } export type Account = RecordOf; @@ -148,8 +149,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { note_emojified: emojify(accountNote, emojiMap), note_plain: unescapeHTML(accountNote), url: - accountJSON.url.startsWith('http://') || - accountJSON.url.startsWith('https://') + accountJSON.url?.startsWith('http://') || + accountJSON.url?.startsWith('https://') ? accountJSON.url : accountJSON.uri, }); diff --git a/app/javascript/mastodon/models/notification_group.ts b/app/javascript/mastodon/models/notification_group.ts index d98e755aa2c..9394cbbed88 100644 --- a/app/javascript/mastodon/models/notification_group.ts +++ b/app/javascript/mastodon/models/notification_group.ts @@ -36,6 +36,7 @@ export type NotificationGroupFavourite = export type NotificationGroupReblog = BaseNotificationWithStatus<'reblog'>; export type NotificationGroupStatus = BaseNotificationWithStatus<'status'>; export type NotificationGroupMention = BaseNotificationWithStatus<'mention'>; +export type NotificationGroupQuote = BaseNotificationWithStatus<'quote'>; export type NotificationGroupPoll = BaseNotificationWithStatus<'poll'>; export type NotificationGroupUpdate = BaseNotificationWithStatus<'update'>; export type NotificationGroupFollow = BaseNotification<'follow'>; @@ -87,6 +88,7 @@ export type NotificationGroup = | NotificationGroupReblog | NotificationGroupStatus | NotificationGroupMention + | NotificationGroupQuote | NotificationGroupPoll | NotificationGroupUpdate | NotificationGroupFollow @@ -137,6 +139,7 @@ export function createNotificationGroupFromJSON( case 'reblog': case 'status': case 'mention': + case 'quote': case 'poll': case 'update': { const { status_id: statusId, ...groupWithoutStatus } = group; @@ -209,6 +212,7 @@ export function createNotificationGroupFromNotificationJSON( case 'reblog': case 'status': case 'mention': + case 'quote': case 'poll': case 'update': return { diff --git a/app/javascript/mastodon/polyfills/index.ts b/app/javascript/mastodon/polyfills/index.ts index c001421c363..0ff0dd72690 100644 --- a/app/javascript/mastodon/polyfills/index.ts +++ b/app/javascript/mastodon/polyfills/index.ts @@ -20,5 +20,16 @@ export function loadPolyfills() { loadIntlPolyfills(), // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types needsExtraPolyfills && importExtraPolyfills(), + loadEmojiPolyfills(), ]); } + +// In the case of no /v support, rely on the emojibase data. +async function loadEmojiPolyfills() { + if (!('unicodeSets' in RegExp.prototype)) { + emojiRegexPolyfill = (await import('emojibase-regex/emoji')).default; + } +} + +// Null unless polyfill is needed. +export let emojiRegexPolyfill: RegExp | null = null; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 9e2ee5f55ab..cea8949f23c 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -36,6 +36,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: false, reblog: false, + quote: false, mention: false, poll: false, status: false, @@ -59,6 +60,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: true, reblog: true, + quote: true, mention: true, poll: true, status: true, @@ -72,6 +74,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: true, reblog: true, + quote: true, mention: true, poll: true, status: true, diff --git a/app/javascript/mastodon/selectors/notifications.ts b/app/javascript/mastodon/selectors/notifications.ts index ea640406ea1..8c808a2dffe 100644 --- a/app/javascript/mastodon/selectors/notifications.ts +++ b/app/javascript/mastodon/selectors/notifications.ts @@ -26,7 +26,10 @@ const filterNotificationsByAllowedTypes = ( ); } return notifications.filter( - (item) => item.type === 'gap' || allowedType === item.type, + (item) => + item.type === 'gap' || + allowedType === item.type || + (allowedType === 'mention' && item.type === 'quote'), ); }; diff --git a/app/javascript/mastodon/utils/__tests__/cache.test.ts b/app/javascript/mastodon/utils/__tests__/cache.test.ts new file mode 100644 index 00000000000..340a51fdb4b --- /dev/null +++ b/app/javascript/mastodon/utils/__tests__/cache.test.ts @@ -0,0 +1,78 @@ +import { createLimitedCache } from '../cache'; + +describe('createCache', () => { + test('returns expected methods', () => { + const actual = createLimitedCache(); + expect(actual).toBeTypeOf('object'); + expect(actual).toHaveProperty('get'); + expect(actual).toHaveProperty('has'); + expect(actual).toHaveProperty('delete'); + expect(actual).toHaveProperty('set'); + }); + + test('caches values provided to it', () => { + const cache = createLimitedCache(); + cache.set('test', 'result'); + expect(cache.get('test')).toBe('result'); + }); + + test('has returns expected values', () => { + const cache = createLimitedCache(); + cache.set('test', 'result'); + expect(cache.has('test')).toBeTruthy(); + expect(cache.has('not found')).toBeFalsy(); + }); + + test('updates a value if keys are the same', () => { + const cache = createLimitedCache(); + cache.set('test1', 1); + cache.set('test1', 2); + expect(cache.get('test1')).toBe(2); + }); + + test('delete removes an item', () => { + const cache = createLimitedCache(); + cache.set('test', 'result'); + expect(cache.has('test')).toBeTruthy(); + cache.delete('test'); + expect(cache.has('test')).toBeFalsy(); + expect(cache.get('test')).toBeUndefined(); + }); + + test('removes oldest item cached if it exceeds a set size', () => { + const cache = createLimitedCache({ maxSize: 1 }); + cache.set('test1', 1); + cache.set('test2', 2); + expect(cache.get('test1')).toBeUndefined(); + expect(cache.get('test2')).toBe(2); + }); + + test('retrieving a value bumps up last access', () => { + const cache = createLimitedCache({ maxSize: 2 }); + cache.set('test1', 1); + cache.set('test2', 2); + expect(cache.get('test1')).toBe(1); + cache.set('test3', 3); + expect(cache.get('test1')).toBe(1); + expect(cache.get('test2')).toBeUndefined(); + expect(cache.get('test3')).toBe(3); + }); + + test('logs when cache is added to and removed', () => { + const log = vi.fn(); + const cache = createLimitedCache({ maxSize: 1, log }); + cache.set('test1', 1); + expect(log).toHaveBeenLastCalledWith( + 'Added %s to cache, now size %d', + 'test1', + 1, + ); + cache.set('test2', 1); + expect(log).toHaveBeenLastCalledWith( + 'Added %s and deleted %s from cache, now size %d', + 'test2', + 'test1', + 1, + ); + }); +}); diff --git a/app/javascript/mastodon/utils/cache.ts b/app/javascript/mastodon/utils/cache.ts new file mode 100644 index 00000000000..2e3d21bfed4 --- /dev/null +++ b/app/javascript/mastodon/utils/cache.ts @@ -0,0 +1,60 @@ +export interface LimitedCache { + has: (key: CacheKey) => boolean; + get: (key: CacheKey) => CacheValue | undefined; + delete: (key: CacheKey) => void; + set: (key: CacheKey, value: CacheValue) => void; + clear: () => void; +} + +interface LimitedCacheArguments { + maxSize?: number; + log?: (...args: unknown[]) => void; +} + +export function createLimitedCache({ + maxSize = 100, + log = () => null, +}: LimitedCacheArguments = {}): LimitedCache { + const cacheMap = new Map(); + const cacheKeys = new Set(); + + function touchKey(key: CacheKey) { + if (cacheKeys.has(key)) { + cacheKeys.delete(key); + } + cacheKeys.add(key); + } + + return { + has: (key) => cacheMap.has(key), + get: (key) => { + if (cacheMap.has(key)) { + touchKey(key); + } + return cacheMap.get(key); + }, + delete: (key) => cacheMap.delete(key) && cacheKeys.delete(key), + set: (key, value) => { + cacheMap.set(key, value); + touchKey(key); + + const lastKey = cacheKeys.values().toArray().shift(); + if (cacheMap.size > maxSize && lastKey) { + cacheMap.delete(lastKey); + cacheKeys.delete(lastKey); + log( + 'Added %s and deleted %s from cache, now size %d', + key, + lastKey, + cacheMap.size, + ); + } else { + log('Added %s to cache, now size %d', key, cacheMap.size); + } + }, + clear: () => { + cacheMap.clear(); + cacheKeys.clear(); + }, + }; +} diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 5ccd4d27e3f..c5fe46bc931 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -1,3 +1,5 @@ +import initialState from '../initial_state'; + export function isDevelopment() { if (typeof process !== 'undefined') return process.env.NODE_ENV === 'development'; @@ -9,3 +11,20 @@ export function isProduction() { return process.env.NODE_ENV === 'production'; else return import.meta.env.PROD; } + +export type Features = 'modern_emojis'; + +export function isFeatureEnabled(feature: Features) { + return initialState?.features.includes(feature) ?? false; +} + +export function isModernEmojiEnabled() { + try { + return ( + isFeatureEnabled('modern_emojis') && + localStorage.getItem('experiments')?.split(',').includes('modern_emojis') + ); + } catch { + return false; + } +} diff --git a/app/javascript/mastodon/performance.js b/app/javascript/mastodon/utils/performance.ts similarity index 70% rename from app/javascript/mastodon/performance.js rename to app/javascript/mastodon/utils/performance.ts index 1b2092cfc4c..e503e1ef587 100644 --- a/app/javascript/mastodon/performance.js +++ b/app/javascript/mastodon/utils/performance.ts @@ -4,15 +4,15 @@ import * as marky from 'marky'; -import { isDevelopment } from './utils/environment'; +import { isDevelopment } from './environment'; -export function start(name) { +export function start(name: string) { if (isDevelopment()) { marky.mark(name); } } -export function stop(name) { +export function stop(name: string) { if (isDevelopment()) { marky.stop(name); } diff --git a/app/javascript/mastodon/utils/workers.ts b/app/javascript/mastodon/utils/workers.ts new file mode 100644 index 00000000000..02dd66d86e0 --- /dev/null +++ b/app/javascript/mastodon/utils/workers.ts @@ -0,0 +1,29 @@ +/** + * Loads Web Worker that is compatible with cross-origin scripts for CDNs. + * + * Returns null if the environment doesn't support web workers. + */ +export function loadWorker(url: string | URL, options: WorkerOptions = {}) { + if (!('Worker' in window)) { + return null; + } + + try { + // Check if the script origin and the window origin are the same. + const scriptUrl = new URL(import.meta.url); + if (location.origin === scriptUrl.origin) { + // Not cross-origin, can just load normally. + return new Worker(url, options); + } + } catch (err) { + // In case the URL parsing fails. + console.warn('Error instantiating Worker:', err); + } + + // Import the worker script from a same-origin Blob. + const contents = `import ${JSON.stringify(url)};`; + const blob = URL.createObjectURL( + new Blob([contents], { type: 'text/javascript' }), + ); + return new Worker(blob, options); +} diff --git a/app/javascript/material-icons/400-24px/format_quote-fill.svg b/app/javascript/material-icons/400-24px/format_quote-fill.svg new file mode 100644 index 00000000000..f4afa3ed17f --- /dev/null +++ b/app/javascript/material-icons/400-24px/format_quote-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/format_quote.svg b/app/javascript/material-icons/400-24px/format_quote.svg new file mode 100644 index 00000000000..c354385ea93 --- /dev/null +++ b/app/javascript/material-icons/400-24px/format_quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/supervised_user_circle_off-fill.svg b/app/javascript/material-icons/400-24px/supervised_user_circle_off-fill.svg new file mode 100644 index 00000000000..1daf50f858a --- /dev/null +++ b/app/javascript/material-icons/400-24px/supervised_user_circle_off-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/supervised_user_circle_off.svg b/app/javascript/material-icons/400-24px/supervised_user_circle_off.svg new file mode 100644 index 00000000000..060c515ae18 --- /dev/null +++ b/app/javascript/material-icons/400-24px/supervised_user_circle_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/styles/mastodon-light/css_variables.scss b/app/javascript/styles/mastodon-light/css_variables.scss index d9311da1b94..cfb98159271 100644 --- a/app/javascript/styles/mastodon-light/css_variables.scss +++ b/app/javascript/styles/mastodon-light/css_variables.scss @@ -12,6 +12,8 @@ body { --background-color: #fff; --background-color-tint: rgba(255, 255, 255, 80%); --background-filter: blur(10px); + --surface-variant-background-color: #f1ebfb; + --surface-border-color: #cac4d0; --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)}; --rich-text-container-color: rgba(255, 216, 231, 100%); --rich-text-text-color: rgba(114, 47, 83, 100%); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0fd97fb7129..d6f0087cc67 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1433,10 +1433,6 @@ body > [data-popper-placement] { } } -.status--has-quote .quote-inline { - display: none; -} - .status { padding: 16px; min-height: 54px; @@ -1470,10 +1466,6 @@ body > [data-popper-placement] { margin-top: 16px; } - &--is-quote { - border: none; - } - &--in-thread { --thread-margin: calc(46px + 8px); @@ -1860,79 +1852,99 @@ body > [data-popper-placement] { // --status-gutter-width is currently only set inside of // .notification-ungrouped, so everywhere else this will fall back // to the pixel values - --quote-margin: var(--status-gutter-width, 36px); + --quote-margin: var(--status-gutter-width); position: relative; margin-block-start: 16px; margin-inline-start: calc(var(--quote-margin) + var(--thread-margin, 0px)); - border-radius: 8px; + border-radius: 12px; color: var(--nested-card-text); - background: var(--nested-card-background); - border: var(--nested-card-border); - - @container (width > 460px) { - --quote-margin: var(--status-gutter-width, 56px); - } + border: 1px solid var(--surface-border-color); } .status__quote--error { + box-sizing: border-box; display: flex; align-items: center; + justify-content: space-between; gap: 8px; padding: 12px; - font-size: 15px; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + min-height: 56px; + + .link-button { + font-size: inherit; + line-height: inherit; + letter-spacing: inherit; + } } .status__quote-author-button { position: relative; overflow: hidden; - display: inline-flex; - width: auto; - margin-block-start: 10px; - padding: 5px 12px; + display: flex; + margin-top: 8px; + padding: 8px 12px; align-items: center; - gap: 6px; font-family: inherit; font-size: 14px; - font-weight: 700; - line-height: normal; - letter-spacing: 0; - text-decoration: none; - color: $highlight-text-color; - background: var(--nested-card-background); - border: var(--nested-card-border); - border-radius: 4px; - - &:active, - &:focus, - &:hover { - border-color: lighten($highlight-text-color, 4%); - color: lighten($highlight-text-color, 4%); - } - - &:focus-visible { - outline: $ui-button-icon-focus-outline; - } + font-weight: 400; + line-height: 20px; + letter-spacing: 0.25px; + color: $darker-text-color; + background: var(--surface-variant-background-color); + border-radius: 8px; + cursor: default; } -.status__quote-icon { - position: absolute; - inset-block-start: 18px; - inset-inline-start: -40px; - display: block; - width: 26px; - height: 26px; - padding: 5px; - color: #6a49ba; - z-index: 10; +.status--is-quote { + border: none; + padding: 12px; - .status__quote--error & { - inset-block-start: 50%; - transform: translateY(-50%); + .status__info { + padding-bottom: 8px; } - @container (width > 460px) { - inset-inline-start: -50px; + .display-name, + .status__relative-time { + font-size: 14px; + line-height: 20px; + letter-spacing: 0.1px; + } + + .display-name__account { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.5px; + } + + .status__content { + display: -webkit-box; + font-size: 14px; + letter-spacing: 0.25px; + line-height: 20px; + -webkit-line-clamp: 4; + line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + + p { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + } + } + + .media-gallery, + .video-player, + .audio-player, + .attachment-list, + .poll { + margin-top: 8px; } } @@ -2152,6 +2164,27 @@ body > [data-popper-placement] { } } +.learn-more__popout { + gap: 8px; + + &__content { + display: flex; + flex-direction: column; + gap: 4px; + } + + h6 { + font-size: inherit; + font-weight: 500; + line-height: inherit; + letter-spacing: 0.1px; + } + + .link-button { + font-weight: 500; + } +} + .account__wrapper { display: flex; gap: 10px; diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss index 431cdd7a8e8..16ed033b968 100644 --- a/app/javascript/styles/mastodon/css_variables.scss +++ b/app/javascript/styles/mastodon/css_variables.scss @@ -16,6 +16,7 @@ --surface-background-color: #{darken($ui-base-color, 4%)}; --surface-variant-background-color: #{$ui-base-color}; --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)}; + --surface-border-color: #{lighten($ui-base-color, 8%)}; --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)}; --avatar-border-radius: 8px; --media-outline-color: #{rgba(#fcf8ff, 0.15)}; diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index 5b2fbfe594e..cd5f72a06f0 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -1,4 +1,8 @@ import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships'; +import type { + CustomEmojiData, + UnicodeEmojiData, +} from '@/mastodon/features/emoji/types'; import { createAccountFromServerJSON } from '@/mastodon/models/account'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; @@ -68,3 +72,26 @@ export const relationshipsFactory: FactoryFunction = ({ showing_reblogs: true, ...data, }); + +export function unicodeEmojiFactory( + data: Partial = {}, +): UnicodeEmojiData { + return { + hexcode: 'test', + label: 'Test', + unicode: '🧪', + ...data, + }; +} + +export function customEmojiFactory( + data: Partial = {}, +): CustomEmojiData { + return { + shortcode: 'custom', + static_url: 'emoji/custom/static', + url: 'emoji/custom', + visible_in_picker: true, + ...data, + }; +} diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 93b45e80188..64ee9acd052 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -116,6 +116,20 @@ class ActivityPub::Activity fetch_remote_original_status end + def quote_from_request_json(json) + quoted_status_uri = value_or_id(json['object']) + quoting_status_uri = value_or_id(json['instrument']) + return if quoting_status_uri.nil? || quoted_status_uri.nil? + + quoting_status = status_from_uri(quoting_status_uri) + return unless quoting_status.present? && quoting_status.quote.present? + + quoted_status = status_from_uri(quoted_status_uri) + return unless quoted_status.present? && quoted_status.account == @account && quoting_status.quote.quoted_status == quoted_status + + quoting_status.quote + end + def dereference_object! return unless @object.is_a?(String) @@ -143,6 +157,10 @@ class ActivityPub::Activity @follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil? end + def quote_request_from_object + @quote_request_from_object ||= Quote.find_by(quoted_account: @account, activity_uri: object_uri) unless object_uri.nil? + end + def follow_from_object @follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil? end diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 5126e23c6a9..144ba9645c5 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -4,10 +4,13 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity def perform return accept_follow_for_relay if relay_follow? return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? + return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil? case @object['type'] when 'Follow' accept_embedded_follow + when 'QuoteRequest' + accept_embedded_quote_request end end @@ -31,6 +34,29 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow end + def accept_embedded_quote_request + approval_uri = value_or_id(first_of_value(@json['result'])) + return if approval_uri.nil? + + quote = quote_from_request_json(@object) + return unless quote.present? && quote.status.local? + + accept_quote!(quote) + end + + def accept_quote!(quote) + approval_uri = value_or_id(first_of_value(@json['result'])) + return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? + + # NOTE: we are not going through `ActivityPub::VerifyQuoteService` as the `Accept` is as authoritative + # as the stamp, but this means we are not checking the stamp, which may lead to inconsistencies + # in case of an implementation bug + quote.update!(state: :accepted, approval_uri: approval_uri) + + DistributionWorker.perform_async(quote.status_id, { 'update' => true }) + ActivityPub::StatusUpdateDistributionWorker.perform_async(quote.status_id, { 'updated_at' => Time.now.utc.iso8601 }) + end + def accept_follow_for_relay relay.update!(state: :accepted) end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index f7c723757ef..c47c2afc523 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -17,9 +17,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity? with_redis_lock("create:#{object_uri}") do - return if delete_arrived_first?(object_uri) || poll_vote? + Status.uncached do + return if delete_arrived_first?(object_uri) || poll_vote? - @status = find_existing_status + @status = find_existing_status + end if @status.nil? process_status @@ -64,6 +66,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity resolve_thread(@status) resolve_unresolved_mentions(@status) fetch_replies(@status) + fetch_and_verify_quote distribute forward_for_reply end @@ -204,11 +207,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity @quote.status = status @quote.save - - embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @json['context']) - ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: embedded_quote, request_id: @options[:request_id]) - rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS - ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] }) end def process_tags @@ -230,7 +228,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return if @quote_uri.blank? approval_uri = @status_parser.quote_approval_uri - approval_uri = nil if unsupported_uri_scheme?(approval_uri) + approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri) @quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?) end @@ -378,6 +376,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity Rails.logger.warn "Error fetching replies: #{e}" end + def fetch_and_verify_quote + return if @quote.nil? + + embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @json['context']) + ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: embedded_quote, request_id: @options[:request_id], depth: @options[:depth]) + rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS + ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] }) + end + def conversation_from_uri(uri) return nil if uri.nil? return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri) diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 69b7bd03546..ce36cfe763f 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -61,6 +61,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! @quote.reject! + DistributionWorker.perform_async(@quote.status_id, { 'update' => true }) end def forwarder diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 2de03df1580..088360ff981 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -9,21 +9,56 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity quoted_status = status_from_uri(object_uri) return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable? - # For now, we don't support being quoted by external servers - reject_quote_request!(quoted_status) + if Mastodon::Feature.outgoing_quotes_enabled? && StatusPolicy.new(@account, quoted_status).quote? + accept_quote_request!(quoted_status) + else + reject_quote_request!(quoted_status) + end end private + def accept_quote_request!(quoted_status) + status = status_from_uri(instrument_uri) + status ||= import_instrument(quoted_status) + status ||= ActivityPub::FetchRemoteStatusService.new.call(instrument_uri, on_behalf_of: quoted_status.account, request_id: @options[:request_id]) + # TODO: raise if status is nil + + # Sanity check + return unless status.quote.quoted_status == quoted_status && status.account == @account + + status.quote.ensure_quoted_access + status.quote.update!(activity_uri: @json['id']) + status.quote.accept! + + json = Oj.dump(serialize_payload(status.quote, ActivityPub::AcceptQuoteRequestSerializer)) + ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url) + end + + def import_instrument(quoted_status) + return unless @json['instrument'].is_a?(Hash) + + # NOTE: Replacing the object's context by that of the parent activity is + # not sound, but it's consistent with the rest of the codebase + instrument = @json['instrument'].merge({ '@context' => @json['@context'] }) + return if non_matching_uri_hosts?(instrument['id'], @account.uri) + + ActivityPub::FetchRemoteStatusService.new.call(instrument['id'], prefetched_body: instrument, on_behalf_of: quoted_status.account, request_id: @options[:request_id]) + end + def reject_quote_request!(quoted_status) quote = Quote.new( quoted_status: quoted_status, quoted_account: quoted_status.account, - status: Status.new(account: @account, uri: @json['instrument']), + status: Status.new(account: @account, uri: instrument_uri), account: @account, activity_uri: @json['id'] ) json = Oj.dump(serialize_payload(quote, ActivityPub::RejectQuoteRequestSerializer)) ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url) end + + def instrument_uri + value_or_id(@json['instrument']) + end end diff --git a/app/lib/activitypub/activity/reject.rb b/app/lib/activitypub/activity/reject.rb index 886dddb2355..3dafaba1882 100644 --- a/app/lib/activitypub/activity/reject.rb +++ b/app/lib/activitypub/activity/reject.rb @@ -5,10 +5,13 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity return reject_follow_for_relay if relay_follow? return follow_request_from_object.reject! unless follow_request_from_object.nil? return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil? + return reject_quote!(quote_request_from_object) unless quote_request_from_object.nil? case @object['type'] when 'Follow' reject_embedded_follow + when 'QuoteRequest' + reject_embedded_quote_request end end @@ -29,6 +32,20 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity relay.update!(state: :rejected) end + def reject_embedded_quote_request + quote = quote_from_request_json(@object) + return unless quote.present? && quote.status.local? + + reject_quote!(quoting_status.quote) + end + + def reject_quote!(quote) + return unless quote.quoted_account == @account && quote.status.local? + + # TODO: broadcast an update? + quote.reject! + end + def relay @relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil? end diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb index bf5de722103..b9e1d3a62b2 100644 --- a/app/lib/activitypub/case_transform.rb +++ b/app/lib/activitypub/case_transform.rb @@ -12,9 +12,7 @@ module ActivityPub::CaseTransform when Hash then value.deep_transform_keys! { |key| camel_lower(key) } when Symbol then camel_lower(value.to_s).to_sym when String - camel_lower_cache[value] ||= if value.start_with?('_:') - "_:#{value.delete_prefix('_:').underscore.camelize(:lower)}" - elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym) + camel_lower_cache[value] ||= if value.start_with?('_misskey') || LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym) value else value.underscore.camelize(:lower) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index ad3ef72be8a..5a434ed915a 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -152,9 +152,6 @@ class ActivityPub::Parser::StatusParser # Remove the special-meaning actor URI allowed_actors.delete(@options[:actor_uri]) - # Tagged users are always allowed, so remove them - allowed_actors -= as_array(@object['tag']).filter_map { |tag| tag['href'] if equals_or_includes?(tag['type'], 'Mention') } - # Any unrecognized actor is marked as unknown flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:unknown] unless allowed_actors.empty? diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 4d83a9b8238..975763e82fe 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -51,6 +51,13 @@ class ActivityPub::TagManager end end + def approval_uri_for(quote, check_approval: true) + return quote.approval_uri unless quote.quoted_account&.local? + return if check_approval && !quote.accepted? + + account_quote_authorization_url(quote.quoted_account, quote) + end + def key_uri_for(target) [uri_for(target), '#main-key'].join end diff --git a/app/lib/admin/metrics/dimension/base_dimension.rb b/app/lib/admin/metrics/dimension/base_dimension.rb index bd2e4ececbe..0e055e0e75e 100644 --- a/app/lib/admin/metrics/dimension/base_dimension.rb +++ b/app/lib/admin/metrics/dimension/base_dimension.rb @@ -65,4 +65,16 @@ class Admin::Metrics::Dimension::BaseDimension def canonicalized_params params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';') end + + def earliest_status_id + snowflake_id(@start_at.beginning_of_day) + end + + def latest_status_id + snowflake_id(@end_at.end_of_day) + end + + def snowflake_id(datetime) + Mastodon::Snowflake.id_at(datetime, with_random: false) + end end diff --git a/app/lib/admin/metrics/dimension/instance_languages_dimension.rb b/app/lib/admin/metrics/dimension/instance_languages_dimension.rb index 661e6d93b75..5f4bb95cb8f 100644 --- a/app/lib/admin/metrics/dimension/instance_languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/instance_languages_dimension.rb @@ -19,7 +19,7 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di end def sql_array - [sql_query_string, { domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + [sql_query_string, { domain: params[:domain], earliest_status_id:, latest_status_id:, limit: @limit }] end def sql_query_string @@ -36,14 +36,6 @@ class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Di SQL end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def params @params.permit(:domain) end diff --git a/app/lib/admin/metrics/dimension/servers_dimension.rb b/app/lib/admin/metrics/dimension/servers_dimension.rb index 2c8406d52fd..7e3ab603d03 100644 --- a/app/lib/admin/metrics/dimension/servers_dimension.rb +++ b/app/lib/admin/metrics/dimension/servers_dimension.rb @@ -14,7 +14,7 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B end def sql_array - [sql_query_string, { earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + [sql_query_string, { earliest_status_id:, latest_status_id:, limit: @limit }] end def sql_query_string @@ -28,12 +28,4 @@ class Admin::Metrics::Dimension::ServersDimension < Admin::Metrics::Dimension::B LIMIT :limit SQL end - - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end end diff --git a/app/lib/admin/metrics/dimension/space_usage_dimension.rb b/app/lib/admin/metrics/dimension/space_usage_dimension.rb index 3fd8d86856e..c03464ecaa9 100644 --- a/app/lib/admin/metrics/dimension/space_usage_dimension.rb +++ b/app/lib/admin/metrics/dimension/space_usage_dimension.rb @@ -40,7 +40,7 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension def media_size value = [ - MediaAttachment.sum(Arel.sql('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')), + MediaAttachment.sum(MediaAttachment.combined_media_file_size), CustomEmoji.sum(:image_file_size), PreviewCard.sum(:image_file_size), Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')), diff --git a/app/lib/admin/metrics/dimension/tag_languages_dimension.rb b/app/lib/admin/metrics/dimension/tag_languages_dimension.rb index 6e283d2c655..b7b9abc8b6f 100644 --- a/app/lib/admin/metrics/dimension/tag_languages_dimension.rb +++ b/app/lib/admin/metrics/dimension/tag_languages_dimension.rb @@ -19,7 +19,7 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi end def sql_array - [sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + [sql_query_string, { tag_id: tag_id, earliest_status_id:, latest_status_id:, limit: @limit }] end def sql_query_string @@ -39,14 +39,6 @@ class Admin::Metrics::Dimension::TagLanguagesDimension < Admin::Metrics::Dimensi params[:id] end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def params @params.permit(:id) end diff --git a/app/lib/admin/metrics/dimension/tag_servers_dimension.rb b/app/lib/admin/metrics/dimension/tag_servers_dimension.rb index db820e965c5..29145e14871 100644 --- a/app/lib/admin/metrics/dimension/tag_servers_dimension.rb +++ b/app/lib/admin/metrics/dimension/tag_servers_dimension.rb @@ -18,7 +18,7 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension end def sql_array - [sql_query_string, { tag_id: tag_id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id, limit: @limit }] + [sql_query_string, { tag_id: tag_id, earliest_status_id:, latest_status_id:, limit: @limit }] end def sql_query_string @@ -39,14 +39,6 @@ class Admin::Metrics::Dimension::TagServersDimension < Admin::Metrics::Dimension params[:id] end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def params @params.permit(:id) end diff --git a/app/lib/admin/metrics/measure/base_measure.rb b/app/lib/admin/metrics/measure/base_measure.rb index 8b7fe39b55a..eabbe0890b6 100644 --- a/app/lib/admin/metrics/measure/base_measure.rb +++ b/app/lib/admin/metrics/measure/base_measure.rb @@ -104,4 +104,16 @@ class Admin::Metrics::Measure::BaseMeasure def canonicalized_params params.to_h.to_a.sort_by { |k, _v| k.to_s }.map { |k, v| "#{k}=#{v}" }.join(';') end + + def earliest_status_id + snowflake_id(@start_at.beginning_of_day) + end + + def latest_status_id + snowflake_id(@end_at.end_of_day) + end + + def snowflake_id(datetime) + Mastodon::Snowflake.id_at(datetime, with_random: false) + end end diff --git a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb index 996ca52e0bb..00836191f1d 100644 --- a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb +++ b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb @@ -29,7 +29,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: def perform_total_query domain = params[:domain] domain = Instance.by_domain_and_subdomains(params[:domain]).select(:domain) if params[:include_subdomains] - MediaAttachment.joins(:account).merge(Account.where(domain: domain)).sum('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)') + MediaAttachment.joins(:account).merge(Account.where(domain: domain)).sum(MediaAttachment.combined_media_file_size) end def perform_previous_total_query @@ -44,7 +44,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: <<~SQL.squish SELECT axis.*, ( WITH new_media_attachments AS ( - SELECT COALESCE(media_attachments.file_file_size, 0) + COALESCE(media_attachments.thumbnail_file_size, 0) AS size + SELECT #{media_size_total} AS size FROM media_attachments INNER JOIN accounts ON accounts.id = media_attachments.account_id WHERE date_trunc('day', media_attachments.created_at)::date = axis.period @@ -58,6 +58,10 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: SQL end + def media_size_total + MediaAttachment.combined_media_file_size.to_sql + end + def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/instance_statuses_measure.rb b/app/lib/admin/metrics/measure/instance_statuses_measure.rb index 324d427b18b..f0f797876e6 100644 --- a/app/lib/admin/metrics/measure/instance_statuses_measure.rb +++ b/app/lib/admin/metrics/measure/instance_statuses_measure.rb @@ -28,7 +28,7 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure end def sql_array - [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain], earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }] + [sql_query_string, { start_at: @start_at, end_at: @end_at, domain: params[:domain], earliest_status_id:, latest_status_id: }] end def sql_query_string @@ -50,14 +50,6 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure SQL end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/tag_servers_measure.rb b/app/lib/admin/metrics/measure/tag_servers_measure.rb index 5db1076062b..e8d9cc43b8d 100644 --- a/app/lib/admin/metrics/measure/tag_servers_measure.rb +++ b/app/lib/admin/metrics/measure/tag_servers_measure.rb @@ -22,7 +22,7 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base end def sql_array - [sql_query_string, { start_at: @start_at, end_at: @end_at, tag_id: tag.id, earliest_status_id: earliest_status_id, latest_status_id: latest_status_id }] + [sql_query_string, { start_at: @start_at, end_at: @end_at, tag_id: tag.id, earliest_status_id:, latest_status_id: }] end def sql_query_string @@ -45,14 +45,6 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base SQL end - def earliest_status_id - Mastodon::Snowflake.id_at(@start_at.beginning_of_day, with_random: false) - end - - def latest_status_id - Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) - end - def tag @tag ||= Tag.find(params[:id]) end diff --git a/app/lib/antispam.rb b/app/lib/antispam.rb index 4ebf1924854..69c862a5c10 100644 --- a/app/lib/antispam.rb +++ b/app/lib/antispam.rb @@ -33,9 +33,7 @@ class Antispam end def local_preflight_check!(status) - return unless spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } - return unless suspicious_reply_or_mention?(status) - return unless status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago + return unless considered_spam?(status) report_if_needed!(status.account) @@ -44,10 +42,26 @@ class Antispam private + def considered_spam?(status) + (all_time_suspicious?(status) || recent_suspicious?(status)) && suspicious_reply_or_mention?(status) + end + + def all_time_suspicious?(status) + all_time_spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } + end + + def recent_suspicious?(status) + status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago && spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } + end + def spammy_texts redis.smembers('antispam:spammy_texts') end + def all_time_spammy_texts + redis.smembers('antispam:all_time_spammy_texts') + end + def suspicious_reply_or_mention?(status) parent = status.thread return true if parent.present? && !Follow.exists?(account_id: parent.account_id, target_account: status.account_id) diff --git a/app/lib/delivery_failure_tracker.rb b/app/lib/delivery_failure_tracker.rb index 96292923f42..619340a3d16 100644 --- a/app/lib/delivery_failure_tracker.rb +++ b/app/lib/delivery_failure_tracker.rb @@ -3,14 +3,18 @@ class DeliveryFailureTracker include Redisable - FAILURE_DAYS_THRESHOLD = 7 + FAILURE_THRESHOLDS = { + days: 7, + minutes: 5, + }.freeze - def initialize(url_or_host) + def initialize(url_or_host, resolution: :days) @host = url_or_host.start_with?('https://', 'http://') ? Addressable::URI.parse(url_or_host).normalized_host : url_or_host + @resolution = resolution end def track_failure! - redis.sadd(exhausted_deliveries_key, today) + redis.sadd(exhausted_deliveries_key, failure_time) UnavailableDomain.create(domain: @host) if reached_failure_threshold? end @@ -24,6 +28,12 @@ class DeliveryFailureTracker end def days + raise TypeError, 'resolution is not in days' unless @resolution == :days + + failures + end + + def failures redis.scard(exhausted_deliveries_key) || 0 end @@ -32,7 +42,7 @@ class DeliveryFailureTracker end def exhausted_deliveries_days - @exhausted_deliveries_days ||= redis.smembers(exhausted_deliveries_key).sort.map { |date| Date.new(date.slice(0, 4).to_i, date.slice(4, 2).to_i, date.slice(6, 2).to_i) } + @exhausted_deliveries_days ||= redis.smembers(exhausted_deliveries_key).sort.map { |date| Date.new(date.slice(0, 4).to_i, date.slice(4, 2).to_i, date.slice(6, 2).to_i) }.uniq end alias reset! track_success! @@ -89,11 +99,16 @@ class DeliveryFailureTracker "exhausted_deliveries:#{@host}" end - def today - Time.now.utc.strftime('%Y%m%d') + def failure_time + case @resolution + when :days + Time.now.utc.strftime('%Y%m%d') + when :minutes + Time.now.utc.strftime('%Y%m%d%H%M') + end end def reached_failure_threshold? - days >= FAILURE_DAYS_THRESHOLD + failures >= FAILURE_THRESHOLDS[@resolution] end end diff --git a/app/lib/fasp/request.rb b/app/lib/fasp/request.rb index 2002e90bb06..51950a004a2 100644 --- a/app/lib/fasp/request.rb +++ b/app/lib/fasp/request.rb @@ -32,8 +32,12 @@ class Fasp::Request .send(verb, url, body:) validate!(response) + @provider.delivery_failure_tracker.track_success! response.parse if response.body.present? + rescue *::Mastodon::HTTP_CONNECTION_ERRORS + @provider.delivery_failure_tracker.track_failure! + raise end def request_headers(_verb, _url, body = '') diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index 674945c4039..5260a723b31 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -71,6 +71,8 @@ class StatusCacheHydrator payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id) payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id payload[:filtered] = mapped_applied_custom_filter(account_id, status) + # TODO: performance optimization by not loading `Account` twice + payload[:quote_approval][:current_user] = status.quote_policy_for_account(Account.find_by(id: account_id)) if payload[:quote_approval] payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote] end diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index 5fb19643378..3ff04c4b69b 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -30,8 +30,10 @@ class StatusReachFinder [ replied_to_account_id, reblog_of_account_id, + quote_of_account_id, mentioned_account_ids, reblogs_account_ids, + quotes_account_ids, favourites_account_ids, replies_account_ids, ].tap do |arr| @@ -46,6 +48,10 @@ class StatusReachFinder @status.in_reply_to_account_id if distributable? end + def quote_of_account_id + @status.quote&.quoted_account_id + end + def reblog_of_account_id @status.reblog.account_id if @status.reblog? end @@ -54,6 +60,11 @@ class StatusReachFinder @status.mentions.pluck(:account_id) end + # Beware: Quotes can be created without the author having had access to the status + def quotes_account_ids + @status.quotes.pluck(:account_id) if distributable? || unsafe? + end + # Beware: Reblogs can be created without the author having had access to the status def reblogs_account_ids @status.reblogs.rewhere(deleted_at: [nil, @status.deleted_at]).pluck(:account_id) if distributable? || unsafe? diff --git a/app/lib/webfinger.rb b/app/lib/webfinger.rb index 83b5415a33f..c39c25e994a 100644 --- a/app/lib/webfinger.rb +++ b/app/lib/webfinger.rb @@ -84,22 +84,18 @@ class Webfinger def body_from_host_meta host_meta_request.perform do |res| - if res.code == 200 - body_from_webfinger(url_from_template(res.body_with_limit), use_fallback: false) - else - raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}" - end + raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}" unless res.code == 200 + + body_from_webfinger(url_from_template(res.body_with_limit), use_fallback: false) end end def url_from_template(str) link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]') - if link.present? - link['template'].gsub('{uri}', @uri) - else - raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger" - end + raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger" if link.blank? + + link['template'].gsub('{uri}', @uri) rescue Nokogiri::XML::XPath::SyntaxError raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}" end diff --git a/app/lib/webfinger_resource.rb b/app/lib/webfinger_resource.rb index e2027e164de..95de496a6d5 100644 --- a/app/lib/webfinger_resource.rb +++ b/app/lib/webfinger_resource.rb @@ -54,11 +54,9 @@ class WebfingerResource end def username_from_acct - if domain_matches_local? - local_username - else - raise ActiveRecord::RecordNotFound - end + raise ActiveRecord::RecordNotFound unless domain_matches_local? + + local_username end def split_acct diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index a20992dcb56..54dde1bb0dd 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -6,7 +6,7 @@ class NotificationMailer < ApplicationMailer :routing before_action :process_params - with_options only: %i(mention favourite reblog) do + with_options only: %i(mention favourite reblog quote) do before_action :set_status after_action :thread_by_conversation! end @@ -27,6 +27,14 @@ class NotificationMailer < ApplicationMailer end end + def quote + return if @status.blank? + + locale_for_account(@me) do + mail subject: default_i18n_subject(name: @status.account.acct) + end + end + def follow locale_for_account(@me) do mail subject: default_i18n_subject(name: @account.acct) diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb index 7bda388f2a8..6cdc7128ef4 100644 --- a/app/models/account_migration.rb +++ b/app/models/account_migration.rb @@ -74,7 +74,7 @@ class AccountMigration < ApplicationRecord errors.add(:acct, I18n.t('migrations.errors.not_found')) else errors.add(:acct, I18n.t('migrations.errors.missing_also_known_as')) unless target_account.also_known_as.include?(ActivityPub::TagManager.instance.uri_for(account)) - errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id + errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved? && account.moved_to_account_id == target_account.id errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id end end diff --git a/app/models/account_suggestions/friends_of_friends_source.rb b/app/models/account_suggestions/friends_of_friends_source.rb index 707c6ccaec2..d4accd2cea6 100644 --- a/app/models/account_suggestions/friends_of_friends_source.rb +++ b/app/models/account_suggestions/friends_of_friends_source.rb @@ -26,6 +26,7 @@ class AccountSuggestions::FriendsOfFriendsSource < AccountSuggestions::Source AND NOT EXISTS (SELECT 1 FROM mutes m WHERE m.target_account_id = follows.target_account_id AND m.account_id = :id) AND (accounts.domain IS NULL OR NOT EXISTS (SELECT 1 FROM account_domain_blocks b WHERE b.account_id = :id AND b.domain = accounts.domain)) AND NOT EXISTS (SELECT 1 FROM follows f WHERE f.target_account_id = follows.target_account_id AND f.account_id = :id) + AND NOT EXISTS (SELECT 1 FROM follow_requests f WHERE f.target_account_id = follows.target_account_id AND f.account_id = :id) AND follows.target_account_id <> :id AND accounts.discoverable AND accounts.suspended_at IS NULL diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index fd6b4289cee..a5bdd97420f 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -77,6 +77,9 @@ class Admin::ActionLogFilter update_user_role: { target_type: 'UserRole', action: 'update' }.freeze, update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze, unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze, + create_username_block: { target_type: 'UsernameBlock', action: 'create' }.freeze, + update_username_block: { target_type: 'UsernameBlock', action: 'update' }.freeze, + destroy_username_block: { target_type: 'UsernameBlock', action: 'destroy' }.freeze, }.freeze attr_reader :params diff --git a/app/models/announcement_reaction.rb b/app/models/announcement_reaction.rb index 46d9fc290ff..855dfc9e3a6 100644 --- a/app/models/announcement_reaction.rb +++ b/app/models/announcement_reaction.rb @@ -14,7 +14,7 @@ # class AnnouncementReaction < ApplicationRecord - before_validation :set_custom_emoji + before_validation :set_custom_emoji, if: :name? after_commit :queue_publish belongs_to :account @@ -27,7 +27,7 @@ class AnnouncementReaction < ApplicationRecord private def set_custom_emoji - self.custom_emoji = CustomEmoji.local.enabled.find_by(shortcode: name) if name.present? + self.custom_emoji = CustomEmoji.local.enabled.find_by(shortcode: name) end def queue_publish diff --git a/app/models/canonical_email_block.rb b/app/models/canonical_email_block.rb index d09df6f5e2a..4ed160fc262 100644 --- a/app/models/canonical_email_block.rb +++ b/app/models/canonical_email_block.rb @@ -12,24 +12,29 @@ # class CanonicalEmailBlock < ApplicationRecord - include EmailHelper + include CanonicalEmail include Paginable belongs_to :reference_account, class_name: 'Account', optional: true validates :canonical_email_hash, presence: true, uniqueness: true - scope :matching_email, ->(email) { where(canonical_email_hash: email_to_canonical_email_hash(email)) } + scope :matching_email, ->(email) { where(canonical_email_hash: digest(normalize_value_for(:email, email))) } + + def self.block?(email) + matching_email(email).exists? + end + + def self.digest(value) + Digest::SHA256.hexdigest(value) + end def to_log_human_identifier canonical_email_hash end def email=(email) - self.canonical_email_hash = email_to_canonical_email_hash(email) - end - - def self.block?(email) - matching_email(email).exists? + super + self.canonical_email_hash = self.class.digest(self.email) end end diff --git a/app/models/concerns/canonical_email.rb b/app/models/concerns/canonical_email.rb new file mode 100644 index 00000000000..bbc529ff085 --- /dev/null +++ b/app/models/concerns/canonical_email.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module CanonicalEmail + extend ActiveSupport::Concern + + included do + normalizes :email, with: ->(value) { canonicalize_email(value) } + end + + class_methods do + def canonicalize_email(email) + email + .downcase + .split('@', 2) + .then { |local, domain| [canonical_username(local), domain] } + .join('@') + end + + def canonical_username(username) + username + .to_s + .delete('.') + .split('+', 2) + .first + end + end +end diff --git a/app/models/concerns/rate_limitable.rb b/app/models/concerns/rate_limitable.rb index ad1b5e44e36..c6b5d3e0844 100644 --- a/app/models/concerns/rate_limitable.rb +++ b/app/models/concerns/rate_limitable.rb @@ -3,12 +3,8 @@ module RateLimitable extend ActiveSupport::Concern - def rate_limit=(value) - @rate_limit = value - end - - def rate_limit? - @rate_limit + included do + attribute :rate_limit, :boolean, default: false end def rate_limiter(by, options = {}) diff --git a/app/models/concerns/status/fetch_replies_concern.rb b/app/models/concerns/status/fetch_replies_concern.rb index cc117cb5ac6..7ab46481747 100644 --- a/app/models/concerns/status/fetch_replies_concern.rb +++ b/app/models/concerns/status/fetch_replies_concern.rb @@ -33,7 +33,7 @@ module Status::FetchRepliesConcern def should_fetch_replies? # we aren't brand new, and we haven't fetched replies since the debounce window - !local? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && ( + !local? && distributable? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && ( fetched_replies_at.nil? || fetched_replies_at <= FETCH_REPLIES_COOLDOWN_MINUTES.ago ) end diff --git a/app/models/concerns/status/interaction_policy_concern.rb b/app/models/concerns/status/interaction_policy_concern.rb new file mode 100644 index 00000000000..6ad047fd8d5 --- /dev/null +++ b/app/models/concerns/status/interaction_policy_concern.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Status::InteractionPolicyConcern + extend ActiveSupport::Concern + + QUOTE_APPROVAL_POLICY_FLAGS = { + unknown: (1 << 0), + public: (1 << 1), + followers: (1 << 2), + followed: (1 << 3), + }.freeze + + def quote_policy_as_keys(kind) + case kind + when :automatic + policy = quote_approval_policy >> 16 + when :manual + policy = quote_approval_policy & 0xFFFF + end + + QUOTE_APPROVAL_POLICY_FLAGS.keys.select { |key| policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[key]) }.map(&:to_s) + end + + # Returns `:automatic`, `:manual`, `:unknown` or `:denied` + def quote_policy_for_account(other_account, preloaded_relations: {}) + return :denied if other_account.nil? + + following_author = nil + + # Post author is always allowed to quote themselves + return :automatic if account_id == other_account.id + + automatic_policy = quote_approval_policy >> 16 + manual_policy = quote_approval_policy & 0xFFFF + + return :automatic if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public]) + + if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers]) + following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil? + return :automatic if following_author + end + + # We don't know we are allowed by the automatic policy, considering the manual one + return :manual if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public]) + + if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers]) + following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil? + return :manual if following_author + end + + return :unknown if (automatic_policy | manual_policy).anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:unknown]) + + :denied + end +end diff --git a/app/models/concerns/user/activity.rb b/app/models/concerns/user/activity.rb new file mode 100644 index 00000000000..df2713415b3 --- /dev/null +++ b/app/models/concerns/user/activity.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module User::Activity + extend ActiveSupport::Concern + + # The home and list feeds will be stored for this amount of time, and status + # fan-out to followers will include only people active within this time frame. + # + # Lowering the duration may improve performance if many people sign up, but + # most will not check their feed every day. Raising the duration reduces the + # amount of background processing that happens when people become active. + ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days + + included do + scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) } + scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) } + end + + def signed_in_recently? + current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago + end + + private + + def inactive_since_duration? + last_sign_in_at < ACTIVE_DURATION.ago + end +end diff --git a/app/models/concerns/user/confirmation.rb b/app/models/concerns/user/confirmation.rb new file mode 100644 index 00000000000..46fdb0210a5 --- /dev/null +++ b/app/models/concerns/user/confirmation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module User::Confirmation + extend ActiveSupport::Concern + + included do + scope :confirmed, -> { where.not(confirmed_at: nil) } + scope :unconfirmed, -> { where(confirmed_at: nil) } + + def confirm + wrap_email_confirmation { super } + end + end + + def confirmed? + confirmed_at.present? + end + + def unconfirmed? + !confirmed? + end +end diff --git a/app/models/fasp/provider.rb b/app/models/fasp/provider.rb index 37d0b581ca1..9f7be482fed 100644 --- a/app/models/fasp/provider.rb +++ b/app/models/fasp/provider.rb @@ -118,6 +118,10 @@ class Fasp::Provider < ApplicationRecord save! end + def delivery_failure_tracker + @delivery_failure_tracker ||= DeliveryFailureTracker.new(base_url, resolution: :minutes) + end + private def create_keypair diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 964d4e279a1..0b518036b10 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -37,7 +37,7 @@ class FollowRequest < ApplicationRecord if account.local? ListAccount.where(follow_request: self).update_all(follow_request_id: nil, follow_id: follow.id) MergeWorker.perform_async(target_account.id, account.id, 'home') - MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id| + MergeWorker.push_bulk(account.owned_lists.with_list_account(target_account).pluck(:id)) do |list_id| [target_account.id, list_id, 'list'] end end diff --git a/app/models/form/redirect.rb b/app/models/form/redirect.rb index c5b3c1f8f39..6ab95f21f1f 100644 --- a/app/models/form/redirect.rb +++ b/app/models/form/redirect.rb @@ -40,7 +40,7 @@ class Form::Redirect if target_account.nil? errors.add(:acct, I18n.t('migrations.errors.not_found')) else - errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id + errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved? && account.moved_to_account_id == target_account.id errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id end end diff --git a/app/models/form/username_block_batch.rb b/app/models/form/username_block_batch.rb new file mode 100644 index 00000000000..f490391159a --- /dev/null +++ b/app/models/form/username_block_batch.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Form::UsernameBlockBatch < Form::BaseBatch + attr_accessor :username_block_ids + + def save + case action + when 'delete' + delete! + end + end + + private + + def username_blocks + @username_blocks ||= UsernameBlock.where(id: username_block_ids) + end + + def delete! + verify_authorization(:destroy?) + + username_blocks.each do |username_block| + username_block.destroy + log_action :destroy, username_block + end + end + + def verify_authorization(permission) + username_blocks.each { |username_block| authorize(username_block, permission) } + end +end diff --git a/app/models/list.rb b/app/models/list.rb index 76c116ce244..8fd1953ab31 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -32,6 +32,8 @@ class List < ApplicationRecord before_destroy :clean_feed_manager + scope :with_list_account, ->(account) { joins(:list_accounts).where(list_accounts: { account: }) } + private def validate_account_lists_limit diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 2e60f732b81..13ca0d7e3ab 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -298,6 +298,10 @@ class MediaAttachment < ApplicationRecord IMAGE_FILE_EXTENSIONS + VIDEO_FILE_EXTENSIONS + AUDIO_FILE_EXTENSIONS end + def combined_media_file_size + arel_table.coalesce(arel_table[:file_file_size], 0) + arel_table.coalesce(arel_table[:thumbnail_file_size], 0) + end + private def file_styles(attachment) diff --git a/app/models/notification.rb b/app/models/notification.rb index e7ada3399aa..acef474a591 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -30,6 +30,7 @@ class Notification < ApplicationRecord 'FollowRequest' => :follow_request, 'Favourite' => :favourite, 'Poll' => :poll, + 'Quote' => :quote, }.freeze # Please update app/javascript/api_types/notification.ts if you change this @@ -73,6 +74,9 @@ class Notification < ApplicationRecord 'admin.report': { filterable: false, }.freeze, + quote: { + filterable: true, + }.freeze, }.freeze TYPES = PROPERTIES.keys.freeze @@ -81,6 +85,7 @@ class Notification < ApplicationRecord status: :status, reblog: [status: :reblog], mention: [mention: :status], + quote: [quote: :status], favourite: [favourite: :status], poll: [poll: :status], update: :status, @@ -102,6 +107,7 @@ class Notification < ApplicationRecord belongs_to :account_relationship_severance_event, inverse_of: false belongs_to :account_warning, inverse_of: false belongs_to :generated_annual_report, inverse_of: false + belongs_to :quote, inverse_of: :notification end validates :type, inclusion: { in: TYPES } @@ -122,6 +128,8 @@ class Notification < ApplicationRecord favourite&.status when :mention mention&.status + when :quote + quote&.status when :poll poll&.status end @@ -174,6 +182,8 @@ class Notification < ApplicationRecord notification.mention.status = cached_status when :poll notification.poll.status = cached_status + when :quote + notification.quote.status = cached_status end end @@ -192,7 +202,7 @@ class Notification < ApplicationRecord return unless new_record? case activity_type - when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report' + when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report', 'Quote' self.from_account_id = activity&.account_id when 'Mention' self.from_account_id = activity&.status&.account_id diff --git a/app/models/notification_request.rb b/app/models/notification_request.rb index eb9ff93ab72..d95fb58b476 100644 --- a/app/models/notification_request.rb +++ b/app/models/notification_request.rb @@ -49,6 +49,6 @@ class NotificationRequest < ApplicationRecord private def prepare_notifications_count - self.notifications_count = Notification.where(account: account, from_account: from_account, type: :mention, filtered: true).limit(MAX_MEANINGFUL_COUNT).count + self.notifications_count = Notification.where(account: account, from_account: from_account, type: [:mention, :quote], filtered: true).limit(MAX_MEANINGFUL_COUNT).count end end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index 56fe4836355..8e0e13cdb94 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -170,10 +170,9 @@ class PreviewCard < ApplicationRecord private def serialized_authors - if author_name? || author_url? || author_account_id? - PreviewCard::Author - .new(self) - end + return unless author_name? || author_url? || author_account_id? + + PreviewCard::Author.new(self) end def extract_dimensions diff --git a/app/models/quote.rb b/app/models/quote.rb index 89845ed9f49..fea812924cc 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -17,6 +17,10 @@ # status_id :bigint(8) not null # class Quote < ApplicationRecord + include Paginable + + has_one :notification, as: :activity, dependent: :destroy + BACKGROUND_REFRESH_INTERVAL = 1.week.freeze REFRESH_DEADLINE = 6.hours @@ -33,6 +37,7 @@ class Quote < ApplicationRecord before_validation :set_accounts before_validation :set_activity_uri, only: :create, if: -> { account.local? && quoted_account&.remote? } validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? } + validates :approval_uri, absence: true, if: -> { quoted_account&.local? } validate :validate_visibility def accept! @@ -51,6 +56,12 @@ class Quote < ApplicationRecord accepted? || !legacy? end + def ensure_quoted_access + status.mentions.create!(account: quoted_account, silent: true) + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique + nil + end + def schedule_refresh_if_stale! return unless quoted_status_id.present? && approval_uri.present? && updated_at <= BACKGROUND_REFRESH_INTERVAL.ago diff --git a/app/models/status.rb b/app/models/status.rb index e6e9450264b..c99a1f8df56 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -43,16 +43,10 @@ class Status < ApplicationRecord include Status::SnapshotConcern include Status::ThreadingConcern include Status::Visibility + include Status::InteractionPolicyConcern MEDIA_ATTACHMENTS_LIMIT = 4 - QUOTE_APPROVAL_POLICY_FLAGS = { - unknown: (1 << 0), - public: (1 << 1), - followers: (1 << 2), - followed: (1 << 3), - }.freeze - rate_limit by: :account, family: :statuses self.discard_column = :deleted_at @@ -84,6 +78,7 @@ class Status < ApplicationRecord has_many :mentions, dependent: :destroy, inverse_of: :status has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account' has_many :media_attachments, dependent: :nullify + has_many :quotes, foreign_key: 'quoted_status_id', inverse_of: :quoted_status, dependent: :nullify # The `dependent` option is enabled by the initial `mentions` association declaration has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent diff --git a/app/models/tag.rb b/app/models/tag.rb index 8e21ddca82b..dff10111123 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -164,9 +164,10 @@ class Tag < ApplicationRecord end def validate_display_name_change - unless HashtagNormalizer.new.normalize(display_name).casecmp(name).zero? - errors.add(:display_name, - I18n.t('tags.does_not_match_previous_name')) - end + errors.add(:display_name, I18n.t('tags.does_not_match_previous_name')) unless display_name_matches_name? + end + + def display_name_matches_name? + HashtagNormalizer.new.normalize(display_name).casecmp(name).zero? end end diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb index c85b0170fd7..e64369b08be 100644 --- a/app/models/trends/statuses.rb +++ b/app/models/trends/statuses.rb @@ -90,14 +90,28 @@ class Trends::Statuses < Trends::Base def eligible?(status) status.created_at.past? && - status.public_visibility? && - status.account.discoverable? && - !status.account.silenced? && - !status.account.sensitized? && - status.spoiler_text.blank? && - !status.sensitive? && + opted_into_trends?(status) && + !sensitive_content?(status) && !status.reply? && - valid_locale?(status.language) + valid_locale?(status.language) && + (status.quote.nil? || trendable_quote?(status.quote)) + end + + def opted_into_trends?(status) + status.public_visibility? && + status.account.discoverable? && + !status.account.silenced? + end + + def sensitive_content?(status) + status.account.sensitized? || status.spoiler_text.present? || status.sensitive? + end + + def trendable_quote?(quote) + quote.acceptable? && + quote.quoted_status.present? && + opted_into_trends?(quote.quoted_status) && + !sensitive_content?(quote.quoted_status) end def calculate_scores(statuses, at_time) diff --git a/app/models/user.rb b/app/models/user.rb index 762522f2822..aca72daff04 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -58,20 +58,13 @@ class User < ApplicationRecord include LanguagesHelper include Redisable + include User::Activity + include User::Confirmation include User::HasSettings include User::LdapAuthenticable include User::Omniauthable include User::PamAuthenticable - # The home and list feeds will be stored in Redis for this amount - # of time, and status fan-out to followers will include only people - # within this time frame. Lowering the duration may improve performance - # if lots of people sign up, but not a lot of them check their feed - # every day. Raising the duration reduces the amount of expensive - # RegenerationWorker jobs that need to be run when those people come - # to check their feed - ACTIVE_DURATION = ENV.fetch('USER_ACTIVE_DAYS', 7).to_i.days.freeze - devise :two_factor_authenticatable, otp_secret_length: 32 @@ -118,13 +111,9 @@ class User < ApplicationRecord scope :recent, -> { order(id: :desc) } scope :pending, -> { where(approved: false) } scope :approved, -> { where(approved: true) } - scope :confirmed, -> { where.not(confirmed_at: nil) } - scope :unconfirmed, -> { where(confirmed_at: nil) } scope :enabled, -> { where(disabled: false) } scope :disabled, -> { where(disabled: true) } scope :active, -> { confirmed.signed_in_recently.account_not_suspended } - scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) } - scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } scope :matches_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group(users: [:id]) } @@ -143,7 +132,10 @@ class User < ApplicationRecord delegate :can?, to: :role attr_reader :invite_code, :date_of_birth - attr_writer :external, :bypass_registration_checks, :current_account + attr_writer :current_account + + attribute :external, :boolean, default: false + attribute :bypass_registration_checks, :boolean, default: false def self.those_who_can(*any_of_privileges) matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id) @@ -178,14 +170,6 @@ class User < ApplicationRecord end end - def signed_in_recently? - current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago - end - - def confirmed? - confirmed_at.present? - end - def invited? invite_id.present? end @@ -210,12 +194,6 @@ class User < ApplicationRecord account_id end - def confirm - wrap_email_confirmation do - super - end - end - # Mark current email as confirmed, bypassing Devise def mark_email_as_confirmed! wrap_email_confirmation do @@ -231,16 +209,11 @@ class User < ApplicationRecord end def update_sign_in!(new_sign_in: false) - old_current = current_sign_in_at new_current = Time.now.utc - - self.last_sign_in_at = old_current || new_current + self.last_sign_in_at = current_sign_in_at || new_current self.current_sign_in_at = new_current - if new_sign_in - self.sign_in_count ||= 0 - self.sign_in_count += 1 - end + increment(:sign_in_count) if new_sign_in save(validate: false) unless new_record? prepare_returning_user! @@ -262,10 +235,6 @@ class User < ApplicationRecord confirmed? && approved? && !disabled? && !account.unavailable? && !account.memorial? end - def unconfirmed? - !confirmed? - end - def unconfirmed_or_pending? unconfirmed? || pending? end @@ -443,7 +412,7 @@ class User < ApplicationRecord def set_approved self.approved = begin - if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval? + if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval? || sign_up_username_requires_approval? false else open_registrations? || valid_invitation? || external? @@ -466,16 +435,17 @@ class User < ApplicationRecord yield - if new_user - # Avoid extremely unlikely race condition when approving and confirming - # the user at the same time - reload unless approved? + after_confirmation_tasks if new_user + end - if approved? - prepare_new_user! - else - notify_staff_about_pending_account! - end + def after_confirmation_tasks + # Handle condition when approving and confirming a user at the same time + reload unless approved? + + if approved? + prepare_new_user! + else + notify_staff_about_pending_account! end end @@ -498,18 +468,14 @@ class User < ApplicationRecord EmailDomainBlock.requires_approval?(records + [domain], attempt_ip: sign_up_ip) end + def sign_up_username_requires_approval? + account.username? && UsernameBlock.matches?(account.username, allow_with_approval: true) + end + def open_registrations? Setting.registrations_mode == 'open' end - def external? - !!@external - end - - def bypass_registration_checks? - @bypass_registration_checks - end - def sanitize_role self.role = nil if role.present? && role.everyone? end @@ -526,7 +492,7 @@ class User < ApplicationRecord return unless confirmed? ActivityTracker.record('activity:logins', id) - regenerate_feed! if needs_feed_update? + regenerate_feed! if inactive_since_duration? end def notify_staff_about_pending_account! @@ -539,14 +505,10 @@ class User < ApplicationRecord def regenerate_feed! home_feed = HomeFeed.new(account) - unless home_feed.regenerating? - home_feed.regeneration_in_progress! - RegenerationWorker.perform_async(account_id) - end - end + return if home_feed.regenerating? - def needs_feed_update? - last_sign_in_at < ACTIVE_DURATION.ago + home_feed.regeneration_in_progress! + RegenerationWorker.perform_async(account_id) end def validate_email_dns? diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index fd8659dc970..5558ffe04a4 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -43,6 +43,7 @@ class UserSettings setting :reblog, default: false setting :favourite, default: false setting :mention, default: true + setting :quote, default: true setting :follow_request, default: true setting :report, default: true setting :pending_account, default: true diff --git a/app/models/username_block.rb b/app/models/username_block.rb new file mode 100644 index 00000000000..33169a47f82 --- /dev/null +++ b/app/models/username_block.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: username_blocks +# +# id :bigint(8) not null, primary key +# allow_with_approval :boolean default(FALSE), not null +# exact :boolean default(FALSE), not null +# normalized_username :string not null +# username :string not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class UsernameBlock < ApplicationRecord + HOMOGLYPHS = { + '1' => 'i', + '2' => 'z', + '3' => 'e', + '4' => 'a', + '5' => 's', + '7' => 't', + '8' => 'b', + '9' => 'g', + '0' => 'o', + }.freeze + + validates :username, presence: true, uniqueness: true + + scope :matches_exactly, ->(str) { where(exact: true).where(normalized_username: str) } + scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(normalize_value_for(:normalized_username, str)).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) } + + before_save :set_normalized_username + + normalizes :normalized_username, with: ->(value) { value.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS) } + + def comparison + exact? ? 'equals' : 'contains' + end + + def comparison=(val) + self.exact = val == 'equals' + end + + def self.matches?(str, allow_with_approval: false) + matches_exactly(str).or(matches_partially(str)).where(allow_with_approval: allow_with_approval).any? + end + + def to_log_human_identifier + username + end + + private + + def set_normalized_username + self.normalized_username = username + end +end diff --git a/app/models/worker_batch.rb b/app/models/worker_batch.rb index f741071ba95..ab9d8d457b2 100644 --- a/app/models/worker_batch.rb +++ b/app/models/worker_batch.rb @@ -19,19 +19,21 @@ class WorkerBatch redis.hset(key, { 'async_refresh_key' => async_refresh_key, 'threshold' => threshold }) end + def within + raise NoBlockGivenError unless block_given? + + begin + Thread.current[:batch] = self + yield(self) + ensure + Thread.current[:batch] = nil + end + end + # Add jobs to the batch. Usually when the batch is created. # @param [Array] jids def add_jobs(jids) - if jids.blank? - async_refresh_key = redis.hget(key, 'async_refresh_key') - - if async_refresh_key.present? - async_refresh = AsyncRefresh.new(async_refresh_key) - async_refresh.finish! - end - - return - end + return if jids.empty? redis.multi do |pipeline| pipeline.sadd(key('jobs'), jids) @@ -43,7 +45,7 @@ class WorkerBatch # Remove a job from the batch, such as when it's been processed or it has failed. # @param [String] jid - def remove_job(jid) + def remove_job(jid, increment: false) _, pending, processed, async_refresh_key, threshold = redis.multi do |pipeline| pipeline.srem(key('jobs'), jid) pipeline.hincrby(key, 'pending', -1) @@ -52,11 +54,24 @@ class WorkerBatch pipeline.hget(key, 'threshold') end + async_refresh = AsyncRefresh.new(async_refresh_key) if async_refresh_key.present? + async_refresh&.increment_result_count(by: 1) if increment + + if pending.zero? || processed >= (threshold || 1.0).to_f * (processed + pending) + async_refresh&.finish! + cleanup + end + end + + def finish! + async_refresh_key = redis.hget(key, 'async_refresh_key') + if async_refresh_key.present? async_refresh = AsyncRefresh.new(async_refresh_key) - async_refresh.increment_result_count(by: 1) - async_refresh.finish! if pending.zero? || processed >= threshold.to_f * (processed + pending) + async_refresh.finish! end + + cleanup end # Get pending jobs. @@ -76,4 +91,8 @@ class WorkerBatch def key(suffix = nil) "worker_batch:#{@id}#{":#{suffix}" if suffix}" end + + def cleanup + redis.del(key, key('jobs')) + end end diff --git a/app/policies/quote_policy.rb b/app/policies/quote_policy.rb new file mode 100644 index 00000000000..a8be64a7792 --- /dev/null +++ b/app/policies/quote_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class QuotePolicy < ApplicationPolicy + def revoke? + record.quoted_account_id.present? && record.quoted_account_id == current_account&.id + end +end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 540e266427f..b7463869959 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -19,6 +19,11 @@ class StatusPolicy < ApplicationPolicy end end + # This is about requesting a quote post, not validating it + def quote? + show? && record.quote_policy_for_account(current_account, preloaded_relations: @preloaded_relations) != :denied + end + def reblog? !requires_mention? && (!private? || owned?) && show? && !blocking_author? end @@ -31,6 +36,10 @@ class StatusPolicy < ApplicationPolicy owned? end + def list_quotes? + owned? + end + alias unreblog? destroy? def update? diff --git a/app/policies/username_block_policy.rb b/app/policies/username_block_policy.rb new file mode 100644 index 00000000000..9f6b8cfc018 --- /dev/null +++ b/app/policies/username_block_policy.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class UsernameBlockPolicy < ApplicationPolicy + def index? + role.can?(:manage_blocks) + end + + def create? + role.can?(:manage_blocks) + end + + def update? + role.can?(:manage_blocks) + end + + def destroy? + role.can?(:manage_blocks) + end +end diff --git a/app/serializers/activitypub/accept_quote_request_serializer.rb b/app/serializers/activitypub/accept_quote_request_serializer.rb new file mode 100644 index 00000000000..26ecd099b64 --- /dev/null +++ b/app/serializers/activitypub/accept_quote_request_serializer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class ActivityPub::AcceptQuoteRequestSerializer < ActivityPub::Serializer + attributes :id, :type, :actor, :result + + has_one :object, serializer: ActivityPub::QuoteRequestSerializer + + def id + [ActivityPub::TagManager.instance.uri_for(object.quoted_account), '#accepts/quote_requests/', object.id].join + end + + def type + 'Accept' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.quoted_account) + end + + def result + ActivityPub::TagManager.instance.approval_uri_for(object) + end +end diff --git a/app/serializers/activitypub/delete_quote_authorization_serializer.rb b/app/serializers/activitypub/delete_quote_authorization_serializer.rb new file mode 100644 index 00000000000..150f2a4554b --- /dev/null +++ b/app/serializers/activitypub/delete_quote_authorization_serializer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class ActivityPub::DeleteQuoteAuthorizationSerializer < ActivityPub::Serializer + attributes :id, :type, :actor, :to + + # TODO: change the `object` to a `QuoteAuthorization` object instead of just the URI? + attribute :virtual_object, key: :object + + def id + [ActivityPub::TagManager.instance.approval_uri_for(object, check_approval: false), '#delete'].join + end + + def virtual_object + ActivityPub::TagManager.instance.approval_uri_for(object, check_approval: false) + end + + def type + 'Delete' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.quoted_account) + end + + def to + [ActivityPub::TagManager::COLLECTIONS[:public]] + end +end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 7b29e6d69be..05e3cf1bd21 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,7 +3,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer include FormattingHelper - context_extensions :atom_uri, :conversation, :sensitive, :voters_count + context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes, :interaction_policies attributes :id, :type, :summary, :in_reply_to, :published, :url, @@ -30,6 +30,13 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attribute :voters_count, if: :poll_and_voters_count? + attribute :quote, if: :quote? + attribute :quote, key: :_misskey_quote, if: :quote? + attribute :quote, key: :quote_uri, if: :quote? + attribute :quote_authorization, if: :quote_authorization? + + attribute :interaction_policy, if: -> { Mastodon::Feature.outgoing_quotes_enabled? } + def id ActivityPub::TagManager.instance.uri_for(object) end @@ -194,6 +201,40 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer object.preloadable_poll&.voters_count end + def quote? + object.quote&.present? + end + + def quote_authorization? + object.quote.present? && ActivityPub::TagManager.instance.approval_uri_for(object.quote).present? + end + + def quote + # TODO: handle inlining self-quotes + ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status) + end + + def quote_authorization + ActivityPub::TagManager.instance.approval_uri_for(object.quote) + end + + def interaction_policy + approved_uris = [] + + # On outgoing posts, only automatic approval is supported + policy = object.quote_approval_policy >> 16 + approved_uris << ActivityPub::TagManager::COLLECTIONS[:public] if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]) + approved_uris << ActivityPub::TagManager.instance.followers_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]) + approved_uris << ActivityPub::TagManager.instance.following_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followed]) + approved_uris << ActivityPub::TagManager.instance.uri_for(object.account) if approved_uris.empty? + + { + canQuote: { + automaticApproval: approved_uris, + }, + } + end + class MediaAttachmentSerializer < ActivityPub::Serializer context_extensions :blurhash, :focal_point diff --git a/app/serializers/activitypub/quote_authorization_serializer.rb b/app/serializers/activitypub/quote_authorization_serializer.rb new file mode 100644 index 00000000000..faef3dd6866 --- /dev/null +++ b/app/serializers/activitypub/quote_authorization_serializer.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class ActivityPub::QuoteAuthorizationSerializer < ActivityPub::Serializer + include RoutingHelper + + context_extensions :quote_authorizations + + attributes :id, :type, :attributed_to, :interacting_object, :interaction_target + + def id + ActivityPub::TagManager.instance.approval_uri_for(object) + end + + def type + 'QuoteAuthorization' + end + + def attributed_to + ActivityPub::TagManager.instance.uri_for(object.quoted_account) + end + + def interaction_target + ActivityPub::TagManager.instance.uri_for(object.quoted_status) + end + + def interacting_object + ActivityPub::TagManager.instance.uri_for(object.status) + end +end diff --git a/app/serializers/activitypub/quote_request_serializer.rb b/app/serializers/activitypub/quote_request_serializer.rb index 840b653a1c7..e6e4bb9b036 100644 --- a/app/serializers/activitypub/quote_request_serializer.rb +++ b/app/serializers/activitypub/quote_request_serializer.rb @@ -1,11 +1,22 @@ # frozen_string_literal: true class ActivityPub::QuoteRequestSerializer < ActivityPub::Serializer + def self.serializer_for(model, options) + case model.class.name + when 'Status' + ActivityPub::NoteSerializer + else + super + end + end + context_extensions :quote_requests - attributes :id, :type, :actor, :instrument + attributes :id, :type, :actor attribute :virtual_object, key: :object + has_one :instrument + def id object.activity_uri end @@ -23,6 +34,6 @@ class ActivityPub::QuoteRequestSerializer < ActivityPub::Serializer end def instrument - ActivityPub::TagManager.instance.uri_for(object.status) + instance_options[:allow_post_inlining] && object.status.local? ? object.status : ActivityPub::TagManager.instance.uri_for(object.status) end end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 354d384464d..b102f79fdb9 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -65,7 +65,7 @@ class REST::AccountSerializer < ActiveModel::Serializer end def url - ActivityPub::TagManager.instance.url_for(object) + ActivityPub::TagManager.instance.url_for(object) || ActivityPub::TagManager.instance.uri_for(object) end def uri diff --git a/app/serializers/rest/notification_group_serializer.rb b/app/serializers/rest/notification_group_serializer.rb index f4af842e38d..347659bdfef 100644 --- a/app/serializers/rest/notification_group_serializer.rb +++ b/app/serializers/rest/notification_group_serializer.rb @@ -24,7 +24,7 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer end def status_type? - [:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type) + [:favourite, :reblog, :status, :mention, :poll, :update, :quote].include?(object.type) end def report_type? diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index 320bc86961d..033bc1c0425 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -21,7 +21,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer end def status_type? - [:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type) + [:favourite, :reblog, :status, :mention, :poll, :update, :quote].include?(object.type) end def report_type? diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 29e77e7d5b1..4ade8132111 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -32,6 +32,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_one :quote, key: :quote, serializer: REST::QuoteSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer + has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? } def quote object.quote if object.quote&.acceptable? @@ -159,6 +160,14 @@ class REST::StatusSerializer < ActiveModel::Serializer object.active_mentions.to_a.sort_by(&:id) end + def quote_approval + { + automatic: object.quote_policy_as_keys(:automatic), + manual: object.quote_policy_as_keys(:manual), + current_user: object.quote_policy_for_account(current_user&.account), + } + end + private def relationships diff --git a/app/services/activitypub/fetch_all_replies_service.rb b/app/services/activitypub/fetch_all_replies_service.rb index e9c1712ed66..b771b845265 100644 --- a/app/services/activitypub/fetch_all_replies_service.rb +++ b/app/services/activitypub/fetch_all_replies_service.rb @@ -6,7 +6,7 @@ class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService # Limit of replies to fetch per status MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_SINGLE'] || 500).to_i - def call(status_uri, collection_or_uri, max_pages: 1, async_refresh_key: nil, request_id: nil) + def call(status_uri, collection_or_uri, max_pages: 1, batch_id: nil, request_id: nil) @status_uri = status_uri super diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 7173746f2de..0473bb5939f 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -8,9 +8,10 @@ class ActivityPub::FetchRemoteStatusService < BaseService DISCOVERIES_PER_REQUEST = 1000 # Should be called when uri has already been checked for locality - def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil) + def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil, depth: nil) return if domain_not_allowed?(uri) + @depth = depth || 0 @request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}" @json = if prefetched_body.nil? fetch_status(uri, true, on_behalf_of) @@ -52,7 +53,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService return nil if discoveries > DISCOVERIES_PER_REQUEST end - ActivityPub::Activity.factory(activity_json, actor, request_id: @request_id).perform + ActivityPub::Activity.factory(activity_json, actor, request_id: @request_id, depth: @depth).perform end private diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index 25eb275ca5c..327c88d846d 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -6,7 +6,7 @@ class ActivityPub::FetchRepliesService < BaseService # Limit of fetched replies MAX_REPLIES = 5 - def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, async_refresh_key: nil, request_id: nil) + def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, batch_id: nil, request_id: nil) @reference_uri = reference_uri @allow_synchronous_requests = allow_synchronous_requests @@ -15,9 +15,11 @@ class ActivityPub::FetchRepliesService < BaseService @items = filter_replies(@items) - batch = WorkerBatch.new - batch.connect(async_refresh_key) if async_refresh_key.present? - batch.add_jobs(FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }] }) + WorkerBatch.new(batch_id).within do |batch| + FetchReplyWorker.push_bulk(@items) do |reply_uri| + [reply_uri, { 'request_id' => request_id, 'batch_id' => batch.id }] + end + end [@items, n_pages] end diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 9c96c51851f..023eef19a02 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -18,6 +18,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @poll_changed = false @quote_changed = false @request_id = request_id + @quote = nil # Only native types can be updated at the moment return @status if !expected_type? || already_updated_more_recently? @@ -49,6 +50,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService create_edits! end + fetch_and_verify_quote!(@quote, @status_parser.quote_uri) if @quote.present? download_media_files! queue_poll_notifications! @@ -278,10 +280,10 @@ class ActivityPub::ProcessStatusUpdateService < BaseService return unless quote_uri.present? && @status.quote.present? quote = @status.quote - return if quote.quoted_status.present? && ActivityPub::TagManager.instance.uri_for(quote.quoted_status) != quote_uri + return if quote.quoted_status.present? && (ActivityPub::TagManager.instance.uri_for(quote.quoted_status) != quote_uri || quote.quoted_status.local?) approval_uri = @status_parser.quote_approval_uri - approval_uri = nil if unsupported_uri_scheme?(approval_uri) + approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri) quote.update(approval_uri: approval_uri, state: :pending, legacy: @status_parser.legacy_quote?) if quote.approval_uri != @status_parser.quote_approval_uri @@ -293,11 +295,13 @@ class ActivityPub::ProcessStatusUpdateService < BaseService if quote_uri.present? approval_uri = @status_parser.quote_approval_uri - approval_uri = nil if unsupported_uri_scheme?(approval_uri) + approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri) if @status.quote.present? # If the quoted post has changed, discard the old object and create a new one if @status.quote.quoted_status.present? && ActivityPub::TagManager.instance.uri_for(@status.quote.quoted_status) != quote_uri + # Revoke the quote while we get a chance… maybe this should be a `before_destroy` hook? + RevokeQuoteService.new.call(@status.quote) if @status.quote.quoted_account&.local? && @status.quote.accepted? @status.quote.destroy quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?) @quote_changed = true @@ -310,10 +314,10 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @quote_changed = true end + @quote = quote quote.save - - fetch_and_verify_quote!(quote, quote_uri) elsif @status.quote.present? + @quote = nil @status.quote.destroy! @quote_changed = true end diff --git a/app/services/activitypub/verify_quote_service.rb b/app/services/activitypub/verify_quote_service.rb index ad4dfbe310c..2b10de9d9b3 100644 --- a/app/services/activitypub/verify_quote_service.rb +++ b/app/services/activitypub/verify_quote_service.rb @@ -3,13 +3,17 @@ class ActivityPub::VerifyQuoteService < BaseService include JsonLdHelper + MAX_SYNCHRONOUS_DEPTH = 2 + # Optionally fetch quoted post, and verify the quote is authorized - def call(quote, fetchable_quoted_uri: nil, prefetched_quoted_object: nil, prefetched_approval: nil, request_id: nil) + def call(quote, fetchable_quoted_uri: nil, prefetched_quoted_object: nil, prefetched_approval: nil, request_id: nil, depth: nil) @request_id = request_id + @depth = depth || 0 @quote = quote @fetching_error = nil fetch_quoted_post_if_needed!(fetchable_quoted_uri, prefetched_body: prefetched_quoted_object) + return handle_local_quote! if quote.quoted_account&.local? return if fast_track_approval! || quote.approval_uri.blank? @json = fetch_approval_object(quote.approval_uri, prefetched_body: prefetched_approval) @@ -31,6 +35,15 @@ class ActivityPub::VerifyQuoteService < BaseService private + def handle_local_quote! + @quote.update!(approval_uri: nil) + if StatusPolicy.new(@quote.account, @quote.quoted_status).quote? + @quote.accept! + else + @quote.reject! + end + end + # FEP-044f defines rules that don't require the approval flow def fast_track_approval! return false if @quote.quoted_status_id.blank? @@ -42,14 +55,7 @@ class ActivityPub::VerifyQuoteService < BaseService true end - # Always allow someone to quote posts in which they are mentioned - if @quote.quoted_status.active_mentions.exists?(mentions: { account_id: @quote.account_id }) - @quote.accept! - - true - else - false - end + false end def fetch_approval_object(uri, prefetched_body: nil) @@ -72,10 +78,12 @@ class ActivityPub::VerifyQuoteService < BaseService return if uri.nil? || @quote.quoted_status.present? status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status) - status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id) + raise Mastodon::RecursionLimitExceededError if @depth > MAX_SYNCHRONOUS_DEPTH && status.nil? + + status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id, depth: @depth + 1) @quote.update(quoted_status: status) if status.present? - rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e + rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e @fetching_error = e end @@ -90,7 +98,7 @@ class ActivityPub::VerifyQuoteService < BaseService # It's not safe to fetch if the inlined object is cross-origin or doesn't match expectations return if object['id'] != uri || non_matching_uri_hosts?(@quote.approval_uri, object['id']) - status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id) + status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id, depth: @depth) if status.present? @quote.update(quoted_status: status) diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index f3aa479c153..41a4e210e15 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -40,6 +40,7 @@ class FanOutOnWriteService < BaseService deliver_to_self! unless @options[:skip_notifications] + notify_quoted_account! notify_mentioned_accounts! notify_about_update! if update? end @@ -69,6 +70,12 @@ class FanOutOnWriteService < BaseService FeedManager.instance.push_to_home(@account, @status, update: update?) if @account.local? end + def notify_quoted_account! + return unless @status.quote&.quoted_account&.local? && @status.quote&.accepted? + + LocalNotificationWorker.perform_async(@status.quote.quoted_account_id, @status.quote.id, 'Quote', 'quote') + end + def notify_mentioned_accounts! @status.active_mentions.where.not(id: @options[:silenced_account_ids] || []).joins(:account).merge(Account.local).select(:id, :account_id).reorder(nil).find_in_batches do |mentions| LocalNotificationWorker.push_bulk(mentions) do |mention| diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 2928c85390a..5ff1b63503c 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -82,7 +82,7 @@ class FollowService < BaseService LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, 'follow') MergeWorker.perform_async(@target_account.id, @source_account.id, 'home') - MergeWorker.push_bulk(List.where(account: @source_account).joins(:list_accounts).where(list_accounts: { account_id: @target_account.id }).pluck(:id)) do |list_id| + MergeWorker.push_bulk(@source_account.owned_lists.with_list_account(@target_account).pluck(:id)) do |list_id| [@target_account.id, list_id, 'list'] end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index f820f969a6e..95563698f45 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -247,7 +247,7 @@ class NotifyService < BaseService end def update_notification_request! - return unless @notification.type == :mention + return unless %i(mention quote).include?(@notification.type) notification_request = NotificationRequest.find_or_initialize_by(account_id: @recipient.id, from_account_id: @notification.from_account_id) notification_request.last_status_id = @notification.target_status.id diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index ac4b535ea9d..103186b21e7 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -19,6 +19,7 @@ class PostStatusService < BaseService # @option [String] :text Message # @option [Status] :thread Optional status to reply to # @option [Status] :quoted_status Optional status to quote + # @option [String] :quote_approval_policy Approval policy for quotes, one of `public`, `followers` or `nobody` # @option [Boolean] :sensitive # @option [String] :visibility # @option [String] :spoiler_text @@ -93,16 +94,10 @@ class PostStatusService < BaseService def attach_quote!(status) return if @quoted_status.nil? - # NOTE: for now this is only for convenience in testing, as we don't support the request flow nor serialize quotes in ActivityPub - # we only support incoming quotes so far + status.quote = Quote.create(quoted_status: @quoted_status, status: status) + status.quote.ensure_quoted_access - status.quote = Quote.new(quoted_status: @quoted_status) - status.quote.accept! if @status.account == @quoted_status.account || @quoted_status.active_mentions.exists?(mentions: { account_id: status.account_id }) - - # TODO: the following has yet to be implemented: - # - handle approval of local users (requires the interactionPolicy PR) - # - produce a QuoteAuthorization for quotes of local users - # - send a QuoteRequest for quotes of remote users + status.quote.accept! if @quoted_status.local? && StatusPolicy.new(@status.account, @quoted_status).quote? end def safeguard_mentions!(status) @@ -146,6 +141,7 @@ class PostStatusService < BaseService DistributionWorker.perform_async(@status.id) ActivityPub::DistributionWorker.perform_async(@status.id) PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll + ActivityPub::QuoteRequestWorker.perform_async(@status.quote.id) if @status.quote&.quoted_status.present? && !@status.quote&.quoted_status&.local? end def validate_media! @@ -220,6 +216,7 @@ class PostStatusService < BaseService language: valid_locale_cascade(@options[:language], @account.user&.preferred_posting_language, I18n.default_locale), application: @options[:application], rate_limit: @options[:with_rate_limit], + quote_approval_policy: @options[:quote_approval_policy], }.compact end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 2adb8c1edbe..f61cb632b23 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -47,6 +47,9 @@ class RemoveStatusService < BaseService remove_media end + # Revoke the quote while we get a chance… maybe this should be a `before_destroy` hook? + RevokeQuoteService.new.call(@status.quote) if @status.quote&.quoted_account&.local? && @status.quote&.accepted? + @status.destroy! if permanently? end end diff --git a/app/services/revoke_quote_service.rb b/app/services/revoke_quote_service.rb new file mode 100644 index 00000000000..f4bc07c9da9 --- /dev/null +++ b/app/services/revoke_quote_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class RevokeQuoteService < BaseService + include Payloadable + + def call(quote) + @quote = quote + @account = quote.quoted_account + + @quote.reject! + distribute_update! + distribute_stamp_deletion! + end + + private + + def distribute_update! + return if @quote.status_id.nil? + + DistributionWorker.perform_async(@quote.status_id, { 'update' => true }) + end + + def distribute_stamp_deletion! + ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url| + [signed_activity_json, @account.id, inbox_url] + end + end + + def inboxes + [ + @quote.status, + @quote.quoted_status, + ].compact.map { |status| StatusReachFinder.new(status, unsafe: true).inboxes }.flatten.uniq + end + + def signed_activity_json + @signed_activity_json ||= Oj.dump(serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true)) + end +end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index b3f2cd66f67..1ea8af69926 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -34,7 +34,7 @@ class UnfollowService < BaseService unless @options[:skip_unmerge] UnmergeWorker.perform_async(@target_account.id, @source_account.id, 'home') - UnmergeWorker.push_bulk(List.where(account: @source_account).joins(:list_accounts).where(list_accounts: { account_id: @target_account.id }).pluck(:list_id)) do |list_id| + UnmergeWorker.push_bulk(@source_account.owned_lists.with_list_account(@target_account).pluck(:list_id)) do |list_id| [@target_account.id, list_id, 'list'] end end diff --git a/app/services/unmute_service.rb b/app/services/unmute_service.rb index 9262961f7d7..0a9604bae2e 100644 --- a/app/services/unmute_service.rb +++ b/app/services/unmute_service.rb @@ -9,7 +9,7 @@ class UnmuteService < BaseService if account.following?(target_account) MergeWorker.perform_async(target_account.id, account.id, 'home') - MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id| + MergeWorker.push_bulk(account.owned_lists.with_list_account(target_account).pluck(:id)) do |list_id| [target_account.id, list_id, 'list'] end end diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index 7837d37c959..4b871211a46 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -115,6 +115,7 @@ class UpdateStatusService < BaseService @status.spoiler_text = @options[:spoiler_text] || '' if @options.key?(:spoiler_text) @status.sensitive = @options[:sensitive] || @options[:spoiler_text].present? if @options.key?(:sensitive) || @options.key?(:spoiler_text) @status.language = valid_locale_cascade(@options[:language], @status.language, @status.account.user&.preferred_posting_language, I18n.default_locale) + @status.quote_approval_policy = @options[:quote_approval_policy] if @options[:quote_approval_policy].present? # We raise here to rollback the entire transaction raise NoChangesSubmittedError unless significant_changes? diff --git a/app/validators/unreserved_username_validator.rb b/app/validators/unreserved_username_validator.rb index 55a8c835fae..f20f4a7494b 100644 --- a/app/validators/unreserved_username_validator.rb +++ b/app/validators/unreserved_username_validator.rb @@ -28,14 +28,6 @@ class UnreservedUsernameValidator < ActiveModel::Validator end def settings_username_reserved? - settings_has_reserved_usernames? && settings_reserves_username? - end - - def settings_has_reserved_usernames? - Setting.reserved_usernames.present? - end - - def settings_reserves_username? - Setting.reserved_usernames.include?(@username.downcase) + UsernameBlock.matches?(@username, allow_with_approval: false) end end diff --git a/app/views/admin/accounts/_counters.html.haml b/app/views/admin/accounts/_counters.html.haml index 00ab98d094a..3c99da9f2c8 100644 --- a/app/views/admin/accounts/_counters.html.haml +++ b/app/views/admin/accounts/_counters.html.haml @@ -30,9 +30,9 @@ = t('admin.accounts.suspended') - elsif account.silenced? = t('admin.accounts.silenced') - - elsif account.local? && account.user&.disabled? + - elsif account.local? && account.user_disabled? = t('admin.accounts.disabled') - - elsif account.local? && !account.user&.confirmed? + - elsif account.local? && !account.user_confirmed? = t('admin.accounts.confirming') - elsif account.local? && !account.user_approved? = t('admin.accounts.pending') diff --git a/app/views/admin/accounts/_field.html.haml b/app/views/admin/accounts/_field.html.haml new file mode 100644 index 00000000000..ce8d80785e6 --- /dev/null +++ b/app/views/admin/accounts/_field.html.haml @@ -0,0 +1,9 @@ +-# locals: (field:, account:) +%dl + %dt.emojify{ title: field.name } + = prerender_custom_emojis(h(field.name), account.emojis) + %dd{ title: field.value, class: field_verified_class(field.verified?) } + - if field.verified? + %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) } + = material_symbol 'check' + = prerender_custom_emojis(account_field_value_format(field, with_rel_me: false), account.emojis) diff --git a/app/views/admin/accounts/_local_account.html.haml b/app/views/admin/accounts/_local_account.html.haml index 892afcc5428..bff752332c6 100644 --- a/app/views/admin/accounts/_local_account.html.haml +++ b/app/views/admin/accounts/_local_account.html.haml @@ -34,7 +34,7 @@ %tr %th= t('admin.accounts.email_status') %td - - if account.user&.confirmed? + - if account.user_confirmed? = t('admin.accounts.confirmed') - else = t('admin.accounts.confirming') diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index f148b9a0822..977967c58fb 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -7,25 +7,17 @@ = render 'application/card', account: @account -- account = @account -- fields = account.fields -- unless fields.empty? && account.note.blank? +- if @account.fields? || @account.note? .admin-account-bio - - unless fields.empty? + - if @account.fields? %div .account__header__fields - - fields.each do |field| - %dl - %dt.emojify{ title: field.name }= prerender_custom_emojis(h(field.name), account.emojis) - %dd{ title: field.value, class: custom_field_classes(field) } - - if field.verified? - %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) } - = material_symbol 'check' - = prerender_custom_emojis(account_field_value_format(field, with_rel_me: false), account.emojis) + = render partial: 'field', collection: @account.fields, locals: { account: @account } - - if account.note.present? + - if @account.note? %div - .account__header__content.emojify= prerender_custom_emojis(account_bio_format(account), account.emojis) + .account__header__content.emojify + = prerender_custom_emojis(account_bio_format(@account), @account.emojis) = render 'admin/accounts/counters', account: @account diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index c977011e1ff..d241844ea2f 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -78,7 +78,7 @@ %h3= t('admin.instances.availability.title') %p - = t('admin.instances.availability.description_html', count: DeliveryFailureTracker::FAILURE_DAYS_THRESHOLD) + = t('admin.instances.availability.description_html', count: DeliveryFailureTracker::FAILURE_THRESHOLDS[:days]) .availability-indicator %ul.availability-indicator__graphic diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 6f762d94ebf..44664b85fd9 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -42,7 +42,7 @@ %span.red= t('admin.accounts.suspended') - elsif target_account.silenced? %span.red= t('admin.accounts.silenced') - - elsif target_account.user&.disabled? + - elsif target_account.user_disabled? %span.red= t('admin.accounts.disabled') - else %span.neutral= t('admin.accounts.no_limits_imposed') diff --git a/app/views/admin/username_blocks/_form.html.haml b/app/views/admin/username_blocks/_form.html.haml new file mode 100644 index 00000000000..bfbbb18e2f9 --- /dev/null +++ b/app/views/admin/username_blocks/_form.html.haml @@ -0,0 +1,16 @@ +.fields-group + = form.input :username, + wrapper: :with_block_label, + input_html: { autocomplete: 'new-password', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT } + +.fields-group + = form.input :comparison, + as: :select, + wrapper: :with_block_label, + collection: %w(equals contains), + include_blank: false, + label_method: ->(type) { I18n.t(type, scope: 'admin.username_blocks.comparison') } + +.fields-group + = form.input :allow_with_approval, + wrapper: :with_label diff --git a/app/views/admin/username_blocks/_username_block.html.haml b/app/views/admin/username_blocks/_username_block.html.haml new file mode 100644 index 00000000000..617ec65bc6d --- /dev/null +++ b/app/views/admin/username_blocks/_username_block.html.haml @@ -0,0 +1,12 @@ +.batch-table__row + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = f.check_box :username_block_ids, { multiple: true, include_hidden: false }, username_block.id + .sr-only= username_block.username + .batch-table__row__content.pending-account + .pending-account__header + = t(username_block.exact? ? 'admin.username_blocks.matches_exactly_html' : 'admin.username_blocks.contains_html', string: content_tag(:samp, link_to(username_block.username, edit_admin_username_block_path(username_block)))) + %br/ + - if username_block.allow_with_approval? + = t('admin.email_domain_blocks.allow_registrations_with_approval') + - else + = t('admin.username_blocks.block_registrations') diff --git a/app/views/admin/username_blocks/edit.html.haml b/app/views/admin/username_blocks/edit.html.haml new file mode 100644 index 00000000000..eee0fedef07 --- /dev/null +++ b/app/views/admin/username_blocks/edit.html.haml @@ -0,0 +1,10 @@ +- content_for :page_title do + = t('admin.username_blocks.edit.title') + += simple_form_for @username_block, url: admin_username_block_path(@username_block) do |form| + = render 'shared/error_messages', object: @username_block + + = render form + + .actions + = form.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/username_blocks/index.html.haml b/app/views/admin/username_blocks/index.html.haml new file mode 100644 index 00000000000..697edfda51a --- /dev/null +++ b/app/views/admin/username_blocks/index.html.haml @@ -0,0 +1,26 @@ +- content_for :page_title do + = t('admin.username_blocks.title') + +- content_for :heading_actions do + = link_to t('admin.username_blocks.add_new'), new_admin_username_block_path, class: 'button' + += form_with model: @form, url: batch_admin_username_blocks_path do |f| + = hidden_field_tag :page, params[:page] || 1 + + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all + = check_box_tag :batch_checkbox_all, nil, false + .batch-table__toolbar__actions + = f.button safe_join([material_symbol('close'), t('admin.username_blocks.delete')]), + class: 'table-action-link', + data: { confirm: t('admin.reports.are_you_sure') }, + name: :delete, + type: :submit + .batch-table__body + - if @username_blocks.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'username_block', collection: @username_blocks, locals: { f: f } + += paginate @username_blocks diff --git a/app/views/admin/username_blocks/new.html.haml b/app/views/admin/username_blocks/new.html.haml new file mode 100644 index 00000000000..0f5bd27952b --- /dev/null +++ b/app/views/admin/username_blocks/new.html.haml @@ -0,0 +1,10 @@ +- content_for :page_title do + = t('admin.username_blocks.new.title') + += simple_form_for @username_block, url: admin_username_blocks_path do |form| + = render 'shared/error_messages', object: @username_block + + = render form + + .actions + = form.button :button, t('admin.username_blocks.new.create'), type: :submit diff --git a/app/views/notification_mailer/quote.html.haml b/app/views/notification_mailer/quote.html.haml new file mode 100644 index 00000000000..139f4b2e8de --- /dev/null +++ b/app/views/notification_mailer/quote.html.haml @@ -0,0 +1,16 @@ += content_for :heading do + = render 'application/mailer/heading', + image_url: frontend_asset_url('images/mailer-new/heading/quote.png'), + subtitle: t('notification_mailer.quote.body', name: @status.account.pretty_acct), + title: t('notification_mailer.quote.title') +%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' } + %tr + %td.email-body-padding-td + %table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' } + %tr + %td.email-inner-card-td + = render 'status', status: @status, time_zone: @me.user_time_zone + %table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' } + %tr + %td.email-padding-top-24 + = render 'application/mailer/button', text: t('notification_mailer.mention.action'), url: web_url("@#{@status.account.pretty_acct}/#{@status.id}") diff --git a/app/views/notification_mailer/quote.text.erb b/app/views/notification_mailer/quote.text.erb new file mode 100644 index 00000000000..6e3c67b7651 --- /dev/null +++ b/app/views/notification_mailer/quote.text.erb @@ -0,0 +1,5 @@ +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> + +<%= raw t('notification_mailer.quote.body', name: @status.account.pretty_acct) %> + +<%= render 'status', status: @status %> diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index a8e179d0191..08bcc32e9b4 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -18,6 +18,7 @@ = ff.input :'notification_emails.reblog', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.reblog') = ff.input :'notification_emails.favourite', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.favourite') = ff.input :'notification_emails.mention', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.mention') + = ff.input :'notification_emails.quote', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.quote') .fields-group = ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails') diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index dd79695db7f..e02bd2b1773 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -21,6 +21,7 @@ .fields-group.fields-row__column.fields-row__column-6 = ff.input :default_privacy, collection: Status.selectable_visibilities, + selected: current_user.setting_default_privacy, hint: false, include_blank: false, label_method: ->(visibility) { safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, diff --git a/app/workers/activitypub/fetch_all_replies_worker.rb b/app/workers/activitypub/fetch_all_replies_worker.rb index ab9eebc4ec7..128bfe7e8af 100644 --- a/app/workers/activitypub/fetch_all_replies_worker.rb +++ b/app/workers/activitypub/fetch_all_replies_worker.rb @@ -16,7 +16,9 @@ class ActivityPub::FetchAllRepliesWorker MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i def perform(root_status_id, options = {}) + @batch = WorkerBatch.new(options['batch_id']) @root_status = Status.remote.find_by(id: root_status_id) + return unless @root_status&.should_fetch_replies? @root_status.touch(:fetched_replies_at) @@ -45,6 +47,8 @@ class ActivityPub::FetchAllRepliesWorker # Workers shouldn't be returning anything, but this is used in tests fetched_uris + ensure + @batch.remove_job(jid) end private @@ -53,9 +57,10 @@ class ActivityPub::FetchAllRepliesWorker # status URI, or the prefetched body of the Note object def get_replies(status, max_pages, options = {}) replies_collection_or_uri = get_replies_uri(status) + return if replies_collection_or_uri.nil? - ActivityPub::FetchAllRepliesService.new.call(value_or_id(status), replies_collection_or_uri, max_pages: max_pages, async_refresh_key: "context:#{@root_status.id}:refresh", **options.deep_symbolize_keys) + ActivityPub::FetchAllRepliesService.new.call(value_or_id(status), replies_collection_or_uri, max_pages: max_pages, **options.deep_symbolize_keys) end # Get the URI of the replies collection of a status @@ -78,9 +83,10 @@ class ActivityPub::FetchAllRepliesWorker # @param root_status_uri [String] def get_root_replies(root_status_uri, options = {}) root_status_body = fetch_resource(root_status_uri, true) + return if root_status_body.nil? - FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys, 'prefetched_body' => root_status_body }) + FetchReplyWorker.perform_async(root_status_uri, { **options.deep_stringify_keys.except('batch_id'), 'prefetched_body' => root_status_body }) get_replies(root_status_body, MAX_PAGES, options) end diff --git a/app/workers/activitypub/quote_request_worker.rb b/app/workers/activitypub/quote_request_worker.rb new file mode 100644 index 00000000000..0540492f863 --- /dev/null +++ b/app/workers/activitypub/quote_request_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ActivityPub::QuoteRequestWorker < ActivityPub::RawDistributionWorker + def perform(quote_id) + @quote = Quote.find(quote_id) + @account = @quote.account + + distribute! + rescue ActiveRecord::RecordNotFound + true + end + + protected + + def inboxes + @inboxes ||= [@quote.quoted_account&.inbox_url].compact + end + + def payload + @payload ||= Oj.dump(serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account, allow_post_inlining: true)) + end +end diff --git a/app/workers/activitypub/status_update_distribution_worker.rb b/app/workers/activitypub/status_update_distribution_worker.rb index a79ede2bf61..7f70fcaecc6 100644 --- a/app/workers/activitypub/status_update_distribution_worker.rb +++ b/app/workers/activitypub/status_update_distribution_worker.rb @@ -17,10 +17,10 @@ class ActivityPub::StatusUpdateDistributionWorker < ActivityPub::DistributionWor def activity ActivityPub::ActivityPresenter.new( - id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @status.edited_at.to_i].join, + id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @options[:updated_at]&.to_datetime&.to_i || @status.edited_at.to_i].join, type: 'Update', actor: ActivityPub::TagManager.instance.uri_for(@status.account), - published: @status.edited_at, + published: @options[:updated_at]&.to_datetime || @status.edited_at, to: ActivityPub::TagManager.instance.to(@status), cc: ActivityPub::TagManager.instance.cc(@status), virtual_object: @status diff --git a/app/workers/fetch_reply_worker.rb b/app/workers/fetch_reply_worker.rb index da3b9a8c131..4f11b75cc51 100644 --- a/app/workers/fetch_reply_worker.rb +++ b/app/workers/fetch_reply_worker.rb @@ -7,9 +7,9 @@ class FetchReplyWorker sidekiq_options queue: 'pull', retry: 3 def perform(child_url, options = {}) - batch = WorkerBatch.new(options.delete('batch_id')) if options['batch_id'] - FetchRemoteStatusService.new.call(child_url, **options.symbolize_keys) + batch = WorkerBatch.new(options.delete('batch_id')) if options['batch_id'] + result = FetchRemoteStatusService.new.call(child_url, **options.symbolize_keys) ensure - batch&.remove_job(jid) + batch&.remove_job(jid, increment: result&.previously_new_record?) end end diff --git a/app/workers/mention_resolve_worker.rb b/app/workers/mention_resolve_worker.rb index 8c5938aeaf1..d14adb3cf31 100644 --- a/app/workers/mention_resolve_worker.rb +++ b/app/workers/mention_resolve_worker.rb @@ -22,11 +22,7 @@ class MentionResolveWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end private diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index a18f38556bb..1a5745a86ae 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -81,7 +81,7 @@ class MoveWorker def copy_account_notes! AccountNote.where(target_account: @source_account).find_each do |note| - text = I18n.with_locale(note.account.user&.locale.presence || I18n.default_locale) do + text = I18n.with_locale(note.account.user_locale.presence || I18n.default_locale) do I18n.t('move_handler.copy_account_note_text', acct: @source_account.acct) end @@ -104,7 +104,7 @@ class MoveWorker def carry_blocks_over! @source_account.blocked_by_relationships.where(account: Account.local).find_each do |block| - unless block.account.blocking?(@target_account) || block.account.following?(@target_account) + unless skip_block_move?(block) BlockService.new.call(block.account, @target_account) add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text') end @@ -115,19 +115,29 @@ class MoveWorker def carry_mutes_over! @source_account.muted_by_relationships.where(account: Account.local).find_each do |mute| - MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless mute.account.muting?(@target_account) || mute.account.following?(@target_account) - add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text') + unless skip_mute_move?(mute) + MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) + add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text') + end rescue => e @deferred_error = e end end def add_account_note_if_needed!(account, id) - unless AccountNote.exists?(account: account, target_account: @target_account) - text = I18n.with_locale(account.user&.locale.presence || I18n.default_locale) do - I18n.t(id, acct: @source_account.acct) - end - AccountNote.create!(account: account, target_account: @target_account, comment: text) + return if AccountNote.exists?(account: account, target_account: @target_account) + + text = I18n.with_locale(account.user_locale.presence || I18n.default_locale) do + I18n.t(id, acct: @source_account.acct) end + AccountNote.create!(account: account, target_account: @target_account, comment: text) + end + + def skip_mute_move?(mute) + mute.account.muting?(@target_account) || mute.account.following?(@target_account) + end + + def skip_block_move?(block) + block.account.blocking?(@target_account) || block.account.following?(@target_account) end end diff --git a/app/workers/publish_scheduled_status_worker.rb b/app/workers/publish_scheduled_status_worker.rb index 0ec081de917..bcf20b49431 100644 --- a/app/workers/publish_scheduled_status_worker.rb +++ b/app/workers/publish_scheduled_status_worker.rb @@ -9,7 +9,7 @@ class PublishScheduledStatusWorker scheduled_status = ScheduledStatus.find(scheduled_status_id) scheduled_status.destroy! - return true if scheduled_status.account.user.disabled? + return true if scheduled_status.account.user_disabled? PostStatusService.new.call( scheduled_status.account, diff --git a/app/workers/redownload_avatar_worker.rb b/app/workers/redownload_avatar_worker.rb index df17b7718dc..c4c659f73e8 100644 --- a/app/workers/redownload_avatar_worker.rb +++ b/app/workers/redownload_avatar_worker.rb @@ -20,10 +20,6 @@ class RedownloadAvatarWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end end diff --git a/app/workers/redownload_header_worker.rb b/app/workers/redownload_header_worker.rb index 3b142ec5f98..2d600e29641 100644 --- a/app/workers/redownload_header_worker.rb +++ b/app/workers/redownload_header_worker.rb @@ -20,10 +20,6 @@ class RedownloadHeaderWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end end diff --git a/app/workers/redownload_media_worker.rb b/app/workers/redownload_media_worker.rb index 343caa32c23..5342ec0b2d4 100644 --- a/app/workers/redownload_media_worker.rb +++ b/app/workers/redownload_media_worker.rb @@ -20,10 +20,6 @@ class RedownloadMediaWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end end diff --git a/app/workers/remote_account_refresh_worker.rb b/app/workers/remote_account_refresh_worker.rb index 9632936b547..5a4cbdf7260 100644 --- a/app/workers/remote_account_refresh_worker.rb +++ b/app/workers/remote_account_refresh_worker.rb @@ -15,10 +15,6 @@ class RemoteAccountRefreshWorker rescue Mastodon::UnexpectedResponseError => e response = e.response - if response_error_unsalvageable?(response) - # Give up - else - raise e - end + raise(e) unless response_error_unsalvageable?(response) end end diff --git a/config/environments/development.rb b/config/environments/development.rb index ca9e876e26b..79b491869cc 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -19,6 +19,9 @@ Rails.application.configure do # Enable server timing. config.server_timing = true + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + config.asset_host = ENV['CDN_HOST'] if ENV['CDN_HOST'].present? + # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 0dbc0873855..b934696bda6 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -72,6 +72,8 @@ ignore_unused: - 'preferences.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use - 'edit_profile.other' # some locales are missing other keys, therefore leading i18n-tasks to detect `preferences` as plural and not finding use - 'admin.terms_of_service.generate' # temporarily disabled + - 'admin.username_blocks.matches_exactly_html' + - 'admin.username_blocks.contains_html' ignore_inconsistent_interpolations: - '*.one' diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index f558ee5fe0e..853b99d32fd 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -126,7 +126,7 @@ class Rack::Attack end throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req| - if req.post? && req.path_matches?('/auth/password') + if req.post? && req.path_matches?('/auth/confirmation') req.params.dig('user', 'email').presence elsif req.post? && req.path == '/api/v1/emails/confirmations' req.authenticated_user_id diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 3c2f12780c0..7edaf38a60a 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative '../../lib/mastodon/sidekiq_middleware' +require_relative '../../lib/mastodon/worker_batch_middleware' Sidekiq.configure_server do |config| config.redis = REDIS_CONFIGURATION.sidekiq @@ -72,14 +73,12 @@ Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Mastodon::SidekiqMiddleware - end - - config.server_middleware do |chain| chain.add SidekiqUniqueJobs::Middleware::Server end config.client_middleware do |chain| chain.add SidekiqUniqueJobs::Middleware::Client + chain.add Mastodon::WorkerBatchMiddleware end config.on(:startup) do @@ -105,6 +104,7 @@ Sidekiq.configure_client do |config| config.client_middleware do |chain| chain.add SidekiqUniqueJobs::Middleware::Client + chain.add Mastodon::WorkerBatchMiddleware end config.logger.level = Logger.const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s) diff --git a/config/locales/activerecord.az.yml b/config/locales/activerecord.az.yml index e9ba86bc793..1f7f9375c91 100644 --- a/config/locales/activerecord.az.yml +++ b/config/locales/activerecord.az.yml @@ -1 +1,77 @@ +--- az: + activerecord: + attributes: + poll: + expires_at: Son tarix + options: Seçimlər + user: + agreement: Xidmət razılaşması + email: E-poçt ünvanı + locale: Lokal + password: Parol + user/account: + username: İstifadəçi adı + user/invite_request: + text: Səbəb + errors: + attributes: + domain: + invalid: yararlı bir domen adı deyil + messages: + invalid_domain_on_line: "%{value} yararlı bir domen adı deyil" + models: + account: + attributes: + fields: + fields_with_values_missing_labels: əskik etiketli dəyərlər ehtiva edir + username: + invalid: yalnız hərf, rəqəm və altdan xətt ehtiva etməlidir + reserved: rezerv edilib + admin/webhook: + attributes: + url: + invalid: yararlı bir URL deyil + doorkeeper/application: + attributes: + website: + invalid: yararlı bir URL deyil + import: + attributes: + data: + malformed: yanlış formatdadır + list_account: + attributes: + account_id: + taken: artıq siyahıdadır + must_be_following: izlənilən bir hesab olmalıdır + status: + attributes: + reblog: + taken: göndərişi artıq mövcuddur + terms_of_service: + attributes: + effective_date: + too_soon: çox tezdir, %{date} tarixindən sonra olmalıdır + user: + attributes: + date_of_birth: + below_limit: yaş limitinin altındadır + email: + blocked: icazə verilməyən bir e-poçt provayderi istifadə edir + unreachable: mövcud olaraq görünmür + role_id: + elevated: hazırkı rolunuzdan yüksək ola bilməz + user_role: + attributes: + permissions_as_keys: + dangerous: təməl rol üçün güvənli olmayan icazələri ehtiva edir + elevated: hazırkı rolunuzun sahib olmadığı icazələri ehtiva edə bilməz + own_role: hazırkı rolunuzla dəyişdirilə bilməz + position: + elevated: hazırkı rolunuzdan yüksək ola bilməz + own_role: hazırkı rolunuzla dəyişdirilə bilməz + webhook: + attributes: + events: + invalid_permissions: hüquqlarınız olmayan tədbirləri ehtiva edə bilməz diff --git a/config/locales/activerecord.pl.yml b/config/locales/activerecord.pl.yml index 62edc03c0ea..29cace6db53 100644 --- a/config/locales/activerecord.pl.yml +++ b/config/locales/activerecord.pl.yml @@ -49,8 +49,14 @@ pl: attributes: reblog: taken: status już istnieje + terms_of_service: + attributes: + effective_date: + too_soon: jest zbyt wcześnie, musi być później niż %{date} user: attributes: + date_of_birth: + below_limit: jest poniżej granicy wiekowej email: blocked: używa niedozwolonego dostawcy poczty elektronicznej unreachable: wydaje się nie istnieć diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 2fe619a60c7..026972b2256 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -2047,8 +2047,6 @@ ar: ownership: لا يمكن تثبيت منشور نشره شخص آخر reblog: لا يمكن تثبيت إعادة نشر quote_policies: - followers: المتابعين والمستخدمين المذكورين - nobody: المستخدمين المذكورين فقط public: الجميع title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/az.yml b/config/locales/az.yml index e9ba86bc793..516f03f6040 100644 --- a/config/locales/az.yml +++ b/config/locales/az.yml @@ -1 +1,168 @@ +--- az: + about: + about_mastodon_html: 'Gələcəyin sosial şəbəkəsi: Reklam yoxdur, korporativ müşahidə yoxdur, etik dizayn və mərkəziyyətsizlik! Mastodon ilə öz verilərinizə sahib çıxın!' + admin: + account_actions: + title: "%{acct} üzərində moderasiya əməliyyatını icra et" + account_moderation_notes: + created_msg: Moderasiya notu uğurla yaradıldı! + destroyed_msg: Moderasiya notu uğurla məhv edildi! + accounts: + delete: Veriləri sil + destroyed_msg: "%{username} - verilərinin tezliklə silinməsi növbədədir" + moderation: + title: Moderasiya + moderation_notes: Moderasiya notları + remote_suspension_irreversible: Bu hesabın veriləri geri qaytarılmayacaq şəkildə silinib. + remote_suspension_reversible_hint_html: Hesabın fəaliyyəti öz serverində dayandırılıb və verilər %{date} tarixində tamamilə silinəcək. O vaxta qədər, uzaq server hər hansısa mənfi təsir olmadan bu hesabı bərpa edə bilər. Hesabın bütün verilərini dərhal silmək istəyirsinizsə, bunu aşağıdan edə bilərsiniz. + reset_password: Parolu sıfırla + security_measures: + only_password: Yalnız parol + password_and_2fa: Parol və 2FA + suspension_irreversible: Bu hesabın veriləri geri qaytarılmayacaq şəkildə silinib. Hesabı istifadəyə yararlı etmək üçün hesab fəaliyyətinin dayandırılma prosesini ləğv edə bilərsiniz, ancaq daha əvvəl sahib olduğunuz heç bir veri geri qaytarılmayacaq. + action_logs: + action_types: + reset_password_user: Parolu sıfırla + actions: + approve_appeal_html: "%{name}, moderasiya qərarına %{target} tərəfindən verilən etirazı təsdiqlədi" + reject_appeal_html: "%{name}, moderasiya qərarına %{target} tərəfindən verilən etirazı rədd etdi" + reset_password_user_html: "%{name}, %{target} istifadəçisinin parolunu sıfırladı" + dashboard: + software: Yazılım + instances: + moderation: + title: Moderasiya + moderation_notes: + create: Moderasiya notu əlavə et + description_html: Notlara baxın, gələcəkdə özünüz və digər moderatorlar üçün notlar buraxın + title: Moderasiya notları + reports: + actions: + suspend_description_html: Hesab və onun bütün məzmunları əlçatmaz olacaq və nəticədə silinəcək və onunla əlaqə qurmaq mümkün olmayacaq. 30 gün ərzində geri qaytarıla bilər. Bu hesaba aid bütün hesabatları bağlayır. + assigned: Təyin edilmiş moderator + confirm_action: "@%{acct} üzərindəki moderasiya əməliyyatını təsdiqlə" + notes_description_html: Notlara baxın, gələcəkdə özünüz və digər moderatorlar üçün notlar buraxın + quick_actions_description_html: 'Cəld bir əməliyyat edin və ya bildirilən məzmuna baxmaq üçün aşağı diyirlərin:' + roles: + categories: + moderation: Moderasiya + privileges: + delete_user_data: İstifadəçi verilərini sil + delete_user_data_description: İstifadəçilərin, digər istifadəçilərin verilərini gecikmə olmadan silməsinə icazə verir + manage_appeals_description: İstifadəçilərin moderasiya əməliyyatlarına etdiyi etirazları incələməsinə icazə verir + manage_reports_description: İstifadəçilərin hesabatları incələməsinə və bunlara qarşı moderasiya əməliyyatlarını icra etməsinə icazə verir + manage_settings: Ayarları idarə et + manage_settings_description: İstifadəçilərin sayt ayarlarını dəyişdirməsinə icazə verir + manage_taxonomies_description: İstifadəçilərin trend məzmunu incələməsinə və mövzu etiketləri ayarlarını güncəlləməsinə icazə verir + manage_user_access: İstifadəçi erişimini idarə et + manage_user_access_description: İstifadəçilərin, digər istifadəçilərin iki faktorlu kimlik doğrulamasını sıradan çıxartmasına, onların e-poçt ünvanlarını dəyişdirməsinə və onların parolunu sıfırlamasına icazə verir + manage_users_description: İstifadəçilərin digər istifadəçilərin detallarını görməsinə və onlara qarşı moderasiya əməliyyatlarını icra etməsinə icazə verir + view_dashboard_description: İstifadəçilərin idarəetmə lövhəsinə və müxtəlif metriklərə erişməsinə icazə verir + view_devops_description: İstifadəçilərin Sidekiq və pgHero idarəetmə lövhələrinə erişməsinə icazə verir + settings: + registrations: + moderation_recommandation: Hər kəs üçün qeydiyyatı açmazdan əvvəl lütfən əmin olun ki, adekvat və reaktiv moderasiya komandanız var! + registrations_mode: + warning_hint: Moderasiya komandanızın spam və zərərli qeydiyyatları vaxtında idarə edə biləcəyinə əmin deyilsinizsə, “Qeydiyyat üçün təsdiq tələb olunur”dan istifadə etməyi tövsiyə edirik. + title: Server ayarları + statuses: + metadata: Meta veri + system_checks: + elasticsearch_version_check: + message_html: 'Uyumlu olmayan Elasticsearch versiyası: %{value}' + tags: + updated_msg: Mövzu etiketi ayarları uğurla güncəlləndi + admin_mailer: + auto_close_registrations: + body: Son vaxtlarda moderator fəaliyyətinin olmamasına görə, %{instance} üzərindəki qeydiyyatlar avtomatik olaraq manual yoxlanış tələb edəcək şəkildə dəyişdirilib, beləliklə %{instance} potensial zərərli aktyorlar tərəfindən istifadə edilən platforma çevrilməyəcək. İstənilən vaxt açıq qeydiyyat rejiminə qaytara bilərsiniz. + new_appeal: + body: "%{target}, %{date} tarixində %{action_taken_by} tərəfindən verilmiş %{type} moderasiya qərarına etiraz edir. Yazılanlar:" + next_steps: Moderasiya qərarını geri almaq üçün etirazı təsdiqləyə, ya da etirazı yox saya bilərsiniz. + subject: "%{username}, %{instance} üzərindəki bir moderasiya qərarına etiraz edir" + appearance: + animations_and_accessibility: Animasiyalar və erişiləbilənlik + applications: + regenerate_token: Erişim tokenini təkrar yarat + token_regenerated: Erişim tokeni uğurla yaradıldı + your_token: Erişim tokeniniz + auth: + confirmations: + wrong_email_hint: Əgər bu e-poçt ünvanı doğru deyilsə, hesab ayarlarında onu dəyişdirə bilərsiniz. + forgot_password: Parolunuzu unutmusunuz? + invalid_reset_password_token: Parol sıfırlama tokeni yararsızdır və ya vaxtı bitib. Lütfən yenisini tələb edin. + link_to_otp: Telefonunuzdan iki faktorlu kodu və ya bir geri qaytarma kodunu daxil edin + reset_password: Parolu sıfırla + rules: + preamble: Bunlar, %{domain} moderatorları tərəfindən təyin edilib və tətbiq edilib. + preamble_invited: Davam etməzdən əvvəl, lütfən %{domain} moderatorları tərəfindən təyin edilmiş qaydaları nəzərdən keçirin. + set_new_password: Yeni parol təyin et + status: + self_destruct: "%{domain} bağlandığı üçün, hesabınıza yalnız məhdud erişiminiz olacaq." + challenge: + hint_html: "İpucu: Sonrakı bir saat ərzində sizdən parolu soruşmayacağıq." + invalid_password: Yararsız parol + prompt: Davam etmək üçün parolu təsdiqlə + deletes: + confirm_password: Kimliyinizi doğrulamaq üçün hazırkı parolunuzu daxil edin + exports: + archive_takeout: + hint_html: "Göndərişlərinizin və yüklədiyiniz medianın bir arxivini tələb edə bilərsiniz. Xaricə köçürülmüş verilər, istənilən uyumlu yazılım tərəfindən oxuna bilən ActivityPub formatında olacaq. Hər 7 gündə bir dəfə arxiv tələb edə bilərsiniz." + filters: + edit: + statuses_hint_html: Bu filtr, aşağıdakı açar sözləri ilə uyuşmasından asılı olmayaraq fərdi göndərişləri seçmək üçün tətbiq olunur. Göndərişləri incələyin və ya filtrdən silin. + generic: + all_matching_items_selected_html: + one: Axtarışınızla uyuşan %{count} element seçilib. + other: Axtarışınızla uyuşan %{count} element seçilib. + select_all_matching_items: + one: Axtarışınızla uyuşan <0>%{count} elementi seçin. + other: Axtarışınızla uyuşan <0>%{count} elementi seçin. + imports: + errors: + incompatible_type: Seçilmiş daxilə köçürmə növü ilə uyumlu deyil + invites: + prompt: Bu serverə erişim icazəsi vermək üçün keçid yaradın və başqaları ilə paylaşın + login_activities: + authentication_methods: + password: parol + description_html: Əgər tanımadığınız bir fəaliyyəti görsəniz, parolunuzu dəyişdirməyi və iki faktorlu kimlik doğrulamanı fəallaşdırmağı düşünə bilərsiniz + migrations: + warning: + disabled_account: Hazırkı hesabınız daha sonra istifadəyə yararsız olacaq. Ancaq, verilərin xaricə köçürülməsinə, həmçinin təkrar aktivləşdirmə prosesinə erişə biləcəksiniz. + moderation: + title: Moderasiya + sessions: + browsers: + edge: Microsoft Edge + settings: + account_settings: Hesab ayarları + development: Gəlişdirmə + strikes: Moderasiya pozuntuları + statuses_cleanup: + enabled_hint: Aşağıdakı istisnalardan heç birinə uyuşmadığı müddətcə, göndərişləriniz qeyd edilmiş yaş həddinə çatdıqda avtomatik silinir + tags: + does_not_match_previous_name: əvvəlki adla uyuşmur + two_factor_authentication: + generate_recovery_codes: Geri qaytarma kodlarını yarat + lost_recovery_codes: Geri qaytarma kodları, telefonunuzu itirdiyiniz halda hesabınıza yenidən erişməyinizə imkan verir. Geri qaytarma kodlarınızı itirsəniz, onları təkrar yarada bilərsiniz. Köhnə geri qaytarma kodlarınız yararsız sayılacaq. + recovery_codes: Geri qaytarma kodlarını nüsxələ + recovery_codes_regenerated: Geri qaytarma kodları uğurla yaradıldı + recovery_instructions_html: Telefonunuza erişə bilmirsinizsə, hesabınıza təkrar erişə bilmək üçün aşağıdakı geri qaytarma kodlarından birini istifadə edə bilərsiniz. Geri qaytarma kodlarını etibarlı yerdə saxlayın. Misal üçün, bunları çap edib digər vacib sənədlərin yanında saxlaya bilərsiniz. + user_mailer: + appeal_approved: + action: Hesab ayarları + suspicious_sign_in: + change_password: parolu dəyişdir + subject: Hesabınıza yeni bir IP ünvanından erişildi + warning: + explanation: + disable: Artıq hesabınızı istifadə edə bilməzsiniz, ancaq profiliniz və digər veriləriniz olduğu kimi qalacaq. Verilərinizin bir nüsxəsini tələb edə, hesab ayarlarınızı dəyişdirə və ya hesabınızı silə bilərsiniz. + suspend: Hesabınızı artıq istifadə edə bilməzsiniz, profiliniz və digər veriləriniz artıq əlçatmazdır. Təxminən 30 gün ərzində verilər tamamilə silinənə qədər verilərinizin bir nüsxəsini tələb etmək üçün hələ də hesabınıza giriş edə bilərsiniz, ancaq hesab fəaliyyətinin dayandırılması prosesini ləğv etməyinizi önləmək üçün bəzi təməl veriləri saxlayacağıq. + welcome: + feature_creativity: Mastodon özünüzü onlayn mühitdə ifadə etməyinizə kömək edəcək səs, video və şəkil göndərişləri, erişiləbilənlik açıqlamaları, anketlər, məzmun xəbərdarlıqları, animasiyalı avatarlar, özəl emojilər, kiçik şəkli kəsmə nəzarəti və daha çoxunu dəstəkləyir. Öz sənətinizi, musiqinizi və ya podkastınızı dərc edirsinizsə, Mastodon sizin üçün buradadır. + feature_moderation_title: Olmalı olduğu şəkildə moderasiya + users: + go_to_sso_account_settings: Kimlik provayderinizin hesab ayarlarına gedin + otp_lost_help_html: Hər ikisinə də erişə bilmirsinizsə, %{email} ilə əlaqə saxlayın + seamless_external_login: Xarici bir server üzərindən giriş etdiyiniz üçün parol və e-poçt ayarları mövcud deyil. diff --git a/config/locales/be.yml b/config/locales/be.yml index 601a81b6487..2855230b156 100644 --- a/config/locales/be.yml +++ b/config/locales/be.yml @@ -1842,8 +1842,6 @@ be: ownership: Немагчыма замацаваць чужы допіс reblog: Немагчыма замацаваць пашырэнне quote_policies: - followers: Падпісчыкі і згаданыя карыстальнікі - nobody: Толькі згаданыя карыстальнікі public: Усе title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/bg.yml b/config/locales/bg.yml index cbb0b682fd3..077a729b919 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -1857,8 +1857,6 @@ bg: ownership: Публикация на някого другиго не може да бъде закачена reblog: Раздуване не може да бъде закачано quote_policies: - followers: Последователи и споменати потребители - nobody: Само споменатите потребители public: Всеки title: "%{name}: „%{quote}“" visibilities: diff --git a/config/locales/br.yml b/config/locales/br.yml index e5b9ff2559c..4bfd268d15a 100644 --- a/config/locales/br.yml +++ b/config/locales/br.yml @@ -560,6 +560,8 @@ br: one: "%{count} skeudenn" other: "%{count} skeudenn" two: "%{count} skeudenn" + errors: + quoted_status_not_found: War a seblant, n'eus ket eus an embannadenn emaoc'h o klask menegiñ. pin_errors: ownership: N'hallit ket spilhennañ embannadurioù ar re all quote_policies: diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 1c858fe4cde..7dfa928f57b 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1660,6 +1660,10 @@ ca: title: Nova menció poll: subject: Ha finalitzat l'enquesta de %{name} + quote: + body: 'La vostra publicació ha estat citada per %{name}:' + subject: "%{name} ha citat la vostra publicació" + title: Nova citació reblog: body: "%{name} ha impulsat el teu estat:" subject: "%{name} ha impulsat el teu estat" @@ -1870,6 +1874,7 @@ ca: edited_at_html: Editat %{date} errors: in_reply_not_found: El tut al qual intentes respondre sembla que no existeix. + quoted_status_not_found: Sembla que la publicació que vols citar no existeix. over_character_limit: Límit de caràcters de %{max} superat pin_errors: direct: Els tuts que només són visibles per als usuaris mencionats no poden ser fixats @@ -1877,8 +1882,8 @@ ca: ownership: No es pot fixar el tut d'algú altre reblog: No es pot fixar un impuls quote_policies: - followers: Seguidors i usuaris mencionats - nobody: Només usuaris mencionats + followers: Només els vostres seguidors + nobody: Ningú public: Tothom title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 1ad8df6e71f..5582a02740e 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -196,6 +196,7 @@ cs: create_relay: Vytvořit relay create_unavailable_domain: Vytvořit nedostupnou doménu create_user_role: Vytvořit roli + create_username_block: Vytvořit pravidlo pro uživatelská jména demote_user: Snížit roli uživatele destroy_announcement: Odstranit oznámení destroy_canonical_email_block: Odblokovat email @@ -209,6 +210,7 @@ cs: destroy_status: Odstranit Příspěvek destroy_unavailable_domain: Smazat nedostupnou doménu destroy_user_role: Zničit roli + destroy_username_block: Odstranit pravidlo pro uživatelská jména disable_2fa_user: Vypnout 2FA disable_custom_emoji: Zakázat vlastní emoji disable_relay: Deaktivovat relay @@ -243,6 +245,7 @@ cs: update_report: Upravit hlášení update_status: Aktualizovat Příspěvek update_user_role: Aktualizovat roli + update_username_block: Aktualizovat pravidlo pro uživatelská jména actions: approve_appeal_html: Uživatel %{name} schválil odvolání proti rozhodnutí moderátora %{target} approve_user_html: "%{name} schválil registraci od %{target}" @@ -261,6 +264,7 @@ cs: create_relay_html: "%{name} vytvořil*a relay %{target}" create_unavailable_domain_html: "%{name} zastavil doručování na doménu %{target}" create_user_role_html: "%{name} vytvořil %{target} roli" + create_username_block_html: "%{name} přidali pravidlo pro uživatelská jména obsahující %{target}" demote_user_html: Uživatel %{name} degradoval uživatele %{target} destroy_announcement_html: Uživatel %{name} odstranil oznámení %{target} destroy_canonical_email_block_html: "%{name} odblokoval*a e-mail s hashem %{target}" @@ -274,6 +278,7 @@ cs: destroy_status_html: Uživatel %{name} odstranil příspěvek uživatele %{target} destroy_unavailable_domain_html: "%{name} obnovil doručování na doménu %{target}" destroy_user_role_html: "%{name} odstranil %{target} roli" + destroy_username_block_html: "%{name} odstranili pravidlo pro uživatelská jména obsahující %{target}" disable_2fa_user_html: Uživatel %{name} vypnul dvoufázové ověřování pro uživatele %{target} disable_custom_emoji_html: Uživatel %{name} zakázal emoji %{target} disable_relay_html: "%{name} deaktivoval*a relay %{target}" @@ -308,6 +313,7 @@ cs: update_report_html: "%{name} aktualizoval hlášení %{target}" update_status_html: Uživatel %{name} aktualizoval příspěvek uživatele %{target} update_user_role_html: "%{name} změnil %{target} roli" + update_username_block_html: "%{name} aktualizovali pravidlo pro uživatelská jména obsahující %{target}" deleted_account: smazaný účet empty: Nebyly nalezeny žádné záznamy. filter_by_action: Filtrovat podle akce @@ -1121,6 +1127,25 @@ cs: other: Použit %{count} lidmi za poslední týden title: Doporučení & Trendy trending: Populární + username_blocks: + add_new: Přidat + block_registrations: Blokovat registrace + comparison: + contains: Obsahuje + equals: Rovná se + contains_html: Obsahuje %{string} + created_msg: Vytvořeno pravidlo pro uživatelská jména + delete: Smazat + edit: + title: Upravit pravidlo pro uživatelská jména + matches_exactly_html: Rovná se %{string} + new: + create: Vytvořit pravidlo + title: Vytvořit nové pravidlo pro uživatelská jména + no_username_block_selected: Nebyla změněna žádná pravidla pro uživatelská jména, protože žádná nebyla vybrána + not_permitted: Není povoleno + title: Pravidla uživatelských jmen + updated_msg: Pravidlo pro jména uživatelů bylo úspěšně aktualizováno warning_presets: add_new: Přidat nové delete: Smazat @@ -1740,6 +1765,10 @@ cs: title: Nová zmínka poll: subject: Anketa od %{name} skončila + quote: + body: 'Váš příspěvek citoval účet %{name}:' + subject: "%{name} citovali váš příspěvek" + title: Nová citace reblog: body: 'Uživatel %{name} boostnul váš příspěvek:' subject: Uživatel %{name} boostnul váš příspěvek @@ -1958,6 +1987,7 @@ cs: edited_at_html: Upraven %{date} errors: in_reply_not_found: Příspěvek, na který se pokoušíte odpovědět, neexistuje. + quoted_status_not_found: Zdá se, že příspěvek, který se pokoušíte citovat neexistuje. over_character_limit: byl překročen limit %{max} znaků pin_errors: direct: Příspěvky viditelné pouze zmíněným uživatelům nelze připnout @@ -1965,8 +1995,8 @@ cs: ownership: Nelze připnout příspěvek někoho jiného reblog: Boosty nelze připnout quote_policies: - followers: Sledující a zmínění uživatelé - nobody: Pouze zmínění uživatelé + followers: Pouze vaši sledující + nobody: Nikdo public: Všichni title: "%{name}: „%{quote}“" visibilities: diff --git a/config/locales/cy.yml b/config/locales/cy.yml index d7242fbf2e1..e8990d34820 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -2043,6 +2043,7 @@ cy: edited_at_html: Wedi'i olygu %{date} errors: in_reply_not_found: Nid yw'n ymddangos bod y postiad rydych chi'n ceisio ei ateb yn bodoli. + quoted_status_not_found: Nid yw'n ymddangos bod y postiad rydych chi'n ceisio'i ddyfynnu yn bodoli. over_character_limit: wedi mynd y tu hwnt i'r terfyn nodau o %{max} pin_errors: direct: Nid oes modd pinio postiadau sy'n weladwy i ddefnyddwyr a grybwyllwyd yn unig @@ -2050,8 +2051,6 @@ cy: ownership: Nid oes modd pinio postiad rhywun arall reblog: Nid oes modd pinio hwb quote_policies: - followers: Dilynwyr a defnyddwyr wedi'u crybwyll - nobody: Dim ond defnyddwyr wedi'u crybwyll public: Pawb title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/da.yml b/config/locales/da.yml index b822ae9cb17..20e3173c5ab 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -190,6 +190,7 @@ da: create_relay: Opret Videresendelse create_unavailable_domain: Opret Utilgængeligt Domæne create_user_role: Opret rolle + create_username_block: Opret brugernavn-regel demote_user: Degradér bruger destroy_announcement: Slet bekendtgørelse destroy_canonical_email_block: Slet e-mailblokering @@ -203,6 +204,7 @@ da: destroy_status: Slet indlæg destroy_unavailable_domain: Slet Utilgængeligt Domæne destroy_user_role: Ødelæg rolle + destroy_username_block: Slet brugernavn-regel disable_2fa_user: Deaktivér 2FA disable_custom_emoji: Deaktivér tilpasset emoji disable_relay: Deaktivér Videresendelse @@ -237,6 +239,7 @@ da: update_report: Opdatér anmeldelse update_status: Opdatér indlæg update_user_role: Opdatér rolle + update_username_block: Opdatér brugernavn-regel actions: approve_appeal_html: "%{name} godkendte moderationsafgørelsesappellen fra %{target}" approve_user_html: "%{name} godkendte tilmeldingen fra %{target}" @@ -255,6 +258,7 @@ da: create_relay_html: "%{name} oprettede videresendelsen %{target}" create_unavailable_domain_html: "%{name} stoppede levering til domænet %{target}" create_user_role_html: "%{name} oprettede %{target}-rolle" + create_username_block_html: "%{name} tilføjede regel for brugernavne indeholdende %{target}" demote_user_html: "%{name} degraderede brugeren %{target}" destroy_announcement_html: "%{name} slettede bekendtgørelsen %{target}" destroy_canonical_email_block_html: "%{name} afblokerede e-mailen med hash'et %{target}" @@ -268,6 +272,7 @@ da: destroy_status_html: "%{name} fjernede indlægget fra %{target}" destroy_unavailable_domain_html: "%{name} genoptog levering til domænet %{target}" destroy_user_role_html: "%{name} slettede %{target}-rolle" + destroy_username_block_html: "%{name} fjernede regel for brugernavne indeholdende %{target}" disable_2fa_user_html: "%{name} deaktiverede tofaktorkravet for brugeren %{target}" disable_custom_emoji_html: "%{name} deaktiverede emojien %{target}" disable_relay_html: "%{name} deaktiverede videresendelsen %{target}" @@ -302,6 +307,7 @@ da: update_report_html: "%{name} opdaterede rapporten %{target}" update_status_html: "%{name} opdaterede indlægget fra %{target}" update_user_role_html: "%{name} ændrede %{target}-rolle" + update_username_block_html: "%{name} opdaterede regel for brugernavne indeholdende %{target}" deleted_account: slettet konto empty: Ingen logger fundet. filter_by_action: Filtrér efter handling @@ -1085,6 +1091,25 @@ da: other: Brugt af %{count} personer den seneste uge title: Anbefalinger og trends trending: Trender + username_blocks: + add_new: Tilføj ny + block_registrations: Blokér registreringer + comparison: + contains: Indeholder + equals: Er lig + contains_html: Indeholder %{string} + created_msg: Brugernavn-regel er hermed oprettet + delete: Slet + edit: + title: Redigér brugernavn-regel + matches_exactly_html: Er lig med %{string} + new: + create: Opret regel + title: Opret ny brugernavn-regel + no_username_block_selected: Ingen brugernavn-regler ændret (ingen var valgt) + not_permitted: Ikke tilladt + title: Brugernavn-regler + updated_msg: Brugernavn-regel opdateret warning_presets: add_new: Tilføj ny delete: Slet @@ -1205,7 +1230,7 @@ da: prefix_invited_by_user: "@%{name} inviterer dig ind på denne Mastodon-server!" prefix_sign_up: Tilmeld dig Mastodon i dag! suffix: Du vil med en konto kunne følge personer, indsende opdateringer og udveksle beskeder med brugere fra enhver Mastodon-server, og meget mere! - didnt_get_confirmation: Intet bekræftelseslink modtaget? + didnt_get_confirmation: Ikke modtaget bekræftelseslink? dont_have_your_security_key: Har ikke din sikkerhedsnøgle? forgot_password: Glemt din adgangskode? invalid_reset_password_token: Adgangskodenulstillingstoken ugyldigt eller udløbet. Anmod om et nyt. @@ -1394,7 +1419,7 @@ da: filters: contexts: account: Profiler - home: Hjemmetidslinje + home: Hjem og lister notifications: Notifikationer public: Offentlig tidslinje thread: Samtaler @@ -1403,7 +1428,7 @@ da: keywords: Nøgleord statuses: Individuelle indlæg statuses_hint_html: Dette filter gælder for udvalgte, individuelle indlæg, uanset om de matcher nøgleordene nedenfor. Gennemgå eller fjern indlæg fra filteret. - title: Redigere filter + title: Rediger filter errors: deprecated_api_multiple_keywords: Disse parametre kan ikke ændres fra denne applikation, da de gælder for flere end ét filternøgleord. Brug en nyere applikation eller webgrænsefladen. invalid_context: Ingen eller ugyldig kontekst angivet @@ -1662,6 +1687,10 @@ da: title: Ny omtale poll: subject: En afstemning fra %{name} er afsluttet + quote: + body: 'Dit indlæg blev citeret af %{name}:' + subject: "%{name} citerede dit indlæg" + title: Nyt citat reblog: body: 'Dit indlæg blev fremhævet af %{name}:' subject: "%{name} fremhævede dit indlæg" @@ -1719,7 +1748,7 @@ da: privacy: Privatliv privacy_hint_html: Styr, hvor meget du vil afsløre til gavn for andre. Folk opdager interessante profiler og apps ved at gennemse andres følgere og se, hvilke apps de sender fra, men du foretrækker måske at holde det skjult. reach: Rækkevidde - reach_hint_html: Indstil om du vil blive opdaget og fulgt af nye mennesker. Ønsker du, at dine indlæg skal vises på Udforsk-siden? Ønsker du, at andre skal se dig i deres følg-anbefalinger? Ønsker du at acceptere alle nye følgere automatisk, eller vil du have detaljeret kontrol over hver og en? + reach_hint_html: Indstil, om du vil opdages og følges af nye personer. Vil du have, at dine indlæg skal vises på Udforsk-siden? Vil du have, at andre skal kunne se dig i deres følg-anbefalinger? Vil du acceptere alle nye følgere automatisk eller have detaljeret kontrol over hver enkelt? search: Søgning search_hint_html: Indstil hvordan du vil findes. Ønsker du, at folk skal finde dig gennem hvad du har skrevet offentligt? Vil du have folk udenfor Mastodon til at finde din profil, når de søger på nettet? Vær opmærksom på, at det ikke kan garanteres at dine offentlige indlæg er udelukket fra alle søgemaskiner. title: Fortrolighed og rækkevidde @@ -1872,6 +1901,7 @@ da: edited_at_html: Redigeret %{date} errors: in_reply_not_found: Indlægget, der forsøges besvaret, ser ikke ud til at eksistere. + quoted_status_not_found: Indlægget, du forsøger at citere, ser ikke ud til at eksistere. over_character_limit: grænsen på %{max} tegn overskredet pin_errors: direct: Indlæg, som kun kan ses af omtalte brugere, kan ikke fastgøres @@ -1879,8 +1909,8 @@ da: ownership: Andres indlæg kan ikke fastgøres reblog: En fremhævelse kan ikke fastgøres quote_policies: - followers: Følgere og nævnte brugere - nobody: Kun nævnte brugere + followers: Kun dine følgere + nobody: Ingen public: Alle title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/de.yml b/config/locales/de.yml index 1eb5ef1c4f9..69ff2ac5454 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -190,6 +190,7 @@ de: create_relay: Relay erstellen create_unavailable_domain: Nicht verfügbare Domain erstellen create_user_role: Rolle erstellen + create_username_block: Regel für Profilnamen erstellen demote_user: Benutzer*in herabstufen destroy_announcement: Ankündigung löschen destroy_canonical_email_block: E-Mail-Sperre entfernen @@ -203,6 +204,7 @@ de: destroy_status: Beitrag entfernen destroy_unavailable_domain: Nicht-verfügbare Domain entfernen destroy_user_role: Rolle entfernen + destroy_username_block: Regel für Profilnamen löschen disable_2fa_user: 2FA deaktivieren disable_custom_emoji: Eigenes Emoji deaktivieren disable_relay: Relay deaktivieren @@ -237,6 +239,7 @@ de: update_report: Meldung aktualisieren update_status: Beitrag aktualisieren update_user_role: Rolle bearbeiten + update_username_block: Regel für Profilnamen aktualisieren actions: approve_appeal_html: "%{name} hat den Einspruch gegen eine Moderationsentscheidung von %{target} genehmigt" approve_user_html: "%{name} genehmigte die Registrierung von %{target}" @@ -255,6 +258,7 @@ de: create_relay_html: "%{name} erstellte ein Relay %{target}" create_unavailable_domain_html: "%{name} beendete die Zustellung an die Domain %{target}" create_user_role_html: "%{name} erstellte die Rolle %{target}" + create_username_block_html: "%{name} erstellte eine Regel für Profilnamen mit %{target}" demote_user_html: "%{name} stufte %{target} herunter" destroy_announcement_html: "%{name} löschte die Ankündigung %{target}" destroy_canonical_email_block_html: "%{name} entsperrte die E-Mail mit dem Hash %{target}" @@ -268,6 +272,7 @@ de: destroy_status_html: "%{name} entfernte einen Beitrag von %{target}" destroy_unavailable_domain_html: "%{name} nahm die Zustellung an die Domain %{target} wieder auf" destroy_user_role_html: "%{name} löschte die Rolle %{target}" + destroy_username_block_html: "%{name} entfernte eine Regel für Profilnamen mit %{target}" disable_2fa_user_html: "%{name} deaktivierte die Zwei-Faktor-Authentisierung für %{target}" disable_custom_emoji_html: "%{name} deaktivierte das Emoji %{target}" disable_relay_html: "%{name} deaktivierte das Relay %{target}" @@ -302,6 +307,7 @@ de: update_report_html: "%{name} überarbeitete die Meldung %{target}" update_status_html: "%{name} überarbeitete einen Beitrag von %{target}" update_user_role_html: "%{name} änderte die Rolle von %{target}" + update_username_block_html: "%{name} aktualisierte eine Regel für Profilnamen mit %{target}" deleted_account: gelöschtes Konto empty: Protokolle nicht gefunden. filter_by_action: Nach Aktion filtern @@ -1085,6 +1091,25 @@ de: other: In den vergangenen 7 Tagen von %{count} Profilen verwendet title: Empfehlungen & Trends trending: Angesagt + username_blocks: + add_new: Neue Regel + block_registrations: Registrierungen sperren + comparison: + contains: Enthält + equals: Entspricht + contains_html: Enthält %{string} + created_msg: Regel für Profilnamen erfolgreich erstellt + delete: Löschen + edit: + title: Regel für Profilnamen bearbeiten + matches_exactly_html: Entspricht %{string} + new: + create: Regel erstellen + title: Neue Regel für Profilnamen erstellen + no_username_block_selected: Keine Regeln für Profilnamen wurden geändert, weil keine ausgewählt wurde(n) + not_permitted: Nicht gestattet + title: Regeln für Profilnamen + updated_msg: Regel für Profilnamen erfolgreich aktualisiert warning_presets: add_new: Neu hinzufügen delete: Löschen @@ -1662,6 +1687,10 @@ de: title: Neue Erwähnung poll: subject: Eine Umfrage von %{name} ist beendet + quote: + body: 'Dein Beitrag wurde von %{name} zitiert:' + subject: "%{name} zitierte deinen Beitrag" + title: Neuer zitierter Beitrag reblog: body: 'Dein Beitrag wurde von %{name} geteilt:' subject: "%{name} teilte deinen Beitrag" @@ -1872,6 +1901,7 @@ de: edited_at_html: 'Bearbeitet: %{date}' errors: in_reply_not_found: Der Beitrag, auf den du antworten möchtest, scheint nicht zu existieren. + quoted_status_not_found: Der Beitrag, den du zitieren möchtest, scheint nicht zu existieren. over_character_limit: Begrenzung von %{max} Zeichen überschritten pin_errors: direct: Beiträge, die nur für erwähnte Profile sichtbar sind, können nicht angeheftet werden @@ -1879,8 +1909,8 @@ de: ownership: Du kannst nur eigene Beiträge anheften reblog: Du kannst keine geteilten Beiträge anheften quote_policies: - followers: Follower und erwähnte Profile - nobody: Nur erwähnte Profile + followers: Nur meine Follower + nobody: Niemand public: Alle title: "%{name}: „%{quote}“" visibilities: diff --git a/config/locales/devise.az.yml b/config/locales/devise.az.yml index 6a02cd7e497..d472634f0a1 100644 --- a/config/locales/devise.az.yml +++ b/config/locales/devise.az.yml @@ -4,16 +4,16 @@ az: confirmations: confirmed: E-poçt ünvanınız uğurla təsdiqləndi. send_instructions: Bir neçə dəqiqə ərzində e-poçt ünvanınızı necə təsdiqləyəcəyinizə dair təlimatları olan bir e-məktub alacaqsınız. Bu e-məktubu almamısınızsa, spam qovluğunuzu yoxlayın. - send_paranoid_instructions: E-poçt ünvanınız verilənlər bazamızda varsa, bir neçə dəqiqədən sonra e-poçt ünvanınızı necə təsdiqləyəcəyinizə dair təlimatları olan bir e-məktub alacaqsınız. Bu e-məktubu almamısınızsa, spam qovluğunuzu yoxlayın. + send_paranoid_instructions: E-poçt ünvanınız veri bazamızda varsa, bir neçə dəqiqə sonra e-poçt ünvanınızı necə təsdiqləyəcəyinizə dair təlimatları olan bir e-poçt alacaqsınız. Bu e-poçtu almamısınızsa, spam qovluğunuzu yoxlayın. failure: already_authenticated: Siz artıq daxil olmusunuz. inactive: Hesabınız hələ aktivləşdirilməyib. - invalid: Səhv %{authentication_keys} və ya parol. + invalid: Yararsız %{authentication_keys} və ya parol. last_attempt: Hesabınız blok olmamışdan əvvəl bir dəfə də cəhdiniz var. - locked: Hesabınız bloklandı. - not_found_in_database: Səhv %{authentication_keys} və ya parol. + locked: Hesabınız kilidlənib. + not_found_in_database: Yararsız %{authentication_keys} və ya parol. omniauth_user_creation_failure: Bu kimlik üçün hesab yaradarkən xəta. - pending: Hesabınız hələ yoxlanışdadır. + pending: Hesabınız hələ incələnir. timeout: Sessiyanın vaxtı bitdi. Xahiş edirik davam etmək üçün yenidən daxil olun. unauthenticated: Davam etmək üçün daxil olmaq və ya qeydiyyatdan keçmək lazımdır. unconfirmed: Davam etmək üçün e-poçt ünvanınızı təsdiqləməlisiniz. @@ -22,25 +22,81 @@ az: action: E-poçt ünvanını təsdiqlə action_with_app: Təsdiqlə və %{app}-a geri qayıt explanation: Siz %{host} saytında bu e-poçt ilə hesab yaratmısınız. Onu aktivləşdirməkdən bir klik uzaqlıqdasınız. Əgər bu siz olmamısınızsa, zəhmət olmasa, bu e-məktuba məhəl qoymayın. - explanation_when_pending: Bu e-poçt ünvanı ilə %{host} saytına dəvət üçün müraciət etmisiniz. Siz e-poçt ünvanınızı təsdiqlədikdən sonra müraciətinizi nəzərdən keçirəcəyik. Siz məlumatlarınızı dəyişdirmək və ya hesabınızı silmək üçün daxil ola bilərsiniz, lakin hesabınız təsdiqlənənə qədər əksər funksiyaları istifadə edə bilməzsiniz. Müraciətiniz rədd edilərsə, məlumatlarınız silinəcək, buna görə də sizdən heç bir tədbir tələb olunmayacaq. Əgər bu siz deyildinizsə, zəhmət, bu e-məktuba məhəl qoymayın. + explanation_when_pending: Bu e-poçt ünvanı ilə %{host} ünvanına dəvət üçün müraciət etmisiniz. E-poçt ünvanınızı təsdiqlədikdən sonra müraciətinizi nəzərdən keçirəcəyik. Hesab məlumatlarını dəyişdirmək və ya hesabınızı silmək üçün giriş edə bilərsiniz, ancaq hesabınız təsdiqlənənə qədər əksər funksiyalara erişə bilməyəcəksiniz. Müraciətinizə rədd cavabı gəlsə, veriləriniz silinəcək, sizdən heç bir əməliyyat etməyiniz istənilməyəcək. Əgər bu siz deyilsinizsə, lütfən bu e-poçtu yox sayın. extra_html: Həmçinin zəhmət olmasa, serverin qaydalarınıistifadə şərtlərini oxuyun. subject: 'Mastodon: %{instance} üçün təsdiqlənmə təlimatları' title: E-poçt ünvanını təsdiqlə email_changed: explanation: 'Hesabınız üçün e-poçt ünvanı buna dəyişdirilir:' - extra: E-poçtunuzu dəyişməmisinizsə, çox güman ki, kimsə hesabınıza giriş əldə edib. Zəhmət olmasa, parolunuzu dərhal dəyişdirin və ya hesabınıza daxil ola bilməyəcəksinizsə, server admini ilə əlaqə saxlayın. + extra: E-poçtunuzu dəyişməmisinizsə, çox güman ki, kimsə hesabınıza erişib. Hesabınıza giriş edə bilmirsinizsə, lütfən parolunuzu dərhal dəyişdirin və ya server admini ilə əlaqə saxlayın. subject: 'Mastodon: E-poçt dəyişdirildi' title: Yeni e-poçt ünvanı password_change: explanation: Hesabınızın parolu dəyişdirilib. - extra: Parolunuzu dəyişməmisinizsə, çox güman ki, kimsə hesabınıza giriş əldə edib. Zəhmət olmasa, parolunuzu dərhal dəyişdirin və ya hesabınıza daxil ola bilməyəcəksinizsə, server admini ilə əlaqə saxlayın. + extra: Parolunuzu dəyişməmisinizsə, çox güman ki, kimsə hesabınıza erişib. Hesabınıza giriş edə bilmirsinizsə, lütfən parolunuzu dərhal dəyişdirin və ya server admini ilə əlaqə saxlayın. subject: 'Mastodon: Parol dəyişdirildi' title: Parol dəyişdirildi reconfirmation_instructions: explanation: E-poçtunuzu dəyişdirmək üçün yeni ünvanı təsdiqləyin. - extra: Əgər bu dəyişiklik sizin tərəfinizdən deyilsə, zəhmət olmasa, bu e-məktuba məhəl qoymayın. Siz yuxarıdakı linkə daxil olana qədər Mastodon hesabının e-poçt ünvanı dəyişməyəcək. + extra: Bu dəyişikliyi siz etməmisinizsə, lütfən bu e-poçtu yox sayın. Yuxarıdakı keçidə erişənə qədər Mastodon hesabının e-poçt ünvanı dəyişməyəcək. subject: 'Mastodon: %{instance} üçün e-poçtu təsdiqlə' title: E-poçt ünvanını təsdiqlə reset_password_instructions: action: Parolu dəyiş explanation: Siz hesabınız üçün yeni parol tələb etmisiniz. + extra: Bunu siz tələb etməmisinizsə, lütfən bu e-poçtu yox sayın. Parolunuz, yuxarıdakı keçidə erişənə və siz yeni birini yaradana qədər dəyişməyəcək. + subject: 'Mastodon: Parol sıfırlama təlimatları' + title: Parolu sıfırla + two_factor_disabled: + explanation: Giriş etmək, indi yalnız e-poçt ünvanı və parol ilə mümkündür. + subject: 'Mastodon: İki faktorlu kimlik doğrulama sıradan çıxarılıb' + subtitle: İki faktorlu kimlik doğrulama hesabınız üçün sıradan çıxarılıb. + title: 2FA sıradan çıxarılıb + two_factor_enabled: + explanation: Giriş etmək üçün cütləşdirilmiş TOTP tətbiqi tərəfindən yaradılmış bir token tələb olunur. + subject: 'Mastodon: İki faktorlu kimlik doğrulama fəaldır' + subtitle: İki faktorlu kimlik doğrulama hesabınız üçün fəallaşdırılıb. + title: 2FA fəaldır + two_factor_recovery_codes_changed: + explanation: Əvvəlki geri qaytarma kodları yararsız sayıldı və yeniləri yaradıldı. + subject: 'Mastodon: İki faktorlu geri qaytarma kodları təkrar yaradılıb' + subtitle: Əvvəlki geri qaytarma kodları yararsız sayıldı və yeniləri yaradıldı. + title: 2FA geri qaytarma kodları dəyişdirilib + unlock_instructions: + subject: 'Mastodon: Kilid açma təlimatları' + webauthn_credential: + added: + explanation: Aşağıdakı güvənlik açarı, hesabınıza əlavə edilib + subject: 'Mastodon: Yeni güvənlik açarı' + title: Yeni bir güvənlik açarı əlavə edilib + deleted: + explanation: Aşağıdakı güvənlik açarı, hesabınızdan silinib + subject: 'Mastodon: Güvənlik açarı silindi' + title: Güvənlik açarlarınızdan biri silinib + webauthn_disabled: + explanation: Güvənlik açarı ilə kimlik doğrulama hesabınız üçün sıradan çıxarılıb. + extra: Giriş, artıq yalnız cütləşdirilmiş TOTP tətbiqi tərəfindən yaradılmış token ilə mümkündür. + subject: 'Mastodon: Güvənlik açarları ilə kimlik doğrulama sıradan çıxarılıb' + title: Güvənlik açarları sıradan çıxarılıb + webauthn_enabled: + explanation: Güvənlik açarı ilə kimlik doğrulama hesabınız üçün fəallaşdırılıb. + extra: Güvənlik açarınız artıq giriş üçün istifadə edilə bilər + subject: 'Mastodon: Güvənlik açarı ilə kimlik doğrulama fəaldır' + title: Güvənlik açarları fəaldır + omniauth_callbacks: + failure: '%{kind} üzərindən kimliyiniz doğrulana bilmədi, çünki "%{reason}".' + success: "%{kind} hesabından kimliyiniz uğurla doğrulandı." + passwords: + no_token: Parol sıfırlama e-poçtunu istifadə etmədən bu səhifəyə erişə bilməzsiniz. Əgər parol sıfırlama e-poçtu ilə bura gəlmisinizsə, lütfən verilmiş tam URL-ni istifadə etdiyinizə əmin olun. + send_instructions: E-poçt ünvanınız veri bazamızda varsa, bir neçə dəqiqə sonra e-poçt ünvanınızda parolu geri qaytarma keçidini alacaqsınız. Bu e-poçtu almamısınızsa, spam qovluğunuzu yoxlayın. + send_paranoid_instructions: E-poçt ünvanınız veri bazamızda varsa, bir neçə dəqiqə sonra e-poçt ünvanınızda parolu geri qaytarma keçidini alacaqsınız. Bu e-poçtu almamısınızsa, spam qovluğunuzu yoxlayın. + updated: Parolunuz uğurla dəyişdirildi. Artıq hesabınıza daxil olmusunuz. + updated_not_active: Parolunuz uğurla dəyişdirildi. + registrations: + destroyed: Xudahafiz! Hesabınız uğurla ləğv edilib. Sizi tezliklə yenidən görməyi ümid edirik. + update_needs_confirmation: Hesabınızı uğurla güncəllədiniz, ancaq yeni e-poçt ünvanını doğrulamağımız lazımdır. Lütfən e-poçtunuzu yoxlayın, yeni e-poçt ünvanınızı təsdiqləmək üçün təsdiq keçidini izləyin. Əgər bu e-poçtu almamısınızsa spam qovluğunu yoxlayın. + updated: Hesabınız uğurla güncəllənib. + sessions: + already_signed_out: Uğurla hesabdan çıxış edildi. + signed_in: Uğurla hesaba daxil olundu. + signed_out: Uğurla hesabdan çıxış edildi. diff --git a/config/locales/devise.ru.yml b/config/locales/devise.ru.yml index 79a988f1898..cc95f730615 100644 --- a/config/locales/devise.ru.yml +++ b/config/locales/devise.ru.yml @@ -53,7 +53,7 @@ ru: subtitle: Двухфакторная аутентификация отключена для вашей учетной записи. title: 2FA отключена two_factor_enabled: - explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением TOTP. + explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением-аутентификатором. subject: 'Mastodon: Двухфакторная аутентификация включена' subtitle: Двухфакторная аутентификация включена для вашей учётной записи. title: 2FA включена @@ -75,7 +75,7 @@ ru: title: Один из ваших электронных ключей удалён webauthn_disabled: explanation: Аутентификация по электронным ключам деактивирована для вашей учетной записи. - extra: Теперь вход возможен с использованием только лишь одноразового кода, сгенерированного сопряжённым приложением TOTP. + extra: Теперь вход возможен с использованием только с помощью одноразового кода, сгенерированного сопряжённым приложением-аутентификатором. subject: 'Mastodon: Аутентификация по электронным ключам деактивирована' title: Вход по электронным ключам деактивирован webauthn_enabled: diff --git a/config/locales/doorkeeper.az.yml b/config/locales/doorkeeper.az.yml index e9ba86bc793..aac8511a1cc 100644 --- a/config/locales/doorkeeper.az.yml +++ b/config/locales/doorkeeper.az.yml @@ -1 +1,26 @@ +--- az: + doorkeeper: + errors: + messages: + invalid_token: + expired: Erişim tokeninin vaxtı bitib + revoked: Erişim tokeni ləğv edilib + unknown: Erişim tokeni yararsızdır + grouped_scopes: + access: + read: Yalnız oxuma erişimi + read/write: Oxuma və yazma erişimi + write: Yalnız yazma erişimi + title: + all: Mastodon hesabınıza tam erişim + scopes: + admin:read: serverdəki bütün veriləri oxuma + admin:write: serverdəki bütün veriləri dəyişdirmə + admin:write:accounts: hesablarda moderasiya əməliyyatlarını icra et + admin:write:canonical_email_blocks: + admin:write:domain_allows: domen icazələri üzərində moderasiya əməliyyatlarını icra et + admin:write:ip_blocks: IP əngəlləmələri üzrə moderasiya əməliyyatlarını icra et + admin:write:reports: hesabatlarda moderasiya əməliyyatlarını icra et + read: hesabınızın bütün verilərini oxuma + write: hesabınızın bütün verilərini dəyişdirmə diff --git a/config/locales/el.yml b/config/locales/el.yml index f7c3df0f54e..cddba5cdfbe 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -190,6 +190,7 @@ el: create_relay: Δημιουργία Relay create_unavailable_domain: Δημιουργία Μη Διαθέσιμου Τομέα create_user_role: Δημιουργία Ρόλου + create_username_block: Δημιουργία Κανόνα Ονόματος Χρήστη demote_user: Υποβιβασμός Χρήστη destroy_announcement: Διαγραφή Ανακοίνωσης destroy_canonical_email_block: Διαγραφή Αποκλεισμού Email @@ -203,6 +204,7 @@ el: destroy_status: Διαγραφή Ανάρτησης destroy_unavailable_domain: Διαγραφή Μη Διαθέσιμου Τομέα destroy_user_role: Καταστροφή Ρόλου + destroy_username_block: Διαγραφή Κανόνα Ονόματος Χρήστη disable_2fa_user: Απενεργοποίηση 2FA disable_custom_emoji: Απενεργοποίηση Προσαρμοσμένων Emoji disable_relay: Απενεργοποίηση Relay @@ -237,6 +239,7 @@ el: update_report: Ενημέρωση Αναφοράς update_status: Ενημέρωση Ανάρτησης update_user_role: Ενημέρωση ρόλου + update_username_block: Ενημέρωση Κανόνα Ονόματος Χρήστη actions: approve_appeal_html: Ο/Η %{name} ενέκρινε την ένσταση της απόφασης των συντονιστών από %{target} approve_user_html: ο/η %{name} ενέκρινε την εγγραφή του %{target} @@ -255,6 +258,7 @@ el: create_relay_html: Ο χρήστης %{name} δημιούργησε ένα relay %{target} create_unavailable_domain_html: Ο/Η %{name} σταμάτησε να τροφοδοτεί τον τομέα %{target} create_user_role_html: Ο/Η %{name} δημιούργησε ρόλο %{target} + create_username_block_html: "%{name} πρόσθεσε κανόνα για ονόματα χρηστών που περιέχουν %{target}" demote_user_html: Ο/Η %{name} υποβίβασε τον χρήστη %{target} destroy_announcement_html: Ο/Η %{name} διέγραψε την ανακοίνωση %{target} destroy_canonical_email_block_html: Ο χρήστης %{name} έκανε άρση αποκλεισμού email με το hash %{target} @@ -268,6 +272,7 @@ el: destroy_status_html: Ο/Η %{name} αφαίρεσε την ανάρτηση του/της %{target} destroy_unavailable_domain_html: Ο/Η %{name} ξανάρχισε να τροφοδοτεί το domain %{target} destroy_user_role_html: Ο/Η %{name} διέγραψε τον ρόλο του %{target} + destroy_username_block_html: "%{name} αφαίρεσε κανόνα για ονόματα χρηστών που περιέχουν %{target}" disable_2fa_user_html: Ο/Η %{name} απενεργοποίησε την απαίτηση για ταυτοποίηση δύο παραγόντων για τον χρήστη %{target} disable_custom_emoji_html: Ο/Η %{name} απενεργοποίησε το emoji %{target} disable_relay_html: Ο χρήστης %{name} απενεργοποίησε το relay %{target} @@ -302,6 +307,7 @@ el: update_report_html: Ο χρήστης %{name} ενημέρωσε την αναφορά %{target} update_status_html: Ο/Η %{name} ενημέρωσε την ανάρτηση του/της %{target} update_user_role_html: Ο/Η %{name} άλλαξε τον ρόλο %{target} + update_username_block_html: "%{name} ενημέρωσε κανόνα για ονόματα χρηστών που περιέχουν %{target}" deleted_account: διαγεγραμμένος λογαριασμός empty: Δεν βρέθηκαν αρχεία καταγραφής. filter_by_action: Φιλτράρισμα ανά ενέργεια @@ -1085,6 +1091,25 @@ el: other: Χρησιμοποιήθηκε από %{count} άτομα την τελευταία εβδομάδα title: Προτάσεις και τάσεις trending: Τάσεις + username_blocks: + add_new: Προσθήκη νέου + block_registrations: Φραγή εγγραφών + comparison: + contains: Περιέχει + equals: Ισούται + contains_html: Περιέχει %{string} + created_msg: Ο κανόνας ονόματος χρήστη δημιουργήθηκε με επιτυχία + delete: Διαγραφή + edit: + title: Επεξεργασία κανόνα ονόματος χρήστη + matches_exactly_html: Ισούται με %{string} + new: + create: Δημιουργία κανόνα + title: Δημιουργία νέου κανόνα ονόματος χρήστη + no_username_block_selected: Δεν άλλαξαν οι κανόνες ονόματος χρήστη καθώς κανένας δεν επιλέχθηκε + not_permitted: Δεν επιτρέπεται + title: Κανόνες ονόματος χρήστη + updated_msg: Ο κανόνας ονόματος χρήστη ενημερώθηκε με επιτυχία warning_presets: add_new: Πρόσθεση νέου delete: Διαγραφή @@ -1662,6 +1687,10 @@ el: title: Νέα επισήμανση poll: subject: Μια δημοσκόπηση του %{name} έληξε + quote: + body: 'Η ανάρτησή σου παρατέθηκε από %{name}:' + subject: Ο/Η %{name} έκανε παράθεση της ανάρτησής σου + title: Νέα παράθεση reblog: body: 'Η ανάρτησή σου ενισχύθηκε από τον/την %{name}:' subject: Ο/Η %{name} ενίσχυσε την ανάρτηση σου @@ -1872,6 +1901,7 @@ el: edited_at_html: Επεξεργάστηκε στις %{date} errors: in_reply_not_found: Η ανάρτηση στην οποία προσπαθείς να απαντήσεις δεν φαίνεται να υπάρχει. + quoted_status_not_found: Η ανάρτηση την οποία προσπαθείς να παραθέσεις δεν φαίνεται να υπάρχει. over_character_limit: υπέρβαση μέγιστου ορίου %{max} χαρακτήρων pin_errors: direct: Αναρτήσεις που είναι ορατές μόνο στους αναφερόμενους χρήστες δεν μπορούν να καρφιτσωθούν @@ -1879,8 +1909,8 @@ el: ownership: Δεν μπορείς να καρφιτσώσεις ανάρτηση κάποιου άλλου reblog: Οι ενισχύσεις δεν καρφιτσώνονται quote_policies: - followers: Ακόλουθοι και αναφερόμενοι χρήστες - nobody: Μόνο αναφερόμενοι χρήστες + followers: Μόνο οι ακόλουθοί σου + nobody: Κανένας public: Όλοι title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index a65ee021318..564900027c1 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -1879,8 +1879,6 @@ en-GB: ownership: Someone else's post cannot be pinned reblog: A boost cannot be pinned quote_policies: - followers: Followers and mentioned users - nobody: Only mentioned users public: Everyone title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/en.yml b/config/locales/en.yml index 4df63f4c738..06db5e3cfce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -190,6 +190,7 @@ en: create_relay: Create Relay create_unavailable_domain: Create Unavailable Domain create_user_role: Create Role + create_username_block: Create Username Rule demote_user: Demote User destroy_announcement: Delete Announcement destroy_canonical_email_block: Delete Email Block @@ -203,6 +204,7 @@ en: destroy_status: Delete Post destroy_unavailable_domain: Delete Unavailable Domain destroy_user_role: Destroy Role + destroy_username_block: Delete Username Rule disable_2fa_user: Disable 2FA disable_custom_emoji: Disable Custom Emoji disable_relay: Disable Relay @@ -237,6 +239,7 @@ en: update_report: Update Report update_status: Update Post update_user_role: Update Role + update_username_block: Update Username Rule actions: approve_appeal_html: "%{name} approved moderation decision appeal from %{target}" approve_user_html: "%{name} approved sign-up from %{target}" @@ -255,6 +258,7 @@ en: create_relay_html: "%{name} created a relay %{target}" create_unavailable_domain_html: "%{name} stopped delivery to domain %{target}" create_user_role_html: "%{name} created %{target} role" + create_username_block_html: "%{name} added rule for usernames containing %{target}" demote_user_html: "%{name} demoted user %{target}" destroy_announcement_html: "%{name} deleted announcement %{target}" destroy_canonical_email_block_html: "%{name} unblocked email with the hash %{target}" @@ -268,6 +272,7 @@ en: destroy_status_html: "%{name} removed post by %{target}" destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}" destroy_user_role_html: "%{name} deleted %{target} role" + destroy_username_block_html: "%{name} removed rule for usernames containing %{target}" disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}" disable_custom_emoji_html: "%{name} disabled emoji %{target}" disable_relay_html: "%{name} disabled the relay %{target}" @@ -302,6 +307,7 @@ en: update_report_html: "%{name} updated report %{target}" update_status_html: "%{name} updated post by %{target}" update_user_role_html: "%{name} changed %{target} role" + update_username_block_html: "%{name} updated rule for usernames containing %{target}" deleted_account: deleted account empty: No logs found. filter_by_action: Filter by action @@ -1085,6 +1091,25 @@ en: other: Used by %{count} people over the last week title: Recommendations & Trends trending: Trending + username_blocks: + add_new: Add new + block_registrations: Block registrations + comparison: + contains: Contains + equals: Equals + contains_html: Contains %{string} + created_msg: Successfully created username rule + delete: Delete + edit: + title: Edit username rule + matches_exactly_html: Equals %{string} + new: + create: Create rule + title: Create new username rule + no_username_block_selected: No username rules were changed as none were selected + not_permitted: Not permitted + title: Username rules + updated_msg: Successfully updated username rule warning_presets: add_new: Add new delete: Delete @@ -1662,6 +1687,10 @@ en: title: New mention poll: subject: A poll by %{name} has ended + quote: + body: 'Your post was quoted by %{name}:' + subject: "%{name} quoted your post" + title: New quote reblog: body: 'Your post was boosted by %{name}:' subject: "%{name} boosted your post" @@ -1873,6 +1902,7 @@ en: edited_at_html: Edited %{date} errors: in_reply_not_found: The post you are trying to reply to does not appear to exist. + quoted_status_not_found: The post you are trying to quote does not appear to exist. over_character_limit: character limit of %{max} exceeded pin_errors: direct: Posts that are only visible to mentioned users cannot be pinned @@ -1880,8 +1910,8 @@ en: ownership: Someone else's post cannot be pinned reblog: A boost cannot be pinned quote_policies: - followers: Followers and mentioned users - nobody: Only mentioned users + followers: Only your followers + nobody: Nobody public: Everyone title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 69c6c361bf2..f3e0b8186c6 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -1860,8 +1860,6 @@ eo: ownership: Mesaĝo de iu alia ne povas esti alpinglita reblog: Diskonigo ne povas esti alpinglita quote_policies: - followers: Sekvantoj kaj menciitaj uzantoj - nobody: Nur menciitaj uzantoj public: Ĉiuj title: "%{name}: “%{quote}”" visibilities: diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index 3b26eebd294..04578632ac7 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -190,6 +190,7 @@ es-AR: create_relay: Crear relé create_unavailable_domain: Crear dominio no disponible create_user_role: Crear rol + create_username_block: Crear regla de nombre de usuario demote_user: Descender usuario destroy_announcement: Eliminar anuncio destroy_canonical_email_block: Eliminar bloqueo de correo electrónico @@ -203,6 +204,7 @@ es-AR: destroy_status: Eliminar mensaje destroy_unavailable_domain: Eliminar dominio no disponible destroy_user_role: Destruir rol + destroy_username_block: Eliminar regla de nombre de usuario disable_2fa_user: Deshabilitar 2FA disable_custom_emoji: Deshabilitar emoji personalizado disable_relay: Deshabilitar relé @@ -237,6 +239,7 @@ es-AR: update_report: Actualizar denuncia update_status: Actualizar mensaje update_user_role: Actualizar rol + update_username_block: Actualizar regla de nombre de usuario actions: approve_appeal_html: "%{name} aprobó la solicitud de moderación de %{target}" approve_user_html: "%{name} aprobó el registro de %{target}" @@ -255,6 +258,7 @@ es-AR: create_relay_html: "%{name} creó el relé %{target}" create_unavailable_domain_html: "%{name} detuvo la entrega al dominio %{target}" create_user_role_html: "%{name} creó el rol %{target}" + create_username_block_html: "%{name} agregó una regla para los nombres de usuario que contienen %{target}" demote_user_html: "%{name} bajó de nivel al usuario %{target}" destroy_announcement_html: "%{name} eliminó el anuncio %{target}" destroy_canonical_email_block_html: "%{name} desbloqueó el correo electrónico con el hash %{target}" @@ -268,6 +272,7 @@ es-AR: destroy_status_html: "%{name} eliminó el mensaje de %{target}" destroy_unavailable_domain_html: "%{name} reanudó la entrega al dominio %{target}" destroy_user_role_html: "%{name} eliminó el rol %{target}" + destroy_username_block_html: "%{name} eliminó una regla para los nombres de usuario que contienen %{target}" disable_2fa_user_html: "%{name} deshabilitó el requerimiento de dos factores para el usuario %{target}" disable_custom_emoji_html: "%{name} deshabilitó el emoji %{target}" disable_relay_html: "%{name} deshabilitó el relé %{target}" @@ -302,6 +307,7 @@ es-AR: update_report_html: "%{name} actualizó la denuncia %{target}" update_status_html: "%{name} actualizó el mensaje de %{target}" update_user_role_html: "%{name} cambió el rol %{target}" + update_username_block_html: "%{name} actualizó una regla para los nombres de usuario que contienen %{target}" deleted_account: cuenta eliminada empty: No se encontraron registros. filter_by_action: Filtrar por acción @@ -1085,6 +1091,25 @@ es-AR: other: Usada por %{count} personas durante la última semana title: Recomendaciones y tendencias trending: En tendencia + username_blocks: + add_new: Agregar nueva + block_registrations: Bloquear registros + comparison: + contains: Contiene + equals: Es igual a + contains_html: Contiene %{string} + created_msg: Regla de nombre de usuario creada exitosamente + delete: Eliminar + edit: + title: Editar regla de nombre de usuario + matches_exactly_html: Es igual a %{string} + new: + create: Crear regla + title: Crear nueva regla de nombre de usuario + no_username_block_selected: No se cambiaron las reglas de nombre de usuario, ya que no se seleccionó ninguna + not_permitted: No permitido + title: Reglas de nombre de usuario + updated_msg: Regla de nombre de usuario actualizada exitosamente warning_presets: add_new: Agregar nuevo delete: Eliminar @@ -1662,6 +1687,10 @@ es-AR: title: Nueva mención poll: subject: Terminó una encuesta de %{name} + quote: + body: 'Tu mensaje fue citado por %{name}:' + subject: "%{name} citó tu mensaje" + title: Nueva cita reblog: body: "%{name} adhirió a tu mensaje:" subject: "%{name} adhirió a tu mensaje" @@ -1872,6 +1901,7 @@ es-AR: edited_at_html: Editado el %{date} errors: in_reply_not_found: El mensaje al que intentás responder no existe. + quoted_status_not_found: El mensaje al que intentás citar parece que no existe. over_character_limit: se excedió el límite de %{max} caracteres pin_errors: direct: Los mensajes que sólo son visibles para los usuarios mencionados no pueden ser fijados @@ -1879,8 +1909,8 @@ es-AR: ownership: No se puede fijar el mensaje de otra cuenta reblog: No se puede fijar una adhesión quote_policies: - followers: Seguidores y usuarios mencionados - nobody: Solo usuarios mencionados + followers: Solo tus seguidores + nobody: Nadie public: Todos title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index cfc573fab8d..10186de91ed 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -190,6 +190,7 @@ es-MX: create_relay: Crear Relé create_unavailable_domain: Crear Dominio No Disponible create_user_role: Crear rol + create_username_block: Crear regla de nombre de usuario demote_user: Degradar Usuario destroy_announcement: Eliminar Anuncio destroy_canonical_email_block: Eliminar bloqueo de correo electrónico @@ -203,6 +204,7 @@ es-MX: destroy_status: Eliminar Publicación destroy_unavailable_domain: Eliminar Dominio No Disponible destroy_user_role: Destruir Rol + destroy_username_block: Eliminar regla de nombre de usuario disable_2fa_user: Deshabilitar 2FA disable_custom_emoji: Deshabilitar Emoji Personalizado disable_relay: Desactivar Relé @@ -237,6 +239,7 @@ es-MX: update_report: Actualizar informe update_status: Actualizar Publicación update_user_role: Actualizar Rol + update_username_block: Actualizar regla de nombre de usuario actions: approve_appeal_html: "%{name} aprobó la solicitud de moderación de %{target}" approve_user_html: "%{name} aprobó el registro de %{target}" @@ -255,6 +258,7 @@ es-MX: create_relay_html: "%{name} creó un relé %{target}" create_unavailable_domain_html: "%{name} detuvo las entregas al dominio %{target}" create_user_role_html: "%{name} creó el rol %{target}" + create_username_block_html: "%{name} añadió regla para nombres de usuario que contienen %{target}" demote_user_html: "%{name} degradó al usuario %{target}" destroy_announcement_html: "%{name} eliminó el anuncio %{target}" destroy_canonical_email_block_html: "%{name} ha desbloqueado el correo electrónico con el hash %{target}" @@ -268,6 +272,7 @@ es-MX: destroy_status_html: "%{name} eliminó la publicación por %{target}" destroy_unavailable_domain_html: "%{name} reanudó las entregas al dominio %{target}" destroy_user_role_html: "%{name} eliminó el rol %{target}" + destroy_username_block_html: "%{name} eliminó la regla para los nombres de usuario que contienen %{target}" disable_2fa_user_html: "%{name} desactivó el requisito de dos factores para el usuario %{target}" disable_custom_emoji_html: "%{name} desactivó el emoji %{target}" disable_relay_html: "%{name} desactivó el relé %{target}" @@ -302,6 +307,7 @@ es-MX: update_report_html: "%{name} actualizó el informe %{target}" update_status_html: "%{name} actualizó la publicación de %{target}" update_user_role_html: "%{name} cambió el rol %{target}" + update_username_block_html: "%{name} actualizó una regla para los nombres de usuario que contienen %{target}" deleted_account: cuenta eliminada empty: No se encontraron registros. filter_by_action: Filtrar por acción @@ -1085,6 +1091,25 @@ es-MX: other: Usada por %{count} personas durante la última semana title: Recomendaciones y Tendencias trending: En tendencia + username_blocks: + add_new: Añadir nuevo + block_registrations: Bloquear registros + comparison: + contains: Contiene + equals: Igual + contains_html: Contiene %{string} + created_msg: Regla de nombre de usuario creada correctamente + delete: Eliminar + edit: + title: Editar regla de nombre de usuario + matches_exactly_html: Es igual a %{string} + new: + create: Crear regla + title: Crear nueva regla de nombre de usuario + no_username_block_selected: No se modificaron las reglas de nombre de usuario, ya que no se seleccionó ninguna + not_permitted: No permitido + title: Reglas para el nombre de usuario + updated_msg: Regla de nombre de usuario actualizada correctamente warning_presets: add_new: Añadir nuevo delete: Borrar @@ -1662,6 +1687,10 @@ es-MX: title: Nueva mención poll: subject: Una encuesta de %{name} ha terminado + quote: + body: 'Tu publicación fue citada por %{name}:' + subject: "%{name} citó tu publicación" + title: Nueva cita reblog: body: 'Tu publicación fue impulsada por %{name}:' subject: "%{name} ha impulsado tu publicación" @@ -1872,6 +1901,7 @@ es-MX: edited_at_html: Editado %{date} errors: in_reply_not_found: La publicación a la que estás intentando responder no existe. + quoted_status_not_found: La publicación que intentas citar no parece existir. over_character_limit: Límite de caracteres de %{max} superado pin_errors: direct: Las publicaciones que son visibles solo para los usuarios mencionados no pueden fijarse @@ -1879,8 +1909,8 @@ es-MX: ownership: La publicación de alguien más no puede fijarse reblog: No se puede fijar una publicación impulsada quote_policies: - followers: Seguidores y usuarios mencionados - nobody: Solo usuarios mencionados + followers: Solo tus seguidores + nobody: Nadie public: Cualquiera title: "%{name}: «%{quote}»" visibilities: diff --git a/config/locales/es.yml b/config/locales/es.yml index 4b10f23b132..2536f42669a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -190,6 +190,7 @@ es: create_relay: Crear Relé create_unavailable_domain: Crear Dominio No Disponible create_user_role: Crear Rol + create_username_block: Crear regla de nombre de usuario demote_user: Degradar Usuario destroy_announcement: Eliminar Anuncio destroy_canonical_email_block: Eliminar Bloqueo de Correo Electrónico @@ -203,6 +204,7 @@ es: destroy_status: Eliminar Publicación destroy_unavailable_domain: Eliminar Dominio No Disponible destroy_user_role: Destruir Rol + destroy_username_block: Eliminar regla de nombre de usuario disable_2fa_user: Deshabilitar 2FA disable_custom_emoji: Deshabilitar Emoji Personalizado disable_relay: Desactivar Relé @@ -237,6 +239,7 @@ es: update_report: Actualizar informe update_status: Actualizar Publicación update_user_role: Actualizar Rol + update_username_block: Actualizar regla de nombre de usuario actions: approve_appeal_html: "%{name} aprobó la solicitud de moderación de %{target}" approve_user_html: "%{name} aprobó el registro de %{target}" @@ -255,6 +258,7 @@ es: create_relay_html: "%{name} creó un relé %{target}" create_unavailable_domain_html: "%{name} detuvo las entregas al dominio %{target}" create_user_role_html: "%{name} creó el rol %{target}" + create_username_block_html: "%{name} agregó una regla para los nombres de usuario que contienen %{target}" demote_user_html: "%{name} degradó al usuario %{target}" destroy_announcement_html: "%{name} eliminó el anuncio %{target}" destroy_canonical_email_block_html: "%{name} desbloqueó el correo electrónico con el hash %{target}" @@ -268,6 +272,7 @@ es: destroy_status_html: "%{name} eliminó la publicación de %{target}" destroy_unavailable_domain_html: "%{name} reanudó las entregas al dominio %{target}" destroy_user_role_html: "%{name} eliminó el rol %{target}" + destroy_username_block_html: "%{name} eliminó una regla para los nombres de usuario que contienen %{target}" disable_2fa_user_html: "%{name} desactivó el requisito de dos factores para el usuario %{target}" disable_custom_emoji_html: "%{name} desactivó el emoji %{target}" disable_relay_html: "%{name} desactivó el relé %{target}" @@ -302,6 +307,7 @@ es: update_report_html: "%{name} actualizó el informe %{target}" update_status_html: "%{name} actualizó la publicación de %{target}" update_user_role_html: "%{name} cambió el rol %{target}" + update_username_block_html: "%{name} actualizó una regla para los nombres de usuario que contienen %{target}" deleted_account: cuenta eliminada empty: No se encontraron registros. filter_by_action: Filtrar por acción @@ -1085,6 +1091,25 @@ es: other: Usada por %{count} personas durante la última semana title: Recomendaciones y Tendencias trending: En tendencia + username_blocks: + add_new: Añadir nueva + block_registrations: Bloquear registros + comparison: + contains: Contiene + equals: Es igual a + contains_html: Contiene %{string} + created_msg: Regla de nombre de usuario creada correctamente + delete: Eliminar + edit: + title: Editar regla de nombre de usuario + matches_exactly_html: Es igual a %{string} + new: + create: Crear regla + title: Crear nueva regla de nombre de usuario + no_username_block_selected: No se cambiaron las reglas de nombre de usuario, ya que no se seleccionó ninguno + not_permitted: No permitido + title: Reglas de nombre de usuario + updated_msg: Regla de nombre de usuario actualizada correctamente warning_presets: add_new: Añadir nuevo delete: Borrar @@ -1662,6 +1687,10 @@ es: title: Nueva mención poll: subject: Una encuesta de %{name} ha terminado + quote: + body: 'Tu publicación ha sido citada por %{name}:' + subject: "%{name} citó tu publicación" + title: Nueva cita reblog: body: 'Tu publicación fue impulsada por %{name}:' subject: "%{name} impulsó tu publicación" @@ -1872,6 +1901,7 @@ es: edited_at_html: Editado %{date} errors: in_reply_not_found: La publicación a la que intentas responder no existe. + quoted_status_not_found: La publicación que estás intentando citar no parece existir. over_character_limit: Límite de caracteres de %{max} superado pin_errors: direct: Las publicaciones que son visibles solo para los usuarios mencionados no pueden fijarse @@ -1879,8 +1909,8 @@ es: ownership: La publicación de otra persona no puede fijarse reblog: Una publicación impulsada no puede fijarse quote_policies: - followers: Seguidores y usuarios mencionados - nobody: Solo usuarios mencionados + followers: Solo tus seguidores + nobody: Nadie public: Cualquiera title: "%{name}: «%{quote}»" visibilities: diff --git a/config/locales/et.yml b/config/locales/et.yml index 962f6be9db8..7d96b4c57ed 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -190,6 +190,7 @@ et: create_relay: Loo sõnumivahendusserver create_unavailable_domain: Kättesaamatu domeeni lisamine create_user_role: Loo roll + create_username_block: Lisa kasutajanime reegel demote_user: Alandas kasutaja destroy_announcement: Eemaldas teadaande destroy_canonical_email_block: Kustuta e-posti blokeering @@ -203,6 +204,7 @@ et: destroy_status: Kustuta postitus destroy_unavailable_domain: Kättesaamatu domeeni kustutamine destroy_user_role: Rolli kustutamine + destroy_username_block: Kustuta kasutajanime reegel disable_2fa_user: Keela 2FA disable_custom_emoji: Keelas kohandatud emotikoni disable_relay: Lülita sõnumivahendusserver välja @@ -237,6 +239,7 @@ et: update_report: Uuendamise raport update_status: Uuenda postitust update_user_role: Uuenda rolli + update_username_block: Uuenda kasutajanime reeglit actions: approve_appeal_html: "%{name} kiitis heaks modereerimise otsuse vaidlustuse %{target} poolt" approve_user_html: "%{name} kiitis heaks registreerimise %{target} poolt" @@ -255,6 +258,7 @@ et: create_relay_html: "%{name} lõi sõnumivahendusserveri: %{target}" create_unavailable_domain_html: "%{name} lõpetas edastamise domeeni %{target}" create_user_role_html: "%{name} lõi rolli %{target}" + create_username_block_html: "%{name} lisas kasutajanime reegli, milles sisaldub %{target}" demote_user_html: "%{name} alandas kasutajat %{target}" destroy_announcement_html: "%{name} kustutas teadaande %{target}" destroy_canonical_email_block_html: "%{name} eemaldas blokeeringu e-postilt räsiga %{target}" @@ -268,6 +272,7 @@ et: destroy_status_html: "%{name} kustutas %{target} postituse" destroy_unavailable_domain_html: "%{name} taastas edastamise domeeni %{target}" destroy_user_role_html: "%{name} kustutas %{target} rolli" + destroy_username_block_html: "%{name} eemaldas kasutajanime reegli, milles sisaldub %{target}" disable_2fa_user_html: "%{name} eemaldas kasutaja %{target} kahe etapise nõude" disable_custom_emoji_html: "%{name} keelas emotikooni %{target}" disable_relay_html: "%{name} eemaldas sõnumivahendusserveri kasutuselt: %{target}" @@ -302,6 +307,7 @@ et: update_report_html: "%{name} uuendas raportit %{target}" update_status_html: "%{name} muutis %{target} postitust" update_user_role_html: "%{name} muutis %{target} rolli" + update_username_block_html: "%{name} uuendas kasutajanime reeglit, milles sisaldub %{target}" deleted_account: kustutatud konto empty: Logisi ei leitud. filter_by_action: Filtreeri tegevuse järgi @@ -319,6 +325,8 @@ et: create: Loo teadaanne title: Uus teadaanne preview: + disclaimer: Kuna kasutajd ei saa neist postiitustest keelduda, siis peaksid teavituskirjad keskenduma vaid olulistele teemadele nagi võimalik isiklike andmete leke või serveri tegevuse lõpetamine. + explanation_html: 'See e-kiri saadetakse %{display_count}-le kasutajale. E-kirjas sisaldub järgnev tekst:' title: Info teavituse üle vaatamine publish: Postita published_msg: Teadaande avaldamine õnnestus! @@ -806,6 +814,8 @@ et: preamble: Täpsem kirjeldus, kuidas serverit käitatakse, modereeritakse ja rahastatakse. rules_hint: Reeglitele, millega kasutajad peavad nõustuma, on vastav piirkond. title: Teave + allow_referrer_origin: + title: Luba välistel serveritel näha Mastodoni teenust linkide viitajana appearance: preamble: Kohanda Mastodoni veebiliidest. title: Välimus @@ -984,6 +994,7 @@ et: explanation_html: Esitatud teenusetingimuste näidis on mõeldud ainult teavitamise eesmärgil ja seda ei tohiks tõlgendada kui juriidilist nõuannet mis tahes küsimuses. Palun konsulteeri olukorra ja konkreetsete juriidiliste küsimuste osas oma õigusnõustajaga. title: Teenuse tingimuste seadistamine history: Ajalugu + notified_on_html: 'Kasutajad on teavitatud: %{date}' notify_users: Teata kasutajatele preview: explanation_html: 'See e-kiri saadetakse %{display_count}-le kasutajale, kes olid liitunud enne %{date}. E-kirjas sisaldub järgnev tekst:' @@ -993,6 +1004,7 @@ et: other: Saada %{display_count} e-kirja publish: Postita published_on_html: Postitatud %{date} + save_draft: Salvesta kavandina title: Kasutustingimused title: Administreerimine trends: @@ -1069,6 +1081,25 @@ et: other: Kasutatud %{count} kasutaja poolt viimase nädala jooksul title: Soovitused ja trendid trending: Trendid + username_blocks: + add_new: Lisa uus + block_registrations: Blokeeri registreerimisi + comparison: + contains: Sisaldab + equals: Võrdub + contains_html: 'Sisaldab: %{string}' + created_msg: Kasutajanime reegli lisamine õnnestus + delete: Kustuta + edit: + title: Muuda kasutajanime reeglit + matches_exactly_html: 'Võrdub: %{string}' + new: + create: Lisa reegel + title: Lisa uus kasutajanime reegel + no_username_block_selected: Ühtegi kasutajanimereeglit ei muudetud, kuna midagi polnud valitud + not_permitted: Ei ole lubatud + title: Kasutajanime reeglid + updated_msg: Kasutajanime reegli uuendamine õnnestus warning_presets: add_new: Lisa uus delete: Kustuta @@ -1331,6 +1362,10 @@ et: basic_information: Põhiinfo hint_html: "Kohanda, mida inimesed näevad su avalikul profiilil ja postituste kõrval. Inimesed alustavad tõenäolisemalt sinu jälgimist ja interakteeruvad sinuga, kui sul on täidetud profiil ja profiilipilt." other: Muu + emoji_styles: + auto: Automaatne + native: Rakenduseomane + twemoji: Twemoji errors: '400': Esitatud päring oli vigane või valesti vormindatud. '403': Sul puudub õigus seda lehte vaadata. @@ -1850,6 +1885,7 @@ et: edited_at_html: Muudetud %{date} errors: in_reply_not_found: Postitus, millele üritad vastata, ei näi enam eksisteerivat. + quoted_status_not_found: Postitus, mida üritad tsiteerida, ei näi enam eksisteerivat. over_character_limit: tähtmärkide limiit %{max} ületatud pin_errors: direct: Ei saa kinnitada postitusi, mis on nähtavad vaid mainitud kasutajatele @@ -1857,8 +1893,6 @@ et: ownership: Kellegi teise postitust ei saa kinnitada reblog: Jagamist ei saa kinnitada quote_policies: - followers: Jälgijad ja mainitud kasutajad - nobody: Vaid mainitud kasutajad public: Kõik title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 95c163c3818..b777b05eb95 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1742,8 +1742,6 @@ eu: ownership: Ezin duzu beste norbaiten bidalketa bat finkatu reblog: Bultzada bat ezin da finkatu quote_policies: - followers: Jarraitzaileak eta aipatutako erabiltzaileak - nobody: Aipatutako erabiltzaileak soilik public: Guztiak title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/fa.yml b/config/locales/fa.yml index ec0ed1ca997..913f3848844 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -190,6 +190,7 @@ fa: create_relay: ایجاد رله create_unavailable_domain: ایجاد دامنهٔ ناموجود create_user_role: ایجاد نقش + create_username_block: ایجاد قانون نام‌کاربری demote_user: تنزل کاربر destroy_announcement: حذف اعلامیه destroy_canonical_email_block: حذف انسداد رایانامه @@ -203,6 +204,7 @@ fa: destroy_status: حذف وضعیت destroy_unavailable_domain: حذف دامنهٔ ناموجود destroy_user_role: نابودی نقش + destroy_username_block: حذف قانون نام‌کاربری disable_2fa_user: از کار انداختن ورود دومرحله‌ای disable_custom_emoji: از کار انداختن شکلک سفارشی disable_relay: غیرفعال‌سازی رله @@ -237,6 +239,7 @@ fa: update_report: به‌روز رسانی گزارش update_status: به‌روز رسانی وضعیت update_user_role: به روزرسانی نقش + update_username_block: به‌روزرسانی قانون نام‌کاربری actions: approve_appeal_html: "%{name} درخواست تجدیدنظر تصمیم مدیر را از %{target} پذیرفت" approve_user_html: "%{name} ثبت نام %{target} را تایید کرد" @@ -653,8 +656,8 @@ fa: mark_as_sensitive_description_html: رسانهٔ درون فرستهٔ گزارش شده به عنوان حسّاس علامت خورده و شکایتی ضبط خواهد شد تا بتوانید خلاف‌های آینده از همین حساب را بهتر مدیریت کنید. other_description_html: دیدن انتخاب های بیشتر برای کنترل رفتار حساب و سفارشی سازی ارتباط با حساب گزارش شده. resolve_description_html: هیچ کنشی علیه حساب گزارش شده انجام نخواهد شد. هیچ شکایتی ضبط نشده و گزارش بسته خواهد شد. - silence_description_html: این حساب فقط برای کسانی قابل مشاهده خواهد بود که قبلاً آن را دنبال می کنند یا به صورت دستی آن را جستجو می کنند و دسترسی آن را به شدت محدود می کند. همیشه می توان برگرداند. همه گزارش‌های مربوط به این حساب را می‌بندد. - suspend_description_html: اکانت و تمامی محتویات آن غیرقابل دسترسی و در نهایت حذف خواهد شد و تعامل با آن غیر ممکن خواهد بود. قابل برگشت در عرض 30 روز همه گزارش‌های مربوط به این حساب را می‌بندد. + silence_description_html: حساب فقط برای کسانی که از پیش پی می‌گرفتندش یا به صورت دستی به دنیالش گشته‌اند نمایان خواهد بود که رسشش را شدیداً محدود می‌کند. همواره برگشت‌پذیر است. همهٔ گزارش‌ها علیه این حساب را خواهد بست. + suspend_description_html: حساب و همهٔ محتوایش غیرقابل دسترس شده و در نهایت حذف خواهند شد. تعامل با آن ممکن نخواهد بود. بازگشت‌پذیر تا ۳۰ روز. همهٔ گزارش‌ها علیه این حساب را خواهد بست. actions_description_html: تصمیم گیری کنش اقدامی برای حل این گزارش. در صورت انجام کنش تنبیهی روی حساب گزارش شده، غیر از زمان یکه دستهٔ هرزنامه گزیده باشد، برایش آگاهی رایانامه‌ای فرستاده خواهد شد. actions_description_remote_html: تصمیم بگیرید که چه اقدامی برای حل این گزارش انجام دهید. این فقط بر نحوه ارتباط سرور شما با این حساب راه دور و مدیریت محتوای آن تأثیر می گذارد. actions_no_posts: این گزارش هیچ پست مرتبطی برای حذف ندارد @@ -714,7 +717,7 @@ fa: actions: delete_html: پست های توهین آمیز را حذف کنید mark_as_sensitive_html: رسانه پست های توهین آمیز را به عنوان حساس علامت گذاری کنید - silence_html: دسترسی @%{acct} را به شدت محدود کنید و نمایه و محتویات آنها را فقط برای افرادی که قبلاً آنها را دنبال می‌کنند قابل مشاهده کنید یا به صورت دستی نمایه آن را جستجو کنید + silence_html: محدودیت شدید رسش ‪@%{acct}‬ با نمایان کردن نماگر و محتوایش فقط به افرادی که از پیش پی می‌گرفتندش و به صورت دستی به دنبالش گشته‌اند suspend_html: تعلیق @%{acct}، غیرقابل دسترس کردن نمایه و محتوای آنها و تعامل با آنها غیر ممکن close_report: 'علامت گذاری گزارش #%{id} به عنوان حل شده است' close_reports_html: "همه گزارش‌ها در برابر @%{acct} را به‌عنوان حل‌وفصل علامت‌گذاری کنید" @@ -1085,6 +1088,8 @@ fa: other: در هفته گذشته توسط %{count} نفر استفاده شده است title: توصیه ها و روندها trending: پرطرفدار + username_blocks: + delete: حذف warning_presets: add_new: افزودن تازه delete: زدودن @@ -1662,6 +1667,10 @@ fa: title: اشارهٔ جدید poll: subject: نظرسنجی‌ای از %{name} پایان یافت + quote: + body: 'فرسته‌تان توسط %{name} نقل شد:' + subject: "%{name} فرسته‌تان را نقل کرد" + title: نقل‌قول جدید reblog: body: "%{name} فرستهٔ شما را تقویت کرد:" subject: "%{name} فرستهٔ شما را تقویت کرد" @@ -1872,6 +1881,7 @@ fa: edited_at_html: ویراسته در %{date} errors: in_reply_not_found: به نظر نمی‌رسد وضعیتی که می‌خواهید به آن پاسخ دهید، وجود داشته باشد. + quoted_status_not_found: به نظر نمی‌رسد فرسته‌ای که می‌خواهید نقلش کنید وجود داشته باشد. over_character_limit: از حد مجاز %{max} حرف فراتر رفتید pin_errors: direct: فرسته‌هایی که فقط برای کاربران اشاره شده نمایانند نمی‌توانند سنجاق شوند @@ -1879,8 +1889,8 @@ fa: ownership: نوشته‌های دیگران را نمی‌توان ثابت کرد reblog: تقویت نمی‌تواند سنجاق شود quote_policies: - followers: پی‌گیران و کاربران اشاره شده - nobody: فقط کاربران اشاره شده + followers: تنها پی‌گیرندگانتان + nobody: هیچ‌کس public: هرکسی title: "%{name}: «%{quote}»" visibilities: @@ -2002,7 +2012,7 @@ fa: details: 'جزییات ورود:' explanation: ما ورود به حساب شما را از یک آدرس آی پی جدید شناسایی کرده ایم. further_actions_html: اگر این شما نبودید، توصیه می کنیم فورا %{action} را فعال کنید و برای ایمن نگه داشتن حساب خود، احراز هویت دو مرحله ای را فعال کنید. - subject: حساب شما از یک آدرس آی پی جدید قابل دسترسی است + subject: نشانی آی‌پی جدیدی به حسابتان دسترسی پیدا کرده title: یک ورود جدید terms_of_service_changed: agreement: با ادامه استفاده از %{domain}، با این شرایط موافقت می کنید. اگر با شرایط به‌روزرسانی شده مخالف هستید، می‌توانید در هر زمان با حذف حساب خود، قرارداد خود را با %{domain} فسخ کنید. diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 54aadf60f32..190a58b8ee0 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -190,6 +190,7 @@ fi: create_relay: Luo välittäjä create_unavailable_domain: Luo ei-saatavilla oleva verkkotunnus create_user_role: Luo rooli + create_username_block: Luo käyttäjänimisääntö demote_user: Alenna käyttäjä destroy_announcement: Poista tiedote destroy_canonical_email_block: Poista sähköpostiosoitteen esto @@ -203,6 +204,7 @@ fi: destroy_status: Poista julkaisu destroy_unavailable_domain: Poista ei-saatavilla oleva verkkotunnus destroy_user_role: Hävitä rooli + destroy_username_block: Poista käyttäjänimisääntö disable_2fa_user: Poista kaksivaiheinen todennus käytöstä disable_custom_emoji: Poista mukautettu emoji käytöstä disable_relay: Poista välittäjä käytöstä @@ -237,6 +239,7 @@ fi: update_report: Päivitä raportti update_status: Päivitä julkaisu update_user_role: Päivitä rooli + update_username_block: Päivitä käyttäjänimisääntö actions: approve_appeal_html: "%{name} hyväksyi käyttäjän %{target} valituksen moderointipäätöksestä" approve_user_html: "%{name} hyväksyi käyttäjän %{target} rekisteröitymisen" @@ -255,6 +258,7 @@ fi: create_relay_html: "%{name} loi välittäjän %{target}" create_unavailable_domain_html: "%{name} pysäytti toimituksen verkkotunnukseen %{target}" create_user_role_html: "%{name} loi roolin %{target}" + create_username_block_html: "%{name} lisäsi säännön käyttäjänimille, joihin sisältyy %{target}" demote_user_html: "%{name} alensi käyttäjän %{target}" destroy_announcement_html: "%{name} poisti tiedotteen %{target}" destroy_canonical_email_block_html: "%{name} kumosi eston tiivistettä %{target} vastaavalta sähköpostiosoitteelta" @@ -268,6 +272,7 @@ fi: destroy_status_html: "%{name} poisti käyttäjän %{target} julkaisun" destroy_unavailable_domain_html: "%{name} jatkoi toimitusta verkkotunnukseen %{target}" destroy_user_role_html: "%{name} poisti roolin %{target}" + destroy_username_block_html: "%{name} poisti säännön käyttäjänimiltä, joihin sisältyy %{target}" disable_2fa_user_html: "%{name} poisti käyttäjältä %{target} vaatimuksen kaksivaiheiseen todentamiseen" disable_custom_emoji_html: "%{name} poisti emojin %{target} käytöstä" disable_relay_html: "%{name} poisti välittäjän %{target} käytöstä" @@ -302,6 +307,7 @@ fi: update_report_html: "%{name} päivitti raportin %{target}" update_status_html: "%{name} päivitti käyttäjän %{target} julkaisun" update_user_role_html: "%{name} muutti roolia %{target}" + update_username_block_html: "%{name} päivitti säännön käyttäjänimille, joihin sisältyy %{target}" deleted_account: poisti tilin empty: Lokeja ei löytynyt. filter_by_action: Suodata toimen mukaan @@ -1083,6 +1089,25 @@ fi: other: Käyttänyt %{count} käyttäjää viimeisen viikon aikana title: Suositukset ja trendit trending: Trendaus + username_blocks: + add_new: Lisää uusi + block_registrations: Estä rekisteröitymiset + comparison: + contains: Sisältää + equals: Vastaa + contains_html: Sisältää merkkijonon %{string} + created_msg: Käyttäjänimisääntö luotiin onnistuneesti + delete: Poista + edit: + title: Muokkaa käyttäjänimisääntöä + matches_exactly_html: Vastaa merkkijonoa %{string} + new: + create: Luo sääntö + title: Luo uusi käyttäjänimisääntö + no_username_block_selected: Käyttäjänimisääntöjä ei muutettu, koska yhtään ei ollut valittuna + not_permitted: Ei sallittu + title: Käyttäjänimisäännöt + updated_msg: Käyttäjänimisääntö päivitettiin onnistuneesti warning_presets: add_new: Lisää uusi delete: Poista @@ -1660,6 +1685,10 @@ fi: title: Uusi maininta poll: subject: Äänestys käyttäjältä %{name} on päättynyt + quote: + body: "%{name} lainasi julkaisuasi:" + subject: "%{name} lainasi julkaisuasi" + title: Uusi lainaus reblog: body: "%{name} tehosti julkaisuasi:" subject: "%{name} tehosti julkaisuasi" @@ -1870,6 +1899,7 @@ fi: edited_at_html: Muokattu %{date} errors: in_reply_not_found: Julkaisua, johon yrität vastata, ei näytä olevan olemassa. + quoted_status_not_found: Julkaisua, jota yrität lainata, ei näytä olevan olemassa. over_character_limit: merkkimäärän rajoitus %{max} ylitetty pin_errors: direct: Vain mainituille käyttäjille näkyviä julkaisuja ei voi kiinnittää @@ -1877,8 +1907,8 @@ fi: ownership: Muiden julkaisuja ei voi kiinnittää reblog: Tehostusta ei voi kiinnittää quote_policies: - followers: Seuraajat ja mainitut käyttäjät - nobody: Vain mainitut käyttäjät + followers: Vain seuraajasi + nobody: Ei kukaan public: Kaikki title: "%{name}: ”%{quote}”" visibilities: diff --git a/config/locales/fo.yml b/config/locales/fo.yml index 23a6b589765..b4400bfd03e 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -190,6 +190,7 @@ fo: create_relay: Stovna reiðlag create_unavailable_domain: Stovna navnaøki, sum ikki er tøkt create_user_role: Stovna leiklut + create_username_block: Stovna brúkaranavnsreglu demote_user: Lækka brúkara í tign destroy_announcement: Strika kunngerð destroy_canonical_email_block: Strika t-postablokk @@ -203,6 +204,7 @@ fo: destroy_status: Strika post destroy_unavailable_domain: Strika navnaøki, sum ikki er tøkt destroy_user_role: Bein burtur leiklut + destroy_username_block: Strika brúkaranavnsreglu disable_2fa_user: Ger 2FA óvirkið disable_custom_emoji: Ger serligt kenslutekn óvirkið disable_relay: Ger reiðlag óvirkið @@ -237,6 +239,7 @@ fo: update_report: Dagfør frágreiðing update_status: Dagfør Uppslag update_user_role: Dagfør Leiklut + update_username_block: Dagfør brúkaranavnsreglu actions: approve_appeal_html: "%{name} góðkendi umsjónaráheitan frá %{target}" approve_user_html: "%{name} góðtók umsókn frá %{target}" @@ -255,6 +258,7 @@ fo: create_relay_html: "%{name} gjørdi eitt reiðlag %{target}" create_unavailable_domain_html: "%{name} steðgaði veiting til navnaøkið %{target}" create_user_role_html: "%{name} stovnaði %{target} leiklutin" + create_username_block_html: "%{name} legði reglu afturat fyri brúkaranøvn, sum innihalda %{target}" demote_user_html: "%{name} lækkaði tignina hjá brúkaranum %{target}" destroy_announcement_html: "%{name} strikaðar fráboðanir %{target}" destroy_canonical_email_block_html: "%{name} strikaði blokeringina av teldupostin við hashkodu %{target}" @@ -268,6 +272,7 @@ fo: destroy_status_html: "%{name} slettaði upplegg hjá %{target}" destroy_unavailable_domain_html: "%{name} tók upp aftir veiting til navnaøkið %{target}" destroy_user_role_html: "%{name} slettaði leiklutin hjá %{target}" + destroy_username_block_html: "%{name} strikaði reglu fyri brúkaranøvn, sum innihalda %{target}" disable_2fa_user_html: "%{name} slepti kravið um váttan í tveimum stigum fyri brúkaran %{target}" disable_custom_emoji_html: "%{name} gjørdi kensluteknið %{target} óvirkið" disable_relay_html: "%{name} gjørdi reiðlagið %{target} óvirkið" @@ -302,6 +307,7 @@ fo: update_report_html: "%{name} dagførdi meldingina %{target}" update_status_html: "%{name} dagførdi postin hjá %{target}" update_user_role_html: "%{name} broyttir %{target} leiklutir" + update_username_block_html: "%{name} dagførdi reglu fyri brúkaranøvn, sum innihalda %{target}" deleted_account: strikað konta empty: Eingir loggar funnir. filter_by_action: Filtrera eftir atgerð @@ -1085,6 +1091,25 @@ fo: other: Brúkt av %{count} brúkarum seinastu vikuna title: Tilmæli & rák trending: Vælumtókt + username_blocks: + add_new: Legg afturat + block_registrations: Blokera skrásetingar + comparison: + contains: Inniheldur + equals: Er tað sama sum + contains_html: Inniheldur %{string} + created_msg: Eydnaðist at leggja brúkaranavnsreglu afturat + delete: Strika + edit: + title: Broyt brúkaranavnsreglu + matches_exactly_html: Tað sama sum %{string} + new: + create: Stovna reglu + title: Stovna brúkaranavnsreglu + no_username_block_selected: Ongar brúkaranavnsreglur vóru broyttar, tí ongar vóru valdar + not_permitted: Ikki loyvt + title: Brúkaranavnsreglur + updated_msg: Eydnaðist at dagføra brúkaranavnsreglu warning_presets: add_new: Legg afturat delete: Strika @@ -1662,6 +1687,10 @@ fo: title: Nýggj umrøða poll: subject: Ein spurnarkanning hjá %{name} er endað + quote: + body: 'Postur tín var siteraður av %{name}:' + subject: "%{name} siteraði postin hjá tær" + title: Nýggj sitering reblog: body: 'Postur tín var stimbraður av %{name}:' subject: "%{name} stimbraði tín post" @@ -1872,6 +1901,7 @@ fo: edited_at_html: Rættað %{date} errors: in_reply_not_found: Posturin, sum tú roynir at svara, sýnist ikki at finnast. + quoted_status_not_found: Posturin, sum tú roynir at sitera, sýnist ikki at finnast. over_character_limit: mesta tal av teknum, %{max}, rokkið pin_errors: direct: Postar, sum einans eru sjónligir hjá nevndum brúkarum, kunnu ikki festast @@ -1879,8 +1909,8 @@ fo: ownership: Postar hjá øðrum kunnu ikki festast reblog: Ein stimbran kann ikki festast quote_policies: - followers: Fylgjarar og nevndir brúkarar - nobody: Bara nevndir brúkarar + followers: Einans tey, ið fylgja tær + nobody: Eingin public: Øll title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index bb68494118f..c7da245c4a9 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -319,6 +319,7 @@ fr-CA: create: Créer une annonce title: Nouvelle annonce preview: + disclaimer: Étant donné que les utilisateurs ne peuvent pas s'en retirer, les notifications par courriel devraient être limitées à des annonces importantes telles que des violations de données personnelles ou des notifications de fermeture de serveur. explanation_html: 'L''e-mail sera envoyé à %{display_count} utilisateurs. Le texte suivant sera inclus :' title: Aperçu de la notification d'annonce publish: Publier @@ -1852,7 +1853,9 @@ fr-CA: ownership: Vous ne pouvez pas épingler un message ne vous appartenant pas reblog: Un partage ne peut pas être épinglé quote_policies: - followers: Abonné·e·s et utilisateur·trice·s mentionné·e·s + followers: Vos abonné·es seulement + nobody: Personne + public: Tout le monde title: "%{name} : « %{quote} »" visibilities: direct: Direct diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6fcdb5b9722..918812a8077 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -319,6 +319,7 @@ fr: create: Créer une annonce title: Nouvelle annonce preview: + disclaimer: Étant donné que les utilisateurs ne peuvent pas s'en retirer, les notifications par courriel devraient être limitées à des annonces importantes telles que des violations de données personnelles ou des notifications de fermeture de serveur. explanation_html: 'L''e-mail sera envoyé à %{display_count} utilisateurs. Le texte suivant sera inclus :' title: Aperçu de la notification d'annonce publish: Publier @@ -1852,7 +1853,9 @@ fr: ownership: Vous ne pouvez pas épingler un message ne vous appartenant pas reblog: Un partage ne peut pas être épinglé quote_policies: - followers: Abonné·e·s et utilisateur·trice·s mentionné·e·s + followers: Vos abonné·es seulement + nobody: Personne + public: Tout le monde title: "%{name} : « %{quote} »" visibilities: direct: Direct diff --git a/config/locales/fy.yml b/config/locales/fy.yml index 38752869da1..db7816996a0 100644 --- a/config/locales/fy.yml +++ b/config/locales/fy.yml @@ -190,6 +190,7 @@ fy: create_relay: Relay oanmeitsje create_unavailable_domain: Net beskikber domein oanmeitsje create_user_role: Rol oanmeitsje + create_username_block: Brûkersnammeregel oanmeitsje demote_user: Brûker degradearje destroy_announcement: Meidieling fuortsmite destroy_canonical_email_block: E-mailblokkade fuortsmite @@ -203,6 +204,7 @@ fy: destroy_status: Toot fuortsmite destroy_unavailable_domain: Net beskikber domein fuortsmite destroy_user_role: Rol permanint fuortsmite + destroy_username_block: Brûkersnammeregel fuortsmite disable_2fa_user: Twa-stapsferifikaasje útskeakelje disable_custom_emoji: Lokale emoji útskeakelje disable_relay: Relay útskeakelje @@ -237,6 +239,7 @@ fy: update_report: Rapportaazje bywurkje update_status: Berjocht bywurkje update_user_role: Rol bywurkje + update_username_block: Brûkersnammeregel bywurkje actions: approve_appeal_html: "%{name} hat it beswier tsjin de moderaasjemaatregel fan %{target} goedkard" approve_user_html: "%{name} hat de registraasje fan %{target} goedkard" @@ -255,6 +258,7 @@ fy: create_relay_html: "%{name} hat in relay oanmakke %{target}" create_unavailable_domain_html: "%{name} hat de besoarging foar domein %{target} beëinige" create_user_role_html: "%{name} hat de rol %{target} oanmakke" + create_username_block_html: "%{name} hat in brûkersnammeregel mei %{target} tafoege" demote_user_html: Brûker %{target} is troch %{name} degradearre destroy_announcement_html: "%{name} hat de meidieling %{target} fuortsmiten" destroy_canonical_email_block_html: "%{name} hat it e-mailberjocht mei de hash %{target} deblokkearre" @@ -268,6 +272,7 @@ fy: destroy_status_html: Berjocht fan %{target} is troch %{name} fuortsmiten destroy_unavailable_domain_html: "%{name} hat de besoarging foar domein %{target} opnij starte" destroy_user_role_html: "%{name} hat de rol %{target} fuortsmiten" + destroy_username_block_html: "%{name} hat in brûkersnammeregel mei %{target} fuortsmiten" disable_2fa_user_html: De fereaske twa-stapsferifikaasje foar %{target} is troch %{name} útskeakele disable_custom_emoji_html: Emoji %{target} is troch %{name} útskeakele disable_relay_html: "%{name} hat de relay %{target} útskeakele" @@ -302,6 +307,7 @@ fy: update_report_html: Rapportaazje %{target} is troch %{name} bywurke update_status_html: "%{name} hat de berjochten %{target} bywurke" update_user_role_html: "%{name} hat de rol %{target} wizige" + update_username_block_html: "%{name} hat in brûkersnammeregel mei %{target} bywurke" deleted_account: fuortsmiten account empty: Gjin lochboeken fûn. filter_by_action: Op aksje filterje @@ -1085,6 +1091,25 @@ fy: other: Dizze wike troch %{count} persoanen brûkt title: Oanrekommandaasjes & trends trending: Trending + username_blocks: + add_new: Nije tafoegje + block_registrations: Registraasjes blokkearje + comparison: + contains: Befettet + equals: Is lyk oan + contains_html: Befettet %{string} + created_msg: Brûkersnammeregel mei sukses oanmakke + delete: Fuortsmite + edit: + title: Brûkersnammeregel bywurkje + matches_exactly_html: Is lyk oan %{string} + new: + create: Regel oanmeitsje + title: Brûkersnammeregel oanmeitsje + no_username_block_selected: Der binne gjin brûkersnammeregels wizige, omdat der gjin ien selektearre waard + not_permitted: Net tastien + title: Brûkersnammeregels + updated_msg: Brûkersnammeregel mei sukses bywurke warning_presets: add_new: Nije tafoegje delete: Fuortsmite @@ -1872,6 +1897,7 @@ fy: edited_at_html: Bewurke op %{date} errors: in_reply_not_found: It berjocht wêrop jo probearje te reagearjen liket net te bestean. + quoted_status_not_found: It berjocht dy’t jo probearje te sitearjen liket net te bestean. over_character_limit: Oer de limyt fan %{max} tekens pin_errors: direct: Berjochten dy’t allinnich sichtber binne foar fermelde brûkers kinne net fêstset wurde @@ -1879,8 +1905,6 @@ fy: ownership: In berjocht fan in oar kin net fêstmakke wurde reblog: In boost kin net fêstset wurde quote_policies: - followers: Folgers en fermelde brûkers - nobody: Allinnich fermelde brûkers public: Elkenien title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/ga.yml b/config/locales/ga.yml index 111ae9c56f0..8fc85b9a8db 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -199,6 +199,7 @@ ga: create_relay: Cruthaigh Leaschraolacháin create_unavailable_domain: Cruthaigh Fearann ​​Gan Fáil create_user_role: Cruthaigh Ról + create_username_block: Cruthaigh Riail Ainm Úsáideora demote_user: Ísligh úsáideoir destroy_announcement: Scrios Fógra destroy_canonical_email_block: Scrios Bloc Ríomhphoist @@ -212,6 +213,7 @@ ga: destroy_status: Scrios Postáil destroy_unavailable_domain: Scrios Fearann ​​Gan Fáil destroy_user_role: Scrios ról + destroy_username_block: Scrios an Riail Ainm Úsáideora disable_2fa_user: Díchumasaigh 2FA disable_custom_emoji: Díchumasaigh Emoji Saincheaptha disable_relay: Díchumasaigh Leaschraolacháin @@ -246,6 +248,7 @@ ga: update_report: Tuairisc Nuashonraithe update_status: Nuashonraigh Postáil update_user_role: Nuashonraigh Ról + update_username_block: Nuashonraigh an Riail Ainm Úsáideora actions: approve_appeal_html: Cheadaigh %{name} achomharc ar chinneadh modhnóireachta ó %{target} approve_user_html: Cheadaigh %{name} clárú ó %{target} @@ -264,6 +267,7 @@ ga: create_relay_html: Chruthaigh %{name} athsheoladh %{target} create_unavailable_domain_html: Chuir %{name} deireadh leis an seachadadh chuig fearann ​​%{target} create_user_role_html: Chruthaigh %{name} %{target} ról + create_username_block_html: Chuir %{name} riail leis d'ainmneacha úsáideora ina bhfuil %{target} demote_user_html: "%{name} úsáideoir scriosta %{target}" destroy_announcement_html: "%{name} fógra scriosta %{target}" destroy_canonical_email_block_html: "%{name} ríomhphost díchoiscthe leis an hash %{target}" @@ -277,6 +281,7 @@ ga: destroy_status_html: Bhain %{name} postáil le %{target} destroy_unavailable_domain_html: D'athchrom %{name} ar an seachadadh chuig fearann ​​%{target} destroy_user_role_html: Scrios %{name} ról %{target} + destroy_username_block_html: Bhain %{name} riail as ainmneacha úsáideora ina bhfuil %{target} disable_2fa_user_html: Dhíchumasaigh %{name} riachtanas dhá fhachtóir don úsáideoir %{target} disable_custom_emoji_html: Dhíchumasaigh %{name} emoji %{target} disable_relay_html: Dhíchumasaigh %{name} an athsheoladh %{target} @@ -311,6 +316,7 @@ ga: update_report_html: "%{name} tuairisc nuashonraithe %{target}" update_status_html: "%{name} postáil nuashonraithe faoi %{target}" update_user_role_html: D'athraigh %{name} ról %{target} + update_username_block_html: Nuashonraíodh riail %{name} le haghaidh ainmneacha úsáideora ina bhfuil %{target} deleted_account: cuntas scriosta empty: Níor aimsíodh aon logaí. filter_by_action: Scag de réir gnímh @@ -1139,6 +1145,25 @@ ga: two: Úsáidte ag %{count} duine le seachtain anuas title: Moltaí & Treochtaí trending: Treocht + username_blocks: + add_new: Cuir nua leis + block_registrations: Clárúcháin blocála + comparison: + contains: Ina bhfuil + equals: Cothrom le + contains_html: Tá %{string} ann + created_msg: Cruthaíodh riail ainm úsáideora go rathúil + delete: Scrios + edit: + title: Cuir riail ainm úsáideora in eagar + matches_exactly_html: Cothrom le %{string} + new: + create: Cruthaigh riail + title: Cruthaigh riail ainm úsáideora nua + no_username_block_selected: Níor athraíodh aon rialacha ainm úsáideora mar nár roghnaíodh aon cheann acu + not_permitted: Ní cheadaítear + title: Rialacha ainm úsáideora + updated_msg: Nuashonraíodh an riail ainm úsáideora go rathúil warning_presets: add_new: Cuir nua leis delete: Scrios @@ -1779,6 +1804,10 @@ ga: title: Lua nua poll: subject: Tháinig deireadh le vótaíocht le %{name} + quote: + body: 'Luaigh %{name} do phost:' + subject: Luaigh %{name} do phost + title: Luachan nua reblog: body: 'Treisíodh do phostáil le %{name}:' subject: Mhol %{name} do phostáil @@ -2001,6 +2030,7 @@ ga: edited_at_html: "%{date} curtha in eagar" errors: in_reply_not_found: Is cosúil nach ann don phostáil a bhfuil tú ag iarraidh freagra a thabhairt air. + quoted_status_not_found: Is cosúil nach bhfuil an post atá tú ag iarraidh a lua ann. over_character_limit: teorainn carachtar %{max} sáraithe pin_errors: direct: Ní féidir postálacha nach bhfuil le feiceáil ach ag úsáideoirí luaite a phinnáil @@ -2008,8 +2038,8 @@ ga: ownership: Ní féidir postáil duine éigin eile a phionnáil reblog: Ní féidir treisiú a phinnáil quote_policies: - followers: Leantóirí agus úsáideoirí luaite - nobody: Úsáideoirí luaite amháin + followers: Do leanúna amháin + nobody: Aon duine public: Gach duine title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/gd.yml b/config/locales/gd.yml index f7791fe3544..fec5df06981 100644 --- a/config/locales/gd.yml +++ b/config/locales/gd.yml @@ -1961,8 +1961,6 @@ gd: ownership: Chan urrainn dhut post càich a phrìneachadh reblog: Chan urrainn dhut brosnachadh a phrìneachadh quote_policies: - followers: Luchd-leantainn ’s cleachdaichean le iomradh orra - nobody: Cleachdaichean le iomradh orra a-mhàin public: A h-uile duine title: "%{name}: “%{quote}”" visibilities: diff --git a/config/locales/gl.yml b/config/locales/gl.yml index d19d47ab26b..45d0e62b6af 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -190,6 +190,7 @@ gl: create_relay: Crear Repetidor create_unavailable_domain: Crear dominio Non dispoñible create_user_role: Crear Rol + create_username_block: Crear regra para Identificadores demote_user: Degradar usuaria destroy_announcement: Eliminar anuncio destroy_canonical_email_block: Eliminar bloqueo do correo @@ -203,6 +204,7 @@ gl: destroy_status: Eliminar publicación destroy_unavailable_domain: Eliminar dominio Non dispoñible destroy_user_role: Eliminar Rol + destroy_username_block: Eliminar regra para Identificadores disable_2fa_user: Desactivar 2FA disable_custom_emoji: Desactivar emoticona personalizada disable_relay: Desactivar Repetidor @@ -237,6 +239,7 @@ gl: update_report: Actualización da denuncia update_status: Actualizar publicación update_user_role: Actualizar Rol + update_username_block: Actualizar regra para Identificadores actions: approve_appeal_html: "%{name} aprobou a apelación da decisión da moderación de %{target}" approve_user_html: "%{name} aprobou o rexistro de %{target}" @@ -255,6 +258,7 @@ gl: create_relay_html: "%{name} creou un repetidor en %{target}" create_unavailable_domain_html: "%{name} deixou de interactuar co dominio %{target}" create_user_role_html: "%{name} creou o rol %{target}" + create_username_block_html: "%{name} engadiu unha regra para identificadores que contén %{target}" demote_user_html: "%{name} degradou a usuaria %{target}" destroy_announcement_html: "%{name} eliminou o anuncio %{target}" destroy_canonical_email_block_html: "%{name} desbloqueou o correo con suma de comprobación %{target}" @@ -268,6 +272,7 @@ gl: destroy_status_html: "%{name} eliminou a publicación de %{target}" destroy_unavailable_domain_html: "%{name} retomou a interacción co dominio %{target}" destroy_user_role_html: "%{name} eliminou o rol %{target}" + destroy_username_block_html: "%{name} retirou unha regra para identificadores que contén %{target}" disable_2fa_user_html: "%{name} desactivou o requerimento do segundo factor para a usuaria %{target}" disable_custom_emoji_html: "%{name} desactivou o emoji %{target}" disable_relay_html: "%{name} desactivou o repetidor %{target}" @@ -302,6 +307,7 @@ gl: update_report_html: "%{name} actualizou a denuncia %{target}" update_status_html: "%{name} actualizou a publicación de %{target}" update_user_role_html: "%{name} cambiou o rol %{target}" + update_username_block_html: "%{name} actualizou a regra para identificadores que contén %{target}" deleted_account: conta eliminada empty: Non se atoparon rexistros. filter_by_action: Filtrar por acción @@ -1085,6 +1091,25 @@ gl: other: Utilizado por %{count} persoas na última semana title: Recomendacións e Suxestións trending: Popularidade + username_blocks: + add_new: Engadir nova + block_registrations: Bloqueo dos rexistros + comparison: + contains: Contén + equals: É igual a + contains_html: Contén %{string} + created_msg: Creouse correctamente a regra para identificadores + delete: Eliminar + edit: + title: Editar regra para identificadores + matches_exactly_html: É igual a %{string} + new: + create: Crear regra + title: Crear nova regra para identificadores + no_username_block_selected: Non se cambiou ningunha regra porque non se seleccionou ningunha + not_permitted: Non permitido + title: Regras para identificadores + updated_msg: Actualizouse correctamente a regra warning_presets: add_new: Engadir novo delete: Eliminar @@ -1662,6 +1687,10 @@ gl: title: Nova mención poll: subject: A enquisa de %{name} rematou + quote: + body: 'A túa publicación foi citada por %{name}:' + subject: "%{name} citou a túa publicación" + title: Nova cita reblog: body: 'A túa publicación promovida por %{name}:' subject: "%{name} promoveu a túa publicación" @@ -1872,6 +1901,7 @@ gl: edited_at_html: Editado %{date} errors: in_reply_not_found: A publicación á que tentas responder semella que non existe. + quoted_status_not_found: Parece que a publicación que intentas citar non existe. over_character_limit: Excedeu o límite de caracteres %{max} pin_errors: direct: As publicacións que só son visibles para as usuarias mencionadas non se poden fixar @@ -1879,8 +1909,8 @@ gl: ownership: Non podes fixar a publicación doutra usuaria reblog: Non se poden fixar as mensaxes promovidas quote_policies: - followers: Seguidoras e usuarias mencionadas - nobody: Só usuarias mencionadas + followers: Só para seguidoras + nobody: Ninguén public: Calquera title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/he.yml b/config/locales/he.yml index 50711387649..46be493307c 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -196,6 +196,7 @@ he: create_relay: יצירת ממסר create_unavailable_domain: יצירת דומיין בלתי זמין create_user_role: יצירת תפקיד + create_username_block: יצירת חוק שמות משתמש demote_user: הורדת משתמש בדרגה destroy_announcement: מחיקת הכרזה destroy_canonical_email_block: מחיקת חסימת דואל @@ -209,6 +210,7 @@ he: destroy_status: מחיקת הודעה destroy_unavailable_domain: מחיקת דומיין בלתי זמין destroy_user_role: מחיקת תפקיד + destroy_username_block: מחיקת חוק שמות משתמש disable_2fa_user: השעיית זיהוי דו-גורמי disable_custom_emoji: השעיית אמוג'י מיוחד disable_relay: השבתת ממסר @@ -243,6 +245,7 @@ he: update_report: עדכון דו"ח עבירה update_status: סטטוס עדכון update_user_role: עדכון תפקיד + update_username_block: עדכון חוק שמות משתמש actions: approve_appeal_html: "%{name} אישר/ה ערעור על החלטת מנהלי הקהילה מ-%{target}" approve_user_html: "%{name} אישר/ה הרשמה מ-%{target}" @@ -261,6 +264,7 @@ he: create_relay_html: "%{name} יצרו את הממסר %{target}" create_unavailable_domain_html: "%{name} הפסיק/ה משלוח לדומיין %{target}" create_user_role_html: "%{name} יצר את התפקיד של %{target}" + create_username_block_html: "%{name} הוסיפו חוק לשמות משתמש המכילים %{target}" demote_user_html: "%{name} הוריד/ה בדרגה את המשתמש %{target}" destroy_announcement_html: "%{name} מחק/ה את ההכרזה %{target}" destroy_canonical_email_block_html: "%{name} הסירו חסימה מדואל %{target}" @@ -274,6 +278,7 @@ he: destroy_status_html: ההודעה של %{target} הוסרה ע"י %{name} destroy_unavailable_domain_html: "%{name} התחיל/ה מחדש משלוח לדומיין %{target}" destroy_user_role_html: "%{name} ביטל את התפקיד של %{target}" + destroy_username_block_html: "%{name} הסירו חוק לשמות משתמש המכילים %{target}" disable_2fa_user_html: "%{name} ביטל/ה את הדרישה לאימות דו-גורמי למשתמש %{target}" disable_custom_emoji_html: "%{name} השבית/ה את האמוג'י %{target}" disable_relay_html: "%{name} השביתו את הממסר %{target}" @@ -308,6 +313,7 @@ he: update_report_html: '%{name} עדכן/ה דו"ח %{target}' update_status_html: "%{name} עדכן/ה הודעה של %{target}" update_user_role_html: "%{name} שינה את התפקיד של %{target}" + update_username_block_html: "%{name} עידכנו חוק לשמות משתמש המכילים %{target}" deleted_account: חשבון מחוק empty: לא נמצאו יומנים. filter_by_action: סינון לפי פעולה @@ -1121,6 +1127,25 @@ he: two: הוצגה על ידי %{count} משתמשים במשך השבוע שעבר title: המלצות ונושאים חמים trending: נושאים חמים + username_blocks: + add_new: הוספת חדש + block_registrations: חסימת הרשמות + comparison: + contains: מכיל + equals: שווה + contains_html: מכיל %{string} + created_msg: חוק שמות משתמשים נוצר בהצלחה + delete: מחיקה + edit: + title: עריכת חוק שמות משתמש + matches_exactly_html: מתאים ל־%{string} + new: + create: יצירת כלל + title: יצירת חוק חדש לשמות משתמש + no_username_block_selected: לא בוצעו שינויים בחוקי בחירת שמות שכן לא נבחרו כאלו + not_permitted: שמות אסורים + title: חוקי שמות + updated_msg: חוק שמות משתמשים עודכן בהצלחה warning_presets: add_new: הוספת חדש delete: למחוק @@ -1740,6 +1765,10 @@ he: title: אזכור חדש poll: subject: סקר מאת %{name} הסתיים + quote: + body: 'הודעתך צוטטה על ידי %{name}:' + subject: "%{name} ציטט.ה את הודעתך" + title: ציטוט חדש reblog: body: 'הודעתך הודהדה על ידי %{name}:' subject: הודעתך הודהדה על ידי%{name} @@ -1957,7 +1986,8 @@ he: two: 'מכיל את התגיות האסורות: %{tags}' edited_at_html: נערך ב-%{date} errors: - in_reply_not_found: נראה שההודעה שאת/ה מנסה להגיב לה לא קיימת. + in_reply_not_found: נראה שההודעה שנסית להגיב לה לא קיימת. + quoted_status_not_found: נראה שההודעה שנסית לצטט לא קיימת. over_character_limit: חריגה מגבול התווים של %{max} pin_errors: direct: לא ניתן לקבע הודעות שנראותן מוגבלת למכותבים בלבד @@ -1965,8 +1995,8 @@ he: ownership: הודעות של אחרים לא יכולות להיות מוצמדות reblog: אין אפשרות להצמיד הדהודים quote_policies: - followers: עוקבים ומאוזכרים - nobody: רק מאוזכרים ומאוזכרות + followers: לעוקביך בלבד + nobody: אף אחד public: כולם title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/hu.yml b/config/locales/hu.yml index b46062b2c7b..454f5ce0176 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -190,6 +190,7 @@ hu: create_relay: Továbbító létrehozása create_unavailable_domain: Elérhetetlen domain létrehozása create_user_role: Szerepkör létrehozása + create_username_block: Felhasználónév-szabály létrehozása demote_user: Felhasználó lefokozása destroy_announcement: Közlemény törlése destroy_canonical_email_block: E-mail-tiltás törlése @@ -203,6 +204,7 @@ hu: destroy_status: Bejegyzés törlése destroy_unavailable_domain: Elérhetetlen domain törlése destroy_user_role: Szerepkör eltávolítása + destroy_username_block: Felhasználónév-szabály törlése disable_2fa_user: Kétlépcsős hitelesítés letiltása disable_custom_emoji: Egyéni emodzsi letiltása disable_relay: Továbbító letiltása @@ -237,6 +239,7 @@ hu: update_report: Bejelentés frissítése update_status: Bejegyzés frissítése update_user_role: Szerepkör frissítése + update_username_block: Felhasználónév-szabály frissítése actions: approve_appeal_html: "%{name} jóváhagyott egy fellebbezést %{target} moderátori döntéséről" approve_user_html: "%{name} jóváhagyta %{target} regisztrációját" @@ -255,6 +258,7 @@ hu: create_relay_html: "%{name} létrehozta az átirányítót: %{target}" create_unavailable_domain_html: "%{name} leállította a kézbesítést a %{target} domainbe" create_user_role_html: "%{name} létrehozta a(z) %{target} szerepkört" + create_username_block_html: "%{name} az ezt tartalmazó felhasználónevekre vonatkozó szabályt hozott létre: %{target}" demote_user_html: "%{name} lefokozta %{target} felhasználót" destroy_announcement_html: "%{name} törölte a %{target} közleményt" destroy_canonical_email_block_html: "%{name} engedélyezte a(z) %{target} hashű e-mailt" @@ -268,6 +272,7 @@ hu: destroy_status_html: "%{name} eltávolította %{target} felhasználó bejegyzését" destroy_unavailable_domain_html: "%{name} újraindította a kézbesítést a %{target} domainbe" destroy_user_role_html: "%{name} törölte a(z) %{target} szerepkört" + destroy_username_block_html: "%{name} az ezt tartalmazó felhasználónevekre vonatkozó szabályt törölt: %{target}" disable_2fa_user_html: "%{name} kikapcsolta a kétlépcsős hitelesítést %{target} felhasználó fiókján" disable_custom_emoji_html: "%{name} letiltotta az emodzsit: %{target}" disable_relay_html: "%{name} letiltotta az átirányítót: %{target}" @@ -302,6 +307,7 @@ hu: update_report_html: "%{name} frissítette a %{target} bejelentést" update_status_html: "%{name} frissítette %{target} felhasználó bejegyzését" update_user_role_html: "%{name} módosította a(z) %{target} szerepkört" + update_username_block_html: "%{name} az ezt tartalmazó felhasználónevekre vonatkozó szabályt frissített: %{target}" deleted_account: törölt fiók empty: Nem található napló. filter_by_action: Szűrés művelet alapján @@ -1085,6 +1091,25 @@ hu: other: "%{count} ember használta a múlt héten" title: Ajánlások és trendek trending: Felkapott + username_blocks: + add_new: Új hozzáadása + block_registrations: Regisztrációk blokkolása + comparison: + contains: Tartalmazza + equals: Megegyezik vele + contains_html: 'Tartalmazza ezt: %{string}' + created_msg: Felhasználónév-szabály sikeresen létrehozva + delete: Törlés + edit: + title: Felhasználónév-szabály szerkesztése + matches_exactly_html: 'Megegyezik ezzel: %{string}' + new: + create: Szabály létrehozása + title: Új felhasználónév-szabály létrehozása + no_username_block_selected: Nem változott meg egy felhasználónév-szabály sem, mert semmi sem volt kiválasztva + not_permitted: Nem engedélyezett + title: Felhasználónév-szabályok + updated_msg: Felhasználónév-szabály sikeresen frissítve warning_presets: add_new: Új hozzáadása delete: Törlés @@ -1662,6 +1687,10 @@ hu: title: Új említés poll: subject: "%{name} szavazása véget ért" + quote: + body: 'A bejegyzésedet %{name} idézte:' + subject: "%{name} idézte a bejegyzésedet" + title: Új idézet reblog: body: 'A bejegyzésedet %{name} megtolta:' subject: "%{name} megtolta a bejegyzésedet" @@ -1872,6 +1901,7 @@ hu: edited_at_html: 'Szerkesztve: %{date}' errors: in_reply_not_found: Már nem létezik az a bejegyzés, melyre válaszolni szeretnél. + quoted_status_not_found: Már nem létezik az a bejegyzés, amelyből idézni szeretnél. over_character_limit: túllépted a maximális %{max} karakteres keretet pin_errors: direct: A csak a megemlített felhasználók számára látható bejegyzések nem tűzhetők ki @@ -1879,8 +1909,8 @@ hu: ownership: Nem tűzheted ki valaki más bejegyzését reblog: Megtolt bejegyzést nem tudsz kitűzni quote_policies: - followers: Követők és említett felhasználók - nobody: Csak említett felhasználók + followers: Csak a követőid + nobody: Senki public: Mindenki title: "%{name}: „%{quote}”" visibilities: diff --git a/config/locales/ia.yml b/config/locales/ia.yml index 35dd56aad1d..1c7de3f3822 100644 --- a/config/locales/ia.yml +++ b/config/locales/ia.yml @@ -1042,6 +1042,16 @@ ia: other: Usate per %{count} personas in le ultime septimana title: Recommendationes e tendentias trending: In tendentia + username_blocks: + edit: + title: Modificar regula de nomine de usator + new: + create: Crear regula + title: Crear nove regula de nomine de usator + no_username_block_selected: Necun regula de usator ha essite cambiate perque necun ha essite seligite + not_permitted: Non permittite + title: Regulas de nomine de usator + updated_msg: Regula de nomine de usator actualisate con successo warning_presets: add_new: Adder nove delete: Deler @@ -1832,8 +1842,6 @@ ia: ownership: Le message de alcuno altere non pote esser appunctate reblog: Un impulso non pote esser affixate quote_policies: - followers: Sequitores e usatores mentionate - nobody: Solmente usatores mentionate public: Omnes title: "%{name}: “%{quote}”" visibilities: diff --git a/config/locales/is.yml b/config/locales/is.yml index 5e33189cfb2..91260629159 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -190,6 +190,7 @@ is: create_relay: Búa til endurvarpa create_unavailable_domain: Útbúa lén sem ekki er tiltækt create_user_role: Útbúa hlutverk + create_username_block: Búa til notandanafnsreglu demote_user: Lækka notanda í tign destroy_announcement: Eyða tilkynningu destroy_canonical_email_block: Eyða útilokunarblokk tölvupósts @@ -203,6 +204,7 @@ is: destroy_status: Eyða færslu destroy_unavailable_domain: Eyða léni sem ekki er tiltækt destroy_user_role: Eyða hlutverki + destroy_username_block: Eyða notandanafnsreglu disable_2fa_user: Gera tveggja-þátta auðkenningu óvirka disable_custom_emoji: Gera sérsniðið tjáningartákn óvirkt disable_relay: Gera endurvarpa óvirkan @@ -237,6 +239,7 @@ is: update_report: Uppfæra kæru update_status: Uppfæra færslu update_user_role: Uppfæra hlutverk + update_username_block: Uppfæra notandanafnsreglu actions: approve_appeal_html: "%{name} samþykkti áfrýjun á ákvörðun umsjónarmanns frá %{target}" approve_user_html: "%{name} samþykkti nýskráningu frá %{target}" @@ -255,6 +258,7 @@ is: create_relay_html: "%{name} bjó til endurvarpa %{target}" create_unavailable_domain_html: "%{name} stöðvaði afhendingu til lénsins %{target}" create_user_role_html: "%{name} útbjó %{target} hlutverk" + create_username_block_html: "%{name} bætti við reglu varðandi notendanöfn sem innihalda %{target}" demote_user_html: "%{name} lækkaði notandann %{target} í tign" destroy_announcement_html: "%{name} eyddi tilkynninguni %{target}" destroy_canonical_email_block_html: "%{name} tók af útilokun á tölvupósti með tætigildið %{target}" @@ -268,6 +272,7 @@ is: destroy_status_html: "%{name} fjarlægði færslu frá %{target}" destroy_unavailable_domain_html: "%{name} hóf aftur afhendingu til lénsins %{target}" destroy_user_role_html: "%{name} eyddi hlutverki %{target}" + destroy_username_block_html: "%{name} fjarlægði reglu varðandi notendanöfn sem innihalda %{target}" disable_2fa_user_html: "%{name} gerði kröfu um tveggja-þátta innskráningu óvirka fyrir notandann %{target}" disable_custom_emoji_html: "%{name} gerði tjáningartáknið %{target} óvirkt" disable_relay_html: "%{name} gerði endurvarpann %{target} óvirkan" @@ -302,6 +307,7 @@ is: update_report_html: "%{name} uppfærði kæru %{target}" update_status_html: "%{name} uppfærði færslu frá %{target}" update_user_role_html: "%{name} breytti hlutverki %{target}" + update_username_block_html: "%{name} uppfærði reglu varðandi notendanöfn sem innihalda %{target}" deleted_account: eyddur notandaaðgangur empty: Engar atvikaskrár fundust. filter_by_action: Sía eftir aðgerð @@ -1087,6 +1093,25 @@ is: other: Notað af %{count} aðilum síðustu vikuna title: Meðmæli og vinsælt trending: Vinsælt + username_blocks: + add_new: Bæta við nýju + block_registrations: Loka á nýskráningar + comparison: + contains: Inniheldur + equals: Er jafnt og + contains_html: Inniheldur %{string} + created_msg: Tókst að útbúa notandanafnsreglu + delete: Eyða + edit: + title: Breyta notandanafnsreglu + matches_exactly_html: Er jafnt og %{string} + new: + create: Búa til reglu + title: Búa til nýja notandanafnsreglu + no_username_block_selected: Engum notandanafnsreglum var breytt þar sem engar voru valdar + not_permitted: Ekki leyft + title: Notendanafnareglur + updated_msg: Tókst að uppfæra notandanafnsreglu warning_presets: add_new: Bæta við nýju delete: Eyða @@ -1666,6 +1691,10 @@ is: title: Ný tilvísun poll: subject: Könnun frá %{name} er lokið + quote: + body: "%{name} vitnaði í færsluna þína:" + subject: "%{name} vitnaði í færsluna þína" + title: Ný tilvitnun reblog: body: "%{name} endurbirti færsluna þína:" subject: "%{name} endurbirti færsluna þína" @@ -1876,6 +1905,7 @@ is: edited_at_html: Breytt %{date} errors: in_reply_not_found: Færslan sem þú ert að reyna að svara að er líklega ekki til. + quoted_status_not_found: Færslan sem þú ert að reyna að vitna í virðist ekki vera til. over_character_limit: hámarksfjölda stafa (%{max}) náð pin_errors: direct: Ekki er hægt að festa færslur sem einungis eru sýnilegar þeim notendum sem minnst er á @@ -1883,8 +1913,8 @@ is: ownership: Færslur frá einhverjum öðrum er ekki hægt að festa reblog: Ekki er hægt að festa endurbirtingu quote_policies: - followers: Fylgjendur og notendur sem minnst er á - nobody: Einungis notendur sem minnst er á + followers: Einungis þeir sem fylgjast með þér + nobody: Enginn public: Allir title: "%{name}: „%{quote}‟" visibilities: diff --git a/config/locales/it.yml b/config/locales/it.yml index 2bf9f656ba0..669947d5e85 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -190,6 +190,7 @@ it: create_relay: Crea Relay create_unavailable_domain: Crea Dominio Non Disponibile create_user_role: Crea Ruolo + create_username_block: Crea regola del nome utente demote_user: Retrocedi Utente destroy_announcement: Elimina Annuncio destroy_canonical_email_block: Elimina il blocco dell'e-mail @@ -203,6 +204,7 @@ it: destroy_status: Elimina Toot destroy_unavailable_domain: Elimina Dominio Non Disponibile destroy_user_role: Distruggi Ruolo + destroy_username_block: Cancella regola del nome utente disable_2fa_user: Disabilita A2F disable_custom_emoji: Disabilita Emoji Personalizzata disable_relay: Disabilita Relay @@ -237,6 +239,7 @@ it: update_report: Aggiorna segnalazione update_status: Aggiorna Toot update_user_role: Aggiorna Ruolo + update_username_block: Aggiorna regola del nome utente actions: approve_appeal_html: "%{name} ha approvato il ricorso sulla decisione di moderazione da %{target}" approve_user_html: "%{name} ha approvato l'iscrizione da %{target}" @@ -255,6 +258,7 @@ it: create_relay_html: "%{name} ha creato un relay %{target}" create_unavailable_domain_html: "%{name} ha interrotto la consegna al dominio %{target}" create_user_role_html: "%{name} ha creato il ruolo %{target}" + create_username_block_html: "%{name} ha aggiunto la regola per i nomi utente contenenti %{target}" demote_user_html: "%{name} ha retrocesso l'utente %{target}" destroy_announcement_html: "%{name} ha eliminato l'annuncio %{target}" destroy_canonical_email_block_html: "%{name} ha sbloccato l'e-mail con l'hash %{target}" @@ -268,6 +272,7 @@ it: destroy_status_html: "%{name} ha rimosso il toot di %{target}" destroy_unavailable_domain_html: "%{name} ha ripreso la consegna al dominio %{target}" destroy_user_role_html: "%{name} ha eliminato il ruolo %{target}" + destroy_username_block_html: "%{name} ha rimosso la regola per i nomi utente contenenti %{target}" disable_2fa_user_html: "%{name} ha disabilitato l'autenticazione a due fattori per l'utente %{target}" disable_custom_emoji_html: "%{name} ha disabilitato emoji %{target}" disable_relay_html: "%{name} ha disabilitato il relay %{target}" @@ -302,6 +307,7 @@ it: update_report_html: "%{name} ha aggiornato la segnalazione %{target}" update_status_html: "%{name} ha aggiornato lo status di %{target}" update_user_role_html: "%{name} ha modificato il ruolo %{target}" + update_username_block_html: "%{name} ha aggiornato la regola per i nomi utente contenenti %{target}" deleted_account: account eliminato empty: Nessun log trovato. filter_by_action: Filtra per azione @@ -1085,6 +1091,25 @@ it: other: Usato da %{count} persone nell'ultima settimana title: Raccomandazioni & Tendenze trending: Di tendenza + username_blocks: + add_new: Aggiungi una nuova + block_registrations: Blocco delle registrazioni + comparison: + contains: Contiene + equals: Uguale a + contains_html: Contiene %{string} + created_msg: Regola del nome utente creata con successo + delete: Elimina + edit: + title: Modifica regola del nome utente + matches_exactly_html: Uguale a %{string} + new: + create: Crea regola + title: Crea una nuova regola del nome utente + no_username_block_selected: Non sono state modificate le regole del nome utente in quanto non sono state selezionate + not_permitted: Non consentito + title: Regole del nome utente + updated_msg: Regola del nome utente aggiornata con successo warning_presets: add_new: Aggiungi nuovo delete: Cancella @@ -1664,6 +1689,10 @@ it: title: Nuova menzione poll: subject: Un sondaggio da %{name} è terminato + quote: + body: 'Il tuo post è stato citato da %{name}:' + subject: "%{name} ha citato il tuo post" + title: Nuova citazione reblog: body: 'Il tuo status è stato condiviso da %{name}:' subject: "%{name} ha condiviso il tuo status" @@ -1874,6 +1903,7 @@ it: edited_at_html: Modificato il %{date} errors: in_reply_not_found: Il post a cui stai tentando di rispondere non sembra esistere. + quoted_status_not_found: Il post che stai cercando di citare non sembra esistere. over_character_limit: Limite caratteri superato di %{max} pin_errors: direct: I messaggi visibili solo agli utenti citati non possono essere fissati in cima @@ -1881,8 +1911,8 @@ it: ownership: Non puoi fissare in cima un post di qualcun altro reblog: Un toot condiviso non può essere fissato in cima quote_policies: - followers: Seguaci e utenti menzionati - nobody: Solo gli utenti menzionati + followers: Solo i tuoi seguaci + nobody: Nessuno public: Tutti title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index f3c4d3089ac..43433b7934c 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1825,8 +1825,6 @@ ja: ownership: 他人の投稿を固定することはできません reblog: ブーストを固定することはできません quote_policies: - followers: フォロワーとメンションされたユーザー - nobody: メンションされたユーザーのみ public: 全員 title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/kab.yml b/config/locales/kab.yml index 96f69dcbdc4..4685887a968 100644 --- a/config/locales/kab.yml +++ b/config/locales/kab.yml @@ -485,12 +485,20 @@ kab: title: Ihacṭagen inezzaɣ trending_rank: 'Anezzuɣ #%{rank}' trending: Inezzaɣ + username_blocks: + add_new: Rnu amaynut + comparison: + contains: Igber + new: + create: Rnu alugen + title: Rnu alugen n useqdac amaynut warning_presets: add_new: Rnu amaynut delete: Kkes webhooks: delete: Kkes enable: Rmed + enabled: D urmid admin_mailer: new_report: body: "%{reporter} yettwazen ɣef %{target}" @@ -623,6 +631,7 @@ kab: other: Ayen nniḍen emoji_styles: auto: Awurman + twemoji: Twemoji errors: '404': Asebter i tettnadiḍ ulac-it da. '500': @@ -858,6 +867,8 @@ kab: other: "%{count} n tbidyutin" edited_at_html: Tettwaẓreg ass n %{date} quote_policies: + followers: Ala wid i k·m-yeṭṭafaṛen + nobody: Ula yiwen public: Yal yiwen title: '%{name} : "%{quote}"' visibilities: @@ -904,6 +915,8 @@ kab: user_mailer: appeal_approved: action: Iɣewwaṛen n umiḍan + suspicious_sign_in: + change_password: snifel awal-ik·im n uɛeddi terms_of_service_changed: sign_off: Agraw n %{domain} warning: diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 66c16c899df..52825685013 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -181,7 +181,7 @@ ko: create_canonical_email_block: 이메일 차단 생성 create_custom_emoji: 커스텀 에모지 생성 create_domain_allow: 도메인 허용 생성 - create_domain_block: 도메인 차단 추가 + create_domain_block: 도메인 차단 만들기 create_email_domain_block: 이메일 도메인 차단 생성 create_ip_block: IP 규칙 만들기 create_relay: 릴레이 생성 @@ -1069,6 +1069,12 @@ ko: other: 지난 주 동안 %{count} 명의 사람들이 사용했습니다 title: 추천과 유행 trending: 유행 중 + username_blocks: + add_new: 새로 추가 + new: + create: 규칙 만들기 + title: 새 유저네임 규칙 만들기 + not_permitted: 허용하지 않음 warning_presets: add_new: 새로 추가 delete: 삭제 @@ -1838,8 +1844,6 @@ ko: ownership: 다른 사람의 게시물은 고정될 수 없습니다 reblog: 부스트는 고정될 수 없습니다 quote_policies: - followers: 팔로워와 멘션된 사람들만 - nobody: 멘션된 사람들만 public: 모두 title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 41cf7cae96f..e6fb94944b7 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -188,6 +188,7 @@ lt: create_relay: Kurti perdavimą create_unavailable_domain: Kurti nepasiekiamą domeną create_user_role: Kurti vaidmenį + create_username_block: Kurti naudotojo vardo taisyklę demote_user: Pažeminti naudotoją destroy_announcement: Ištrinti skelbimą destroy_custom_emoji: Ištrinti pasirinktinį jaustuką @@ -199,6 +200,7 @@ lt: destroy_status: Ištrinti įrašą destroy_unavailable_domain: Ištrinti nepasiekiamą domeną destroy_user_role: Sunaikinti vaidmenį + destroy_username_block: Ištrinti naudotojo vardo taisyklę disable_2fa_user: Išjungti 2FA disable_custom_emoji: Išjungti pasirinktinį jaustuką disable_relay: Išjungti perdavimą @@ -231,6 +233,7 @@ lt: update_report: Atnaujinti ataskaitą update_status: Atnaujinti įrašą update_user_role: Atnaujinti vaidmenį + update_username_block: Atnaujinti naudotojo vardo taisyklę actions: approve_appeal_html: "%{name} patvirtino prižiūjimo veiksmo apeliaciją iš %{target}" approve_user_html: "%{name} patvirtino registraciją iš %{target}" @@ -245,6 +248,7 @@ lt: create_relay_html: "%{name} sukūrė perdavimą %{target}" create_unavailable_domain_html: "%{name} sustabdė tiekimą į domeną %{target}" create_user_role_html: "%{name} sukūrė %{target} vaidmenį" + create_username_block_html: "%{name} pridėjo taisyklę naudotojo vardams, turintiems %{target}" demote_user_html: "%{name} pažemino naudotoją %{target}" destroy_announcement_html: "%{name} ištrynė skelbimą %{target}" destroy_custom_emoji_html: "%{name} ištrynė jaustuką %{target}" @@ -256,6 +260,7 @@ lt: destroy_status_html: "%{name} pašalino įrašą %{target}" destroy_unavailable_domain_html: "%{name} pratęsė tiekimą į domeną %{target}" destroy_user_role_html: "%{name} ištrynė %{target} vaidmenį" + destroy_username_block_html: "%{name} pašalino taisyklę naudotojo vardams, turintiems %{target}" disable_2fa_user_html: "%{name} išjungė dviejų veiksnių reikalavimą naudotojui %{target}" disable_custom_emoji_html: "%{name} išjungė jaustuką %{target}" disable_relay_html: "%{name} išjungė perdavimą %{target}" @@ -287,6 +292,7 @@ lt: update_report_html: "%{name} atnaujino ataskaitą %{target}" update_status_html: "%{name} atnaujino įrašą %{target}" update_user_role_html: "%{name} pakeitė %{target} vaidmenį" + update_username_block_html: "%{name} atnaujino taisyklę naudotojo vardams, turintiems %{target}" deleted_account: ištrinta paskyra empty: Žurnalų nerasta. filter_by_action: Filtruoti pagal veiksmą @@ -746,6 +752,22 @@ lt: trending_rank: 'Tendencinga #%{rank}' title: Rekomendacijos ir tendencijos trending: Tendencinga + username_blocks: + add_new: Pridėti naują + block_registrations: Blokuoti registracijas + comparison: + contains: Yra + equals: Lygus + contains_html: Yra %{string} + created_msg: Sėkmingai sukurta naudotojo vardo taisyklė. + delete: Ištrinti + edit: + title: Redaguoti naudotojo vardo taisyklę + matches_exactly_html: Lygus %{string} + new: + create: Kurti taisyklę + title: Kurti naują naudotojo vardo taisyklę + no_username_block_selected: Jokios naudotojo vardo taisyklės nebuvo pakeistos, nes nė viena buvo pasirinkta. warning_presets: add_new: Pridėti naują delete: Ištrinti @@ -912,6 +934,10 @@ lt: your_appeal_rejected: Tavo apeliacija buvo atmesta edit_profile: hint_html: "Tinkink tai, ką žmonės mato tavo viešame profilyje ir šalia įrašų. Kiti žmonės labiau linkę sekti atgal ir bendrauti su tavimi, jei tavo profilis yra užpildytas ir turi profilio nuotrauką." + emoji_styles: + auto: Automatinis + native: Vietiniai + twemoji: Tvejaustukai errors: '403': Jūs neturie prieigos matyti šiam puslapiui. '404': Puslapis nerastas. @@ -1183,6 +1209,8 @@ lt: other: "%{count} vaizdų" boosted_from_html: Pakelta iš %{acct_link} content_warning: 'Turinio įspėjimas: %{warning}' + errors: + quoted_status_not_found: Įrašas, kurį bandote cituoti, atrodo, neegzistuoja. over_character_limit: pasiektas %{max} simbolių limitas pin_errors: limit: Jūs jau prisegėte maksimalų toot'ų skaičų diff --git a/config/locales/lv.yml b/config/locales/lv.yml index f7db52a40f0..da04f36494c 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -1905,8 +1905,6 @@ lv: ownership: Kāda cita ierakstu nevar piespraust reblog: Pastiprinātu ierakstu nevar piespraust quote_policies: - followers: Sekotāji un pieminētie lietotāji - nobody: Tikai pieminētie lietotāji public: Visi title: "%{name}: “%{quote}”" visibilities: diff --git a/config/locales/nan.yml b/config/locales/nan.yml index 354ef8b0f85..a6203584284 100644 --- a/config/locales/nan.yml +++ b/config/locales/nan.yml @@ -97,7 +97,7 @@ nan: silenced: 受限制 suspended: 權限中止ah title: 管理 - moderation_notes: 管理ê註釋 + moderation_notes: 管理ê筆記 most_recent_activity: 最近ê活動時間 most_recent_ip: 最近ê IP no_account_selected: 因為無揀任何口座,所以lóng無改變 @@ -187,6 +187,7 @@ nan: create_relay: 建立中繼 create_unavailable_domain: 建立bē當用ê域名 create_user_role: 建立角色 + create_username_block: 新造使用者號名規則 demote_user: Kā用者降級 destroy_announcement: Thâi掉公告 destroy_canonical_email_block: Thâi掉電子phue ê封鎖 @@ -200,6 +201,7 @@ nan: destroy_status: Thâi掉PO文 destroy_unavailable_domain: Thâi掉bē當用ê域名 destroy_user_role: Thâi掉角色 + destroy_username_block: 共使用者號名規則刣掉 disable_2fa_user: 停止用雙因素認證 disable_custom_emoji: 停止用自訂ê Emoji disable_relay: 停止用中繼 @@ -234,6 +236,7 @@ nan: update_report: 更新檢舉 update_status: 更新PO文 update_user_role: 更新角色 + update_username_block: 更新使用者號名規則 actions: approve_appeal_html: "%{name} 允准 %{target} 所寫ê tuì審核決定ê投訴" approve_user_html: "%{name} 允准 %{target} ê 註冊" @@ -245,18 +248,19 @@ nan: create_announcement_html: "%{name} kā公告 %{target} 建立ah" create_canonical_email_block_html: "%{name} kā hash是 %{target} ê電子phue封鎖ah" create_custom_emoji_html: "%{name} kā 新ê emoji %{target} 傳上去ah" - create_domain_allow_html: "%{name} 允准 %{target} 域名加入聯邦宇宙" + create_domain_allow_html: "%{name} 允准 %{target} 域名加入聯邦" create_domain_block_html: "%{name} 封鎖域名 %{target}" create_email_domain_block_html: "%{name} kā 電子phue域名 %{target} 封鎖ah" create_ip_block_html: "%{name} 建立 IP %{target} ê規則" create_relay_html: "%{name} 建立中繼 %{target}" create_unavailable_domain_html: "%{name} 停止送kàu域名 %{target}" create_user_role_html: "%{name} 建立 %{target} 角色" + create_username_block_html: "%{name} 加添用者ê名包含 %{target} ê規則ah" demote_user_html: "%{name} kā用者 %{target} 降級" destroy_announcement_html: "%{name} kā公告 %{target} thâi掉ah" destroy_canonical_email_block_html: "%{name} kā hash是 %{target} ê電子phue取消封鎖ah" destroy_custom_emoji_html: "%{name} kā 新ê emoji %{target} thâi掉ah" - destroy_domain_allow_html: "%{name} 無允准 %{target} 域名加入聯邦宇宙" + destroy_domain_allow_html: "%{name} 無允准 %{target} 域名加入聯邦" destroy_domain_block_html: "%{name} 取消封鎖域名 %{target}" destroy_email_domain_block_html: "%{name} kā 電子phue域名 %{target} 取消封鎖ah" destroy_instance_html: "%{name} 清除域名 %{target}" @@ -265,6 +269,7 @@ nan: destroy_status_html: "%{name} kā %{target} ê PO文thâi掉" destroy_unavailable_domain_html: "%{name} 恢復送kàu域名 %{target}" destroy_user_role_html: "%{name} thâi掉 %{target} 角色" + destroy_username_block_html: "%{name} thâi掉用者ê名包含 %{target} ê規則ah" disable_2fa_user_html: "%{name} 停止使用者 %{target} 用雙因素驗證" disable_custom_emoji_html: "%{name} kā 新ê emoji %{target} 停止使用ah" disable_relay_html: "%{name} 停止使用中繼 %{target}" @@ -299,6 +304,7 @@ nan: update_report_html: "%{name} 更新 %{target} ê檢舉" update_status_html: "%{name} kā %{target} ê PO文更新" update_user_role_html: "%{name} 更改 %{target} 角色" + update_username_block_html: "%{name} 更新用者ê名包含 %{target} ê規則ah" deleted_account: thâi掉ê口座 empty: Tshuē無log。 filter_by_action: 照動作過濾 @@ -531,16 +537,162 @@ nan: content_policies: comment: 內部ê筆記 description_html: Lí ē當定義用tī所有tuì tsit ê域名kap伊ê子域名來ê口座ê內容政策。 + limited_federation_mode_description_html: Lí通選擇kám beh允准tsit ê域名加入聯邦。 + policies: + reject_media: 拒絕媒體 + reject_reports: 拒絕檢舉 + silence: 限制 + suspend: 中止權限 + policy: 政策 + reason: 公開ê理由 + title: 內容政策 dashboard: + instance_accounts_dimension: 上tsē lâng跟tuè ê口座 + instance_accounts_measure: 儲存ê口座 + instance_followers_measure: lán tī hia ê跟tuè者 + instance_follows_measure: in tī tsia ê跟tuè者 instance_languages_dimension: Tsia̍p用ê語言 + instance_media_attachments_measure: 儲存ê媒體附件 + instance_reports_measure: 關係in ê檢舉 + instance_statuses_measure: 儲存ê PO文 + delivery: + all: 全部 + clear: 清寄送ê錯誤 + failing: 失敗 + restart: 重頭啟動寄送 + stop: 停止寄送 + unavailable: Bē當用 + delivery_available: 通寄送 + delivery_error_days: 寄送錯誤ê日數 + delivery_error_hint: Nā連續 %{count} kang bē當寄送,就ē自動標做bē當寄送。 + destroyed_msg: Tuì %{domain} 來ê資料,teh排隊beh thâi掉。 + empty: Tshuē無域名。 + known_accounts: + other: "%{count} ê知影ê口座" + moderation: + all: 全部 + limited: 受限制 + title: 管理 + moderation_notes: + create: 加添管理筆記 + created_msg: 站臺ê管理記錄成功建立! + description_html: 檢視á是替別ê管理者kap未來ê家己留筆記 + destroyed_msg: 站臺ê管理記錄成功thâi掉! + placeholder: 關係本站、行ê行動,á是其他通幫tsān lí未來管本站ê資訊。 + title: 管理ê筆記 + private_comment: 私人評論 + public_comment: 公開ê評論 + purge: 清除 + purge_description_html: Nā lí想講tsit ê域名ē永永斷線,ē當tuì儲存內底thâi掉uì tsit ê域名來ê所有口座記錄kap相關資料。Huân-sè ē開點á時間。 + title: 聯邦 + total_blocked_by_us: Hōo lán封鎖 + total_followed_by_them: Hōo in跟tuè + total_followed_by_us: Hōo lán跟tuè + total_reported: 關係in ê檢舉 + total_storage: 媒體ê附件 + totals_time_period_hint_html: 下kha顯示ê總計包含ta̍k時ê資料。 + unknown_instance: 佇本服務器,現tsú時iáu無tsit ê域名ê記錄。 invites: + deactivate_all: Lóng停用 filter: + all: 全部 available: 通用ê expired: 過期ê title: 過濾器 title: 邀請 ip_blocks: add_new: 建立規則 + created_msg: 成功加添新ê IP規則 + delete: Thâi掉 + expires_in: + '1209600': 2 禮拜 + '15778476': 6個月 + '2629746': 1 個月 + '31556952': 1 年 + '86400': 1 kang + '94670856': 3 年 + new: + title: 建立新ê IP規則 + no_ip_block_selected: 因為無揀任何IP規則,所以lóng無改變 + title: IP規則 + relationships: + title: "%{acct} ê關係" + relays: + add_new: 加添新ê中繼 + delete: Thâi掉 + description_html: "聯邦ê中繼站 是中lâng ê服侍器,ē tī訂koh公開kàu hit ê中繼站ê服侍器之間,交換tsē-tsē ê 公開PO文。中繼站通幫tsān小型kap中型服侍器tuì聯邦宇宙發現內容,本地ê用者免手動跟tuè遠距離ê服侍器ê別lâng。" + disable: 停止使用 + disabled: 停止使用ê + enable: 啟用 + enable_hint: Lí ê服侍器tsi̍t-ē啟動,ē訂tuì tsit ê中繼逐ê公開PO文,mā ē開始送tsit ê服侍器ê公開PO文kàu hia。 + enabled: 啟用ê + inbox_url: 中繼 URL + pending: Teh等中繼站允准 + save_and_enable: 儲存koh啟用 + setup: 設定中繼ê連結 + signatures_not_enabled: Nā啟用安全模式á是受限ê聯邦模式,中繼可能buē-tàng正常運作 + status: 狀態 + title: 中繼 + report_notes: + created_msg: 檢舉記錄成功建立! + destroyed_msg: 檢舉記錄成功thâi掉! + reports: + account: + notes: + other: "%{count} 篇筆記" + action_log: 審查日誌 + action_taken_by: 操作由 + actions: + delete_description_html: 受檢舉ê PO文ē thâi掉,而且ē用tsi̍t ue̍h橫tsuā記錄,幫tsān lí提升kâng tsi̍t ê用戶未來ê違規。 + mark_as_sensitive_description_html: 受檢舉ê PO文內ê媒體ē標做敏感,而且ē用tsi̍t ue̍h橫tsuā記錄,幫tsān lí提升kâng tsi̍t ê用戶未來ê違規。 + other_description_html: 看其他控制tsit ê口座ê所行,kap自訂聯絡受檢舉ê口座ê選項。 + resolve_description_html: Buē用行動控制受檢舉ê口座,mā無用橫tsuā記錄,而且tsit ê報告ē關掉。 + silence_description_html: 本口座kan-ta ē hōo早前跟tuè ê á是手動tshiau ê看見,大大限制看見ê範圍。設定隨時ē當回復。請關所有tuì tsit ê口座ê檢舉。 + suspend_description_html: Tsit ê口座kap伊ê內容ē bē當用,落尾ē thâi掉,mā bē當hām伊互動。30 kang以內通回復。請關所有tuì tsit ê口座ê檢舉。 + actions_description_html: 決定行siánn物行動來解決tsit ê檢舉。Nā lí tuì受檢舉ê口座採用處罰,電子phue通知ē送予in,除非選擇 Pùn-sò phue 類別。 + actions_description_remote_html: 決定行siánn物行動來解決tsit ê檢舉。Tse kan-ta ē影響 lí ê 服侍器hām tsit ê遠距離服侍器聯絡kap處理伊ê內容ê方法。 + actions_no_posts: Tsit份檢舉無beh thâi掉ê相關PO文 + add_to_report: 加添其他ê內容kàu檢舉 + already_suspended_badges: + local: 已經佇tsit ê服侍器停止權限ah + remote: 已經佇in ê服侍器停止權限ah + are_you_sure: Lí kám確定? + assign_to_self: 分配hōo家kī + assigned: 分配管理者 + by_target_domain: 受檢舉ê口座ê網域 + cancel: 取消 + category: 類別 + category_description_html: Tsit ê 受檢舉ê口座kap/á是內容,ē佇kap tsit ê口座ê聯絡內底引用。 + comment: + none: 無 + comment_description_html: 為著提供其他資訊,%{name} 寫: + confirm: 確認 + confirm_action: 確認kā %{acct} 審核ê動作 + created_at: 檢舉tī + delete_and_resolve: Thâi掉PO文 + forwarded: 轉送ah + forwarded_replies_explanation: 本報告是tuì別站ê用者送ê,關係別站ê內容。本報告轉hōo lí,因為受檢舉ê內容是回應lí ê服侍器ê用者。 + forwarded_to: 有轉送kàu %{domain} + mark_as_resolved: 標做「解決ah」 + mark_as_sensitive: 標做敏感 + mark_as_unresolved: 標做「無解決」 + no_one_assigned: 無lâng + notes: + create: 加添筆記 + create_and_resolve: 標「處理ah」,留筆記 + create_and_unresolve: 留筆記,koh重開 + delete: Thâi掉 + placeholder: 描述有行siánn物行動,á是其他關聯ê更新…… + title: 筆記 + notes_description_html: 檢視á是留筆記hōo別ê管理者kap未來ê家己 + processed_msg: '檢舉 #%{id} 處理成功ah' + quick_actions_description_html: 緊行行動,á是giú kàu下kha,看檢舉ê內容: + remote_user_placeholder: tuì %{instance} 來ê遠距離用者 + reopen: 重頭phah開檢舉 + report: '檢舉 #%{id}' + roles: + privileges: + manage_announcements: 管理公告 statuses: language: 語言 trends: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index b53aa68a652..206992ec8c0 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -190,6 +190,7 @@ nl: create_relay: Relay aanmaken create_unavailable_domain: Niet beschikbaar domein aanmaken create_user_role: Rol aanmaken + create_username_block: Gebruikersnaam-regel aanmaken demote_user: Gebruiker degraderen destroy_announcement: Mededeling verwijderen destroy_canonical_email_block: E-mailblokkade verwijderen @@ -203,6 +204,7 @@ nl: destroy_status: Toot verwijderen destroy_unavailable_domain: Niet beschikbaar domein verwijderen destroy_user_role: Rol permanent verwijderen + destroy_username_block: Gebruikersnaam-regel verwijderen disable_2fa_user: Tweestapsverificatie uitschakelen disable_custom_emoji: Lokale emojij uitschakelen disable_relay: Relay uitschakelen @@ -237,6 +239,7 @@ nl: update_report: Rapportage bijwerken update_status: Bericht bijwerken update_user_role: Rol bijwerken + update_username_block: Gebruikersnaam-regel bijwerken actions: approve_appeal_html: "%{name} heeft het bezwaar tegen de moderatiemaatregel van %{target} goedgekeurd" approve_user_html: "%{name} heeft de registratie van %{target} goedgekeurd" @@ -255,6 +258,7 @@ nl: create_relay_html: "%{name} heeft een relay aangemaakt %{target}" create_unavailable_domain_html: "%{name} heeft de bezorging voor domein %{target} beëindigd" create_user_role_html: "%{name} maakte de rol %{target} aan" + create_username_block_html: "%{name} heeft regel toegevoegd voor gebruikersnamen die %{target} bevatten" demote_user_html: Gebruiker %{target} is door %{name} gedegradeerd destroy_announcement_html: "%{name} heeft de mededeling %{target} verwijderd" destroy_canonical_email_block_html: "%{name} deblokkeerde e-mail met de hash %{target}" @@ -268,6 +272,7 @@ nl: destroy_status_html: Bericht van %{target} is door %{name} verwijderd destroy_unavailable_domain_html: "%{name} heeft de bezorging voor domein %{target} hervat" destroy_user_role_html: "%{name} verwijderde de rol %{target}" + destroy_username_block_html: "%{name} heeft regel verwijderd voor gebruikersnamen die %{target} bevatten" disable_2fa_user_html: De vereiste tweestapsverificatie voor %{target} is door %{name} uitgeschakeld disable_custom_emoji_html: Emoji %{target} is door %{name} uitgeschakeld disable_relay_html: "%{name} heeft de relay %{target} uitgeschakeld" @@ -302,6 +307,7 @@ nl: update_report_html: Rapportage %{target} is door %{name} bijgewerkt update_status_html: "%{name} heeft de berichten van %{target} bijgewerkt" update_user_role_html: "%{name} wijzigde de rol %{target}" + update_username_block_html: "%{name} heeft regel bijgewerkt voor gebruikersnamen die %{target} bevatten" deleted_account: verwijderd account empty: Geen logs gevonden. filter_by_action: Op actie filteren @@ -1085,6 +1091,25 @@ nl: other: Door %{count} mensen tijdens de afgelopen week gebruikt title: Aanbevelingen & trends trending: Trending + username_blocks: + add_new: Nieuwe toevoegen + block_registrations: Registraties blokkeren + comparison: + contains: Bevat + equals: Is gelijk aan + contains_html: Bevat %{string} + created_msg: Gebruikersnaam-regel succesvol aangemaakt + delete: Verwijderen + edit: + title: Gebruikersnaam-regel bewerken + matches_exactly_html: Is gelijk aan %{string} + new: + create: Regel aanmaken + title: Nieuwe gebruikersnaam-regel aanmaken + no_username_block_selected: Er zijn geen gebruikersnaam-regels gewijzigd omdat er geen zijn geselecteerd + not_permitted: Niet toegestaan + title: Gebruikersnaam-regels + updated_msg: Gebruikersnaam-regel succesvol bijgewerkt warning_presets: add_new: Nieuwe toevoegen delete: Verwijderen @@ -1662,6 +1687,10 @@ nl: title: Nieuwe vermelding poll: subject: Een peiling van %{name} is beëindigd + quote: + body: 'Jouw bericht werd door %{name} geciteerd:' + subject: "%{name} heeft jouw bericht geciteerd" + title: Nieuw citaat reblog: body: 'Jouw bericht werd door %{name} geboost:' subject: "%{name} boostte jouw bericht" @@ -1872,6 +1901,7 @@ nl: edited_at_html: Bewerkt op %{date} errors: in_reply_not_found: Het bericht waarop je probeert te reageren lijkt niet te bestaan. + quoted_status_not_found: Het bericht die je probeert te citeren lijkt niet te bestaan. over_character_limit: Limiet van %{max} tekens overschreden pin_errors: direct: Berichten die alleen zichtbaar zijn voor vermelde gebruikers, kunnen niet worden vastgezet @@ -1879,8 +1909,8 @@ nl: ownership: Een bericht van iemand anders kan niet worden vastgemaakt reblog: Een boost kan niet worden vastgezet quote_policies: - followers: Volgers en vermelde gebruikers - nobody: Alleen vermelde gebruikers + followers: Alleen jouw volgers + nobody: Niemand public: Iedereen title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 7e3b453371f..14c26ea74a0 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1875,8 +1875,6 @@ nn: ownership: Du kan ikkje festa andre sine tut reblog: Ei framheving kan ikkje festast quote_policies: - followers: Tilhengjarar og nemnde folk - nobody: Berre nemnde folk public: Alle title: "%{name}: «%{quote}»" visibilities: diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 36897280f5d..29de53637cd 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -196,6 +196,7 @@ pl: create_relay: Utwórz przekaźnik create_unavailable_domain: Utwórz niedostępną domenę create_user_role: Utwórz rolę + create_username_block: Utwórz zasadę nazwy użytkownika demote_user: Zdegraduj użytkownika destroy_announcement: Usuń ogłoszenie destroy_canonical_email_block: Usuń blokadę e-mail @@ -209,6 +210,7 @@ pl: destroy_status: Usuń wpis destroy_unavailable_domain: Usuń niedostępną domenę destroy_user_role: Zlikwiduj rolę + destroy_username_block: Usuń zasadę nazwy użytkownika disable_2fa_user: Wyłącz 2FA disable_custom_emoji: Wyłącz niestandardowe emoji disable_relay: Wyłącz przekaźnik @@ -243,6 +245,7 @@ pl: update_report: Wiadomości or raporcie update_status: Aktualizuj wpis update_user_role: Aktualizuj rolę + update_username_block: Zaktualizuj zasadę nazwy użytkownika actions: approve_appeal_html: "%{name} zatwierdził(-a) odwołanie decyzji moderacyjnej od %{target}" approve_user_html: "%{name} zatwierdził rejestrację od %{target}" @@ -261,6 +264,7 @@ pl: create_relay_html: "%{name} utworzył przekaźnik %{target}" create_unavailable_domain_html: "%{name} przestał(a) doręczać na domenę %{target}" create_user_role_html: "%{name} utworzył rolę %{target}" + create_username_block_html: Użytkownik %{name} dodał zasadę dla nazw użytkowników zawierających %{target} demote_user_html: "%{name} zdegradował(a) użytkownika %{target}" destroy_announcement_html: "%{name} usunął(-ęła) ogłoszenie %{target}" destroy_canonical_email_block_html: "%{name} odblokował(a) e-mail z hashem %{target}" @@ -274,6 +278,7 @@ pl: destroy_status_html: "%{name} usunął(-ęła) wpis użytkownika %{target}" destroy_unavailable_domain_html: "%{name} wznowił(a) doręczanie do domeny %{target}" destroy_user_role_html: "%{name} usunął rolę %{target}" + destroy_username_block_html: Użytkownik %{name} usunął zasadę dla nazw użytkowników zawierających %{target} disable_2fa_user_html: "%{name} wyłączył(a) uwierzytelnianie dwuskładnikowe użytkownikowi %{target}" disable_custom_emoji_html: "%{name} wyłączył(a) emoji %{target}" disable_relay_html: "%{name} wyłączył przekaźnik %{target}" @@ -308,6 +313,7 @@ pl: update_report_html: "%{target} zaktualizowany przez %{name}" update_status_html: "%{name} zaktualizował(a) wpis użytkownika %{target}" update_user_role_html: "%{name} zmienił rolę %{target}" + update_username_block_html: Użytkownik %{name} zaktualizował zasadę dla nazw użytkowników zawierających %{target} deleted_account: usunięte konto empty: Nie znaleziono aktywności w dzienniku. filter_by_action: Filtruj według działania @@ -315,6 +321,7 @@ pl: title: Dziennik działań administracyjnych unavailable_instance: "(domena niedostępna)" announcements: + back: Powrót do ogłoszeń destroyed_msg: Pomyślnie usunięto ogłoszenie! edit: title: Edytuj ogłoszenie @@ -323,6 +330,9 @@ pl: new: create: Utwórz ogłoszenie title: Nowe ogłoszenie + preview: + explanation_html: 'Wiadomość e-mail zostanie wysłana do %{display_count} użytkowników. Otrzymają oni wiadomość o następującej treści:' + title: Podgląd powiadomienia publish: Opublikuj published_msg: Pomyślnie opublikowano ogłoszenie! scheduled_for: Zaplanowano na %{time} @@ -491,6 +501,29 @@ pl: new: title: Importuj zablokowane domeny no_file: Nie wybrano pliku + fasp: + debug: + callbacks: + created_at: 'Utworzono:' + delete: Usuń + ip: Adres IP + providers: + active: Aktywne + base_url: Podstawowy adres URL + delete: Usuń + edit: Edytuj dostawców + finish_registration: Zakończ rejestrację + name: Nazwa + providers: Dostawca + registration_requested: Wymagana rejestracja + registrations: + confirm: Zatwierdź + reject: Odrzuć + save: Zapisz + sign_in: Zaloguj się + status: Status + title: Dostawcy usług pomocniczych Fediverse (Fediverse Auxiliary Service Providers) + title: FASP follow_recommendations: description_html: "Polecane obserwacje pomagają nowym użytkownikom szybko odnaleźć interesujące treści. Jeżeli użytkownik nie wchodził w interakcje z innymi wystarczająco często, aby powstały spersonalizowane rekomendacje, polecane są te konta. Są one obliczane każdego dnia na podstawie kombinacji kont o największej liczbie niedawnej aktywności i największej liczbie lokalnych obserwatorów dla danego języka." language: Dla języka @@ -565,6 +598,8 @@ pl: all: Wszystkie limited: Ograniczone title: Moderacja + moderation_notes: + title: Notatki moderacyjne private_comment: Prywatny komentarz public_comment: Publiczny komentarz purge: Wyczyść @@ -779,11 +814,16 @@ pl: title: Role rules: add_new: Dodaj zasadę + add_translation: Dodaj tłumaczenie delete: Usuń description_html: Chociaż większość twierdzi, że przeczytała i zgadza się z warunkami korzystania z usługi, zwykle ludzie nie czytają ich, dopóki nie pojawi się problem. Ułatw użytkownikom szybkie przejrzenie zasad serwera, umieszczając je na prostej liście punktowanej. Postaraj się, aby poszczególne zasady były krótkie i proste, ale staraj się też nie dzielić ich na wiele oddzielnych elementów. edit: Edytuj zasadę empty: Jeszcze nie zdefiniowano zasad serwera. + move_down: Przenieś w dół + move_up: Przenieś w górę title: Regulamin serwera + translation: Tłumaczenie + translations: Tłumaczenia settings: about: manage_rules: Zarządzaj regułami serwera @@ -809,6 +849,7 @@ pl: discovery: follow_recommendations: Polecane konta preamble: Prezentowanie interesujących treści ma kluczowe znaczenie dla nowych użytkowników, którzy mogą nie znać nikogo z Mastodona. Kontroluj, jak różne funkcje odkrywania działają na Twoim serwerze. + privacy: Prywatność profile_directory: Katalog profilów public_timelines: Publiczne osie czasu publish_statistics: Publikuj statystyki @@ -1066,6 +1107,22 @@ pl: other: Użyte przez %{count} osób w ciągu ostatniego tygodnia title: Rekomendacje i Trendy trending: Popularne + username_blocks: + add_new: Dodaj nową + comparison: + contains: Zawiera + equals: Równa się + contains_html: Zawiera %{string} + delete: Usuń + edit: + title: Edytuj zasadę nazwy użytkownika + matches_exactly_html: Równa się %{string} + new: + create: Dodaj zasadę + title: Utwórz zasadę nazwy użytkownika + not_permitted: Brak uprawnień + title: Zasady nazwy użytkownika + updated_msg: Pomyślnie zaktualizowano zasadę nazwy użytkownika warning_presets: add_new: Dodaj nowy delete: Usuń @@ -1332,6 +1389,10 @@ pl: basic_information: Podstawowe informacje hint_html: "Dostosuj to, co ludzie widzą na Twoim profilu publicznym i obok Twoich wpisów. Inne osoby są bardziej skłonne obserwować Cię i wchodzić z Tobą w interakcje, gdy masz wypełniony profil i zdjęcie profilowe." other: Inne + emoji_styles: + auto: Automatycznie + native: Natywny + twemoji: Twemoji errors: '400': Wysłane zgłoszenie jest nieprawidłowe lub uszkodzone. '403': Nie masz uprawnień, aby wyświetlić tę stronę. @@ -1899,12 +1960,17 @@ pl: edited_at_html: Edytowane %{date} errors: in_reply_not_found: Post, na który próbujesz odpowiedzieć, nie istnieje. + quoted_status_not_found: Wpis, który próbujesz zacytować, nie istnieje. over_character_limit: limit %{max} znaków przekroczony pin_errors: direct: Nie możesz przypiąć wpisu, który jest widoczny tylko dla wspomnianych użytkowników limit: Przekroczyłeś maksymalną liczbę przypiętych wpisów ownership: Nie możesz przypiąć cudzego wpisu reblog: Nie możesz przypiąć podbicia wpisu + quote_policies: + followers: Tylko obserwujący + nobody: Nikt + public: Wszyscy title: '%{name}: "%{quote}"' visibilities: direct: Bezpośredni diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 1e630275ce8..a4d5184f37e 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1870,8 +1870,6 @@ pt-BR: ownership: As publicações dos outros não podem ser fixadas reblog: Um impulso não pode ser fixado quote_policies: - followers: Seguidores e usuários mencionados - nobody: Apenas usuários mencionados public: Todos title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index f2171b80786..3be630dbcdc 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -190,6 +190,7 @@ pt-PT: create_relay: Criar retransmissor create_unavailable_domain: Criar domínio indisponível create_user_role: Criar função + create_username_block: Criar Regra de Nome de Utilizador demote_user: Despromover utilizador destroy_announcement: Eliminar mensagem de manutenção destroy_canonical_email_block: Eliminar bloqueio de e-mail @@ -203,6 +204,7 @@ pt-PT: destroy_status: Eliminar publicação destroy_unavailable_domain: Eliminar domínio indisponível destroy_user_role: Eliminar função + destroy_username_block: Eliminar Regra de Nome de Utilizador disable_2fa_user: Desativar 2FA disable_custom_emoji: Desativar emoji personalizado disable_relay: Desativar retransmissor @@ -237,6 +239,7 @@ pt-PT: update_report: Atualizar denúncia update_status: Atualizar publicação update_user_role: Atualizar função + update_username_block: Atualizar Regra de Nome de Utilizador actions: approve_appeal_html: "%{name} aprovou a contestação da decisão de moderação de %{target}" approve_user_html: "%{name} aprovou a inscrição de %{target}" @@ -255,6 +258,7 @@ pt-PT: create_relay_html: "%{name} criou o retransmissor %{target}" create_unavailable_domain_html: "%{name} parou as entregas ao domínio %{target}" create_user_role_html: "%{name} criou a função %{target}" + create_username_block_html: "%{name} adicionou regra para nomes de utilizadores que contêm %{target}" demote_user_html: "%{name} despromoveu o utilizador %{target}" destroy_announcement_html: "%{name} eliminou a mensagem de manutenção %{target}" destroy_canonical_email_block_html: "%{name} desbloqueou o e-mail com a hash %{target}" @@ -268,6 +272,7 @@ pt-PT: destroy_status_html: "%{name} removeu a publicação de %{target}" destroy_unavailable_domain_html: "%{name} retomou as entregas ao domínio %{target}" destroy_user_role_html: "%{name} eliminou a função %{target}" + destroy_username_block_html: "%{name} removeu regra para nomes de utilizadores que contêm %{target}" disable_2fa_user_html: "%{name} desativou o requerimento de autenticação em dois passos para o utilizador %{target}" disable_custom_emoji_html: "%{name} desativou o emoji %{target}" disable_relay_html: "%{name} desativou o retransmissor %{target}" @@ -302,6 +307,7 @@ pt-PT: update_report_html: "%{name} atualizou a denúncia %{target}" update_status_html: "%{name} atualizou a publicação de %{target}" update_user_role_html: "%{name} alterou a função %{target}" + update_username_block_html: "%{name} atualizou regra para nomes de utilizadores que contêm %{target}" deleted_account: conta eliminada empty: Não foram encontrados registos. filter_by_action: Filtrar por ação @@ -1085,6 +1091,25 @@ pt-PT: other: Utilizada por %{count} pessoas na última semana title: Recomendações e destaques trending: Em destaque + username_blocks: + add_new: Adicionar novo + block_registrations: Bloquear inscrições + comparison: + contains: Contém + equals: Igual a + contains_html: Contém %{string} + created_msg: Regra de nome de utilizador criada com sucesso + delete: Eliminar + edit: + title: Editar regra de nome de utilizador + matches_exactly_html: Igual a %{string} + new: + create: Criar regra + title: Criar nova regra de utilizador + no_username_block_selected: Não foi alterada nenhuma regra de nome de utilizador, pois nenhuma foi selecionada + not_permitted: Não permitido + title: Regras de nome de utilizador + updated_msg: Regra de nome de utilizador atualizada com sucesso warning_presets: add_new: Adicionar novo delete: Eliminar @@ -1662,6 +1687,10 @@ pt-PT: title: Nova menção poll: subject: A sondagem de %{name} terminou + quote: + body: 'A sua publicação foi citada por %{name}:' + subject: "%{name} citou a sua publicação" + title: Nova citação reblog: body: 'A tua publicação foi impulsionada por %{name}:' subject: "%{name} impulsionou a tua publicação" @@ -1872,6 +1901,7 @@ pt-PT: edited_at_html: Editado em %{date} errors: in_reply_not_found: A publicação a que estás a tentar responder parece não existir. + quoted_status_not_found: A publicação que está a tentar citar parece não existir. over_character_limit: limite de caracteres %{max} excedido pin_errors: direct: As publicações que só são visíveis para os utilizadores mencionados não podem ser fixadas @@ -1879,8 +1909,8 @@ pt-PT: ownership: Não podem ser fixadas publicações de outras pessoas reblog: Não é possível fixar um impulso quote_policies: - followers: Seguidores e utilizadores mencionados - nobody: Apenas utilizadores mencionados + followers: Apenas os seus seguidores + nobody: Ninguém public: Todos title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 971846789b6..001e48c650c 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1352,7 +1352,7 @@ ru: edit_profile: basic_information: Основные данные hint_html: "Здесь вы можете изменить всё то, что будет отображаться в вашем публичном профиле и рядом с вашими постами. На вас будут чаще подписываться и с вами будут чаще взаимодействовать, если у вас будет заполнен профиль и добавлено фото профиля." - other: Прочее + other: Разное emoji_styles: auto: Автоматически native: Как в системе @@ -1393,7 +1393,7 @@ ru: featured_tags: add_new: Добавить errors: - limit: Вы уже добавили максимальное число хештегов + limit: Вы достигли максимального количества хештегов, которые можно рекомендовать в профиле hint_html: "Рекомендуйте самые важные для вас хештеги в своём профиле. Это отличный инструмент для того, чтобы держать подписчиков в курсе ваших долгосрочных проектов и творческих работ. Рекомендации хештегов заметны в вашем профиле и предоставляют быстрый доступ к вашим постам." filters: contexts: @@ -1477,56 +1477,56 @@ ru: other: Проверьте введённые вами данные! Далее по странице вы можете увидеть %{count} сообщений об ошибке imports: errors: - empty: Пустой CSV-файл - incompatible_type: Несовместимость с выбранным типом импорта - invalid_csv_file: 'Неверный файл CSV. Ошибка: %{error}' + empty: Файл CSV пуст + incompatible_type: Несовместим с выбранным типом данных для импорта + invalid_csv_file: 'Ошибка при чтении файла CSV: %{error}' over_rows_processing_limit: содержит более %{count} строк too_large: Файл слишком большой failures: Ошибки - imported: Импортирован - mismatched_types_warning: Возможно, вы выбрали неверный тип для этого импорта, пожалуйста, перепроверьте. + imported: Импортировано + mismatched_types_warning: По-видимому, вы выбрали неверный тип данных для импорта. Проверьте всё внимательно! modes: merge: Объединить - merge_long: Сохранить имеющиеся данные и добавить новые. - overwrite: Перезаписать - overwrite_long: Перезаписать имеющиеся данные новыми. + merge_long: Добавить новые данные к уже имеющимся + overwrite: Заменить + overwrite_long: Заменить имеющиеся данные новыми overwrite_preambles: blocking_html: - few: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунта, из файла %{filename}. - many: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунов, из файла %{filename}. - one: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунт, из файла %{filename}. - other: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунтов, из файла %{filename}. + few: Вы собираетесь заменить свой список заблокированных пользователей данными из файла %{filename}, после чего вы будете блокировать %{count} пользователей. + many: Вы собираетесь заменить свой список заблокированных пользователей данными из файла %{filename}, после чего вы будете блокировать %{count} пользователей. + one: Вы собираетесь заменить свой список заблокированных пользователей данными из файла %{filename}, после чего вы будете блокировать %{count} пользователя. + other: Вы собираетесь заменить свой список заблокированных пользователей данными из файла %{filename}, после чего вы будете блокировать %{count} пользователей. bookmarks_html: - few: Вы собираетесь заменить свои закладки, в которых сейчас %{count} поста, из файла %{filename}. - many: Вы собираетесь заменить свои закладки, в которых сейчас %{count} постов, из файла %{filename}. - one: Вы собираетесь заменить свои закладки, в которых сейчас %{count} пост, из файла %{filename}. - other: Вы собираетесь заменить свои закладки, в которых сейчас %{count} постов, из файла %{filename}. + few: Вы собираетесь заменить свои закладки данными из файла %{filename}, после чего у вас в закладках будет %{count} поста. + many: Вы собираетесь заменить свои закладки данными из файла %{filename}, после чего у вас в закладках будет %{count} постов. + one: Вы собираетесь заменить свои закладки данными из файла %{filename}, после чего у вас в закладках будет %{count} пост. + other: Вы собираетесь заменить свои закладки данными из файла %{filename}, после чего у вас в закладках будет %{count} постов. domain_blocking_html: - few: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} домена, из файла %{filename}. - many: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} доменов, из файла %{filename}. - one: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} домен, из файла %{filename}. - other: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} доменов, из файла %{filename}. + few: Вы собираетесь заменить свой список заблокированных доменов данными из файла %{filename}, после чего вы будете блокировать %{count} домена. + many: Вы собираетесь заменить свой список заблокированных доменов данными из файла %{filename}, после чего вы будете блокировать %{count} доменов. + one: Вы собираетесь заменить свой список заблокированных доменов данными из файла %{filename}, после чего вы будете блокировать %{count} домен. + other: Вы собираетесь заменить свой список заблокированных доменов данными из файла %{filename}, после чего вы будете блокировать %{count} доменов. following_html: - few: Вы собираетесь подписаться на %{count} аккаунта из файла %{filename} и отписаться от всех прочих. - many: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename} и отписаться от всех прочих. - one: Вы собираетесь подписаться на %{count} аккаунт из файла %{filename} и отписаться от всех прочих. - other: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename} и отписаться от всех прочих. + few: Вы собираетесь подписаться на %{count} пользователей из файла %{filename} и отписаться от всех прочих. + many: Вы собираетесь подписаться на %{count} пользователей из файла %{filename} и отписаться от всех прочих. + one: Вы собираетесь подписаться на %{count} пользователя из файла %{filename} и отписаться от всех прочих. + other: Вы собираетесь подписаться на %{count} пользователей из файла %{filename} и отписаться от всех прочих. lists_html: - few: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунта. - many: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунтов. - one: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будет добавлен %{count} аккаунт. - other: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунтов. + few: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} пользователя. + many: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} пользователей. + one: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будет добавлен %{count} пользователь. + other: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} пользователей. muting_html: - few: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. - many: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. - one: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунта из файла %{filename}. - other: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. + few: Вы собираетесь заменить свой список игнорируемых пользователей данными из файла %{filename}, после чего вы будете игнорировать %{count} пользователей. + many: Вы собираетесь заменить свой список игнорируемых пользователей данными из файла %{filename}, после чего вы будете игнорировать %{count} пользователей. + one: Вы собираетесь заменить свой список игнорируемых пользователей данными из файла %{filename}, после чего вы будете игнорировать %{count} пользователя. + other: Вы собираетесь заменить свой список игнорируемых пользователей данными из файла %{filename}, после чего вы будете игнорировать %{count} пользователей. preambles: blocking_html: - few: Вы собираетесь заблокировать %{count} аккаунта из файла %{filename}. - many: Вы собираетесь заблокировать %{count} аккаунтов из файла %{filename}. - one: Вы собираетесь заблокировать %{count} аккаунт из файла %{filename}. - other: Вы собираетесь заблокировать %{count} аккаунтов из файла %{filename}. + few: Вы собираетесь заблокировать %{count} пользователей из файла %{filename}. + many: Вы собираетесь заблокировать %{count} пользователей из файла %{filename}. + one: Вы собираетесь заблокировать %{count} пользователя из файла %{filename}. + other: Вы собираетесь заблокировать %{count} пользователей из файла %{filename}. bookmarks_html: few: Вы собираетесь добавить %{count} поста из файла %{filename} в свои закладки. many: Вы собираетесь добавить %{count} постов из файла %{filename} в свои закладки. @@ -1538,52 +1538,52 @@ ru: one: Вы собираетесь заблокировать %{count} домен из файла %{filename}. other: Вы собираетесь заблокировать %{count} доменов из файла %{filename}. following_html: - few: Вы собираетесь подписаться на %{count} аккаунта из файла %{filename}. - many: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename}. - one: Вы собираетесь подписаться на %{count} аккаунт из файла %{filename}. - other: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename}. + few: Вы собираетесь подписаться на %{count} пользователей из файла %{filename}. + many: Вы собираетесь подписаться на %{count} пользователей из файла %{filename}. + one: Вы собираетесь подписаться на %{count} пользователя из файла %{filename}. + other: Вы собираетесь подписаться на %{count} пользователей из файла %{filename}. lists_html: - few: Вы собираетесь добавить %{count} аккаунта из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. - many: Вы собираетесь добавить %{count} аккаунтов из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. - one: Вы собираетесь добавить %{count} аккаунт из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. - other: Вы собираетесь добавить %{count} аккаунтов из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + few: Вы собираетесь добавить %{count} пользователей из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + many: Вы собираетесь добавить %{count} пользователей из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + one: Вы собираетесь добавить %{count} пользователя из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + other: Вы собираетесь добавить %{count} пользователей из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. muting_html: - few: Вы собираетесь начать игнорировать %{count} аккаунта из файла %{filename}. - many: Вы собираетесь начать игнорировать %{count} аккаунтов из файла %{filename}. - one: Вы собираетесь начать игнорировать %{count} аккаунт из файла %{filename}. - other: Вы собираетесь начать игнорировать %{count} аккаунтов из файла %{filename}. - preface: Вы можете загрузить некоторые данные, например, списки людей, на которых Вы подписаны или которых блокируете, в Вашу учётную запись на этом узле из файлов, экспортированных с другого узла. - recent_imports: Недавно импортированное + few: Вы собираетесь игнорировать %{count} пользователей из файла %{filename}. + many: Вы собираетесь игнорировать %{count} пользователей из файла %{filename}. + one: Вы собираетесь игнорировать %{count} пользователя из файла %{filename}. + other: Вы собираетесь игнорировать %{count} пользователей из файла %{filename}. + preface: Вы можете перенести прежде экспортированные с другого сервера данные, такие как блокируемые вами пользователи и ваши подписки. + recent_imports: История импорта states: - finished: Готово - in_progress: В процессе - scheduled: Запланировано - unconfirmed: Неподтвержденный - status: Статус - success: Ваши данные были успешно загружены и будут обработаны с должной скоростью - time_started: Началось в + finished: Завершён + in_progress: Выполняется + scheduled: Запланирован + unconfirmed: Не подтверждён + status: Состояние + success: Ваши данные были загружены и в скором времени будут обработаны + time_started: Начат titles: - blocking: Импорт заблокированных аккаунтов + blocking: Импорт списка заблокированных пользователей bookmarks: Импорт закладок - domain_blocking: Импорт заблокированных доменов - following: Импорт последующих аккаунтов - lists: Импортировать список - muting: Импорт отключенных аккаунтов + domain_blocking: Импорт списка заблокированных доменов + following: Импорт подписок + lists: Импорт списков + muting: Импорт списка игнорируемых пользователей type: Тип импорта type_groups: constructive: Подписки и закладки - destructive: Блокировки и игнорируемые + destructive: Чёрный список types: - blocking: Список блокировки + blocking: Заблокированные пользователи bookmarks: Закладки - domain_blocking: Список доменных блокировок + domain_blocking: Заблокированные домены following: Подписки lists: Списки - muting: Список глушения + muting: Игнорируемые пользователи upload: Загрузить invites: delete: Удалить - expired: Истекло + expired: Срок действия истёк expires_in: '1800': 30 минут '21600': 6 часов @@ -1592,121 +1592,127 @@ ru: '604800': 1 неделя '86400': 1 день expires_in_prompt: Бессрочно - generate: Сгенерировать + generate: Создать ссылку invalid: Это приглашение недействительно - invited_by: 'Вас пригласил(а):' + invited_by: 'Вы были приглашены этим пользователем:' max_uses: few: "%{count} раза" many: "%{count} раз" one: "%{count} раз" - other: "%{count} раза" + other: "%{count} раз" max_uses_prompt: Без ограничения - prompt: Создавайте и делитесь ссылками с другими, чтобы предоставить им доступом к этому узлу. + prompt: Создавайте приглашения и делитесь ими с другими людьми, чтобы они могли зарегистрироваться на этом сервере table: expires_at: Истекает - uses: Исп. - title: Пригласить людей + uses: Регистрации + title: Приглашения lists: errors: - limit: Вы достигли максимального количества пользователей + limit: Вы достигли максимального количества списков login_activities: authentication_methods: - otp: приложение двухфакторной аутентификации - password: пароль - sign_in_token: код безопасности электронной почты - webauthn: ключи безопасности - description_html: Если вы видите неопознанное действие, смените пароль и/или включите двухфакторную авторизацию. - empty: Нет доступной истории входов - failed_sign_in_html: Неудачная попытка входа используя %{method} через %{browser} (%{ip}) - successful_sign_in_html: Успешный вход используя %{method} через %{browser} (%{ip}) + otp: приложения для генерации кодов + password: пароля + webauthn: электронного ключа + description_html: Если вы заметили действия, которых не совершали, вам следует сменить пароль и включить двухфакторную аутентификацию. + empty: История входов отсутствует + failed_sign_in_html: Неудачная попытка входа при помощи %{method} с IP-адреса %{ip} (%{browser}) + successful_sign_in_html: Вход при помощи %{method} с IP-адреса %{ip} (%{browser}) title: История входов mail_subscriptions: unsubscribe: - action: Да, отписаться + action: Да, я хочу отписаться complete: Подписка отменена - confirmation_html: Вы точно желаете отписаться от всех уведомления типа «%{type}», доставляемых из сервера Mastodon %{domain} на ваш адрес электронной почты %{email}? Вы всегда сможете подписаться снова в настройках e-mail уведомлений. - resubscribe_html: Если вы отписались от рассылки по ошибке, вы можете повторно подписаться на рассылку в настройках настроек почтовых уведомлений. - success_html: Вы больше не будете получать %{type} для Mastodon на %{domain} на вашу электронную почту %{email}. + confirmation_html: Вы уверены в том, что хотите отписаться от всех %{type}, которые вы получаете на адрес %{email} для учётной записи на сервере Mastodon %{domain}? Вы всегда сможете подписаться снова в настройках уведомлений по электронной почте. + emails: + notification_emails: + favourite: уведомлений о добавлении ваших постов в избранное + follow: уведомлений о новых подписчиках + follow_request: уведомлений о новых запросах на подписку + mention: уведомлений о новых упоминаниях + reblog: уведомлений о продвижении ваших постов + resubscribe_html: Если вы отписались по ошибке и хотите подписаться снова, перейдите на страницу настройки уведомлений по электронной почте. + success_html: Вы отказались от %{type}, которые вы получали на адрес %{email} для вашей учётной записи на сервере Mastodon %{domain}. title: Отписаться media_attachments: validations: images_and_video: Нельзя добавить видео к посту с изображениями - not_found: Медиа %{ids} не найдено или уже прикреплено к другому сообщению - not_ready: Не удаётся прикрепить файлы, обработка которых не завершена. Повторите попытку чуть позже! - too_many: Нельзя добавить более 4 файлов + not_found: Медиа %{ids} не найдены или уже прикреплены к другому посту + not_ready: Обработка некоторых прикреплённых файлов ещё не окончена. Подождите немного и попробуйте снова! + too_many: Можно прикрепить не более 4 файлов migrations: - acct: имя@домен новой учётной записи + acct: Куда cancel: Отменить переезд - cancel_explanation: Отмена перенаправления повторно активирует текущую учётную запись, но не вернёт обратно подписчиков, которые были перемещены на другую. - cancelled_msg: Переезд был успешно отменён. + cancel_explanation: После отмены перенаправления ваша текущая учётная запись снова станет активна, но ранее перенесённые подписчики не будут возвращены. + cancelled_msg: Переезд отменён. errors: - already_moved: это та же учётная запись, на которую вы мигрировали - missing_also_known_as: не ссылается на эту учетную запись + already_moved: не может быть той учётной записью, куда уже настроен переезд + missing_also_known_as: должна быть связанной учётной записью move_to_self: не может быть текущей учётной записью - not_found: не удалось найти + not_found: не найдена on_cooldown: Вы пока не можете переезжать followers_count: Подписчиков на момент переезда incoming_migrations: Переезд со старой учётной записи - incoming_migrations_html: Переезжаете с другой учётной записи? Настройте псевдоним для переноса подписчиков. + incoming_migrations_html: Вы можете добавить связанную учётную запись, если собираетесь перенести оттуда подписчиков. moved_msg: Теперь ваша учётная запись перенаправляет к %{acct}, туда же перемещаются подписчики. - not_redirecting: Для вашей учётной записи пока не настроено перенаправление. + not_redirecting: Прямо сейчас ваша учётная запись никуда не перенаправлена. on_cooldown: Вы уже недавно переносили свою учётную запись. Эта возможность будет снова доступна через %{count} дн. - past_migrations: Прошлые переезды + past_migrations: История переездов proceed_with_move: Перенести подписчиков redirected_msg: Ваша учётная запись теперь перенаправляется на %{acct}. redirecting_to: Ваша учётная запись перенаправляет к %{acct}. set_redirect: Настроить перенаправление warning: - backreference_required: Новая учётная запись должна быть сначала настроена так, чтоб ссылаться на текущую - before: 'Прежде чем продолжить, внимательно прочитайте следующую информацию:' - cooldown: После переезда наступает период, в течение которого вы не сможете ещё раз переехать - disabled_account: Вашу текущую учётная запись впоследствии нельзя будет больше использовать. При этом, у вас будет доступ к экспорту данных, а также к повторной активации учётной записи. - followers: Это действие перенесёт всех ваших подписчиков с текущей учётной записи на новую - only_redirect_html: Или же вы можете просто настроить перенаправление в ваш профиль. + backreference_required: Текущая учётная запись сначала должна быть добавлена как связанная в настройках новой учётной записи + before: 'Внимательно ознакомьтесь со следующими замечаниями перед тем как продолжить:' + cooldown: После переезда наступит период ожидания, в течение которого переезд будет невозможен + disabled_account: Переезд приведёт к тому, что вашу текущую учётную запись нельзя будет полноценно использовать. Тем не менее у вас останется доступ к экспорту данных и повторной активации учётной записи. + followers: В результате переезда все ваши подписчики будут перенесены с текущей учётной записи на новую + only_redirect_html: Также вы можете настроить перенаправление без переноса подписчиков. other_data: Никакие другие данные не будут автоматически перенесены - redirect: Профиль этой учётной записи будет обновлён с заметкой о перенаправлении, а также исключён из поиска + redirect: Профиль текущей учётной записи будет исключён из поиска, а в нём появится объявление о переезде moderation: title: Модерация move_handler: carry_blocks_over_text: Этот пользователь переехал с учётной записи %{acct}, которую вы заблокировали. - carry_mutes_over_text: Этот пользователь перешёл с учётной записи %{acct}, которую вы игнорируете. + carry_mutes_over_text: Этот пользователь переехал с учётной записи %{acct}, которую вы игнорируете. copy_account_note_text: 'Этот пользователь переехал с %{acct}, вот ваша предыдущая заметка о нём:' navigation: toggle_menu: Переключить меню notification_mailer: admin: report: - subject: "%{name} отправил жалобу" + subject: Поступила жалоба от %{name} sign_up: - subject: "%{name} зарегистрирован" + subject: "%{name} зарегистрировался (-лась) на сервере" favourite: body: "%{name} добавил(а) ваш пост в избранное:" subject: "%{name} добавил(а) ваш пост в избранное" - title: Понравившийся статус + title: Ваш пост добавили в избранное follow: body: "%{name} теперь подписан(а) на вас!" subject: "%{name} теперь подписан(а) на вас" title: Новый подписчик follow_request: - action: Управление запросами на подписку + action: Перейти к запросам на подписку body: "%{name} отправил(а) вам запрос на подписку" subject: "%{name} хочет подписаться на вас" title: Новый запрос на подписку mention: action: Ответить - body: 'Вас упомянул(а) %{name} в:' + body: "%{name} упомянул(а) вас:" subject: "%{name} упомянул(а) вас" title: Новое упоминание poll: subject: Опрос %{name} завершился reblog: - body: 'Ваш пост был продвинут %{name}:' + body: "%{name} продвинул(а) ваш пост:" subject: "%{name} продвинул(а) ваш пост" - title: Новое продвижение + title: Ваш пост продвинули status: - subject: "%{name} только что запостил(а)" + subject: "%{name} опубликовал(а) новый пост" update: - subject: "%{name} изменил(а) пост" + subject: "%{name} отредактировал(а) пост" notifications: email_events: События для уведомлений по электронной почте email_events_hint: 'Выберите события, для которых вы хотели бы получать уведомления:' @@ -1718,91 +1724,92 @@ ru: billion: млрд million: млн quadrillion: квадрлн - thousand: тыс + thousand: тыс. trillion: трлн + unit: '' otp_authentication: - code_hint: Для подтверждения введите код, сгенерированный приложением-аутентификатором - description_html: Подключив двуфакторную авторизацию, для входа в свою учётную запись вам будет необходим смартфон и приложение-аутентификатор на нём, которое будет генерировать специальные временные коды. Без этих кодов войти в учётную запись не получиться, даже если все данные верны, что существенно увеличивает безопасность вашей учётной записи. + code_hint: Для подтверждения введите код из приложения-аутентификатора + description_html: Подключите двухфакторную аутентификацию с использованием специального приложения-аутентификатора, и тогда для входа в вашу учётную запись необходимо будет иметь при себе смартфон, который будет генерировать одноразовые коды. enable: Включить - instructions_html: "Отсканируйте этот QR-код с помощью приложения-аутентификатора, такого как Google Authenticator, Яндекс.Ключ или andOTP. После сканирования и добавления, приложение начнёт генерировать коды, которые потребуется вводить для завершения входа в учётную запись." - manual_instructions: 'Если отсканировать QR-код не получается или не представляется возможным, вы можете ввести ключ настройки вручную:' - setup: Настроить - wrong_code: Введенный код недействителен! Время сервера и время устройства правильно? + instructions_html: "Откройте Google Authenticator или другое приложение-аутентификатор на вашем смартфоне и отсканируйте этот QR-код. В дальнейшем это приложение будет генерировать одноразовые коды, которые потребуется вводить для подтверждения входа в вашу учётную запись." + manual_instructions: 'Если отсканировать QR-код не получается, введите секретный ключ вручную:' + setup: Подключить + wrong_code: Одноразовый код, который вы ввели, не подходит! Совпадает ли время на устройстве с временем на сервере? pagination: - newer: Новее - next: След - older: Старше - prev: Пред + newer: Позже + next: Вперёд + older: Раньше + prev: Назад truncate: "…" polls: errors: - already_voted: Вы уже голосовали в этом опросе + already_voted: Вы уже проголосовали в этом опросе duplicate_options: не должны повторяться duration_too_long: слишком велика duration_too_short: слишком мала - expired: Опрос уже завершился - invalid_choice: Выбранного варианта голосования не существует + expired: Этот опрос уже завершился + invalid_choice: Выбранного вами варианта ответа не существует over_character_limit: не должны превышать %{max} символов self_vote: Вы не можете голосовать в своих опросах too_few_options: должны содержать не меньше двух опций too_many_options: должны ограничиваться максимум %{max} опциями preferences: - other: Остальное - posting_defaults: Настройки отправки по умолчанию + other: Разное + posting_defaults: Предустановки для новых постов public_timelines: Публичные ленты privacy: - hint_html: "Настройте, как вы хотите, чтобы ваш профиль и ваши сообщения были найдены. Различные функции в Mastodon могут помочь вам охватить более широкую аудиторию, если они включены. Уделите время изучению этих настроек, чтобы убедиться, что они подходят для вашего случая использования." - privacy: Конфиденциальность - privacy_hint_html: Определите, какую информацию вы хотите раскрыть в интересах других. Люди находят интересные профили и приложения, просматривая список подписчиков других людей и узнавая, из каких приложений они публикуют свои сообщения, но вы можете предпочесть скрыть это. - reach: Охват - reach_hint_html: Укажите, хотите ли вы, чтобы новые люди обнаруживали вас и могли следить за вами. Хотите ли вы, чтобы ваши сообщения появлялись на экране Обзора? Хотите ли вы, чтобы другие люди видели вас в своих рекомендациях? Хотите ли вы автоматически принимать всех новых подписчиков или иметь возможность детально контролировать каждого из них? + hint_html: "Здесь вы можете определить то, как другие смогут обнаружить ваши посты и ваш профиль. Множество разных функций в Mastodon существуют для того, чтобы помочь вам выйти на более широкую аудиторию, если вы того захотите. Ознакомьтесь с этими настройками и в случае необходимости измените их согласно вашим желаниям." + privacy: Приватность + privacy_hint_html: Решите, сколько данных о себе вы готовы раскрыть ради того, чтобы они пошли на пользу другим. Просматривая ваши подписки, кто-то может обнаружить профили интересных людей, а ещё кто-нибудь может найти своё любимое приложение, увидев его название рядом с вашими постами. Тем не менее вы можете предпочесть не раскрывать эту информацию. + reach: Видимость + reach_hint_html: Решите, нужна ли вам новая аудитория и новые подписчики. Настройте по своему желанию, показывать ли ваши посты в разделе «Актуальное», рекомендовать ли ваш профиль другим людям, принимать ли всех новых подписчиков автоматически или рассматривать каждый запрос на подписку в отдельности. search: Поиск - search_hint_html: Определите, как вас могут найти. Хотите ли вы, чтобы люди находили вас по тому, о чём вы публично писали? Хотите ли вы, чтобы люди за пределами Mastodon находили ваш профиль при поиске в Интернете? Следует помнить, что полное исключение из всех поисковых систем не может быть гарантировано для публичной информации. - title: Приватность и доступ + search_hint_html: Решите, нужно ли вам скрыть себя из поиска. Настройте по своему желанию то, можно ли будет найти вас по публичным постам, а также то, можно ли будет кому угодно в интернете найти ваш профиль с помощью поисковых сайтов. Имейте в виду, что невозможно гарантировать полное исключение общедоступной информации из всех поисковых систем. + title: Приватность и видимость privacy_policy: title: Политика конфиденциальности reactions: errors: - limit_reached: Достигнут лимит разных реакций - unrecognized_emoji: не является распознанным эмодзи + limit_reached: К этому объявлению уже добавлено максимальное количество уникальных реакций + unrecognized_emoji: не соответствует ни одному известному названию эмодзи redirects: - prompt: Если вы доверяете этой ссылке, нажмите на нее, чтобы продолжить. + prompt: Переходите по ссылке только в том случае, если доверяете сайту, на который она ведёт. title: Вы покидаете %{instance}. relationships: - activity: Активность учётной записи - confirm_follow_selected_followers: Вы уверены, что хотите подписаться на выбранных подписчиков? - confirm_remove_selected_followers: Вы уверены, что хотите удалить выбранных подписчиков? - confirm_remove_selected_follows: Вы уверены, что хотите удалить выбранные подписки? - dormant: Заброшенная + activity: Фильтр по активности + confirm_follow_selected_followers: Вы уверены, что хотите взаимно подписаться на выбранных пользователей? + confirm_remove_selected_followers: Вы уверены, что хотите убрать выбранных пользователей из подписчиков? + confirm_remove_selected_follows: Вы уверены, что хотите отписаться от выбранных пользователей? + dormant: Неактивные follow_failure: Не удалось подписаться за некоторыми из выбранных аккаунтов. - follow_selected_followers: Подписаться на выбранных подписчиков + follow_selected_followers: Подписаться в ответ followers: Подписчики following: Подписки invited: Приглашённые - last_active: По последней активности - most_recent: По недавности - moved: Мигрировавшая + last_active: Сначала активные + most_recent: Сначала новые + moved: Перенаправленные mutual: Взаимные подписки - primary: Основная - relationship: Связь - remove_selected_domains: Удалить всех подписчиков для выбранных доменов - remove_selected_followers: Удалить выбранных подписчиков + primary: Действующие + relationship: Вид отношений + remove_selected_domains: Убрать всех подписчиков с того же сервера + remove_selected_followers: Убрать из подписчиков remove_selected_follows: Отписаться от выбранных пользователей - status: Статус учётной записи + status: Фильтр по состоянию учётной записи remote_follow: missing_resource: Поиск требуемого перенаправления URL для Вашей учётной записи завершился неудачей reports: errors: - invalid_rules: не ссылается на действительные правила + invalid_rules: должны соответствовать правилам сервера rss: content_warning: 'Предупреждение о содержании:' descriptions: account: Публичные посты @%{acct} - tag: 'Публичные посты отмеченные хэштегом #%{hashtag}' + tag: 'Публичные посты с хештегом #%{hashtag}' scheduled_statuses: - over_daily_limit: Вы превысили лимит в %{limit} запланированных постов на указанный день - over_total_limit: Вы превысили лимит на %{limit} запланированных постов - too_soon: дата публикации должна быть в будущем + over_daily_limit: За сутки можно создать не более %{limit} отложенных постов + over_total_limit: Всего можно создать не более %{limit} отложенных постов + too_soon: задано слишком рано self_destruct: lead_html: К сожалению, %{domain} закрывается навсегда. Если вас учётная запись находиться здесь вы не сможете продолжить использовать его, но вы можете запросить резервную копию ваших данных. title: Этот сервер закрывается @@ -1811,13 +1818,13 @@ ru: browser: Браузер browsers: alipay: Alipay - blackberry: Blackberry + blackberry: BlackBerry chrome: Chrome edge: Microsoft Edge electron: Electron firefox: Firefox generic: Неизвестный браузер - huawei_browser: Huawei Browser + huawei_browser: Браузер Huawei ie: Internet Explorer micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Browser @@ -1829,10 +1836,10 @@ ru: uc_browser: UC Browser unknown_browser: Неизвестный браузер weibo: Weibo - current_session: Текущая сессия + current_session: Текущий сеанс date: Дата - description: "%{browser} на %{platform}" - explanation: Здесь отображаются все браузеры, с которых выполнен вход в вашу учётную запись. Авторизованные приложения находятся в секции «Приложения». + description: "%{platform}, %{browser}" + explanation: Здесь перечислены все устройства, на которых вы используете свою учётную запись Mastodon. Также вы можете ip: IP platforms: adobe_air: Adobe Air @@ -1841,60 +1848,60 @@ ru: chrome_os: ChromeOS firefox_os: Firefox OS ios: iOS - kai_os: OS Кай + kai_os: KaiOS linux: Linux - mac: Mac + mac: macOS unknown_platform: Неизвестная платформа windows: Windows windows_mobile: Windows Mobile windows_phone: Windows Phone revoke: Завершить - revoke_success: Сессия завершена - title: Сессии - view_authentication_history: Посмотреть историю входов с учётной записью + revoke_success: Сеанс завершён + title: Сеансы + view_authentication_history: просмотреть историю входов в вашу учётную запись settings: account: Учётная запись - account_settings: Управление учётной записью - aliases: Псевдонимы учётной записи + account_settings: Настройки учётной записи + aliases: Связанные учётные записи appearance: Внешний вид authorized_apps: Приложения back: Назад в Mastodon delete: Удаление учётной записи development: Разработчикам - edit_profile: Изменить профиль + edit_profile: " Данные профиля" export: Экспорт - featured_tags: Избранные хэштеги + featured_tags: Рекомендации хештегов import: Импорт import_and_export: Импорт и экспорт - migrate: Миграция учётной записи - notifications: E-mail уведомление + migrate: Настройка перенаправления + notifications: Уведомления по эл. почте preferences: Настройки profile: Профиль relationships: Подписки и подписчики severed_relationships: Разорванные отношения - statuses_cleanup: Авто-удаление постов + statuses_cleanup: Автоудаление постов strikes: Замечания модерации two_factor_authentication: Подтверждение входа webauthn_authentication: Электронные ключи severed_relationships: download: Скачать (%{count}) event_type: - account_suspension: Приостановка аккаунта (%{target_name}) - domain_block: Приостановка сервера (%{target_name}) + account_suspension: Пользователь был заблокирован модераторами (%{target_name}) + domain_block: Сервер был заблокирован модераторами (%{target_name}) user_domain_block: Вы заблокировали %{target_name} lost_followers: Потерянные подписчики lost_follows: Потерянные подписки - preamble: Вы можете потерять подписчиков и последователей, если заблокируете домен или, если ваши модераторы решат приостановить работу удаленного сервера. Когда это произойдет, вы сможете загрузить списки разорванных отношений, чтобы проверить их и, возможно, импортировать на другой сервер. + preamble: Когда вы блокируете сервер или это делают модераторы вашего сервера, вы теряете подписчиков и перестаёте быть подписаны на пользователей с заблокированного сервера. После блокировки вы сможете скачать списки пользователей, отношения с которыми были разорваны, чтобы рассмотреть их или чтобы импортировать их на другом сервере. purged: Информация об этом сервере была удалена администраторами вашего сервера. type: Событие statuses: attached: audio: - few: "%{count} аудиозаписи" - many: "%{count} аудиозаписей" - one: "%{count} аудиозапись" - other: "%{count} аудиозаписи" - description: 'Вложение: %{attached}' + few: "%{count} аудиофайла" + many: "%{count} аудиофайлов" + one: "%{count} аудиофайл" + other: "%{count} аудиофайлов" + description: Прикреплено %{attached} image: few: "%{count} изображения" many: "%{count} изображений" @@ -1906,52 +1913,57 @@ ru: one: "%{count} видео" other: "%{count} видео" boosted_from_html: Продвижение польз. %{acct_link} - content_warning: 'Спойлер: %{warning}' + content_warning: 'Предупреждение о содержании: %{warning}' default_language: Тот же, что язык интерфейса disallowed_hashtags: few: 'содержались запрещённые хэштеги: %{tags}' many: 'содержались запрещённые хэштеги: %{tags}' one: 'содержался запрещённый хэштег: %{tags}' other: 'содержались запрещённые хэштеги: %{tags}' - edited_at_html: Редактировано %{date} + edited_at_html: 'Дата последнего изменения: %{date}' errors: - in_reply_not_found: Пост, на который вы пытаетесь ответить, не существует или удалён. - over_character_limit: превышен лимит символов (%{max}) + in_reply_not_found: Пост, на который вы собирались ответить, был удалён или не существует. + quoted_status_not_found: Пост, который вы собирались процитировать, был удалён или не существует. + over_character_limit: превышает максимально допустимую длину (%{max} символов) pin_errors: - direct: Сообщения, видимые только упомянутым пользователям, не могут быть закреплены - limit: Вы закрепили максимально возможное число постов + direct: Нельзя закрепить пост, который доступен только тем, кто был упомянут в нём + limit: Вы достигли максимального количества постов, которые можно закрепить в профиле ownership: Нельзя закрепить чужой пост - reblog: Нельзя закрепить продвинутый пост - title: '%{name}: "%{quote}"' + reblog: Нельзя закрепить продвижение + quote_policies: + followers: Только ваши подписчики + nobody: Никто + public: Кто угодно + title: "%{name}: «%{quote}»" visibilities: - direct: Адресованный - private: Для подписчиков - private_long: Показывать только подписчикам - public: Для всех - public_long: Показывать всем - unlisted: Скрывать из лент - unlisted_long: Показывать всем, но не отображать в публичных лентах + direct: Личное упоминание + private: Только для подписчиков + private_long: Доступен только вашим подписчикам + public: Публичный + public_long: Доступен кому угодно + unlisted: Скрытый + unlisted_long: Доступен кому угодно, но не отображается в публичных лентах statuses_cleanup: - enabled: Автоматически удалять устаревшие посты - enabled_hint: Автоматически удаляет ваши посты после того, как они достигли определённого возрастного порога, за некоторыми исключениями ниже. + enabled: Автоматически удалять старые посты + enabled_hint: По истечении определённого срока с момента публикации ваши посты, кроме соответствующих отмеченным ниже исключениям, будут автоматически удалены exceptions: Исключения - explanation: Из-за того, что удаление постов — это ресурсоёмкий процесс, оно производится медленно со временем, когда сервер менее всего загружен. По этой причине, посты могут удаляться не сразу, а спустя определённое время, по достижению возрастного порога. - ignore_favs: Игнорировать избранное - ignore_reblogs: Игнорировать продвижения + explanation: Удаление постов — это ресурсоёмкий процесс, поэтому оно производится постепенно, с течением времени, когда сервер менее всего загружен. По этой причине посты могут удаляться не сразу по прошествии установленного срока, а спустя некоторое время. + ignore_favs: Не учитывать добавление в избранное + ignore_reblogs: Не учитывать продвижения interaction_exceptions: Исключения на основе взаимодействий interaction_exceptions_explanation: 'Обратите внимание: нет никаких гарантий, что посты будут удалены, после того, как они единожды перешли порог по отметкам «избранного» или продвижений.' - keep_direct: Не удалять адресованные посты - keep_direct_hint: Не удалять ваши посты с «адресованной» видимостью. + keep_direct: Не удалять личные сообщения + keep_direct_hint: Те ваши посты, которые видны только упомянутым в них людям, не будут удалены keep_media: Не удалять посты с вложениями - keep_media_hint: Не удалять ваши посты, содержащие любые медийные вложения. + keep_media_hint: Те ваши посты, которые содержат медиавложения, не будут удалены keep_pinned: Не удалять закреплённые посты - keep_pinned_hint: Не удалять ваши посты, которые закреплены в профиле. + keep_pinned_hint: Те ваши посты, которые вы закрепили в своём профиле, не будут удалены keep_polls: Не удалять опросы - keep_polls_hint: Не удалять ваши посты с опросами. - keep_self_bookmark: Не удалять закладки - keep_self_bookmark_hint: Не удалять ваши посты с закладками. - keep_self_fav: Оставить посты, отмеченные «избранными» - keep_self_fav_hint: Не удалять ваши посты, если вы отметили их как «избранные». + keep_polls_hint: Те ваши посты, которые содержат опросы, не будут удалены + keep_self_bookmark: Не удалять посты, добавленные в закладки + keep_self_bookmark_hint: Те ваши посты, которые вы добавили в закладки, не будут удалены + keep_self_fav: Не удалять посты, добавленные в избранное + keep_self_fav_hint: Те ваши посты, которые вы добавили в избранное, не будут удалены min_age: '1209600': 2 недели '15778476': 6 месяцев @@ -1961,11 +1973,11 @@ ru: '604800': 1 неделя '63113904': 2 года '7889238': 3 месяца - min_age_label: Возрастной порог - min_favs: Порог отметок «избранного» - min_favs_hint: Не удалять ваши посты, у которых количество отметок «избранного» достигло указанного выше значения. Оставьте поле пустым, чтобы удалять посты независимо от количества отметок - min_reblogs: Порог продвижений - min_reblogs_hint: Не удаляет ваши посты, количество продвижений которых достигло указанного выше значения. Оставьте поле пустым, чтобы удалять посты независимо от количества продвижений. + min_age_label: Интервал между публикацией и удалением поста + min_favs: Количество звёздочек, при котором пост не будет удалён + min_favs_hint: Те ваши посты, которые были добавлены в избранное столько раз, сколько вы укажете выше, не будут удалены. Оставьте поле пустым, чтобы удалять посты без учёта количества звёздочек + min_reblogs: Количество продвижений, при котором пост не будет удалён + min_reblogs_hint: Те ваши посты, которые были продвинуты столько раз, сколько вы укажете выше, не будут удалены. Оставьте поле пустым, чтобы удалять посты без учёта количества продвижений stream_entries: sensitive_content: Содержимое деликатного характера strikes: diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml index 1bb2128f19b..ba1f3c12189 100644 --- a/config/locales/simple_form.ar.yml +++ b/config/locales/simple_form.ar.yml @@ -56,7 +56,6 @@ ar: scopes: ما هي المجالات المسموح بها في التطبيق ؟ إن قمت باختيار أعلى المجالات فيمكنك الاستغناء عن الخَيار اليدوي. setting_aggregate_reblogs: لا تقم بعرض المشارَكات الجديدة لمنشورات قد قُمتَ بمشاركتها سابقا (هذا الإجراء يعني المشاركات الجديدة فقط التي تلقيتَها) setting_always_send_emails: عادة لن تُرسَل إليك إشعارات البريد الإلكتروني عندما تكون نشطًا على ماستدون - setting_default_quote_policy: يسمح بالاقتباس دائما للمستخدمين المذكورين. هذا الإعداد سوف يكون نافذ المفعول فقط للمشاركات التي تم إنشاؤها مع إصدار ماستدون القادم، ولكن يمكنك تحديد تفضيلاتك للإعداد لذلك setting_default_sensitive: تُخفى الوسائط الحساسة تلقائيا ويمكن اظهارها عن طريق النقر عليها setting_display_media_default: إخفاء الوسائط المُعيَّنة كحساسة setting_display_media_hide_all: إخفاء كافة الوسائط دائمًا diff --git a/config/locales/simple_form.az.yml b/config/locales/simple_form.az.yml index e9ba86bc793..b03505358f8 100644 --- a/config/locales/simple_form.az.yml +++ b/config/locales/simple_form.az.yml @@ -1 +1,32 @@ +--- az: + simple_form: + hints: + admin_account_action: + include_statuses: İstifadəçi, hansı göndərişlərin moderasiya əməliyyatına və ya xəbərdarlığına səbəb olduğunu görəcək + defaults: + current_password: Təhlükəsizlik səbəblərinə görə lütfən hazırkı hesabın parolunu daxil edin + phrase: Mətndəki böyük-kiçik hərfdən və ya göndərişin məzmun xəbərdarlığından asılı olmayaraq uyuşdurulacaq + ip_block: + severities: + no_access: Bütün resurslara erişimi əngəllə + sessions: + otp: 'Telefon tətbiqiniz tərəfindən yaradılmış iki faktorlu kodu daxil edin və ya geri qaytarma kodlarınızdan birini istifadə edin:' + user_role: + permissions_as_keys: Bu rola sahib istifadəçilər bunlara erişə biləcək... + labels: + defaults: + confirm_new_password: Yeni parolu təsdiqlə + confirm_password: Parolu təsdiqlə + current_password: Hazırkı parol + data: Veri + new_password: Yeni parol + password: Parol + setting_system_scrollbars_ui: Sistemin ilkin diyircəyini istifadə edin + ip_block: + severities: + no_access: Erişimi əngəllə + notification_emails: + appeal: Kimsə, bir moderasiya qərarına etiraz edir + username_block: + username: Uyuşacaq söz diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml index e43970dba98..928b17ed1d1 100644 --- a/config/locales/simple_form.bg.yml +++ b/config/locales/simple_form.bg.yml @@ -56,7 +56,6 @@ bg: scopes: Указва до кои API има достъп приложението. Ако изберете диапазон от най-високо ниво, няма нужда да избирате индивидуални. setting_aggregate_reblogs: Без показване на нови подсилвания за публикации, които са неотдавна подсилени (засяга само новополучени подсилвания) setting_always_send_emails: Обикновено известията по имейл няма да са изпратени при дейна употреба на Mastodon - setting_default_quote_policy: Споменатите потребители винаги им е позволено да цитират. Тази настройка ще се отрази за публикациите, създадени със следващата версия на Mastodon, но може да изберете предпочитанията си в подготовката setting_default_sensitive: Деликатната мултимедия е скрита по подразбиране и може да се разкрие с едно щракване setting_display_media_default: Скриване на мултимедия отбелязана като деликатна setting_display_media_hide_all: Винаги скриване на мултимедията diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml index 7ad6a71dd1e..ee24975b5db 100644 --- a/config/locales/simple_form.ca.yml +++ b/config/locales/simple_form.ca.yml @@ -56,7 +56,7 @@ ca: scopes: API permeses per a accedir a l'aplicació. Si selecciones un àmbit de nivell superior, no cal que en seleccionis un d'individual. setting_aggregate_reblogs: No mostra els nous impulsos dels tuts que ja s'han impulsat recentment (només afecta als impulsos nous rebuts) setting_always_send_emails: Normalment, no s'enviarà cap notificació per correu electrònic mentre facis servir Mastodon - setting_default_quote_policy: Els usuaris mencionats sempre poden citar. Aquesta configuració només tindrà efecte en les publicacions creades amb la següent versió de Mastodon, però podeu seleccionar-ho en preparació + setting_default_quote_policy: Aquesta configuració només tindrà efecte en les publicacions creades amb la següent versió de Mastodon, però podeu seleccionar-la en preparació. setting_default_sensitive: El contingut sensible està ocult per defecte i es pot mostrar fent-hi clic setting_display_media_default: Amaga el contingut gràfic marcat com a sensible setting_display_media_hide_all: Oculta sempre tot el contingut multimèdia @@ -324,6 +324,7 @@ ca: follow_request: Algú sol·licita seguir-te mention: Algú et menciona pending_account: Un nou compte necessita revisió + quote: Algú us ha citat reblog: Algú comparteix el teu estat report: S'ha enviat l'informe nou software_updates: diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml index 9ee28a9b9f1..b318f15709d 100644 --- a/config/locales/simple_form.cs.yml +++ b/config/locales/simple_form.cs.yml @@ -56,7 +56,7 @@ cs: scopes: Která API bude aplikace moct používat. Pokud vyberete rozsah nejvyššího stupně, nebudete je muset vybírat jednotlivě. setting_aggregate_reblogs: Nezobrazovat nové boosty pro příspěvky, které byly nedávno boostnuty (ovlivňuje pouze nově přijaté boosty) setting_always_send_emails: Jinak nebudou e-mailové notifikace posílány, když Mastodon aktivně používáte - setting_default_quote_policy: Zmínení uživatelé mají vždy povoleno citovat. Toto nastavení se projeví pouze u příspěvků vytvořených s další verzí Mastodonu, ale můžete si vybrat vaše předvolby v předstihu + setting_default_quote_policy: Toto nastavení se projeví pouze u příspěvků vytvořených s další verzí Mastodon, ale svou předvolbu si můžete vybrat předem. setting_default_sensitive: Citlivá média jsou ve výchozím stavu skryta a mohou být zobrazena kliknutím setting_display_media_default: Skrývat média označená jako citlivá setting_display_media_hide_all: Vždy skrývat média @@ -162,6 +162,10 @@ cs: name: Veřejný název role, pokud má být role zobrazena jako odznak permissions_as_keys: Uživatelé s touto rolí budou moci... position: Vyšší role rozhoduje o řešení konfliktů v určitých situacích. Některé akce lze provádět pouze na rolích s nižší prioritou + username_block: + allow_with_approval: Namísto toho, aby se zabránilo registraci, bude vyžadováno vaše schválení + comparison: Mějte prosím na paměti 'Scunthorpe problém' při blokování částečných shod + username: Bude součástí shod bez ohledu na kapitalizace a běžné homoglyfy jako "4" pro "a" nebo "3" pro "e" webhook: events: Zvolte odesílané události template: Sestavte si vlastní JSON payload pomocí interpolace proměnných. Pro výchozí JSON ponechte prázdné. @@ -327,6 +331,7 @@ cs: follow_request: Někdo požádal o možnost vás sledovat mention: Někdo vás zmínil pending_account: Je třeba posoudit nový účet + quote: Někdo vás citoval reblog: Někdo boostnul váš příspěvek report: Je odesláno nové hlášení software_updates: @@ -373,6 +378,10 @@ cs: name: Název permissions_as_keys: Oprávnění position: Priorita + username_block: + allow_with_approval: Povolit registrace se schválením + comparison: Srovnávací metoda + username: Na základě slov webhook: events: Zapnuté události template: Šablona payloadu diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index a1390a3cc62..260d7530005 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -56,7 +56,6 @@ cy: scopes: Pa APIs y bydd y rhaglen yn cael mynediad iddynt. Os dewiswch gwmpas lefel uchaf, nid oes angen i chi ddewis rhai unigol. setting_aggregate_reblogs: Peidiwch â dangos hybiau newydd ar bostiadau sydd wedi cael eu hybu'n ddiweddar (dim ond yn effeithio ar hybiau newydd ei dderbyn) setting_always_send_emails: Fel arfer ni fydd hysbysiadau e-bost yn cael eu hanfon pan fyddwch chi wrthi'n defnyddio Mastodon - setting_default_quote_policy: Mae defnyddwyr sy'n cael eu crybwyll yn cael dyfynnu bob amser. Dim ond ar gyfer postiadau a grëwyd gyda'r fersiwn nesaf o Mastodon y bydd y gosodiad hwn yn dod i rym, ond gallwch ddewis eich dewis wrth baratoi setting_default_sensitive: Mae cyfryngau sensitif wedi'u cuddio yn rhagosodedig a gellir eu datgelu trwy glicio setting_display_media_default: Cuddio cyfryngau wedi eu marcio'n sensitif setting_display_media_hide_all: Cuddio cyfryngau bob tro diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index 09041a4113d..296ce49131c 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -9,8 +9,8 @@ da: fields: Din hjemmeside, dine pronominer, din alder, eller hvad du har lyst til. indexable: Dine offentlige indlæg vil kunne vises i Mastodon-søgeresultater. Folk, som har interageret med dem, vil kunne finde dem uanset. note: 'Du kan @omtale andre personer eller #hashtags.' - show_collections: Folk vil ikke kunne tjekke dine Følger og Følgere. Folk, du selv følger, vil stadig kunne se dette. - unlocked: Man vil kunne følges af folk uden først at godkende dem. Ønsker man at gennemgå Følg-anmodninger og individuelt acceptere/afvise nye følgere, så fjern markeringen. + show_collections: Folk vil kunne se, hvem du følger, og hvem der følger dig. Personer, som du følger, vil kunne se, at du følger dem. + unlocked: Folk vil kunne følge dig uden at anmode om godkendelse. Fjern markeringen, hvis du vil gennemgå anmodninger om at følge, og vælge, om du vil acceptere eller afvise nye følgere. account_alias: acct: Angiv brugernavn@domain for den konto, hvorfra du vil flytte account_migration: @@ -56,7 +56,7 @@ da: scopes: De API'er, som applikationen vil kunne tilgå. Vælges en topniveaudstrækning, vil detailvalg være unødvendige. setting_aggregate_reblogs: Vis ikke nye fremhævelser for nyligt fremhævede indlæg (påvirker kun nyligt modtagne fremhævelser) setting_always_send_emails: Normalt sendes ingen e-mailnotifikationer under aktivt brug af Mastodon - setting_default_quote_policy: Nævnte brugere har altid lov til at citere. Denne indstilling vil kun træde i kraft for indlæg oprettet med den næste Mastodon-version, men du kan som forberedelse vælge din præference + setting_default_quote_policy: Denne indstilling træder kun i kraft for indlæg oprettet med den næste Mastodon-version, men egne præference kan vælges som forberedelse. setting_default_sensitive: Sensitive medier er som standard skjult og kan vises med et klik setting_display_media_default: Skjul medier med sensitiv-markering setting_display_media_hide_all: Skjul altid medier @@ -75,7 +75,7 @@ da: featured_tag: name: 'Her er nogle af dine hyppigst brugte hashtags:' filters: - action: Vælg handlingen til eksekvering, når et indlæg matcher filteret + action: Vælg, hvilken handling, der skal udføres, når et indlæg matcher filteret actions: blur: Skjul medier bag en advarsel, uden at skjule selve teksten hide: Skjul det filtrerede indhold fuldstændigt og gør, som om det ikke eksisterer @@ -160,6 +160,10 @@ da: name: Offentligt rollennavn, hvis rollen er opsat til fremstå som et badge permissions_as_keys: Brugere med denne rolle vil kunne tilgå... position: Højere rolle bestemmer konfliktløsning i visse situationer. Visse handlinger kan kun udføres på roller med lavere prioritet + username_block: + allow_with_approval: I stedet for at forhindre tilmelding helt, vil matchende tilmeldinger kræve din godkendelse + comparison: Vær opmærksom på Scunthorpe-problemet ved blokering af delvise match + username: Matches uanset minuskel/majuskel-brug og alm. homoglyffer, såsom "4" for "a" eller "3" for "e" webhook: events: Vælg begivenheder at sende template: Skriv din egen JSON nyttelast ved hjælp af variabel interpolation. Lad feltet stå tomt for standard JSON. @@ -325,6 +329,7 @@ da: follow_request: Nogen anmodede om at følge dig mention: Nogen omtalte dig pending_account: Ny konto kræver gennemgang + quote: Nogen citerede dig reblog: Nogen fremhævede dit indlæg report: Ny anmeldelse indsendt software_updates: @@ -371,6 +376,10 @@ da: name: Navn permissions_as_keys: Tilladelser position: Prioritet + username_block: + allow_with_approval: Tillad registreringer med godkendelse + comparison: Sammenligningsmetode + username: Ord, som skal matches webhook: events: Aktive begivenheder template: Payload skabelon diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 04fa49ba576..62185c80f0e 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -56,7 +56,7 @@ de: scopes: Welche Schnittstellen der Applikation erlaubt sind. Wenn du einen Top-Level-Scope auswählst, dann musst du nicht jeden einzelnen darunter auswählen. setting_aggregate_reblogs: Beiträge, die erst kürzlich geteilt wurden, werden nicht noch einmal angezeigt (betrifft nur zukünftig geteilte Beiträge) setting_always_send_emails: Normalerweise werden Benachrichtigungen nicht per E-Mail versendet, wenn du gerade auf Mastodon aktiv bist - setting_default_quote_policy: Erwähnte Profile dürfen immer zitieren. Diese Einstellung gilt nur für Beiträge, die mit der zukünftigen Mastodon-Version erstellt wurden. Als Vorbereitung darauf kannst du bereits jetzt die Einstellung vornehmen + setting_default_quote_policy: Diese Einstellung gilt nur für Beiträge, die mit der zukünftigen Mastodon-Version erstellt wurden. Als Vorbereitung darauf kannst du bereits jetzt die Einstellung vornehmen. setting_default_sensitive: Medien, die mit einer Inhaltswarnung versehen worden sind, werden – je nach Einstellung – erst nach einem zusätzlichen Klick angezeigt setting_display_media_default: Medien mit Inhaltswarnung ausblenden setting_display_media_hide_all: Medien immer ausblenden @@ -160,6 +160,10 @@ de: name: Name der Rolle, der auch öffentlich als Badge angezeigt wird, sofern dies unten aktiviert ist permissions_as_keys: Benutzer*innen mit dieser Rolle haben Zugriff auf... position: Höhere Rollen entscheiden über Konfliktlösungen zu gewissen Situationen. Bestimmte Aktionen können nur mit geringfügigeren Rollen durchgeführt werden + username_block: + allow_with_approval: Anstatt Registrierungen komplett zu verhindern, benötigen übereinstimmende Treffer eine Genehmigung + comparison: Bitte beachte das Scunthorpe-Problem, wenn teilweise übereinstimmende Treffer gesperrt werden + username: Abgleich erfolgt unabhängig der Groß- und Kleinschreibung sowie gängiger Homoglyphen („4“ für „a“ oder „3“ für „e“) webhook: events: Zu sendende Ereignisse auswählen template: Erstelle deine eigenen JSON-Nutzdaten mit Hilfe von Variablen-Interpolation. Leer lassen für Standard-JSON. @@ -325,6 +329,7 @@ de: follow_request: Jemand möchte mir folgen mention: Ich wurde erwähnt pending_account: Ein neues Konto muss überprüft werden + quote: Jemand zitierte dich reblog: Mein Beitrag wurde geteilt report: Eine neue Meldung wurde eingereicht software_updates: @@ -371,6 +376,10 @@ de: name: Name permissions_as_keys: Berechtigungen position: Priorität + username_block: + allow_with_approval: Registrierungen mit Genehmigung zulassen + comparison: Vergleichsmethode + username: Übereinstimmendes Wort webhook: events: Aktivierte Ereignisse template: Nutzdaten-Vorlage diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index cc222cc85ee..fcbe7dc32d1 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -56,7 +56,7 @@ el: scopes: Ποια API θα επιτρέπεται στην εφαρμογή να χρησιμοποιήσεις. Αν επιλέξεις κάποιο υψηλό εύρος εφαρμογής, δε χρειάζεται να επιλέξεις και το καθένα ξεχωριστά. setting_aggregate_reblogs: Απόκρυψη των νέων αναρτήσεων για τις αναρτήσεις που έχουν ενισχυθεί πρόσφατα (επηρεάζει μόνο τις νέες ενισχύσεις) setting_always_send_emails: Κανονικά οι ειδοποιήσεις μέσω ηλεκτρονικού ταχυδρομείου δεν θα αποστέλλονται όταν χρησιμοποιείτε ενεργά το Mastodon - setting_default_quote_policy: Οι αναφερόμενοι χρήστες επιτρέπεται πάντα να παραθέτουν. Αυτή η ρύθμιση θα τεθεί σε ισχύ μόνο για αναρτήσεις που δημιουργήθηκαν με την επόμενη έκδοση Mastodon, αλλά μπορείς να επιλέξετε την προτίμησή σου κατά την προετοιμασία + setting_default_quote_policy: Αυτή η ρύθμιση θα τεθεί σε ισχύ μόνο για αναρτήσεις που δημιουργήθηκαν με την επόμενη έκδοση του Mastodon, αλλά μπορείτε να επιλέξετε την προτίμησή σας κατά την προετοιμασία. setting_default_sensitive: Τα ευαίσθητα πολυμέσα είναι κρυμμένα και εμφανίζονται με ένα κλικ setting_display_media_default: Απόκρυψη ευαίσθητων πολυμέσων setting_display_media_hide_all: Μόνιμη απόκρυψη όλων των πολυμέσων @@ -160,6 +160,10 @@ el: name: Δημόσιο όνομα του ρόλου, εάν ο ρόλος έχει οριστεί να εμφανίζεται ως σήμα permissions_as_keys: Οι χρήστες με αυτόν τον ρόλο θα έχουν πρόσβαση σε... position: Ανώτεροι ρόλοι αποφασίζει την επίλυση συγκρούσεων σε ορισμένες περιπτώσεις. Ορισμένες ενέργειες μπορούν να εκτελεστούν μόνο σε ρόλους με χαμηλότερη προτεραιότητα + username_block: + allow_with_approval: Αντί να αποτρέψετε την οριστική εγγραφή, η αντιστοίχιση εγγραφών θα απαιτήσει την έγκρισή σας + comparison: Παρακαλώ να λάβετε υπόψη το Πρόβλημα Scunthorpe κατά τη φραγή μερικών αντιστοιχίσεων + username: Θα αντιστοιχηθεί ανεξάρτητα από τα κεφαλαία/πεζά και τα κοινά ομόγλυφα όπως "4" για "α" ή "3" για "e" webhook: events: Επιλέξτε συμβάντα για αποστολή template: Σύνθεσε το δικό σου JSON payload χρησιμοποιώντας μεταβλητή παρεμβολή. Άφησε κενό για προεπιλογή JSON. @@ -231,8 +235,8 @@ el: setting_always_send_emails: Πάντα να αποστέλλονται ειδοποίησεις μέσω email setting_auto_play_gif: Αυτόματη αναπαραγωγή των GIF setting_boost_modal: Επιβεβαίωση πριν την προώθηση - setting_default_language: Γλώσσα δημοσιεύσεων - setting_default_privacy: Ιδιωτικότητα δημοσιεύσεων + setting_default_language: Γλώσσα κατά την ανάρτηση + setting_default_privacy: Ιδιωτικότητα αναρτήσεων setting_default_quote_policy: Ποιος μπορεί να παραθέσει setting_default_sensitive: Σημείωση όλων των πολυμέσων ως ευαίσθητου περιεχομένου setting_delete_modal: Επιβεβαίωση πριν τη διαγραφή ενός τουτ @@ -325,6 +329,7 @@ el: follow_request: Κάποιος ζήτησε να σε ακολουθήσει mention: Κάποιος σε επισήμανε pending_account: Νέος λογαριασμός χρειάζεται αναθεώρηση + quote: Κάποιος σε παρέθεσε reblog: Κάποιος ενίσχυσε την ανάρτηση σου report: Υποβλήθηκε νέα αναφορά software_updates: @@ -371,6 +376,10 @@ el: name: Όνομα permissions_as_keys: Δικαιώματα position: Προτεραιότητα + username_block: + allow_with_approval: Να επιτρέπονται εγγραφές με έγκριση + comparison: Μέθοδος σύγκρισης + username: Λέξη για αντιστοίχιση webhook: events: Ενεργοποιημένα συμβάντα template: Πρότυπο payload diff --git a/config/locales/simple_form.en-GB.yml b/config/locales/simple_form.en-GB.yml index c61acd3248b..494f0c7b2a6 100644 --- a/config/locales/simple_form.en-GB.yml +++ b/config/locales/simple_form.en-GB.yml @@ -56,7 +56,6 @@ en-GB: scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon - setting_default_quote_policy: Mentioned users are always allowed to quote. This setting will only take effect for posts created with the next Mastodon version, but you can select your preference in preparation setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click setting_display_media_default: Hide media marked as sensitive setting_display_media_hide_all: Always hide media diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 1b410a802d5..d79899e908b 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -56,7 +56,7 @@ en: scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon - setting_default_quote_policy: Mentioned users are always allowed to quote. This setting will only take effect for posts created with the next Mastodon version, but you can select your preference in preparation + setting_default_quote_policy: This setting will only take effect for posts created with the next Mastodon version, but you can select your preference in preparation. setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click setting_display_media_default: Hide media marked as sensitive setting_display_media_hide_all: Always hide media @@ -160,6 +160,10 @@ en: name: Public name of the role, if role is set to be displayed as a badge permissions_as_keys: Users with this role will have access to... position: Higher role decides conflict resolution in certain situations. Certain actions can only be performed on roles with a lower priority + username_block: + allow_with_approval: Instead of preventing sign-up outright, matching sign-ups will require your approval + comparison: Please be mindful of the Scunthorpe Problem when blocking partial matches + username: Will be matched regardless of casing and common homoglyphs like "4" for "a" or "3" for "e" webhook: events: Select events to send template: Compose your own JSON payload using variable interpolation. Leave blank for default JSON. @@ -325,6 +329,7 @@ en: follow_request: Someone requested to follow you mention: Someone mentioned you pending_account: New account needs review + quote: Someone quoted you reblog: Someone boosted your post report: New report is submitted software_updates: @@ -371,6 +376,10 @@ en: name: Name permissions_as_keys: Permissions position: Priority + username_block: + allow_with_approval: Allow registrations with approval + comparison: Method of comparison + username: Word to match webhook: events: Enabled events template: Payload template diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml index e4c8f941ceb..f3972d01a99 100644 --- a/config/locales/simple_form.es-AR.yml +++ b/config/locales/simple_form.es-AR.yml @@ -56,7 +56,7 @@ es-AR: scopes: Qué APIs de la aplicación tendrán acceso. Si seleccionás el alcance de nivel más alto, no necesitás seleccionar las individuales. setting_aggregate_reblogs: No mostrar nuevas adhesiones de los mensajes que fueron recientemente adheridos (sólo afecta a las adhesiones recibidas recientemente) setting_always_send_emails: Normalmente las notificaciones por correo electrónico no se enviarán cuando estés usando Mastodon activamente - setting_default_quote_policy: Los usuarios mencionados siempre pueden citar. Este ajuste solo afecta a las publicaciones creadas con la próxima versión de Mastodon, pero podés seleccionar tus preferencias con antelación. + setting_default_quote_policy: Este ajuste solo tendrá efecto en mensajes creados usando la próxima versión mayor de Mastodon, pero podés configurarlo con anticipación. setting_default_sensitive: El contenido de medios sensibles está oculto predeterminadamente y puede ser mostrado con un clic setting_display_media_default: Ocultar medios marcados como sensibles setting_display_media_hide_all: Siempre ocultar todos los medios @@ -160,6 +160,10 @@ es-AR: name: Nombre público del rol, si el rol se establece para que se muestre como una insignia permissions_as_keys: Los usuarios con este rol tendrán acceso a… position: Un rol más alto decide la resolución de conflictos en ciertas situaciones. Ciertas acciones sólo pueden llevarse a cabo en roles con prioridad inferior + username_block: + allow_with_approval: En lugar de impedir el registro total, los registros coincidentes requerirán tu aprobación + comparison: Por favor, tené en cuenta el Problema de Scunthorpe al bloquear coincidencias parciales + username: Coincidirá sin importar la capitalización de letras y los homoglifos comunes como «4» para «a», o «3» para «e» webhook: events: Seleccionar eventos para enviar template: Creá tu propio archivo JSON usando interpolación variable. Dejalo en blanco para usar el archivo JSON predeterminado. @@ -325,6 +329,7 @@ es-AR: follow_request: Una cuenta solicita seguirte mention: Una cuenta te menciona pending_account: Una nueva cuenta necesita revisión + quote: Alguien te citó reblog: Una cuenta adhiere a tu mensaje report: Se envió una nueva denuncia software_updates: @@ -371,6 +376,10 @@ es-AR: name: Nombre permissions_as_keys: Permisos position: Prioridad + username_block: + allow_with_approval: Permitir registros con aprobación + comparison: Método de comparación + username: Palabra a coincidir webhook: events: Eventos habilitados template: Plantilla de carga diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml index e8080b2a768..6b20f6e6aea 100644 --- a/config/locales/simple_form.es-MX.yml +++ b/config/locales/simple_form.es-MX.yml @@ -56,7 +56,7 @@ es-MX: scopes: Qué APIs de la aplicación tendrán acceso. Si seleccionas el alcance de nivel mas alto, no necesitas seleccionar las individuales. setting_aggregate_reblogs: No mostrar nuevos impulsos para las publicaciones que han sido recientemente impulsadas (sólo afecta a las publicaciones recibidas recientemente) setting_always_send_emails: Normalmente las notificaciones por correo electrónico no se enviarán cuando estés usando Mastodon activamente - setting_default_quote_policy: Los usuarios mencionados siempre pueden citar. Esta configuración solo se aplicará a las publicaciones creadas con la próxima versión de Mastodon, pero puedes seleccionar tus preferencias anticipadamente + setting_default_quote_policy: Esta configuración solo tendrá efecto en las publicaciones creadas con la próxima versión de Mastodon, pero puedes seleccionar tu preferencia como preparación. setting_default_sensitive: El contenido multimedia sensible está oculto por defecto y puede ser mostrado con un clic setting_display_media_default: Ocultar contenido multimedia marcado como sensible setting_display_media_hide_all: Siempre ocultar todo el contenido multimedia @@ -160,6 +160,10 @@ es-MX: name: Nombre público del rol, si el rol se establece para que se muestre como una insignia permissions_as_keys: Los usuarios con este rol tendrán acceso a... position: Un rol superior decide la resolución de conflictos en ciertas situaciones. Ciertas acciones sólo pueden llevarse a cabo en roles con menor prioridad + username_block: + allow_with_approval: En lugar de impedir directamente el registro, los registros coincidentes requerirán tu aprobación + comparison: Por favor ten en cuenta el problema de Scunthorpe al bloquear coincidencias parciales + username: Se emparejará independientemente de las mayúsculas y minúsculas y de los homógrafos comunes como «4» por «a» o «3» por «e» webhook: events: Seleccionar eventos para enviar template: Crea tu propio JSON usando interpolación variable. Déjalo en blanco para el JSON predeterminado. @@ -325,6 +329,7 @@ es-MX: follow_request: Enviar correo electrónico cuando alguien solicita seguirte mention: Enviar correo electrónico cuando alguien te mencione pending_account: Enviar correo electrónico cuando una nueva cuenta necesita revisión + quote: Alguien te citó reblog: Enviar correo electrónico cuando alguien comparta su publicación report: Nuevo reporte enviado software_updates: @@ -371,6 +376,10 @@ es-MX: name: Nombre permissions_as_keys: Permisos position: Prioridad + username_block: + allow_with_approval: Permitir registros con aprobación previa + comparison: Método de comparación + username: Palabra a coincidir webhook: events: Eventos habilitados template: Plantilla de carga diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index 31519771e17..53115056f47 100644 --- a/config/locales/simple_form.es.yml +++ b/config/locales/simple_form.es.yml @@ -56,7 +56,7 @@ es: scopes: Qué APIs de la aplicación tendrán acceso. Si seleccionas el alcance de nivel mas alto, no necesitas seleccionar las individuales. setting_aggregate_reblogs: No mostrar nuevos impulsos para las publicaciones que han sido recientemente impulsadas (sólo afecta a los impulsos recibidos recientemente) setting_always_send_emails: Normalmente las notificaciones por correo electrónico no se enviarán cuando estés usando Mastodon activamente - setting_default_quote_policy: Los usuarios mencionados siempre pueden citar. Este ajuste solo afecta a las publicaciones creadas con la próxima versión de Mastodon, pero puedes seleccionar tus preferencias anticipadamente + setting_default_quote_policy: Este ajuste solo tendrá efecto en publicaciones creadas con la próxima versión de Mastodon, pero puedes configurarlo previamente. setting_default_sensitive: El contenido multimedia sensible está oculto por defecto y puede ser mostrado con un click setting_display_media_default: Ocultar contenido multimedia marcado como sensible setting_display_media_hide_all: Siempre ocultar todo el contenido multimedia @@ -160,6 +160,10 @@ es: name: Nombre público del rol, si el rol se establece para que se muestre como una insignia permissions_as_keys: Los usuarios con este rol tendrán acceso a... position: Un rol superior decide la resolución de conflictos en ciertas situaciones. Ciertas acciones sólo pueden llevarse a cabo en roles con menor prioridad + username_block: + allow_with_approval: En lugar de impedir directamente el registro, los registros coincidentes requerirán tu aprobación + comparison: Por favor, ten en cuenta el problema de Scunthorpe al bloquear coincidencias parciales + username: Se emparejará independientemente de la mayúscula o minúscula y de homógrafos comunes como «4» por «a» o «3» por «e» webhook: events: Seleccionar eventos para enviar template: Crea tu propio JSON usando interpolación variable. Déjalo en blanco para el JSON predeterminado. @@ -325,6 +329,7 @@ es: follow_request: Enviar correo electrónico cuando alguien solicita seguirte mention: Enviar correo electrónico cuando alguien te mencione pending_account: Enviar correo electrónico cuando una nueva cuenta necesita revisión + quote: Alguien te ha citado reblog: Enviar correo electrónico cuando alguien comparta su publicación report: Nuevo informe enviado software_updates: @@ -371,6 +376,10 @@ es: name: Nombre permissions_as_keys: Permisos position: Prioridad + username_block: + allow_with_approval: Permitir registros con aprobación + comparison: Método de comparación + username: Palabra a coincidir webhook: events: Eventos habilitados template: Plantilla de carga diff --git a/config/locales/simple_form.et.yml b/config/locales/simple_form.et.yml index f013f454caa..2217eee7a3e 100644 --- a/config/locales/simple_form.et.yml +++ b/config/locales/simple_form.et.yml @@ -10,6 +10,7 @@ et: indexable: Sinu avalikud postitused võivad ilmuda Mastodoni otsingutulemustes. Inimesed, kes on sinu postitustele reageerinud, saavad neid otsida nii või naa. note: 'Saad @mainida teisi inimesi või #silte.' show_collections: Inimesed saavad sirvida su jälgijaid ja jälgitavaid. Inimesed, keda sa jälgid, näevad seda sõltumata häälestuse valikust. + unlocked: Teised kasutajad saavad sind jälgima hakata nõusolekut küsimata. Eemalda märge, kui soovid jälgimistaotlusi üle vaadata ja valida, kas nõustuda või keelduda uute jälgijatega. account_alias: acct: Sisesta konto kasutajanimi@domeen, mille soovid siia ümber kolida account_migration: @@ -59,6 +60,7 @@ et: setting_display_media_default: Peida tundlikuks märgitud meedia setting_display_media_hide_all: Alati peida kõik meedia setting_display_media_show_all: Alati näita tundlikuks märgistatud meedia + setting_system_scrollbars_ui: Kehtib vaid Safaril ja Chrome'il põhinevatel tavaarvuti veebibrauserite puhul setting_use_blurhash: Värvid põhinevad peidetud visuaalidel, kuid hägustavad igasuguseid detaile setting_use_pending_items: Voo automaatse kerimise asemel peida ajajoone uuendused kliki taha username: Võid kasutada ladina tähti, numbreid ja allkriipsu diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml index 239591e05f9..be04b7b0267 100644 --- a/config/locales/simple_form.eu.yml +++ b/config/locales/simple_form.eu.yml @@ -56,7 +56,6 @@ eu: scopes: Zeintzuk API atzitu ditzakeen aplikazioak. Goi mailako arloa aukeratzen baduzu, ez dituzu azpikoak aukeratu behar. setting_aggregate_reblogs: Ez erakutsi bultzada berriak berriki bultzada jaso duten tootentzat (berriki jasotako bultzadei eragiten die bakarrik) setting_always_send_emails: Normalean eposta jakinarazpenak ez dira bidaliko Mastodon aktiboki erabiltzen ari zaren bitartean - setting_default_quote_policy: Aipaturiko erabiltzaileek beti dute aipatzeko baimena. Ezarpen honek Mastodon-en hurrengo bertsioarekin sortutako argitalpenetan bakarrik izango du eragina, baina prestatzean lehentasuna hauta dezakezu setting_default_sensitive: Multimedia hunkigarria lehenetsita ezkutatzen da, eta sakatuz ikusi daiteke setting_display_media_default: Ezkutatu hunkigarri gisa markatutako multimedia setting_display_media_hide_all: Ezkutatu multimedia guztia beti diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml index f80097832ca..c9842f07ea0 100644 --- a/config/locales/simple_form.fa.yml +++ b/config/locales/simple_form.fa.yml @@ -56,7 +56,7 @@ fa: scopes: واسط‌های برنامه‌نویسی که این برنامه به آن دسترسی دارد. اگر بالاترین سطح دسترسی را انتخاب کنید، دیگر نیازی به انتخاب سطح‌های پایینی ندارید. setting_aggregate_reblogs: برای تقویت‌هایی که به تازگی برایتان نمایش داده شده‌اند، تقویت‌های بیشتر را نمایش نده (فقط روی تقویت‌های اخیر تأثیر می‌گذارد) setting_always_send_emails: در حالت عادی آگاهی‌های رایانامه‌ای هنگامی که فعّالانه از ماستودون استفاده می‌کنید فرستاده نمی‌شوند - setting_default_quote_policy: کاربران اشاره شده همواره مجاز به نقل قولند. این تنظیمات تنها روی فرسته‌های ایجاد شده با نگارش بعدی ماستودون موثّر است، ولی می‌توانید ترجیحاتتان را پیشاپیش بگزینید + setting_default_quote_policy: این تنظیمات تنها روی فرسته‌هایی ایجاد شده با نگارش بعدی Mastodon اعمال خواهد شد، اما می‌توانی ترجیحات خود را از قبل انتخاب کنید. setting_default_sensitive: تصاویر حساس به طور پیش‌فرض پنهان هستند و می‌توانند با یک کلیک آشکار شوند setting_display_media_default: تصویرهایی را که به عنوان حساس علامت زده شده‌اند پنهان کن setting_display_media_hide_all: همیشه همهٔ عکس‌ها و ویدیوها را پنهان کن @@ -150,6 +150,9 @@ fa: min_age: نباید کم‌تر از کمینهٔ زمان لازم از سوی قوانین حقوقیتان باشد. user: chosen_languages: اگر انتخاب کنید، تنها نوشته‌هایی که به زبان‌های برگزیدهٔ شما نوشته شده‌اند در فهرست نوشته‌های عمومی نشان داده می‌شوند + date_of_birth: + one: برای استفاده از %{domain} باید مطمئن شویم کمینه %{count} سال را دارید. این مورد را ذخیره نخواهیم کرد. + other: برای استفاده از %{domain} باید مطمئن شویم کمینه %{count} سال را دارید. این مورد را ذخیره نخواهیم کرد. role: نقش کنترل می کند که کاربر چه مجوزهایی دارد. user_role: color: رنگی که برای نقش در سرتاسر UI استفاده می شود، به عنوان RGB در قالب هگز @@ -230,7 +233,7 @@ fa: setting_boost_modal: نمایش پیغام تأیید پیش از تقویت کردن setting_default_language: زبان نوشته‌های شما setting_default_privacy: حریم خصوصی نوشته‌ها - setting_default_quote_policy: افراد مجاز به نقل قول + setting_default_quote_policy: افراد مجاز به نقل setting_default_sensitive: همیشه تصاویر را به عنوان حساس علامت بزن setting_delete_modal: نمایش پیغام تأیید پیش از پاک کردن یک نوشته setting_disable_hover_cards: از کار انداختن پیش‌نمایش نمایه هنگام رفتن رویش @@ -322,6 +325,7 @@ fa: follow_request: شخصی خواست پیتان بگیرد mention: شخصی از شما نام برد pending_account: حساب تازهٔ نیازمند بررسی + quote: شخصی شما را نقل کرد reblog: شخصی فرسته‌تان را تقویت کرد report: گزارش جدیدی فرستاده شد software_updates: diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index 5a806fb8e76..7ccf8ab5f7f 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -56,11 +56,12 @@ fi: scopes: Mihin ohjelmointirajapintoihin sovelluksella on pääsy. Jos valitset ylätason käyttöoikeuden, sinun ei tarvitse valita yksittäisiä. setting_aggregate_reblogs: Älä näytä uusia tehostuksia julkaisuille, joita on äskettäin tehostettu (koskee vain juuri vastaanotettuja tehostuksia) setting_always_send_emails: Yleensä sähköposti-ilmoituksia ei lähetetä, kun käytät Mastodonia aktiivisesti - setting_default_quote_policy: Mainitut käyttäjät saavat aina lainata. Tämä asetus koskee vain julkaisuja, jotka on luotu seuraavalla Mastodon-versiolla, mutta voit valita asetuksesi valmistautuaksesi + setting_default_quote_policy: Tämä asetus tulee voimaan vain julkaisuissa, jotka on luotu seuraavalla Mastodon-versiolla, mutta voit valita asetuksesi jo etukäteen. setting_default_sensitive: Arkaluonteinen media piilotetaan oletusarvoisesti, ja se voidaan näyttää yhdellä napsautuksella setting_display_media_default: Piilota arkaluonteiseksi merkitty mediasisältö setting_display_media_hide_all: Piilota mediasisältö aina setting_display_media_show_all: Näytä mediasisältö aina + setting_emoji_style: Miten emojit näkyvät. ”Automaattinen” pyrkii käyttämään natiiveja emojeita, mutta Twemoji-emojeita käytetään varavaihtoehtoina vanhoissa selaimissa. setting_system_scrollbars_ui: Koskee vain Safari- ja Chrome-pohjaisia työpöytäselaimia setting_use_blurhash: Liukuvärit perustuvat piilotettujen kuvien väreihin mutta sumentavat yksityiskohdat setting_use_pending_items: Piilota aikajanan päivitykset napsautuksen taakse syötteen automaattisen vierityksen sijaan @@ -148,6 +149,9 @@ fi: min_age: Ei pidä alittaa lainkäyttöalueesi lakien vaatimaa vähimmäisikää. user: chosen_languages: Jos valitset kieliä oheisesta luettelosta, vain niidenkieliset julkaisut näkyvät sinulle julkisilla aikajanoilla + date_of_birth: + one: Meidän tulee varmistaa, että olet vähintään %{count}, jotta voit käyttää %{domain}. Emme tallenna tätä. + other: Meidän tulee varmistaa, että olet vähintään %{count}, jotta voit käyttää %{domain}. Emme tallenna tätä. role: Rooli määrää, millaiset käyttöoikeudet käyttäjällä on. user_role: color: Väri, jota käytetään roolille kaikkialla käyttöliittymässä, RGB-heksadesimaalimuodossa @@ -155,6 +159,10 @@ fi: name: Roolin julkinen nimi, jos rooli on asetettu näytettäväksi merkkinä permissions_as_keys: Käyttäjillä, joilla on tämä rooli, on käyttöoikeus… position: Korkeampi rooli ratkaisee konfliktit tietyissä tilanteissa. Tiettyjä toimia voidaan suorittaa vain rooleilla, joiden prioriteetti on pienempi + username_block: + allow_with_approval: Sen sijaan, että rekisteröityminen estetään kokonaan, sääntöä vastaavat rekisteröitymiset edellyttävät hyväksyntääsi + comparison: Ota Scunthorpe-ongelma huomioon, kun estät osittaisia osumia + username: Vastaa riippumatta aakkoskoosta ja yleisistä homoglyyfeistä kuten ”4” merkille ”a” ja ”3” merkille ”e” webhook: events: Valitse lähetettävät tapahtumat template: Luo oma JSON-hyötykuorma käyttäen muuttujien interpolointia. Jätä kenttä tyhjäksi käyttääksesi vakio-JSON-kuormaa. @@ -320,6 +328,7 @@ fi: follow_request: Joku pyysi lupaa seurata sinua mention: Joku mainitsi sinut pending_account: Uusi tili tarvitsee tarkastuksen + quote: Joku lainasi sinua reblog: Joku tehosti julkaisuasi report: Uusi raportti lähetettiin software_updates: @@ -348,6 +357,7 @@ fi: admin_email: Sähköpostiosoite oikeudellisille ilmoituksille arbitration_address: Fyysinen osoite välimiesmenettelyn ilmoituksille arbitration_website: Sähköpostiosoite välimiesmenettelyn ilmoituksille + choice_of_law: Sovellettava lainsäädäntö dmca_address: Fyysinen osoite DMCA-/tekijänoikeusilmoituksille dmca_email: Sähköpostiosoite DMCA-/tekijänoikeusilmoituksille domain: Verkkotunnus @@ -365,6 +375,10 @@ fi: name: Nimi permissions_as_keys: Käyttöoikeudet position: Prioriteetti + username_block: + allow_with_approval: Salli rekisteröitymiset hyväksynnällä + comparison: Vertailumenetelmä + username: Vastattava sana webhook: events: Käytössä olevat tapahtumat template: Hyötykuormapohja diff --git a/config/locales/simple_form.fo.yml b/config/locales/simple_form.fo.yml index c15ba7f0dc3..54e0e98c7de 100644 --- a/config/locales/simple_form.fo.yml +++ b/config/locales/simple_form.fo.yml @@ -56,7 +56,7 @@ fo: scopes: Hvørji API nýtsluskipanin fær atgongd til. Velur tú eitt vav á hægsta stigi, so er ikki neyðugt at velja tey einstøku. setting_aggregate_reblogs: Vís ikki nýggjar stimbranir fyri postar, sum nýliga eru stimbraðir (ávirkar einans stimbranir, ið eru móttiknar fyri kortum) setting_always_send_emails: Vanliga vera teldupostfráboðanir ikki sendar, tá tú virkin brúkar Mastodon - setting_default_quote_policy: Nevndir brúkarar hava altíð loyvi at sitera. Hendan stillingin verður bara virkin fyri postar, sum verða stovnaðir í næstu Mastodon útgávuni, men sum fyrireiking til tað, kanst tú velja tína stilling longu nú + setting_default_quote_policy: Hendan stillingin verður bara virkin fyri postar, sum verða stovnaðir í næstu Mastodon útgávuni, men sum fyrireiking til tað, kanst tú velja tína stilling longu nú. setting_default_sensitive: Viðkvæmar miðlafílur eru fjaldar og kunnu avdúkast við einum klikki setting_display_media_default: Fjal miðlafílur, sum eru merktar sum viðkvæmar setting_display_media_hide_all: Fjal altíð miðlafílur @@ -160,6 +160,10 @@ fo: name: Almenna navnið á leiklutinum, um leikluturin er settur at verða vístur sum eitt tignarmerki permissions_as_keys: Brúkarar við hesum leiklutinum fara at fáa atgongd til... position: Hægri leiklutur er avgerandi fyri loysn av ósemjum í ávísum støðum. Ávísar atgerðir kunnu einans verða gjørdar móti leiklutum, sum hava eina lægri raðfesting + username_block: + allow_with_approval: Í staðin fyri at forða heilt fyri skráseting, fara samsvarandi skrásetingar at krevja, at tú góðkennir tær + comparison: Vinarliga gev Scunthorpe-trupulleikanum gætur, tá tú blokerar lutvís samsvar + username: Samsvar vera funnin óansæð stórar og smáar bókstavir og vanligar homoglyffir, sosum "4" fyri "a" og "3" fyri "e" webhook: events: Vel hendingar at senda template: Samanset títt egna JSON farm við at brúka variabul-interpolasjón. Lat vera blankt fyri sjálvgaldandi JSON. @@ -325,6 +329,7 @@ fo: follow_request: Onkur biður um at fylgja tær mention: Onkur nevndi teg pending_account: Nýggj konta krevur viðgerð + quote: Onkur siteraði teg reblog: Onkur stimbraði postin hjá tær report: Nýggj melding er send inn software_updates: @@ -371,6 +376,10 @@ fo: name: Navn permissions_as_keys: Loyvi position: Raðfesting + username_block: + allow_with_approval: Loyv skrásetingum við góðkenning + comparison: Samanberingarmetoda + username: Orð, sum skal samsvara webhook: events: Virknar hendingar template: Farmaformur diff --git a/config/locales/simple_form.fr-CA.yml b/config/locales/simple_form.fr-CA.yml index 8856c8906c1..51a5b432a3b 100644 --- a/config/locales/simple_form.fr-CA.yml +++ b/config/locales/simple_form.fr-CA.yml @@ -56,6 +56,7 @@ fr-CA: scopes: À quelles APIs l’application sera autorisée à accéder. Si vous sélectionnez une permission générale, vous n’avez pas besoin de sélectionner les permissions plus précises. setting_aggregate_reblogs: Ne pas afficher les nouveaux partages pour les messages déjà récemment partagés (n’affecte que les partages futurs) setting_always_send_emails: Normalement, les notifications par courriel ne seront pas envoyées lorsque vous utilisez Mastodon activement + setting_default_quote_policy: Ce paramètre ne prendra effet que pour les messages créés avec la prochaine version de Mastodon, mais vous pouvez sélectionner votre préférence en avance. setting_default_sensitive: Les médias sensibles sont cachés par défaut et peuvent être révélés d’un simple clic setting_display_media_default: Masquer les médias marqués comme sensibles setting_display_media_hide_all: Toujours masquer les médias @@ -228,6 +229,7 @@ fr-CA: setting_boost_modal: Demander confirmation avant de partager un message setting_default_language: Langue de publication setting_default_privacy: Confidentialité des messages + setting_default_quote_policy: Autoriser les citations pour setting_default_sensitive: Toujours marquer les médias comme sensibles setting_delete_modal: Demander confirmation avant de supprimer un message setting_disable_hover_cards: Désactiver l'aperçu du profil au survol diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 53c4fbfba72..90218f8b8ef 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -56,6 +56,7 @@ fr: scopes: À quelles APIs l’application sera autorisée à accéder. Si vous sélectionnez une permission générale, vous n’avez pas besoin de sélectionner les permissions plus précises. setting_aggregate_reblogs: Ne pas afficher les nouveaux partages pour les messages déjà récemment partagés (n’affecte que les partages futurs) setting_always_send_emails: Normalement, les notifications par courriel ne seront pas envoyées lorsque vous utilisez Mastodon activement + setting_default_quote_policy: Ce paramètre ne prendra effet que pour les messages créés avec la prochaine version de Mastodon, mais vous pouvez sélectionner votre préférence en avance. setting_default_sensitive: Les médias sensibles sont cachés par défaut et peuvent être révélés d’un simple clic setting_display_media_default: Masquer les médias marqués comme sensibles setting_display_media_hide_all: Toujours masquer les médias @@ -228,6 +229,7 @@ fr: setting_boost_modal: Demander confirmation avant de partager un message setting_default_language: Langue de publication setting_default_privacy: Confidentialité des messages + setting_default_quote_policy: Autoriser les citations pour setting_default_sensitive: Toujours marquer les médias comme sensibles setting_delete_modal: Demander confirmation avant de supprimer un message setting_disable_hover_cards: Désactiver l'aperçu du profil au survol diff --git a/config/locales/simple_form.fy.yml b/config/locales/simple_form.fy.yml index 584da81f3d9..690522b8cf6 100644 --- a/config/locales/simple_form.fy.yml +++ b/config/locales/simple_form.fy.yml @@ -56,7 +56,6 @@ fy: scopes: Ta hokker API’s hat de tapassing tagong. Wannear’t jo in tastimming fan it boppeste nivo kieze, hoege jo gjin yndividuele tastimmingen mear te kiezen. setting_aggregate_reblogs: Gjin nije boosts toane foar berjochten dy’t resintlik noch boost binne (hat allinnich effekt op nij ûntfongen boosts) setting_always_send_emails: Normaliter wurde der gjin e-mailmeldingen ferstjoerd wannear’t jo aktyf Mastodon brûke - setting_default_quote_policy: It is foar brûkers dy’t fermeld wurde altyd tastien om te sitearjen. Dizze ynstelling is allinnich fan tapassing foar berjochten dy’t makke binne mei de folgjende Mastodon-ferzje, mar jo kinne jo foarkar no al ynstelle setting_default_sensitive: Gefoelige media wurdt standert ferstoppe en kin mei ien klik toand wurde setting_display_media_default: As gefoelich markearre media ferstopje setting_display_media_hide_all: Media altyd ferstopje diff --git a/config/locales/simple_form.ga.yml b/config/locales/simple_form.ga.yml index 59cd532fd99..e9afcc8d427 100644 --- a/config/locales/simple_form.ga.yml +++ b/config/locales/simple_form.ga.yml @@ -56,7 +56,7 @@ ga: scopes: Cé na APIanna a mbeidh cead ag an bhfeidhmchlár rochtain a fháil orthu. Má roghnaíonn tú raon feidhme barrleibhéil, ní gá duit cinn aonair a roghnú. setting_aggregate_reblogs: Ná taispeáin treisithe nua do phoist a treisíodh le déanaí (ní dhéanann difear ach do threisithe nuafhaighte) setting_always_send_emails: Go hiondúil ní sheolfar fógraí ríomhphoist agus tú ag úsáid Mastodon go gníomhach - setting_default_quote_policy: Ceadaítear d’úsáideoirí luaite lua a dhéanamh i gcónaí. Ní bheidh an socrú seo i bhfeidhm ach amháin maidir le poist a cruthaíodh leis an gcéad leagan eile de Mastodon, ach is féidir leat do rogha féin a roghnú agus tú ag ullmhú + setting_default_quote_policy: Ní bheidh an socrú seo i bhfeidhm ach amháin maidir le poist a cruthaíodh leis an gcéad leagan eile de Mastodon, ach is féidir leat do rogha féin a roghnú agus tú ag ullmhú. setting_default_sensitive: Tá meáin íogair i bhfolach de réir réamhshocraithe agus is féidir iad a nochtadh le cliceáil setting_display_media_default: Folaigh meáin atá marcáilte mar íogair setting_display_media_hide_all: Folaigh meáin i gcónaí @@ -163,6 +163,10 @@ ga: name: Ainm poiblí an róil, má tá an ról socraithe le taispeáint mar shuaitheantas permissions_as_keys: Beidh rochtain ag úsáideoirí a bhfuil an ról seo acu ar... position: Cinneann ról níos airde réiteach coinbhleachta i gcásanna áirithe. Ní féidir gníomhartha áirithe a dhéanamh ach amháin ar róil a bhfuil tosaíocht níos ísle acu + username_block: + allow_with_approval: In ionad cosc iomlán a chur ar chlárú, beidh ort do cheadú a fháil chun clárúcháin a mheaitseáil + comparison: Tabhair aird ar Fhadhb Scunthorpe agus tú ag blocáil cluichí páirteacha + username: Déanfar é a mheaitseáil beag beann ar an gcásáil agus homaiglifí coitianta cosúil le "4" in ionad "a" nó "3" in ionad "e" webhook: events: Roghnaigh imeachtaí le seoladh template: Cum do phálasta JSON féin ag baint úsáide as idirshuíomh athróg. Fág bán le haghaidh JSON réamhshocraithe. @@ -328,6 +332,7 @@ ga: follow_request: D'iarr duine éigin tú a leanúint mention: Luaigh duine éigin tú pending_account: Ní mór athbhreithniú a dhéanamh ar chuntas nua + quote: Luaigh duine éigin thú reblog: Mhol duine éigin do phostáil report: Tá tuairisc nua curtha isteach software_updates: @@ -374,6 +379,10 @@ ga: name: Ainm permissions_as_keys: Ceadanna position: Tosaíocht + username_block: + allow_with_approval: Ceadaigh clárúcháin le ceadú + comparison: Modh comparáide + username: Focal le meaitseáil webhook: events: Imeachtaí cumasaithe template: Teimpléad pá-ualach diff --git a/config/locales/simple_form.gd.yml b/config/locales/simple_form.gd.yml index dfcd2590d94..7cdc27750b4 100644 --- a/config/locales/simple_form.gd.yml +++ b/config/locales/simple_form.gd.yml @@ -56,7 +56,6 @@ gd: scopes: Na APIan a dh’fhaodas an aplacaid inntrigeadh. Ma thaghas tu sgòp air ìre as àirde, cha leig thu leas sgòpaichean fa leth a thaghadh. setting_aggregate_reblogs: Na seall brosnachaidhean ùra do phostaichean a chaidh a bhrosnachadh o chionn goirid (cha doir seo buaidh ach air brosnachaidhean ùra o seo a-mach) setting_always_send_emails: Mar as àbhaist, cha dèid brathan puist-d a chur nuair a a bhios tu ri Mastodon gu cunbhalach - setting_default_quote_policy: Faodaidh luchd-cleachdaidh le iomradh orra luaidh an-còmhnaidh. Cha bhi an roghainn seo ann sàs ach air postaichean a thèid a chruthachadh leis an ath-thionndadh de Mhastodon ach ’s urrainn dhut do roghainn a thaghadh airson ullachadh dha. setting_default_sensitive: Thèid meadhanan frionasach fhalach a ghnàth is gabhaidh an nochdadh le briogadh orra setting_display_media_default: Falaich meadhanan ris a bheil comharra gu bheil iad frionasach setting_display_media_hide_all: Falaich na meadhanan an-còmhnaidh diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index 58722de43d2..1225e67b739 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -56,7 +56,7 @@ gl: scopes: A que APIs terá acceso a aplicación. Se escolles un ámbito de alto nivel, non precisas seleccionar elementos individuais. setting_aggregate_reblogs: Non mostrar novas promocións de publicacións que foron promovidas recentemente (só afecta a promocións recén recibidas) setting_always_send_emails: Como norma xeral non che enviamos correos electrónicos se usas activamente Mastodon - setting_default_quote_policy: As usuarias mencionadas sempre teñen permiso para citar. Este axuste só ten efecto para publicacións creadas coa próxima versión de Mastodon, pero xa podes ir preparando o axuste. + setting_default_quote_policy: O axuste só afectará ás publicación creadas coa próxima versión de Mastodon, pero podes establecer o axuste con antelación. setting_default_sensitive: Medios sensibles marcados como ocultos por defecto e móstranse cun click setting_display_media_default: Ocultar medios marcados como sensibles setting_display_media_hide_all: Ocultar sempre os medios @@ -160,6 +160,10 @@ gl: name: Nome público do rol, se o rol se mostra como unha insignia permissions_as_keys: As usuarias con este rol terán acceso a... position: O rol superior decide nos conflitos en certas situacións. Algunhas accións só poden aplicarse sobre roles cunha prioridade menor + username_block: + allow_with_approval: No lugar de evitar a cración directa de contas, as contas mediante regras van precisar a túa aprobación + comparison: Ten en conta o Sunthorpe Problem cando se bloquean coincidencias parciais + username: Vai crear unha coincidencia sen importar se son maiúsculas ou minúsculas e homoglifos como «4» por «a» ou «3» por «e». webhook: events: Escoller eventos a enviar template: Crea o teu propio JSON interpolando variables. Déixao en branco para usar o JSON predeterminado. @@ -325,6 +329,7 @@ gl: follow_request: Enviar un correo cando alguén solicita seguirte mention: Enviar un correo cando alguén te menciona pending_account: Enviar un correo cando unha nova conta precisa revisión + quote: Citoute alguén reblog: Enviar un correo cando alguén promociona a tua mensaxe report: Enviouse unha nova denuncia software_updates: @@ -371,6 +376,10 @@ gl: name: Nome permissions_as_keys: Permisos position: Prioridade + username_block: + allow_with_approval: Permitir crear contas con aprobación + comparison: Método de comparación + username: Palabra a comparar webhook: events: Eventos activados template: Modelo de carga diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml index 988195c633a..69c9f00cc59 100644 --- a/config/locales/simple_form.he.yml +++ b/config/locales/simple_form.he.yml @@ -56,7 +56,7 @@ he: scopes: לאיזה ממשק יורשה היישום לגשת. בבחירת תחום כללי, אין צורך לבחור ממשקים ספציפיים. setting_aggregate_reblogs: לא להראות הדהודים של הודעות שהודהדו לאחרונה (משפיע רק על הדהודים שהתקבלו לא מזמן) setting_always_send_emails: בדרך כלל התראות דוא"ל לא יישלחו בזמן שימוש פעיל במסטודון - setting_default_quote_policy: משתמשיםות מאוזכריםות תמיד חופשיים לצטט. הכיוונון הזה משפיע רק על פרסומים שישלחו בגרסאות מסטודון עתידיות, ניתן לבחור את העדפתך כהכנה לגרסא שתבוא + setting_default_quote_policy: הכיוונון הזה משפיע רק על פרסומים שישלחו בגרסאות מסטודון עתידיות, ניתן לבחור את העדפתך כהכנה לגרסא שתבוא. setting_default_sensitive: מדיה רגישה מוסתרת כברירת מחדל וניתן להציגה בקליק setting_display_media_default: הסתרת מדיה המסומנת כרגישה setting_display_media_hide_all: הסתר מדיה תמיד @@ -162,6 +162,10 @@ he: name: שם ציבורי של התפקיד, במידה והתפקיד מוגדר ככזה שמופיע כתג permissions_as_keys: למשתמשים בתפקיד זה תהיה גישה ל... position: תפקיד גבוה יותר מכריע בחילוקי דעות במצבים מסוימים. פעולות מסוימות יכולות להתבצע רק על תפקידים בדרגה נמוכה יותר + username_block: + allow_with_approval: במקום למנוע הרשמה לחלוטין, הרשמות חדשות יצטרכו לחכות לאישורך + comparison: יש להזהר מחסימת חלקי שמות קצרים מדי כדי להמנע מהתופעה הידועה בשם Scunthorpe problem + username: ההתאמה תתבצע ללא קשר לגודל אותיות או להומוגליפים נפוצים כגון "4" במקום "a" או "3" במקום "e" webhook: events: בחר אירועים לשליחה template: ניתן להרכיב מטען JSON משלך בשימוש בשילוב משתנים. יש להשאיר ריק בשביל JSON ברירת המחדל. @@ -327,6 +331,7 @@ he: follow_request: מישהו.י ביקש.ה לעקוב אחריך mention: שליחת דוא"ל כשפונים אלייך pending_account: נדרשת סקירה של חשבון חדש + quote: שליחת דוא"ל כשמצטטים הודעה שלך reblog: שליחת דוא"ל כשמהדהדים הודעה שלך report: דו"ח חדש הוגש software_updates: @@ -373,6 +378,10 @@ he: name: שם permissions_as_keys: הרשאות position: עדיפות + username_block: + allow_with_approval: הרשאת הרשמה לאחר אישור + comparison: שיטת השוואה + username: מילה להתאמה webhook: events: אירועים מאופשרים template: תבנית מטען diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml index 61ca09b951f..f34d9a5232f 100644 --- a/config/locales/simple_form.hu.yml +++ b/config/locales/simple_form.hu.yml @@ -56,7 +56,7 @@ hu: scopes: Mely API-kat érheti el az alkalmazás. Ha felső szintű hatáskört választasz, nem kell egyesével kiválasztanod az alatta lévőeket. setting_aggregate_reblogs: Ne mutassunk megtolásokat olyan bejegyzésekhez, melyeket nemrég toltak meg (csak új megtolásokra lép életbe) setting_always_send_emails: Alapesetben nem küldünk e-mail-értesítéseket, ha aktívan használod a Mastodont - setting_default_quote_policy: A megemlített felhasználók mindig idézhetnek. A beállítás csak a Mastodon következő verziójával készült bejegyzésekre lesz hatással, de előre kiválaszthatod az előnyben részesített beállítást. + setting_default_quote_policy: A beállítás csak a Mastodon következő verziójával készült bejegyzésekre lesz hatással, de előre kiválaszthatod az előnyben részesített beállítást. setting_default_sensitive: A kényes médiatartalmat alapesetben elrejtjük, de egyetlen kattintással előhozható setting_display_media_default: Kényes tartalomnak jelölt média elrejtése setting_display_media_hide_all: Média elrejtése mindig @@ -160,6 +160,10 @@ hu: name: A szerep nyilvános neve, ha a szerepet úgy állították be, hogy jelvényként látható legyen permissions_as_keys: A felhasználók ezzel a szereppel elérhetik a... position: A magasabb szerepkör oldja fel az ütközéseket bizonyos helyzetekben. Bizonyos műveleteket csak alacsonyabb prioritású szerepkörrel lehet elvégezni. + username_block: + allow_with_approval: A regisztráció azonnali megakadályozása helyett az illeszkedő regisztrációkhoz jóváhagyás szükséges + comparison: Vegye figyelembe a Scunthorpe-problémát, amikor részleges egyezéseket blokkol + username: Az illeszkedés egyezőnek tekinti a kis- és nagybetűket, valamint a gyakori homoglifákat, mint a „4” és az „a” vagy a „3” és az „e” webhook: events: Válaszd ki a küldendő eseményeket template: Saját JSON adatcsomagot állíthatsz össze változó-behelyettesítés használatával. Hagyd üresen az alapértelmezett JSON adatcsomaghoz. @@ -325,6 +329,7 @@ hu: follow_request: E-mail küldése, amikor valaki követni szeretne téged mention: E-mail küldése, amikor valaki megemlít téged pending_account: E-mail küldése, ha új fiókot kell engedélyezni + quote: E-mail küldése, amikor valaki idéz téged reblog: Valaki megtolta a bejegyzésedet report: Új bejelentést küldtek be software_updates: @@ -371,6 +376,10 @@ hu: name: Név permissions_as_keys: Engedélyek position: Prioritás + username_block: + allow_with_approval: Regisztráció engedélyezése jóváhagyással + comparison: Összehasonlítás módja + username: Ellenőrzendő szó webhook: events: Engedélyezett események template: Adatcsomag sablon diff --git a/config/locales/simple_form.ia.yml b/config/locales/simple_form.ia.yml index edcb43634a5..78af1c11922 100644 --- a/config/locales/simple_form.ia.yml +++ b/config/locales/simple_form.ia.yml @@ -56,7 +56,6 @@ ia: scopes: Le APIs al quales le application habera accesso. Si tu selige un ambito de nivello superior, non es necessari seliger ambitos individual. setting_aggregate_reblogs: Non monstrar nove impulsos pro messages que ha essite recentemente impulsate (affecta solmente le impulsos novemente recipite) setting_always_send_emails: Normalmente, le notificationes de e-mail non es inviate quando tu activemente usa Mastodon - setting_default_quote_policy: Le usatores mentionate sempre ha permission pro citar. Iste parametro solo habera effecto pro messages create con le proxime version de Mastodon, ma tu pote seliger tu preferentia anticipatemente setting_default_sensitive: Le medios sensibile es celate de ordinario e pote esser revelate con un clic setting_display_media_default: Celar le medios marcate como sensibile setting_display_media_hide_all: Sempre celar contento multimedial diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml index 15e5b92ea67..fc559af7ba7 100644 --- a/config/locales/simple_form.is.yml +++ b/config/locales/simple_form.is.yml @@ -56,7 +56,7 @@ is: scopes: Að hvaða API-kerfisviðmótum forritið fær aðgang. Ef þú velur efsta-stigs svið, þarftu ekki að gefa einstakar heimildir. setting_aggregate_reblogs: Ekki sýna nýjar endurbirtingar á færslum sem hafa nýlega verið endurbirtar (hefur bara áhrif á ný-mótteknar endurbirtingar) setting_always_send_emails: Venjulega eru tilkynningar í tölvupósti ekki sendar þegar þú ert virk/ur í að nota Mastodon - setting_default_quote_policy: Notendur sem minnst er á geta alltaf gert tilvitnanir. Þessi stilling virkar einungis á færslur sem gerðar hafa verið í næstu útgáfu Mastodon, en þú getur samt valið þetta til að undirbúa þig + setting_default_quote_policy: Þessi stilling gildir einungis fyrir færslur sem útbúnar eru með næstu útgáfu Mastodon, en þú getur valið þetta fyrirfram til undirbúnings. setting_default_sensitive: Viðkvæmt myndefni er sjálfgefið falið og er hægt að birta með smelli setting_display_media_default: Fela myndefni sem merkt er viðkvæmt setting_display_media_hide_all: Alltaf fela allt myndefni @@ -160,6 +160,10 @@ is: name: Opinbert heiti hlutverks, ef birta á hlutverk sem merki permissions_as_keys: Notendur með þetta hlutverk munu hafa aðgang að... position: Rétthærra hlutverk ákvarðar lausn árekstra í ákveðnum tilfellum. Sumar aðgerðir er aðeins hægt að framkvæma á hlutverk með lægri forgangi + username_block: + allow_with_approval: Í stað þess að loka alfarið á nýskráningar, munu samsvarandi nýskráningar þurfa samþykki þitt + comparison: Hafðu í huga Scunthorpe-vandamálið (Scunthorpe inniheldur orð sem ýmsar síur reyna að banna) þegar þú útilokar samsvarandi orðhluta + username: Mun samsvara án tillits til stafstöðu eða algengra táknlíkinga á borð við "4" í stað "a" eða "3" í stað "e" webhook: events: Veldu atburði sem á að senda template: Sníddu eigin JSON með breytilegri brúun. Skiljið eftir autt fyrir sjálfgefið JSON. @@ -325,6 +329,7 @@ is: follow_request: Einhver hefur beðið um að fylgjast með þér mention: Einhver minntist á þig pending_account: Nýr notandaaðgangur þarfnast yfirferðar + quote: Einhver vitnaði í þig reblog: Einhver endurbirti færsluna þína report: Ný kæra hefur verið send inn software_updates: @@ -371,6 +376,10 @@ is: name: Nafn permissions_as_keys: Heimildir position: Forgangur + username_block: + allow_with_approval: Leyfa skráningar með samþykki + comparison: Aðferð við samanburð + username: Orð sem samsvara webhook: events: Virkjaðir atburðir template: Sniðmát gagna diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index 7410cd4ae45..f349680a366 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -56,7 +56,7 @@ it: scopes: A quali API l'applicazione potrà avere accesso. Se selezionate un ambito di alto livello, non c'è bisogno di selezionare quelle singole. setting_aggregate_reblogs: Non mostrare nuove condivisioni per toot che sono stati condivisi di recente (ha effetto solo sulle nuove condivisioni) setting_always_send_emails: Normalmente le notifiche e-mail non vengono inviate quando si utilizza attivamente Mastodon - setting_default_quote_policy: Gli utenti menzionati sono sempre in grado di citare. Questa impostazione avrà effetto solo per i post che verranno creati con la prossima versione di Mastodon, ma puoi selezionare le tue preferenze in preparazione del rilascio della prossima versione + setting_default_quote_policy: Questa impostazione avrà effetto solo per i post creati con la prossima versione di Mastodon, ma puoi selezionare le tue preferenze in fase di preparazione. setting_default_sensitive: Media con contenuti sensibili sono nascosti in modo predefinito e possono essere rivelati con un click setting_display_media_default: Nascondi media segnati come sensibili setting_display_media_hide_all: Nascondi sempre tutti i media @@ -160,6 +160,10 @@ it: name: Nome pubblico del ruolo, se il ruolo è impostato per essere visualizzato come distintivo permissions_as_keys: Gli utenti con questo ruolo avranno accesso a... position: Un ruolo più alto decide la risoluzione dei conflitti in determinate situazioni. Alcune azioni possono essere eseguite solo su ruoli con priorità più bassa + username_block: + allow_with_approval: Invece di impedire del tutto l'iscrizione, le iscrizioni corrispondenti richiederanno la tua approvazione + comparison: Si prega di tenere presente il problema di Scunthorpe quando si bloccano corrispondenze parziali + username: Coinciderà indipendentemente da lettere e omoglifi comuni come "4" per "a" o "3" per "e" webhook: events: Seleziona eventi da inviare template: Componi il tuo carico utile JSON utilizzando l'interpolazione variabile. Lascia vuoto per il JSON predefinito. @@ -325,6 +329,7 @@ it: follow_request: Invia email quando qualcuno chiede di seguirti mention: Invia email quando qualcuno ti menziona pending_account: Invia e-mail quando un nuovo account richiede l'approvazione + quote: Qualcuno ti ha citato reblog: Qualcuno ha condiviso il tuo post report: Una nuova segnalazione è stata inviata software_updates: @@ -371,6 +376,10 @@ it: name: Nome permissions_as_keys: Permessi position: Priorità + username_block: + allow_with_approval: Consenti le registrazioni con approvazione + comparison: Metodo di confronto + username: Parola da abbinare webhook: events: Eventi abilitati template: Modello di carico utile diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 98cebfa6c97..a2fe712ed28 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -56,7 +56,6 @@ ja: scopes: アプリの API に許可するアクセス権を選択してください。最上位のスコープを選択する場合、個々のスコープを選択する必要はありません。 setting_aggregate_reblogs: 最近ブーストされた投稿が新たにブーストされても表示しません (設定後受信したものにのみ影響) setting_always_send_emails: 通常、Mastodon からメール通知は行われません。 - setting_default_quote_policy: メンションされたユーザーが常にその投稿を引用できるようになる。 この設定はMastodonの次のバージョンからしか効力を発揮しませんが、現時点で設定を選択しておくことができます setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります setting_display_media_default: 閲覧注意としてマークされたメディアは隠す setting_display_media_hide_all: メディアを常に隠す diff --git a/config/locales/simple_form.kab.yml b/config/locales/simple_form.kab.yml index c0ff7e598e9..d1a288e2139 100644 --- a/config/locales/simple_form.kab.yml +++ b/config/locales/simple_form.kab.yml @@ -27,6 +27,8 @@ kab: username: Tzemreḍ ad tesqedceḍ isekkilen, uṭṭunen akked yijerriden n wadda featured_tag: name: 'Ha-t-an kra seg ihacṭagen i tesseqdaceḍ ussan-a ineggura maḍi :' + form_challenge: + current_password: Tkecmeḍ ɣer temnaḍt taɣellsant imports: data: Afaylu CSV id yusan seg uqeddac-nniḍen n Maṣṭudun invite_request: @@ -143,7 +145,12 @@ kab: name: Ahacṭag terms_of_service: text: Tiwtilin n useqdec + terms_of_service_generator: + domain: Taɣult user: + date_of_birth_1i: Ass + date_of_birth_2i: Ayyur + date_of_birth_3i: Aseggas role: Tamlilt time_zone: Tamnaḍt tasragant user_role: diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index 5700f43d3d6..da8f90d189e 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -56,7 +56,6 @@ ko: scopes: 애플리케이션에 허용할 API들입니다. 최상위 스코프를 선택하면 개별적인 것은 선택하지 않아도 됩니다. setting_aggregate_reblogs: 최근에 부스트 됐던 게시물은 새로 부스트 되어도 보여주지 않기 (새로 받은 부스트에만 적용됩니다) setting_always_send_emails: 기본적으로 마스토돈을 활동적으로 사용하고 있을 때에는 이메일 알림이 보내지지 않습니다 - setting_default_quote_policy: 멘션된 사용자는 항상 인용할 수 있도록 허용됩니다. 이 설정은 다음 마스토돈 버전부터 효과가 적용되지만 미리 준비할 수 있도록 설정을 제공합니다 setting_default_sensitive: 민감한 미디어는 기본적으로 가려져 있으며 클릭해서 볼 수 있습니다 setting_display_media_default: 민감함으로 표시된 미디어 가리기 setting_display_media_hide_all: 모든 미디어를 가리기 diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml index 4aa89ed9ecb..bbd22e3f768 100644 --- a/config/locales/simple_form.lt.yml +++ b/config/locales/simple_form.lt.yml @@ -60,6 +60,7 @@ lt: setting_display_media_default: Slėpti mediją, pažymėtą kaip jautrią setting_display_media_hide_all: Visada slėpti mediją setting_display_media_show_all: Visada rodyti mediją + setting_emoji_style: Kaip rodyti emodžius. „Auto“ bandys naudoti vietinius jaustukus, bet senesnėse naršyklėse grįš prie Tvejaustukų. setting_system_scrollbars_ui: Taikoma tik darbalaukio naršyklėms, karkasiniais „Safari“ ir „Chrome“. setting_use_blurhash: Gradientai pagrįsti paslėptų vizualizacijų spalvomis, bet užgožia bet kokias detales. setting_use_pending_items: Slėpti laiko skalės naujienas po paspaudimo, vietoj automatinio srauto slinkimo. @@ -120,6 +121,11 @@ lt: min_age: Neturėtų būti žemiau mažiausio amžiaus, reikalaujamo pagal jūsų jurisdikcijos įstatymus. user: chosen_languages: Kai pažymėta, viešose laiko skalėse bus rodomi tik įrašai pasirinktomis kalbomis. + date_of_birth: + few: Turime įsitikinti, kad esate bent %{count} norint naudotis %{domain}. Mes to neišsaugosime. + many: Turime įsitikinti, kad esate bent %{count} norint naudotis %{domain}. Mes to neišsaugosime. + one: Turime įsitikinti, kad esate bent %{count} norint naudotis %{domain}. Mes to neišsaugosime. + other: Turime įsitikinti, kad esate bent %{count} norint naudotis %{domain}. Mes to neišsaugosime. role: Vaidmuo valdo, kokius leidimus naudotojas turi. labels: account: @@ -162,6 +168,7 @@ lt: setting_display_media: Medijos rodymas setting_display_media_hide_all: Slėpti viską setting_display_media_show_all: Rodyti viską + setting_emoji_style: Jaustuko stilius setting_expand_spoilers: Visada išplėsti įrašus, pažymėtus turinio įspėjimais setting_hide_network: Slėpti savo socialinę diagramą setting_missing_alt_text_modal: Rodyti patvirtinimo dialogo langą prieš skelbiant mediją be alternatyvaus teksto. diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml index adc546b9adb..0349aa1809c 100644 --- a/config/locales/simple_form.lv.yml +++ b/config/locales/simple_form.lv.yml @@ -56,7 +56,6 @@ lv: scopes: Kuriem API lietotnei būs ļauts piekļūt. Ja atlasa augstākā līmeņa tvērumu, nav nepieciešamas atlasīt atsevišķus. setting_aggregate_reblogs: Nerādīt jaunus izcēlumus ziņām, kas nesen tika palielinātas (ietekmē tikai nesen saņemtos palielinājumus) setting_always_send_emails: Parasti e-pasta paziņojumi netiek sūtīti, kad aktīvi izmantojat Mastodon - setting_default_quote_policy: Pieminētajiem lietotājiem vienmēr ir atļauts citēt. Šis iestatījums stāsies spēkā tikai nākamo Mastodon versiju ierakstiem. Bet jūs tik un tā variet iestatīt savu izvēli, kamēr notiek ieviešana setting_default_sensitive: Pēc noklusējuma jūtīgi informācijas nesēji ir paslēpti, un tos var atklāt ar klikšķi setting_display_media_default: Paslēpt informācijas nesējus, kas atzīmēti kā jūtīgi setting_display_media_hide_all: Vienmēr slēpt multividi diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index 70e302da031..53307d7e11a 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -56,7 +56,7 @@ nl: scopes: Tot welke API's heeft de toepassing toegang. Wanneer je een toestemming van het bovenste niveau kiest, hoef je geen individuele toestemmingen meer te kiezen. setting_aggregate_reblogs: Geen nieuwe boosts tonen voor berichten die recentelijk nog zijn geboost (heeft alleen effect op nieuw ontvangen boosts) setting_always_send_emails: Normaliter worden er geen e-mailmeldingen verstuurd wanneer je actief Mastodon gebruikt - setting_default_quote_policy: Het is voor gebruikers die vermeld worden altijd toegestaan om te citeren. Deze instelling is alleen van kracht voor berichten die gemaakt zijn met de volgende Mastodon-versie, maar je kunt je voorkeur nu alvast instellen + setting_default_quote_policy: Deze instelling is alleen van kracht voor berichten die gemaakt zijn met de volgende Mastodon-versie, maar je kunt je voorkeur nu alvast instellen. setting_default_sensitive: Gevoelige media wordt standaard verborgen en kan met één klik worden getoond setting_display_media_default: Als gevoelig gemarkeerde media verbergen setting_display_media_hide_all: Media altijd verbergen @@ -160,6 +160,10 @@ nl: name: Openbare naam van de rol, wanneer de rol als badge op profielpagina's wordt getoond permissions_as_keys: Gebruikers met deze rol hebben toegang tot... position: Een hogere rol beslist in bepaalde situaties over het oplossen van conflicten. Bepaalde acties kunnen alleen worden uitgevoerd op rollen met een lagere prioriteit + username_block: + allow_with_approval: In plaats van dat het registreren helemaal wordt voorkomen, zullen overeenkomstige registraties jouw goedkeuring vereisen + comparison: Houd rekening met het Scunthorpe-probleem wanneer je gedeeltelijke overeenkomsten blokkeert + username: Wordt gezien als overeenkomst ongeacht de lettergrootte en gangbare lettervervangingen zoals "4" voor "a" of "3" voor "e" webhook: events: Selecteer de te verzenden gebeurtenissen template: Maak een eigen JSON payload aan met variabele interpolatie. Laat leeg voor standaard JSON. @@ -325,6 +329,7 @@ nl: follow_request: Wanneer iemand jou wil volgen mention: Je bent door iemand vermeld pending_account: Wanneer een nieuw account moet worden beoordeeld + quote: Iemand heeft jou geciteerd reblog: Wanneer iemand jouw bericht heeft geboost report: Nieuwe rapportage is ingediend software_updates: @@ -371,6 +376,10 @@ nl: name: Naam permissions_as_keys: Rechten position: Prioriteit + username_block: + allow_with_approval: Registraties met goedkeuring toestaan + comparison: Methode van vergelijking + username: Overeen te komen woord webhook: events: Ingeschakelde gebeurtenissen template: Sjabloon Payload diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml index d23ef70830c..12ae70ffa40 100644 --- a/config/locales/simple_form.nn.yml +++ b/config/locales/simple_form.nn.yml @@ -56,7 +56,6 @@ nn: scopes: API-ane som programmet vil få tilgjenge til. Ettersom du vel eit toppnivåomfang tarv du ikkje velja einskilde API-ar. setting_aggregate_reblogs: Ikkje vis nye framhevingar for tut som nyleg har vorte heva fram (Påverkar berre nylege framhevingar) setting_always_send_emails: Vanlegvis vil ikkje e-postvarsel bli sendt når du brukar Mastodon aktivt - setting_default_quote_policy: Dei nemnde folka får alltid lov å sitera. Denne innstillinga har berre verknad for innlegg som er laga med den neste utgåva av Mastodon, men du kan velja kva du vil ha i førebuingane setting_default_sensitive: Sensitive media vert gøymde som standard, og du syner dei ved å klikka på dei setting_display_media_default: Gøym media som er merka som sensitive setting_display_media_hide_all: Alltid skjul alt media diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index c049822b817..6faef6aa9ca 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -221,6 +221,7 @@ pl: setting_boost_modal: Pytaj o potwierdzenie przed podbiciem setting_default_language: Język wpisów setting_default_privacy: Widoczność wpisów + setting_default_quote_policy: Kto może cytować setting_default_sensitive: Zawsze oznaczaj zawartość multimedialną jako wrażliwą setting_delete_modal: Pytaj o potwierdzenie przed usunięciem wpisu setting_disable_hover_cards: Wyłącz podgląd profilu po najechaniu @@ -229,6 +230,7 @@ pl: setting_display_media_default: Domyślne setting_display_media_hide_all: Ukryj wszystko setting_display_media_show_all: Pokaż wszystko + setting_emoji_style: Styl emoji setting_expand_spoilers: Zawsze rozwijaj wpisy oznaczone ostrzeżeniem o zawartości setting_hide_network: Ukryj swoją sieć setting_missing_alt_text_modal: Pokaż okno potwierdzenia przed opublikowaniem materiałów bez pomocniczego opisu obrazów @@ -266,6 +268,7 @@ pl: favicon: Favicon mascot: Własna ikona media_cache_retention_period: Okres przechowywania pamięci podręcznej + min_age: Wymagany minimalny wiek peers_api_enabled: Opublikuj listę odkrytych serwerów w API profile_directory: Włącz katalog profilów registrations_mode: Kto może się zarejestrować @@ -331,6 +334,7 @@ pl: usable: Pozwól na umieszczanie tego hashtagu w lokalnych wpisach terms_of_service: changelog: Co się zmieniło? + effective_date: Data wejścia w życie text: Warunki korzystania z usługi terms_of_service_generator: admin_email: Adres e-mail przeznaczony do celów prawnych @@ -341,7 +345,11 @@ pl: dmca_email: Adres e-mail dla zgłoszeń naruszenia DMCA/praw autorskich domain: Domena jurisdiction: Jurysdykcja + min_age: Wiek minimalny user: + date_of_birth_1i: Dzień + date_of_birth_2i: Miesiąc + date_of_birth_3i: Rok role: Rola time_zone: Strefa czasowa user_role: @@ -350,6 +358,10 @@ pl: name: Nazwa permissions_as_keys: Uprawnienia position: Priorytet + username_block: + allow_with_approval: Zezwól na rejestracje po zatwierdzeniu + comparison: Metoda porównania + username: Słowo do dopasowania webhook: events: Włączone zdarzenia template: Szablon zawartości diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml index 39bce7c5943..9bcb70fc383 100644 --- a/config/locales/simple_form.pt-BR.yml +++ b/config/locales/simple_form.pt-BR.yml @@ -56,7 +56,6 @@ pt-BR: scopes: Quais APIs o aplicativo vai ter permissão de acessar. Se você selecionar uma autorização de alto nível, você não precisa selecionar individualmente os outros. setting_aggregate_reblogs: Não mostrar novos impulsos para publicações que já foram impulsionadas recentemente (afeta somente os impulsos mais recentes) setting_always_send_emails: Normalmente, as notificações por e-mail não serão enviadas enquanto você estiver usando ativamente o Mastodon - setting_default_quote_policy: Usuários mencionados sempre têm permissão para citar. Esta configuração só terá efeito para postagens criadas com a próxima versão do Mastodon, mas você pode selecionar sua preferência em preparação setting_default_sensitive: Mídia sensível está oculta por padrão e pode ser revelada com um clique setting_display_media_default: Sempre ocultar mídia sensível setting_display_media_hide_all: Sempre ocultar todas as mídias diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml index 811b2ecd501..adc58a7aed0 100644 --- a/config/locales/simple_form.pt-PT.yml +++ b/config/locales/simple_form.pt-PT.yml @@ -56,7 +56,7 @@ pt-PT: scopes: Quais as API a que a aplicação terá permissão para aceder. Se selecionar um âmbito de nível superior, não precisa de selecionar âmbitos individuais. setting_aggregate_reblogs: Não mostrar os novos impulsos para publicações que tenham sido recentemente impulsionadas (apenas afeta os impulsos recentemente recebidos) setting_always_send_emails: Normalmente as notificações por e-mail não serão enviadas quando estiver a utilizar ativamente o Mastodon - setting_default_quote_policy: Os utilizadores mencionados têm sempre permissão para citar. Esta definição só terá efeito para publicações criadas com a próxima versão do Mastodon, mas pode selecionar a sua preferência em antecipação + setting_default_quote_policy: Esta configuração só terá efeito nas publicações criadas com a próxima versão do Mastodon, mas pode desde já selecionar a sua preferência. setting_default_sensitive: Os multimédia sensíveis são ocultados por predefinição e podem ser revelados com um clique/toque setting_display_media_default: Esconder multimédia marcada como sensível setting_display_media_hide_all: Esconder sempre toda a multimédia @@ -160,6 +160,10 @@ pt-PT: name: Nome público da função, se esta estiver definida para ser apresentada com um emblema permissions_as_keys: Utilizadores com esta função terão acesso a... position: Funções mais altas decidem a resolução de conflitos em certas situações. Certas ações só podem ser executadas com certas funções com uma menor prioridade + username_block: + allow_with_approval: Em vez de impedir totalmente a inscrição, as inscrições correspondentes exigirão a sua aprovação + comparison: Tenha em atenção o Problema de Scunthorpe ao bloquear correspondências parciais + username: Terá correspondência independentemente das maiúsculas e minúsculas e de homógrafos comuns, como "4" para "a" ou "3" para "e" webhook: events: Selecione os eventos a enviar template: Componha o seu próprio conteúdo JSON utilizando a interpolação de variáveis. Deixar em branco para o JSON predefinido. @@ -325,6 +329,7 @@ pt-PT: follow_request: Alguém pediu para ser seu seguidor mention: Alguém o mencionou pending_account: Uma nova conta aguarda aprovação + quote: Alguém o citou reblog: Alguém impulsionou uma publicação sua report: Uma nova denúncia foi submetida software_updates: @@ -371,6 +376,10 @@ pt-PT: name: Nome permissions_as_keys: Permissões position: Prioridade + username_block: + allow_with_approval: Permitir inscrições com aprovação + comparison: Método de comparação + username: Palavra a corresponder webhook: events: Eventos ativados template: Modelo de conteúdo diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index cebd5d1281e..5c48d751b55 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -56,7 +56,6 @@ ru: scopes: Выберите, какие API приложение сможет использовать. Разрешения верхнего уровня имплицитно включают в себя все разрешения более низких уровней. setting_aggregate_reblogs: Не показывать новые продвижения постов, которые уже были недавно продвинуты (применяется только к будущим продвижениям) setting_always_send_emails: По умолчанию уведомления не доставляются по электронной почте, пока вы активно используете Mastodon - setting_default_quote_policy: Упомянутые пользователи всегда смогут вас цитировать. Эта настройка будет применена только к постам, созданным в следующей версии Mastodon, но вы можете заранее определить свои предпочтения setting_default_sensitive: Медиа деликатного характера скрыты по умолчанию и могут быть показаны по нажатию на них setting_display_media_default: Скрывать медиа деликатного характера setting_display_media_hide_all: Скрывать все медиа diff --git a/config/locales/simple_form.si.yml b/config/locales/simple_form.si.yml index f0a0dd02865..119e3d253db 100644 --- a/config/locales/simple_form.si.yml +++ b/config/locales/simple_form.si.yml @@ -56,7 +56,6 @@ si: scopes: යෙදුමට ප්‍රවේශ වීමට ඉඩ දෙන්නේ කුමන API වලටද. ඔබ ඉහළ මට්ටමේ විෂය පථයක් තෝරා ගන්නේ නම්, ඔබට තනි ඒවා තෝරා ගැනීමට අවශ්‍ය නොවේ. setting_aggregate_reblogs: මෑතකදී වැඩි කරන ලද පළ කිරීම් සඳහා නව වැඩි කිරීම් නොපෙන්වන්න (අලුතින් ලැබුණු වැඩි කිරීම් වලට පමණක් බලපායි) setting_always_send_emails: ඔබ නිතර මාස්ටඩන් භාවිතා කරන විට වි-තැපැල් දැනුම්දීම් නොලැබෙයි - setting_default_quote_policy: සඳහන් කළ පරිශීලකයින්ට සැමවිටම උපුටා දැක්වීමට අවසර ඇත. මෙම සැකසුම ඊළඟ Mastodon අනුවාදය සමඟ නිර්මාණය කරන ලද පළ කිරීම් සඳහා පමණක් ක්‍රියාත්මක වනු ඇත, නමුත් ඔබට සූදානම් වීමේදී ඔබේ මනාපය තෝරා ගත හැකිය. setting_default_sensitive: සංවේදී මාධ්‍ය පෙරනිමියෙන් සඟවා ඇති අතර ක්ලික් කිරීමකින් හෙළිදරව් කළ හැක setting_display_media_default: සංවේදී බව සලකුණු කළ මාධ්‍ය සඟවන්න setting_display_media_hide_all: සැමවිට මාධ්‍ය සඟවන්න diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml index 5059fbd1e97..0672fe05a44 100644 --- a/config/locales/simple_form.sq.yml +++ b/config/locales/simple_form.sq.yml @@ -56,7 +56,6 @@ sq: scopes: Cilat API do të lejohen të përdorin aplikacioni. Nëse përzgjidhni një shkallë të epërme, nuk ju duhet të përzgjidhni individualet një nga një. setting_aggregate_reblogs: Mos shfaq përforcime të reja për mesazhe që janë përforcuar tani së fundi (prek vetëm përforcime të marra rishtas) setting_always_send_emails: Normalisht s’do të dërgohen njoftime, kur përdorni aktivisht Mastodon-in - setting_default_quote_policy: Përdoruesit e përmendur lejohen përherë të citojnë. Ky rregullim do të ketë efekt vetëm për postime të krijuar me versionin pasues të Mastodon-it, por mund të përzgjidhni paraprakisht parapëlqimin tuaj setting_default_sensitive: Media rezervat fshihet, si parazgjedhje, dhe mund të shfaqet me një klikim setting_display_media_default: Fshih media me shenjën rezervat setting_display_media_hide_all: Fshih përherë mediat diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index a5571e3d4db..96f00e48c25 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -56,7 +56,6 @@ sv: scopes: 'Vilka API: er applikationen kommer tillåtas åtkomst till. Om du väljer en omfattning på högstanivån behöver du inte välja individuella sådana.' setting_aggregate_reblogs: Visa inte nya boostar för inlägg som nyligen blivit boostade (påverkar endast nymottagna boostar) setting_always_send_emails: E-postnotiser kommer vanligtvis inte skickas när du aktivt använder Mastodon - setting_default_quote_policy: Nämnda användare får alltid citeras. Denna inställning kommer att träda i kraft för inlägg som skapats med nästa Mastodon-version, men förbereda dina inställningar för det redan nu setting_default_sensitive: Känslig media döljs som standard och kan visas med ett klick setting_display_media_default: Dölj media markerad som känslig setting_display_media_hide_all: Dölj alltid all media diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml index 0d9122efae4..e403d34e605 100644 --- a/config/locales/simple_form.tr.yml +++ b/config/locales/simple_form.tr.yml @@ -56,7 +56,7 @@ tr: scopes: Uygulamanın erişmesine izin verilen API'ler. Üst seviye bir kapsam seçtiyseniz, bireysel kapsam seçmenize gerek yoktur. setting_aggregate_reblogs: Yakın zamanda teşvik edilmiş gönderiler için yeni teşvikleri göstermeyin (yalnızca yeni alınan teşvikleri etkiler) setting_always_send_emails: Normalde, Mastodon'u aktif olarak kullanırken e-posta bildirimleri gönderilmeyecektir - setting_default_quote_policy: Bahsedilen kullanıcıların her zaman alıntı yapmasına izin verilir. Bu ayar yalnızca bir sonraki Mastodon sürümü ile oluşturulan gönderiler için geçerli olacaktır, ancak tercihinizi hazırlık aşamasında seçebilirsiniz + setting_default_quote_policy: Bu ayar yalnızca bir sonraki Mastodon sürümüyle oluşturulan gönderiler için geçerli olacak, ancak hazırlık aşamasında tercihinizi seçebilirsiniz. setting_default_sensitive: Hassas medya varsayılan olarak gizlidir ve bir tıklama ile gösterilebilir setting_display_media_default: Hassas olarak işaretlenmiş medyayı gizle setting_display_media_hide_all: Medyayı her zaman gizle @@ -160,6 +160,10 @@ tr: name: Rolün, eğer rozet olarak görüntülenmesi ayarlandıysa kullanılacak herkese açık ismi permissions_as_keys: Bu role sahip kullanıcıların şunlara erişimi var... position: Belirli durumlarda çatışmayı çözmek için daha yüksek rol belirleyicidir. Bazı eylemler ancak daha düşük öncelikteki rollere uygulanabilir + username_block: + allow_with_approval: Kayıt işlemini tamamen engellemek yerine, eşleşen kayıtlar onayınızı gerektirecektir + comparison: Kısmi eşleşmeleri engellerken lütfen Scunthorpe Problemini aklınızda bulundurun + username: '"a" için "4" veya "e" için "3" gibi büyük/küçük harfe ve yaygın homogliflere bakılmaksızın eşleştirilecektir' webhook: events: Gönderilecek etkinlikleri seçin template: Değişken değerleme kullanarak kendi JSON yükünüzü oluşturun. Varsayılan JSON için boş bırakın. @@ -325,6 +329,7 @@ tr: follow_request: Biri seni takip etmek istedi mention: Birisi senden bahsetti pending_account: Yeni hesabın incelenmesi gerekiyor + quote: Birisi senden alıntı yaptı reblog: Birisi gönderini boostladı report: Yeni rapor gönderildi software_updates: @@ -371,6 +376,10 @@ tr: name: Ad permissions_as_keys: İzinler position: Öncelik + username_block: + allow_with_approval: Onay ile kayıtlara izin ver + comparison: Karşılaştırma yöntemi + username: Eşleşecek kelime webhook: events: Etkin olaylar template: Yük şablonu diff --git a/config/locales/simple_form.uk.yml b/config/locales/simple_form.uk.yml index 8a0e9012811..1fb1c542f15 100644 --- a/config/locales/simple_form.uk.yml +++ b/config/locales/simple_form.uk.yml @@ -317,6 +317,7 @@ uk: follow_request: Коли хтось запитує дозвіл підписатися mention: Коли хтось згадує вас pending_account: Надсилати електронного листа, коли новий обліковий запис потребує розгляду + quote: Хтось процитував вас reblog: Коли хтось поширює ваш допис report: Нову скаргу надіслано software_updates: diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml index b9b536eece8..b2e79420e8f 100644 --- a/config/locales/simple_form.vi.yml +++ b/config/locales/simple_form.vi.yml @@ -56,7 +56,7 @@ vi: scopes: Ứng dụng sẽ được phép truy cập những API nào. Nếu bạn chọn quyền cấp cao nhất, không cần chọn quyền nhỏ. setting_aggregate_reblogs: Nếu một tút đã được đăng lại thì sẽ không hiện những lượt đăng lại khác trên bảng tin setting_always_send_emails: Bình thường thì sẽ không gửi khi bạn đang dùng Mastodon - setting_default_quote_policy: Thiết lập này chỉ hiệu lực đối với các tút được tạo bằng phiên bản Mastodon tiếp theo, nhưng bạn có thể chọn trước sẵn + setting_default_quote_policy: Thiết lập này chỉ hiệu lực đối với các tút được tạo bằng phiên bản Mastodon tiếp theo, nhưng bạn có thể chọn trước sẵn. setting_default_sensitive: Bắt buộc nhấn vào mới có thể xem setting_display_media_default: Click để xem setting_display_media_hide_all: Luôn ẩn @@ -159,6 +159,10 @@ vi: name: Tên công khai của vai trò, nếu vai trò được đặt để hiển thị dưới dạng huy hiệu permissions_as_keys: Người có vai trò này sẽ có quyền truy cập vào... position: Vai trò cao hơn sẽ có quyền quyết định xung đột trong các tình huống. Các vai trò có mức độ ưu tiên thấp hơn chỉ có thể thực hiện một số hành động nhất định + username_block: + allow_with_approval: Thay vì cấm đăng ký ngay lập tức, bạn sẽ duyệt phù hợp trước khi đăng ký + comparison: Xin hãy lưu ý đến Vấn đề Scunthorpe khi chặn các từ trùng khớp một phần + username: Sẽ được khớp bất kể chữ hoa và chữ tượng hình phổ biến như "4" cho "a" hoặc "3" cho "e" webhook: events: Chọn sự kiện để gửi template: Soạn JSON payload của riêng bạn bằng phép nội suy biến. Để trống để dùng JSON mặc định. @@ -324,6 +328,7 @@ vi: follow_request: Ai đó yêu cầu theo dõi bạn mention: Ai đó nhắc đến bạn pending_account: Phê duyệt tài khoản mới + quote: Ai đó trích dẫn bạn reblog: Ai đó đăng lại tút của bạn report: Ai đó gửi báo cáo software_updates: @@ -370,6 +375,10 @@ vi: name: Tên permissions_as_keys: Quyền position: Mức độ ưu tiên + username_block: + allow_with_approval: Cho đăng ký nhưng duyệt thủ công + comparison: Phương pháp so sánh + username: Khớp từ webhook: events: Những sự kiện đã bật template: Mẫu payload diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index c6b6f0904b9..f754cb07e3b 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -56,7 +56,6 @@ zh-CN: scopes: 哪些 API 被允许使用。如果你勾选了更高一级的范围,就不用单独选中子项目了。 setting_aggregate_reblogs: 不显示最近已经被转嘟过的嘟文(只会影响新收到的转嘟) setting_always_send_emails: 一般情况下,如果你活跃使用 Mastodon,我们不会向你发送电子邮件通知 - setting_default_quote_policy: 总是允许引用被提及的用户。此设置将仅对下个Mastodon版本创建的帖子生效,但您可以在准备中选择您的偏好 setting_default_sensitive: 敏感内容默认隐藏,并在点击后显示 setting_display_media_default: 隐藏被标记为敏感内容的媒体 setting_display_media_hide_all: 始终隐藏媒体 diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml index 4a48c6c3112..8fc48dfa98a 100644 --- a/config/locales/simple_form.zh-TW.yml +++ b/config/locales/simple_form.zh-TW.yml @@ -56,7 +56,7 @@ zh-TW: scopes: 允許使應用程式存取的 API。 若您選擇最高階範圍,則無須選擇個別項目。 setting_aggregate_reblogs: 不顯示最近已被轉嘟之嘟文的最新轉嘟(只影響最新收到的嘟文) setting_always_send_emails: 一般情況下若您活躍使用 Mastodon ,我們不會寄送電子郵件通知 - setting_default_quote_policy: 已提及使用者總是能引用嘟文。此設定將僅生效於下一版本 Mastodon 建立之嘟文,但您可以預先選好您的偏好設定 + setting_default_quote_policy: 此設定將僅生效於下一版本 Mastodon 建立之嘟文,但您可以預先選好您的偏好設定。 setting_default_sensitive: 敏感內容媒體預設隱藏,且按一下即可重新顯示 setting_display_media_default: 隱藏標為敏感內容的媒體 setting_display_media_hide_all: 總是隱藏所有媒體 @@ -159,6 +159,10 @@ zh-TW: name: 角色的公開名稱,如果角色設定為顯示為徽章 permissions_as_keys: 有此角色的使用者將有權存取... position: 某些情況下,衝突的解決方式由更高階的角色決定。某些動作只能由優先程度較低的角色執行 + username_block: + allow_with_approval: 不直接禁止註冊,符合規則之註冊將需要您的審核 + comparison: 當您封鎖部分匹配時,請留心 Scunthorpe 問題 + username: 將匹配不限大小寫與常見同形異義字,如以「4」代替「A」或以「3」代替「E」 webhook: events: 請選擇要傳送的事件 template: 使用變數代換組合您自己的 JSON payload。留白以使用預設 JSON 。 @@ -324,6 +328,7 @@ zh-TW: follow_request: 當有使用者請求跟隨您時,傳送電子郵件通知 mention: 當有使用者於嘟文提及您時,傳送電子郵件通知 pending_account: 有新的帳號需要審核 + quote: 當有使用者引用您的嘟文時 reblog: 當有使用者轉嘟您的嘟文時,傳送電子郵件通知 report: 新回報已遞交 software_updates: @@ -370,6 +375,10 @@ zh-TW: name: 名稱 permissions_as_keys: 權限 position: 優先權 + username_block: + allow_with_approval: 經審核後可註冊 + comparison: 比較方式 + username: 字詞匹配 webhook: events: 已啟用的事件 template: Payload 樣板 diff --git a/config/locales/sq.yml b/config/locales/sq.yml index a29344ac80f..871c1123a73 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -1865,8 +1865,6 @@ sq: ownership: S’mund të fiksohen mesazhet e të tjerëve reblog: S’mund të fiksohet një përforcim quote_policies: - followers: Ndjekës dhe përdorues të përmendur - nobody: Vetëm përdorues të përmendur public: Këdo title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/sv.yml b/config/locales/sv.yml index ca23e350545..25bc593917a 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -1872,6 +1872,7 @@ sv: edited_at_html: 'Ändrad: %{date}' errors: in_reply_not_found: Inlägget du försöker svara på verkar inte existera. + quoted_status_not_found: Inlägget du försöker svara på verkar inte existera. over_character_limit: teckengräns på %{max} har överskridits pin_errors: direct: Inlägg som endast är synliga för nämnda användare kan inte fästas @@ -1879,8 +1880,6 @@ sv: ownership: Någon annans inlägg kan inte fästas reblog: En boost kan inte fästas quote_policies: - followers: Följare och omnämnda användare - nobody: Endast nämnda användare public: Alla title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 89c51ca6e08..5b8dd33720d 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1,7 +1,7 @@ --- tr: about: - about_mastodon_html: Jetub Maxücretsiz ve açık kaynaklı bir sosyal ağdır. Merkezi olmayan yapısı sayesinde diğer ticari sosyal platformların aksine iletişimininizin tek bir firmada tutulmasının/yönetilmesinin önüne geçer. Güvendiğiniz bir sunucuyu seçerek oradaki kişilerle etkileşimde bulunabilirsiniz. Herkes kendi Jetub Max sunucusunu kurabilir ve sorunsuz bir şekilde Jetub Maxsosyal ağına dahil edebilir! + about_mastodon_html: 'Geleceğin sosyal ağı: Reklam yok, kurumsal gözetim yok, etik tasarım ve merkeziyetsizlik! Mastodon ile verilerinizin sahibi olun!' contact_missing: Ayarlanmadı contact_unavailable: Bulunamadı hosted_on: Mastodon %{domain} üzerinde barındırılıyor @@ -190,6 +190,7 @@ tr: create_relay: Aktarıcı Oluştur create_unavailable_domain: Mevcut Olmayan Alan Adı Oluştur create_user_role: Rol Oluştur + create_username_block: Kullanıcı Adı Kuralı Oluştur demote_user: Kullanıcıyı Düşür destroy_announcement: Duyuru Sil destroy_canonical_email_block: E-Posta Engelini Sil @@ -203,6 +204,7 @@ tr: destroy_status: Durumu Sil destroy_unavailable_domain: Mevcut Olmayan Alan Adı Sil destroy_user_role: Rolü Kaldır + destroy_username_block: Kullanıcı Adı Kuralını Sil disable_2fa_user: 2AD Kapat disable_custom_emoji: Özel İfadeyi Devre Dışı Bırak disable_relay: Aktarıcıyı Devre Dışı Bırak @@ -237,6 +239,7 @@ tr: update_report: Raporu Güncelle update_status: Durumu Güncelle update_user_role: Rolü Güncelle + update_username_block: Kullanıcı Adı Kuralını Güncelle actions: approve_appeal_html: "%{name}, %{target} kullanıcısının yönetim kararına itirazını kabul etti" approve_user_html: "%{name}, %{target} konumundan kaydı onayladı" @@ -255,6 +258,7 @@ tr: create_relay_html: "%{name}, %{target} aktarıcısını oluşturdu" create_unavailable_domain_html: "%{name}, %{target} alan adına teslimatı durdurdu" create_user_role_html: "%{name}, %{target} rolünü oluşturdu" + create_username_block_html: "%{name}, %{target} içeren kullanıcı adları için kural ekledi" demote_user_html: "%{name}, %{target} kullanıcısını düşürdü" destroy_announcement_html: "%{name}, %{target} duyurusunu sildi" destroy_canonical_email_block_html: "%{name}, %{target} karmasıyla e-posta engelini kaldırdı" @@ -268,6 +272,7 @@ tr: destroy_status_html: "%{name}, %{target} kullanıcısının gönderisini kaldırdı" destroy_unavailable_domain_html: "%{name}, %{target} alan adına teslimatı sürdürdü" destroy_user_role_html: "%{name}, %{target} rolünü sildi" + destroy_username_block_html: "%{name}, %{target} içeren kullanıcı adları için kural silindi" disable_2fa_user_html: "%{name}, %{target} kullanıcısının iki aşamalı doğrulama gereksinimini kapattı" disable_custom_emoji_html: "%{name}, %{target} emojisini devre dışı bıraktı" disable_relay_html: "%{name}, %{target} aktarıcısını devre dışı bıraktı" @@ -302,6 +307,7 @@ tr: update_report_html: "%{name}, %{target} raporunu güncelledi" update_status_html: "%{name}, %{target} kullanıcısının gönderisini güncelledi" update_user_role_html: "%{name}, %{target} rolünü değiştirdi" + update_username_block_html: "%{name}, %{target} içeren kullanıcı adları için kural güncellendi" deleted_account: hesap silindi empty: Kayıt bulunamadı. filter_by_action: Eyleme göre filtre @@ -1085,6 +1091,25 @@ tr: other: Geçen hafta %{count} kişi tarafından kullanıldı title: Öneriler ve Öne Çıkanlar trending: Öne çıkanlar + username_blocks: + add_new: Yeni ekle + block_registrations: Kayıtları engelle + comparison: + contains: İçerir + equals: Eşit + contains_html: "%{string} içerir" + created_msg: Kullanıcı adı kuralı başarıyla oluşturuldu + delete: Sil + edit: + title: Kullanıcı adı kuralını düzenle + matches_exactly_html: "%{string} değerine eşittir" + new: + create: Kural oluştur + title: Yeni kullanıcı adı kuralı oluştur + no_username_block_selected: Hiçbir kullanıcı adı kuralı değiştirilmedi çünkü hiçbiri seçilmedi + not_permitted: İzin verilmiyor + title: Kullanıcı adı kuralları + updated_msg: Kullanıcı adı kuralı başarıyla güncellendi warning_presets: add_new: Yeni ekle delete: Sil @@ -1571,7 +1596,7 @@ tr: password: parola sign_in_token: e-posta güvenlik kodu webauthn: güvenlik anahtarları - description_html: Eğer tanımadığınız bir faaliyet görüyorsanız, parolanızı değiştirmeyi ve iki aşamalı kimlik doğrulamayı etkinleştirmeyi düşünün. + description_html: Eğer tanımadığınız bir faaliyet görürseniz, parolanızı değiştirmeyi ve iki aşamalı kimlik doğrulamayı etkinleştirmeyi değerlendirin. empty: Kimlik doğrulama geçmişi yok failed_sign_in_html: "%{method} yöntemiyle %{ip} (%{browser}) adresinden başarısız oturum açma girişimi" successful_sign_in_html: "%{method} yöntemiyle %{ip} (%{browser}) adresinden başarılı oturum açma" @@ -1662,6 +1687,10 @@ tr: title: Yeni bahsetme poll: subject: Anket %{name} tarafından sonlandırıldı + quote: + body: "%{name} durumunuzu yeniden paylaştı:" + subject: "%{name} gönderini yeniden paylaştı" + title: Yeni alıntı reblog: body: "%{name} durumunuzu yeniden paylaştı:" subject: "%{name} durumunuzu yeniden paylaştı" @@ -1872,6 +1901,7 @@ tr: edited_at_html: "%{date} tarihinde düzenlendi" errors: in_reply_not_found: Yanıtlamaya çalıştığınız durum yok gibi görünüyor. + quoted_status_not_found: Alıntılamaya çalıştığınız gönderi mevcut görünmüyor. over_character_limit: "%{max} karakter limiti aşıldı" pin_errors: direct: Sadece değinilen kullanıcıların görebileceği gönderiler üstte tutulamaz @@ -1879,8 +1909,8 @@ tr: ownership: Başkasının gönderisi sabitlenemez reblog: Bir gönderi sabitlenemez quote_policies: - followers: Takipçiler ve bahsedilen kullanıcılar - nobody: Sadece bahsedilen kullanıcılar + followers: Yalnızca takipçileriniz + nobody: Hiç kimse public: Herkes title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/uk.yml b/config/locales/uk.yml index ceadfc34664..721655d9a43 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -496,6 +496,15 @@ uk: new: title: Імпорт блокувань домену no_file: Файл не вибрано + fasp: + debug: + callbacks: + delete: Видалити + providers: + delete: Видалити + registrations: + confirm: Підтвердити + save: Зберегти follow_recommendations: description_html: "Слідувати рекомендаціям та допомогти новим користувачам швидко знайти цікавий вміст. Коли користувачі не взаємодіяли з іншими людьми достатньо, щоб сформувати персоналізовані рекомендації, радимо замість цього вказувати ці облікові записи. Вони щоденно переобчислюються з масиву облікових записів з найбільшою кількістю недавніх взаємодій і найбільшою кількістю місцевих підписників розраховується для цієї мови." language: Для мови @@ -1072,6 +1081,11 @@ uk: other: Використали %{count} людей за минулий тиждень title: Рекомендації та тренди trending: Популярне + username_blocks: + comparison: + contains: Містить + equals: Дорівнює + delete: Видалити warning_presets: add_new: Додати новий delete: Видалити @@ -1628,6 +1642,8 @@ uk: title: Нова згадка poll: subject: Опитування від %{name} завершено + quote: + body: 'Ваш пост був процитований %{name}:' reblog: body: "%{name} поширює ваш допис:" subject: "%{name} поширив ваш статус" @@ -1846,6 +1862,7 @@ uk: edited_at_html: Відредаговано %{date} errors: in_reply_not_found: Допису, на який ви намагаєтеся відповісти, не існує. + quoted_status_not_found: Повідомлення, яке ви намагаєтеся цитувати, не існує. over_character_limit: перевищено ліміт символів %{max} pin_errors: direct: Не можливо прикріпити дописи, які видимі лише згаданим користувачам diff --git a/config/locales/vi.yml b/config/locales/vi.yml index d56878d1615..aa2b47a890e 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -187,6 +187,7 @@ vi: create_relay: Tạo relay create_unavailable_domain: Bỏ liên hợp create_user_role: Tạo vai trò + create_username_block: Tạo quy tắc tên người dùng demote_user: Hạ vai trò destroy_announcement: Xóa thông báo destroy_canonical_email_block: Bỏ chặn địa chỉ email biến thể @@ -200,6 +201,7 @@ vi: destroy_status: Xóa tút destroy_unavailable_domain: Cho phép liên hợp destroy_user_role: Xóa vai trò + destroy_username_block: Xóa quy tắc tên người dùng disable_2fa_user: Vô hiệu hóa 2FA disable_custom_emoji: Vô hiệu hóa emoji disable_relay: Tắt relay @@ -234,6 +236,7 @@ vi: update_report: Cập nhật báo cáo update_status: Cập nhật tút update_user_role: Cập nhật vai trò + update_username_block: Cập nhật quy tắc tên người dùng actions: approve_appeal_html: "%{name} đã chấp nhận khiếu nại từ %{target}" approve_user_html: "%{name} đã chấp nhận đăng ký từ %{target}" @@ -252,6 +255,7 @@ vi: create_relay_html: "%{name} đã tạo relay %{target}" create_unavailable_domain_html: "%{name} đã bỏ liên hợp với máy chủ %{target}" create_user_role_html: "%{name} đã tạo vai trò %{target}" + create_username_block_html: "%{name} thêm quy tắc cho tên người dùng chứa %{target}" demote_user_html: "%{name} đã hạ vai trò của %{target}" destroy_announcement_html: "%{name} đã xóa thông báo %{target}" destroy_canonical_email_block_html: "%{name} đã bỏ chặn địa chỉ email biến thể %{target}" @@ -265,6 +269,7 @@ vi: destroy_status_html: "%{name} đã xóa tút của %{target}" destroy_unavailable_domain_html: "%{name} tiếp tục liên hợp với máy chủ %{target}" destroy_user_role_html: "%{name} đã xóa vai trò %{target}" + destroy_username_block_html: "%{name} xóa quy tắc cho tên người dùng chứa %{target}" disable_2fa_user_html: "%{name} đã vô hiệu hóa xác thực 2 bước của %{target}" disable_custom_emoji_html: "%{name} đã ẩn emoji %{target}" disable_relay_html: "%{name} đã tắt relay %{target}" @@ -299,6 +304,7 @@ vi: update_report_html: "%{name} đã cập nhật báo cáo %{target}" update_status_html: "%{name} đã cập nhật tút của %{target}" update_user_role_html: "%{name} đã cập nhật vai trò %{target}" + update_username_block_html: "%{name} đã cập nhật quy tắc cho tên người dùng chứa %{target}" deleted_account: tài khoản đã xóa empty: Không tìm thấy bản ghi. filter_by_action: Theo hành động @@ -1067,6 +1073,25 @@ vi: other: "%{count} người dùng tuần rồi" title: Đề xuất & Xu hướng trending: Xu hướng + username_blocks: + add_new: Thêm mới + block_registrations: Cấm đăng ký + comparison: + contains: Có chứa + equals: Tương tự + contains_html: Có chứa %{string} + created_msg: Đã tạo quy tắc tên người dùng thành công + delete: Xóa + edit: + title: Sửa quy tắc tên người dùng + matches_exactly_html: Có chứa %{string} + new: + create: Tạo quy tắc + title: Tạo quy tắc tên người dùng mới + no_username_block_selected: Chưa chọn quy tắc tên người dùng nào + not_permitted: Cấm + title: Quy tắc tên người dùng + updated_msg: Đã cập nhật quy tắc tên người dùng thành công warning_presets: add_new: Thêm mới delete: Xóa bỏ @@ -1623,6 +1648,10 @@ vi: title: Lượt nhắc mới poll: subject: Vốt của %{name} đã kết thúc + quote: + body: 'Tút của bạn được trích dẫn bởi %{name}:' + subject: "%{name} vừa trích dẫn tút của bạn" + title: Trích dẫn mới reblog: body: Tút của bạn vừa được %{name} đăng lại subject: "%{name} vừa đăng lại tút của bạn" @@ -1829,6 +1858,7 @@ vi: edited_at_html: Sửa %{date} errors: in_reply_not_found: Bạn đang trả lời một tút không còn tồn tại. + quoted_status_not_found: Bạn đang trích dẫn một tút không còn tồn tại. over_character_limit: vượt quá giới hạn %{max} ký tự pin_errors: direct: Không thể ghim những tút nhắn riêng @@ -1836,8 +1866,8 @@ vi: ownership: Không thể ghim tút của người khác reblog: Không thể ghim tút đăng lại quote_policies: - followers: Người được nhắc đến và người theo dõi - nobody: Người được nhắc đến + followers: Chỉ người theo dõi + nobody: Không ai public: Mọi người title: '%{name}: "%{quote}"' visibilities: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index c62dc8e3191..82d0a169cc9 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1827,8 +1827,6 @@ zh-CN: ownership: 不能置顶别人的嘟文 reblog: 不能置顶转嘟 quote_policies: - followers: 关注者和提及的用户 - nobody: 仅提及的用户 public: 所有人 title: "%{name}:“%{quote}”" visibilities: diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 546ac7d989a..dbc9c6b8a2e 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -187,6 +187,7 @@ zh-TW: create_relay: 新增中繼 create_unavailable_domain: 新增無法存取的網域 create_user_role: 新增角色 + create_username_block: 新增使用者名稱規則 demote_user: 將用戶降級 destroy_announcement: 刪除公告 destroy_canonical_email_block: 刪除電子郵件封鎖 @@ -200,6 +201,7 @@ zh-TW: destroy_status: 刪除狀態 destroy_unavailable_domain: 刪除無法存取的網域 destroy_user_role: 移除角色 + destroy_username_block: 刪除使用者名稱規則 disable_2fa_user: 停用兩階段驗證 disable_custom_emoji: 停用自訂 emoji 表情符號 disable_relay: 停用中繼 @@ -234,6 +236,7 @@ zh-TW: update_report: 更新檢舉報告 update_status: 更新狀態 update_user_role: 更新角色 + update_username_block: 變更使用者名稱規則 actions: approve_appeal_html: "%{name} 已批准來自 %{target} 的審核決定申訴" approve_user_html: "%{name} 已批准自 %{target} 而來的註冊" @@ -252,6 +255,7 @@ zh-TW: create_relay_html: "%{name} 已新增中繼 %{target}" create_unavailable_domain_html: "%{name} 停止發送至網域 %{target}" create_user_role_html: "%{name} 已新增 %{target} 角色" + create_username_block_html: "%{name} 已新增使用者名稱包含 %{target} 之規則" demote_user_html: "%{name} 將使用者 %{target} 降級" destroy_announcement_html: "%{name} 已刪除公告 %{target}" destroy_canonical_email_block_html: "%{name} 已解除封鎖 hash 為 %{target} 之電子郵件" @@ -265,6 +269,7 @@ zh-TW: destroy_status_html: "%{name} 已刪除 %{target} 的嘟文" destroy_unavailable_domain_html: "%{name} 已恢復對網域 %{target} 的發送" destroy_user_role_html: "%{name} 已刪除 %{target} 角色" + destroy_username_block_html: "%{name} 已移除使用者名稱包含 %{target} 之規則" disable_2fa_user_html: "%{name} 已停用使用者 %{target} 的兩階段驗證 (2FA) " disable_custom_emoji_html: "%{name} 已停用自訂 emoji 表情符號 %{target}" disable_relay_html: "%{name} 已停用中繼 %{target}" @@ -299,6 +304,7 @@ zh-TW: update_report_html: "%{name} 已更新 %{target} 的檢舉" update_status_html: "%{name} 已更新 %{target} 的嘟文" update_user_role_html: "%{name} 已變更 %{target} 角色" + update_username_block_html: "%{name} 已變更使用者名稱包含 %{target} 之規則" deleted_account: 已刪除帳號 empty: 找不到 log filter_by_action: 按動作過濾 @@ -1069,6 +1075,25 @@ zh-TW: other: 上週被 %{count} 個人使用 title: 推薦內容與熱門趨勢 trending: 熱門 + username_blocks: + add_new: 新增 + block_registrations: 禁止註冊 + comparison: + contains: 包含 + equals: 等於 + contains_html: 包含 %{string} + created_msg: 已成功新增使用者名稱規則 + delete: 刪除 + edit: + title: 編輯使用者名稱規則 + matches_exactly_html: 等於 %{string} + new: + create: 新增規則 + title: 新增使用者名稱規則 + no_username_block_selected: 因未選取任何使用者名稱規則,所以什麼事都沒發生 + not_permitted: 不允許 + title: 使用者名稱規則 + updated_msg: 已成功變更使用者名稱規則 warning_presets: add_new: 新增 delete: 刪除 @@ -1625,6 +1650,10 @@ zh-TW: title: 新的提及 poll: subject: 由 %{name} 發起的投票已結束 + quote: + body: 您的嘟文被 %{name} 引用: + subject: "%{name} 已引用您的嘟文" + title: 新引用 reblog: body: 您的嘟文被 %{name} 轉嘟: subject: "%{name} 已轉嘟您的嘟文" @@ -1830,7 +1859,8 @@ zh-TW: other: 含有不得使用的標籤: %{tags} edited_at_html: 編輯於 %{date} errors: - in_reply_not_found: 您嘗試回覆的嘟文看起來不存在。 + in_reply_not_found: 您嘗試回覆之嘟文似乎不存在。 + quoted_status_not_found: 您嘗試引用之嘟文似乎不存在。 over_character_limit: 已超過 %{max} 字的限制 pin_errors: direct: 無法釘選只有僅提及使用者可見之嘟文 @@ -1838,8 +1868,8 @@ zh-TW: ownership: 不能釘選他人的嘟文 reblog: 不能釘選轉嘟 quote_policies: - followers: 跟隨者與已提及使用者 - nobody: 僅限已提及使用者 + followers: 僅限您之跟隨者 + nobody: 禁止任何人 public: 任何人 title: "%{name}:「%{quote}」" visibilities: diff --git a/config/navigation.rb b/config/navigation.rb index d60f8cbc5b3..a8f686fd8b1 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -59,6 +59,7 @@ SimpleNavigation::Configuration.run do |navigation| current_user.can?(:manage_federation) } s.item :email_domain_blocks, safe_join([material_symbol('mail'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) } + s.item :username_blocks, safe_join([material_symbol('supervised_user_circle_off'), t('admin.username_blocks.title')]), admin_username_blocks_path, highlights_on: %r{/admin/username_blocks}, if: -> { current_user.can?(:manage_blocks) } s.item :ip_blocks, safe_join([material_symbol('hide_source'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) } s.item :action_logs, safe_join([material_symbol('list'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) } end diff --git a/config/routes.rb b/config/routes.rb index 2fff44851e0..49fcf3de792 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -115,6 +115,7 @@ Rails.application.routes.draw do resource :inbox, only: [:create] resources :collections, only: [:show] resource :followers_synchronization, only: [:show] + resources :quote_authorizations, only: [:show] end end diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 3d9f24ae838..97f84da44e7 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -230,4 +230,10 @@ namespace :admin do end resources :software_updates, only: [:index] + + resources :username_blocks, except: [:show, :destroy] do + collection do + post :batch + end + end end diff --git a/config/routes/api.rb b/config/routes/api.rb index 4040a4350fa..83190610d0b 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -18,6 +18,12 @@ namespace :api, format: false do resource :reblog, only: :create post :unreblog, to: 'reblogs#destroy' + resources :quotes, only: :index do + member do + post :revoke + end + end + resource :favourite, only: :create post :unfavourite, to: 'favourites#destroy' diff --git a/config/settings.yml b/config/settings.yml index ba81fcb8c6a..7d2f0a00c07 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -20,28 +20,6 @@ defaults: &defaults trends: true trends_as_landing_page: true trendable_by_default: false - reserved_usernames: - - abuse - - account - - accounts - - admin - - administration - - administrator - - admins - - help - - helpdesk - - instance - - mod - - moderator - - moderators - - mods - - owner - - root - - security - - server - - staff - - support - - webmaster disallowed_hashtags: # space separated string or list of hashtags without the hash bootstrap_timeline_accounts: '' activity_api_enabled: true diff --git a/db/migrate/20250717003848_create_username_blocks.rb b/db/migrate/20250717003848_create_username_blocks.rb new file mode 100644 index 00000000000..01649d06574 --- /dev/null +++ b/db/migrate/20250717003848_create_username_blocks.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class CreateUsernameBlocks < ActiveRecord::Migration[8.0] + def change + create_table :username_blocks do |t| + t.string :username, null: false + t.string :normalized_username, null: false + t.boolean :exact, null: false, default: false + t.boolean :allow_with_approval, null: false, default: false + + t.timestamps + end + + add_index :username_blocks, 'lower(username)', unique: true, name: 'index_username_blocks_on_username_lower_btree' + add_index :username_blocks, :normalized_username + + reversible do |dir| + dir.up do + load Rails.root.join('db', 'seeds', '05_blocked_usernames.rb') + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0237b476445..272d6fac182 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_06_27_132728) do +ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -1238,6 +1238,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_27_132728) do t.datetime "updated_at", null: false end + create_table "username_blocks", force: :cascade do |t| + t.string "username", null: false + t.string "normalized_username", null: false + t.boolean "exact", default: false, null: false + t.boolean "allow_with_approval", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index "lower((username)::text)", name: "index_username_blocks_on_username_lower_btree", unique: true + t.index ["normalized_username"], name: "index_username_blocks_on_normalized_username" + end + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.datetime "created_at", precision: nil, null: false diff --git a/db/seeds/04_admin.rb b/db/seeds/04_admin.rb index 887b4a22130..43290c47a46 100644 --- a/db/seeds/04_admin.rb +++ b/db/seeds/04_admin.rb @@ -7,7 +7,17 @@ if Rails.env.development? admin = Account.where(username: 'admin').first_or_initialize(username: 'admin') admin.save(validate: false) - user = User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, role: UserRole.find_by(name: 'Owner'), account: admin, agreement: true, approved: true) + user = User.where(email: "admin@#{domain}").first_or_initialize( + email: "admin@#{domain}", + password: 'mastodonadmin', + password_confirmation: 'mastodonadmin', + confirmed_at: Time.now.utc, + role: UserRole.find_by(name: 'Owner'), + account: admin, + agreement: true, + approved: true, + bypass_registration_checks: true + ) user.save! user.approve! end diff --git a/db/seeds/05_blocked_usernames.rb b/db/seeds/05_blocked_usernames.rb new file mode 100644 index 00000000000..8bfe536c898 --- /dev/null +++ b/db/seeds/05_blocked_usernames.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +%w( + abuse + account + accounts + admin + administration + administrator + admins + help + helpdesk + instance + mod + moderator + moderators + mods + owner + root + security + server + staff + support + webmaster +).each do |str| + UsernameBlock.create_with(username: str, exact: true).find_or_create_by(username: str) +end + +%w( + mastodon + mastadon +).each do |str| + UsernameBlock.create_with(username: str, exact: false).find_or_create_by(username: str) +end diff --git a/docker-compose.yml b/docker-compose.yml index e5d94b390f4..39823fc812c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.4.1 + image: ghcr.io/mastodon/mastodon:v4.4.3 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/mastodon/mastodon-streaming:v4.4.1 + image: ghcr.io/mastodon/mastodon-streaming:v4.4.3 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.4.1 + image: ghcr.io/mastodon/mastodon:v4.4.3 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/eslint.config.mjs b/eslint.config.mjs index 3d00a4adce9..43aabc51100 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -406,6 +406,12 @@ export default tseslint.config([ globals: globals.vitest, }, }, + { + files: ['**/*.test.*'], + rules: { + 'no-global-assign': 'off', + }, + }, { files: ['**/*.stories.ts', '**/*.stories.tsx', '.storybook/*'], rules: { diff --git a/lib/exceptions.rb b/lib/exceptions.rb index c8c81983825..18a99ace2a4 100644 --- a/lib/exceptions.rb +++ b/lib/exceptions.rb @@ -14,6 +14,7 @@ module Mastodon class InvalidParameterError < Error; end class SignatureVerificationError < Error; end class MalformedHeaderError < Error; end + class RecursionLimitExceededError < Error; end class UnexpectedResponseError < Error attr_reader :response diff --git a/lib/mastodon/cli/media.rb b/lib/mastodon/cli/media.rb index 1059eb60660..02c9894c36d 100644 --- a/lib/mastodon/cli/media.rb +++ b/lib/mastodon/cli/media.rb @@ -313,9 +313,7 @@ module Mastodon::CLI end def combined_media_sum - Arel.sql(<<~SQL.squish) - COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0) - SQL + MediaAttachment.combined_media_file_size end def preload_records_from_mixed_objects(objects) diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 7cb5dec6772..cb8ff3f462c 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -17,7 +17,7 @@ module Mastodon end def default_prerelease - 'alpha.1' + 'alpha.2' end def prerelease diff --git a/lib/mastodon/worker_batch_middleware.rb b/lib/mastodon/worker_batch_middleware.rb new file mode 100644 index 00000000000..c4623013327 --- /dev/null +++ b/lib/mastodon/worker_batch_middleware.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Mastodon::WorkerBatchMiddleware + def call(_worker, msg, _queue, _redis_pool = nil) + if (batch = Thread.current[:batch]) + batch.add_jobs([msg['jid']]) + end + + yield + end +end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index f76d1891611..2aad68d53d8 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -3,420 +3,439 @@ namespace :dev do desc 'Populate database with test data. Can be run multiple times. Should not be run in production environments' task populate_sample_data: :environment do - # Create a valid account to showcase multiple post types - showcase_account = Account.create_with(username: 'showcase_account').find_or_create_by!(id: 10_000_000) - showcase_user = User.create_with( - account_id: showcase_account.id, - agreement: true, - password: SecureRandom.hex, - email: ENV.fetch('TEST_DATA_SHOWCASE_EMAIL', 'showcase_account@joinmastodon.org'), - confirmed_at: Time.now.utc, - approved: true, - bypass_registration_checks: true - ).find_or_create_by!(id: 10_000_000) - showcase_user.mark_email_as_confirmed! - showcase_user.approve! + Chewy.strategy(:mastodon) do + # Create a valid account to showcase multiple post types + showcase_account = Account.create_with(username: 'showcase_account').find_or_create_by!(id: 10_000_000) + showcase_user = User.create_with( + account_id: showcase_account.id, + agreement: true, + password: SecureRandom.hex, + email: ENV.fetch('TEST_DATA_SHOWCASE_EMAIL', 'showcase_account@joinmastodon.org'), + confirmed_at: Time.now.utc, + approved: true, + bypass_registration_checks: true + ).find_or_create_by!(id: 10_000_000) + showcase_user.mark_email_as_confirmed! + showcase_user.approve! - french_post = Status.create_with( - text: 'Ceci est un sondage public écrit en Français', - language: 'fr', - account: showcase_account, - visibility: :public, - poll_attributes: { - voters_count: 0, + french_post = Status.create_with( + text: 'Ceci est un sondage public écrit en Français', + language: 'fr', account: showcase_account, - expires_at: 1.day.from_now, - options: ['ceci est un choix', 'ceci est un autre choix'], - multiple: false, - } - ).find_or_create_by!(id: 10_000_000) + visibility: :public, + poll_attributes: { + voters_count: 0, + account: showcase_account, + expires_at: 1.day.from_now, + options: ['ceci est un choix', 'ceci est un autre choix'], + multiple: false, + } + ).find_or_create_by!(id: 10_000_000) - private_mentionless = Status.create_with( - text: 'This is a private message written in English', - language: 'en', - account: showcase_account, - visibility: :private - ).find_or_create_by!(id: 10_000_001) - - public_self_reply_with_cw = Status.create_with( - text: 'This is a public self-reply written in English; it has a CW and a multi-choice poll', - spoiler_text: 'poll (CW example)', - language: 'en', - account: showcase_account, - visibility: :public, - thread: french_post, - poll_attributes: { - voters_count: 0, + private_mentionless = Status.create_with( + text: 'This is a private message written in English', + language: 'en', account: showcase_account, - expires_at: 1.day.from_now, - options: ['this is a choice', 'this is another choice', 'you can chose any number of them'], - multiple: true, - } - ).find_or_create_by!(id: 10_000_002) - ProcessHashtagsService.new.call(public_self_reply_with_cw) + visibility: :private + ).find_or_create_by!(id: 10_000_001) - unlisted_self_reply_with_cw_tag_mention = Status.create_with( - text: 'This is an unlisted (Quiet Public) self-reply written in #English; it has a CW, mentions @showcase_account, and uses an emoji 🦣', - spoiler_text: 'CW example', - language: 'en', - account: showcase_account, - visibility: :unlisted, - thread: public_self_reply_with_cw - ).find_or_create_by!(id: 10_000_003) - Mention.find_or_create_by!(status: unlisted_self_reply_with_cw_tag_mention, account: showcase_account) - ProcessHashtagsService.new.call(unlisted_self_reply_with_cw_tag_mention) + public_self_reply_with_cw = Status.create_with( + text: 'This is a public self-reply written in English; it has a CW and a multi-choice poll', + spoiler_text: 'poll (CW example)', + language: 'en', + account: showcase_account, + visibility: :public, + thread: french_post, + poll_attributes: { + voters_count: 0, + account: showcase_account, + expires_at: 1.day.from_now, + options: ['this is a choice', 'this is another choice', 'you can chose any number of them'], + multiple: true, + } + ).find_or_create_by!(id: 10_000_002) + ProcessHashtagsService.new.call(public_self_reply_with_cw) - media_attachment = MediaAttachment.create_with( - account: showcase_account, - file: File.open('spec/fixtures/files/600x400.png'), - description: 'Mastodon logo' - ).find_or_create_by!(id: 10_000_000) - status_with_media = Status.create_with( - text: "This is a public status with a picture and tags. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test", - ordered_media_attachment_ids: [media_attachment.id], - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_004) - media_attachment.update(status_id: status_with_media.id) - ProcessHashtagsService.new.call(status_with_media) + unlisted_self_reply_with_cw_tag_mention = Status.create_with( + text: 'This is an unlisted (Quiet Public) self-reply written in #English; it has a CW, mentions @showcase_account, and uses an emoji 🦣', + spoiler_text: 'CW example', + language: 'en', + account: showcase_account, + visibility: :unlisted, + thread: public_self_reply_with_cw + ).find_or_create_by!(id: 10_000_003) + Mention.find_or_create_by!(status: unlisted_self_reply_with_cw_tag_mention, account: showcase_account) + ProcessHashtagsService.new.call(unlisted_self_reply_with_cw_tag_mention) - media_attachment = MediaAttachment.create_with( - account: showcase_account, - file: File.open('spec/fixtures/files/600x400.png'), - description: 'Mastodon logo' - ).find_or_create_by!(id: 10_000_001) - status_with_sensitive_media = Status.create_with( - text: "This is the same public status with a picture and tags, but it is marked as sensitive. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test", - ordered_media_attachment_ids: [media_attachment.id], - account: showcase_account, - visibility: :public, - sensitive: true, - thread: status_with_media - ).find_or_create_by!(id: 10_000_005) - media_attachment.update(status_id: status_with_sensitive_media.id) - ProcessHashtagsService.new.call(status_with_sensitive_media) - - media_attachment = MediaAttachment.create_with( - account: showcase_account, - file: File.open('spec/fixtures/files/600x400.png'), - description: 'Mastodon logo' - ).find_or_create_by!(id: 10_000_002) - status_with_cw_media = Status.create_with( - text: "This is the same public status with a picture and tags, but it is behind a CW. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test", - spoiler_text: 'Mastodon logo', - ordered_media_attachment_ids: [media_attachment.id], - account: showcase_account, - visibility: :public, - sensitive: true, - thread: status_with_sensitive_media - ).find_or_create_by!(id: 10_000_006) - media_attachment.update(status_id: status_with_cw_media.id) - ProcessHashtagsService.new.call(status_with_cw_media) - - media_attachment = MediaAttachment.create_with( - account: showcase_account, - file: File.open('spec/fixtures/files/boop.ogg'), - description: 'Mastodon boop' - ).find_or_create_by!(id: 10_000_003) - status_with_audio = Status.create_with( - text: "This is the same public status with an audio file and tags. The attached picture has an alt text\n\n#Mastodon #English #Test", - ordered_media_attachment_ids: [media_attachment.id], - account: showcase_account, - visibility: :public, - thread: status_with_cw_media - ).find_or_create_by!(id: 10_000_007) - media_attachment.update(status_id: status_with_audio.id) - ProcessHashtagsService.new.call(status_with_audio) - - media_attachment = MediaAttachment.create_with( - account: showcase_account, - file: File.open('spec/fixtures/files/boop.ogg'), - description: 'Mastodon boop' - ).find_or_create_by!(id: 10_000_004) - status_with_sensitive_audio = Status.create_with( - text: "This is the same public status with an audio file and tags, but it is marked as sensitive. The attached picture has an alt text\n\n#Mastodon #English #Test", - ordered_media_attachment_ids: [media_attachment.id], - account: showcase_account, - visibility: :public, - sensitive: true, - thread: status_with_audio - ).find_or_create_by!(id: 10_000_008) - media_attachment.update(status_id: status_with_sensitive_audio.id) - ProcessHashtagsService.new.call(status_with_sensitive_audio) - - media_attachment = MediaAttachment.create_with( - account: showcase_account, - file: File.open('spec/fixtures/files/boop.ogg'), - description: 'Mastodon boop' - ).find_or_create_by!(id: 10_000_005) - status_with_cw_audio = Status.create_with( - text: "This is the same public status with an audio file and tags, but it is behind a CW. The attached picture has an alt text\n\n#Mastodon #English #Test", - spoiler_text: 'Mastodon boop', - ordered_media_attachment_ids: [media_attachment.id], - account: showcase_account, - visibility: :public, - sensitive: true, - thread: status_with_sensitive_audio - ).find_or_create_by!(id: 10_000_009) - media_attachment.update(status_id: status_with_cw_audio.id) - ProcessHashtagsService.new.call(status_with_cw_audio) - - media_attachments = [ - MediaAttachment.create_with( + media_attachment = MediaAttachment.create_with( account: showcase_account, file: File.open('spec/fixtures/files/600x400.png'), description: 'Mastodon logo' - ).find_or_create_by!(id: 10_000_006), - MediaAttachment.create_with( + ).find_or_create_by!(id: 10_000_000) + status_with_media = Status.create_with( + text: "This is a public status with a picture and tags. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test", + ordered_media_attachment_ids: [media_attachment.id], + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_004) + media_attachment.update(status_id: status_with_media.id) + ProcessHashtagsService.new.call(status_with_media) + + media_attachment = MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/600x400.png'), + description: 'Mastodon logo' + ).find_or_create_by!(id: 10_000_001) + status_with_sensitive_media = Status.create_with( + text: "This is the same public status with a picture and tags, but it is marked as sensitive. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test", + ordered_media_attachment_ids: [media_attachment.id], + account: showcase_account, + visibility: :public, + sensitive: true, + thread: status_with_media + ).find_or_create_by!(id: 10_000_005) + media_attachment.update(status_id: status_with_sensitive_media.id) + ProcessHashtagsService.new.call(status_with_sensitive_media) + + media_attachment = MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/600x400.png'), + description: 'Mastodon logo' + ).find_or_create_by!(id: 10_000_002) + status_with_cw_media = Status.create_with( + text: "This is the same public status with a picture and tags, but it is behind a CW. The attached picture has an alt text\n\n#Mastodon #Logo #English #Test", + spoiler_text: 'Mastodon logo', + ordered_media_attachment_ids: [media_attachment.id], + account: showcase_account, + visibility: :public, + sensitive: true, + thread: status_with_sensitive_media + ).find_or_create_by!(id: 10_000_006) + media_attachment.update(status_id: status_with_cw_media.id) + ProcessHashtagsService.new.call(status_with_cw_media) + + media_attachment = MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/boop.ogg'), + description: 'Mastodon boop' + ).find_or_create_by!(id: 10_000_003) + status_with_audio = Status.create_with( + text: "This is the same public status with an audio file and tags. The attached picture has an alt text\n\n#Mastodon #English #Test", + ordered_media_attachment_ids: [media_attachment.id], + account: showcase_account, + visibility: :public, + thread: status_with_cw_media + ).find_or_create_by!(id: 10_000_007) + media_attachment.update(status_id: status_with_audio.id) + ProcessHashtagsService.new.call(status_with_audio) + + media_attachment = MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/boop.ogg'), + description: 'Mastodon boop' + ).find_or_create_by!(id: 10_000_004) + status_with_sensitive_audio = Status.create_with( + text: "This is the same public status with an audio file and tags, but it is marked as sensitive. The attached picture has an alt text\n\n#Mastodon #English #Test", + ordered_media_attachment_ids: [media_attachment.id], + account: showcase_account, + visibility: :public, + sensitive: true, + thread: status_with_audio + ).find_or_create_by!(id: 10_000_008) + media_attachment.update(status_id: status_with_sensitive_audio.id) + ProcessHashtagsService.new.call(status_with_sensitive_audio) + + media_attachment = MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/boop.ogg'), + description: 'Mastodon boop' + ).find_or_create_by!(id: 10_000_005) + status_with_cw_audio = Status.create_with( + text: "This is the same public status with an audio file and tags, but it is behind a CW. The attached picture has an alt text\n\n#Mastodon #English #Test", + spoiler_text: 'Mastodon boop', + ordered_media_attachment_ids: [media_attachment.id], + account: showcase_account, + visibility: :public, + sensitive: true, + thread: status_with_sensitive_audio + ).find_or_create_by!(id: 10_000_009) + media_attachment.update(status_id: status_with_cw_audio.id) + ProcessHashtagsService.new.call(status_with_cw_audio) + + media_attachments = [ + MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/600x400.png'), + description: 'Mastodon logo' + ).find_or_create_by!(id: 10_000_006), + MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/attachment.jpg') + ).find_or_create_by!(id: 10_000_007), + MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/avatar-high.gif'), + description: 'Walking cartoon cat' + ).find_or_create_by!(id: 10_000_008), + MediaAttachment.create_with( + account: showcase_account, + file: File.open('spec/fixtures/files/text.png'), + description: 'Text saying “Hello Mastodon”' + ).find_or_create_by!(id: 10_000_009), + ] + status_with_multiple_attachments = Status.create_with( + text: "This is a post with multiple attachments, not all of which have a description\n\n#Mastodon #English #Test", + spoiler_text: 'multiple attachments', + ordered_media_attachment_ids: media_attachments.pluck(:id), + account: showcase_account, + visibility: :public, + sensitive: true, + thread: status_with_cw_audio + ).find_or_create_by!(id: 10_000_010) + media_attachments.each { |attachment| attachment.update!(status_id: status_with_multiple_attachments.id) } + ProcessHashtagsService.new.call(status_with_multiple_attachments) + + remote_account = Account.create_with( + username: 'fake.example', + domain: 'example.org', + uri: 'https://example.org/foo/bar', + url: 'https://example.org/foo/bar', + locked: true + ).find_or_create_by!(id: 10_000_001) + + remote_formatted_post = Status.create_with( + text: <<~HTML, +

This is a post with a variety of HTML in it

+

For instance, this text is bold and this one as well, while this text is stricken through and this one as well.

+
+

This thing, here, is a block quote
with some bold as well

+
    +
  • a list item
  • +
  • + and another with +
      +
    • nested
    • +
    • items!
    • +
    +
  • +
+
+
// And this is some code
+          // with two lines of comments
+          
+

And this is inline code

+

Finally, please observe this Ruby element: 明日 (Ashita)

+ HTML + account: remote_account, + uri: 'https://example.org/foo/bar/baz', + url: 'https://example.org/foo/bar/baz' + ).find_or_create_by!(id: 10_000_011) + Status.create_with(account: showcase_account, reblog: remote_formatted_post).find_or_create_by!(id: 10_000_012) + + unattached_quote_post = Status.create_with( + text: 'This is a quote of a post that does not exist', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_013) + Quote.create_with( + status: unattached_quote_post, + quoted_status: nil + ).find_or_create_by!(id: 10_000_000) + + self_quote = Status.create_with( + text: 'This is a quote of a public self-post', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_014) + Quote.create_with( + status: self_quote, + quoted_status: status_with_media, + state: :accepted + ).find_or_create_by!(id: 10_000_001) + + nested_self_quote = Status.create_with( + text: 'This is a quote of a public self-post which itself is a self-quote', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_015) + Quote.create_with( + status: nested_self_quote, + quoted_status: self_quote, + state: :accepted + ).find_or_create_by!(id: 10_000_002) + + recursive_self_quote = Status.create_with( + text: 'This is a recursive self-quote; no real reason for it to exist, but just to make sure we handle them gracefuly', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_016) + Quote.create_with( + status: recursive_self_quote, + quoted_status: recursive_self_quote, + state: :accepted + ).find_or_create_by!(id: 10_000_003) + + self_private_quote = Status.create_with( + text: 'This is a public post of a private self-post: the quoted post should not be visible to non-followers', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_017) + Quote.create_with( + status: self_private_quote, + quoted_status: private_mentionless, + state: :accepted + ).find_or_create_by!(id: 10_000_004) + + uncwed_quote_cwed = Status.create_with( + text: 'This is a quote without CW of a quoted post that has a CW', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_018) + Quote.create_with( + status: uncwed_quote_cwed, + quoted_status: public_self_reply_with_cw, + state: :accepted + ).find_or_create_by!(id: 10_000_005) + + cwed_quote_cwed = Status.create_with( + text: 'This is a quote with a CW of a quoted post that itself has a CW', + spoiler_text: 'Quote post with a CW', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_019) + Quote.create_with( + status: cwed_quote_cwed, + quoted_status: public_self_reply_with_cw, + state: :accepted + ).find_or_create_by!(id: 10_000_006) + + pending_quote_post = Status.create_with( + text: 'This quote post is pending', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_020) + Quote.create_with( + status: pending_quote_post, + quoted_status: remote_formatted_post, + activity_uri: 'https://foo/bar', + state: :pending + ).find_or_create_by!(id: 10_000_007) + + rejected_quote_post = Status.create_with( + text: 'This quote post is rejected', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_021) + Quote.create_with( + status: rejected_quote_post, + quoted_status: remote_formatted_post, + activity_uri: 'https://foo/foo', + state: :rejected + ).find_or_create_by!(id: 10_000_008) + + revoked_quote_post = Status.create_with( + text: 'This quote post is revoked', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_022) + Quote.create_with( + status: revoked_quote_post, + quoted_status: remote_formatted_post, + activity_uri: 'https://foo/baz', + state: :revoked + ).find_or_create_by!(id: 10_000_009) + + StatusPin.create_with(account: showcase_account, status: public_self_reply_with_cw).find_or_create_by!(id: 10_000_000) + StatusPin.create_with(account: showcase_account, status: private_mentionless).find_or_create_by!(id: 10_000_001) + + showcase_account.update!( + display_name: 'Mastodon test/showcase account', + note: 'Test account to showcase many Mastodon features. Most of its posts are public, but some are private!' + ) + + remote_quote = Status.create_with( + text: <<~HTML, +

This is a self-quote of a remote formatted post

+

RE: https://example.org/foo/bar/baz

+ HTML + account: remote_account, + uri: 'https://example.org/foo/bar/quote', + url: 'https://example.org/foo/bar/quote' + ).find_or_create_by!(id: 10_000_023) + Quote.create_with( + status: remote_quote, + quoted_status: remote_formatted_post, + state: :accepted + ).find_or_create_by!(id: 10_000_010) + Status.create_with( + account: showcase_account, + reblog: remote_quote + ).find_or_create_by!(id: 10_000_024) + + media_attachment = MediaAttachment.create_with( account: showcase_account, file: File.open('spec/fixtures/files/attachment.jpg') - ).find_or_create_by!(id: 10_000_007), - MediaAttachment.create_with( + ).find_or_create_by!(id: 10_000_010) + quote_post_with_media = Status.create_with( + text: "This is a status with a picture and tags which also quotes a status with a picture.\n\n#Mastodon #Test", + ordered_media_attachment_ids: [media_attachment.id], account: showcase_account, - file: File.open('spec/fixtures/files/avatar-high.gif'), - description: 'Walking cartoon cat' - ).find_or_create_by!(id: 10_000_008), - MediaAttachment.create_with( + visibility: :public + ).find_or_create_by!(id: 10_000_025) + media_attachment.update(status_id: quote_post_with_media.id) + ProcessHashtagsService.new.call(quote_post_with_media) + Quote.create_with( + status: quote_post_with_media, + quoted_status: status_with_media, + state: :accepted + ).find_or_create_by!(id: 10_000_011) + + showcase_sidekick_account = Account.create_with(username: 'showcase_sidekick').find_or_create_by!(id: 10_000_002) + sidekick_user = User.create_with( + account_id: showcase_sidekick_account.id, + agreement: true, + password: SecureRandom.hex, + email: ENV.fetch('TEST_DATA_SHOWCASE_SIDEKICK_EMAIL', 'showcase_sidekick@joinmastodon.org'), + confirmed_at: Time.now.utc, + approved: true, + bypass_registration_checks: true + ).find_or_create_by!(id: 10_000_001) + sidekick_user.mark_email_as_confirmed! + sidekick_user.approve! + + sidekick_post = Status.create_with( + text: 'This post only exists to be quoted.', + account: showcase_sidekick_account, + visibility: :public + ).find_or_create_by!(id: 10_000_026) + sidekick_quote_post = Status.create_with( + text: 'This is a quote of a different user.', account: showcase_account, - file: File.open('spec/fixtures/files/text.png'), - description: 'Text saying “Hello Mastodon”' - ).find_or_create_by!(id: 10_000_009), - ] - status_with_multiple_attachments = Status.create_with( - text: "This is a post with multiple attachments, not all of which have a description\n\n#Mastodon #English #Test", - spoiler_text: 'multiple attachments', - ordered_media_attachment_ids: media_attachments.pluck(:id), - account: showcase_account, - visibility: :public, - sensitive: true, - thread: status_with_cw_audio - ).find_or_create_by!(id: 10_000_010) - media_attachments.each { |attachment| attachment.update!(status_id: status_with_multiple_attachments.id) } - ProcessHashtagsService.new.call(status_with_multiple_attachments) + visibility: :public + ).find_or_create_by!(id: 10_000_027) + Quote.create_with( + status: sidekick_quote_post, + quoted_status: sidekick_post, + activity_uri: 'https://foo/cross-account-quote', + state: :accepted + ).find_or_create_by!(id: 10_000_012) - remote_account = Account.create_with( - username: 'fake.example', - domain: 'example.org', - uri: 'https://example.org/foo/bar', - url: 'https://example.org/foo/bar', - locked: true - ).find_or_create_by!(id: 10_000_001) - - remote_formatted_post = Status.create_with( - text: <<~HTML, -

This is a post with a variety of HTML in it

-

For instance, this text is bold and this one as well, while this text is stricken through and this one as well.

-
-

This thing, here, is a block quote
with some bold as well

-
    -
  • a list item
  • -
  • - and another with -
      -
    • nested
    • -
    • items!
    • -
    -
  • -
-
-
// And this is some code
-        // with two lines of comments
-        
-

And this is inline code

-

Finally, please observe this Ruby element: 明日 (Ashita)

- HTML - account: remote_account, - uri: 'https://example.org/foo/bar/baz', - url: 'https://example.org/foo/bar/baz' - ).find_or_create_by!(id: 10_000_011) - Status.create_with(account: showcase_account, reblog: remote_formatted_post).find_or_create_by!(id: 10_000_012) - - unattached_quote_post = Status.create_with( - text: 'This is a quote of a post that does not exist', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_013) - Quote.create_with( - status: unattached_quote_post, - quoted_status: nil - ).find_or_create_by!(id: 10_000_000) - - self_quote = Status.create_with( - text: 'This is a quote of a public self-post', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_014) - Quote.create_with( - status: self_quote, - quoted_status: status_with_media, - state: :accepted - ).find_or_create_by!(id: 10_000_001) - - nested_self_quote = Status.create_with( - text: 'This is a quote of a public self-post which itself is a self-quote', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_015) - Quote.create_with( - status: nested_self_quote, - quoted_status: self_quote, - state: :accepted - ).find_or_create_by!(id: 10_000_002) - - recursive_self_quote = Status.create_with( - text: 'This is a recursive self-quote; no real reason for it to exist, but just to make sure we handle them gracefuly', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_016) - Quote.create_with( - status: recursive_self_quote, - quoted_status: recursive_self_quote, - state: :accepted - ).find_or_create_by!(id: 10_000_003) - - self_private_quote = Status.create_with( - text: 'This is a public post of a private self-post: the quoted post should not be visible to non-followers', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_017) - Quote.create_with( - status: self_private_quote, - quoted_status: private_mentionless, - state: :accepted - ).find_or_create_by!(id: 10_000_004) - - uncwed_quote_cwed = Status.create_with( - text: 'This is a quote without CW of a quoted post that has a CW', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_018) - Quote.create_with( - status: uncwed_quote_cwed, - quoted_status: public_self_reply_with_cw, - state: :accepted - ).find_or_create_by!(id: 10_000_005) - - cwed_quote_cwed = Status.create_with( - text: 'This is a quote with a CW of a quoted post that itself has a CW', - spoiler_text: 'Quote post with a CW', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_019) - Quote.create_with( - status: cwed_quote_cwed, - quoted_status: public_self_reply_with_cw, - state: :accepted - ).find_or_create_by!(id: 10_000_006) - - pending_quote_post = Status.create_with( - text: 'This quote post is pending', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_020) - Quote.create_with( - status: pending_quote_post, - quoted_status: remote_formatted_post, - activity_uri: 'https://foo/bar', - state: :pending - ).find_or_create_by!(id: 10_000_007) - - rejected_quote_post = Status.create_with( - text: 'This quote post is rejected', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_021) - Quote.create_with( - status: rejected_quote_post, - quoted_status: remote_formatted_post, - activity_uri: 'https://foo/foo', - state: :rejected - ).find_or_create_by!(id: 10_000_008) - - revoked_quote_post = Status.create_with( - text: 'This quote post is revoked', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_022) - Quote.create_with( - status: revoked_quote_post, - quoted_status: remote_formatted_post, - activity_uri: 'https://foo/baz', - state: :revoked - ).find_or_create_by!(id: 10_000_009) - - StatusPin.create_with(account: showcase_account, status: public_self_reply_with_cw).find_or_create_by!(id: 10_000_000) - StatusPin.create_with(account: showcase_account, status: private_mentionless).find_or_create_by!(id: 10_000_001) - - showcase_account.update!( - display_name: 'Mastodon test/showcase account', - note: 'Test account to showcase many Mastodon features. Most of its posts are public, but some are private!' - ) - - remote_quote = Status.create_with( - text: <<~HTML, -

This is a self-quote of a remote formatted post

-

RE: https://example.org/foo/bar/baz

- HTML - account: remote_account, - uri: 'https://example.org/foo/bar/quote', - url: 'https://example.org/foo/bar/quote' - ).find_or_create_by!(id: 10_000_023) - Quote.create_with( - status: remote_quote, - quoted_status: remote_formatted_post, - state: :accepted - ).find_or_create_by!(id: 10_000_010) - Status.create_with( - account: showcase_account, - reblog: remote_quote - ).find_or_create_by!(id: 10_000_024) - - media_attachment = MediaAttachment.create_with( - account: showcase_account, - file: File.open('spec/fixtures/files/attachment.jpg') - ).find_or_create_by!(id: 10_000_010) - quote_post_with_media = Status.create_with( - text: "This is a status with a picture and tags which also quotes a status with a picture.\n\n#Mastodon #Test", - ordered_media_attachment_ids: [media_attachment.id], - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_025) - media_attachment.update(status_id: quote_post_with_media.id) - ProcessHashtagsService.new.call(quote_post_with_media) - Quote.create_with( - status: quote_post_with_media, - quoted_status: status_with_media, - state: :accepted - ).find_or_create_by!(id: 10_000_011) - - showcase_sidekick_account = Account.create_with(username: 'showcase_sidekick').find_or_create_by!(id: 10_000_002) - sidekick_user = User.create_with( - account_id: showcase_sidekick_account.id, - agreement: true, - password: SecureRandom.hex, - email: ENV.fetch('TEST_DATA_SHOWCASE_SIDEKICK_EMAIL', 'showcase_sidekick@joinmastodon.org'), - confirmed_at: Time.now.utc, - approved: true, - bypass_registration_checks: true - ).find_or_create_by!(id: 10_000_001) - sidekick_user.mark_email_as_confirmed! - sidekick_user.approve! - - sidekick_post = Status.create_with( - text: 'This post only exists to be quoted.', - account: showcase_sidekick_account, - visibility: :public - ).find_or_create_by!(id: 10_000_026) - sidekick_quote_post = Status.create_with( - text: 'This is a quote of a different user.', - account: showcase_account, - visibility: :public - ).find_or_create_by!(id: 10_000_027) - Quote.create_with( - status: sidekick_quote_post, - quoted_status: sidekick_post, - activity_uri: 'https://foo/cross-account-quote', - state: :accepted - ).find_or_create_by!(id: 10_000_012) + quoted = Status.create_with( + text: 'This should have a preview card: https://joinmastodon.org', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_028) + LinkCrawlWorker.perform_async(10_000_028) + quoting = Status.create_with( + text: 'This should quote a post with a preview card', + account: showcase_account, + visibility: :public + ).find_or_create_by!(id: 10_000_029) + Quote.create_with( + status: quoting, + quoted_status: quoted, + state: :accepted + ).find_or_create_by!(id: 10_000_013) + end end end diff --git a/package.json b/package.json index 1d1952e262f..6109b42875b 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "cocoon-js-vanilla": "^1.5.1", "color-blend": "^4.0.0", "core-js": "^3.30.2", - "cross-env": "^7.0.3", + "cross-env": "^10.0.0", + "debug": "^4.4.1", "detect-passive-events": "^2.0.3", "emoji-mart": "npm:emoji-mart-lazyload@latest", "emojibase": "^16.0.0", @@ -137,6 +138,7 @@ "@storybook/react-vite": "^9.0.4", "@testing-library/dom": "^10.2.0", "@testing-library/react": "^16.0.0", + "@types/debug": "^4", "@types/emoji-mart": "3.0.14", "@types/escape-html": "^1.0.2", "@types/hoist-non-react-statics": "^3.3.1", @@ -167,13 +169,14 @@ "eslint": "^9.23.0", "eslint-import-resolver-typescript": "^4.2.5", "eslint-plugin-formatjs": "^5.3.1", - "eslint-plugin-import": "~2.31.0", - "eslint-plugin-jsdoc": "^51.0.0", + "eslint-plugin-import": "~2.32.0", + "eslint-plugin-jsdoc": "^52.0.0", "eslint-plugin-jsx-a11y": "~6.10.2", "eslint-plugin-promise": "~7.2.1", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-storybook": "^9.0.4", + "fake-indexeddb": "^6.0.1", "globals": "^16.0.0", "husky": "^9.0.11", "lint-staged": "^16.0.0", diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb deleted file mode 100644 index 1e01709262b..00000000000 --- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Api::Web::PushSubscriptionsController do - render_views - - let(:user) { Fabricate(:user) } - - let(:create_payload) do - { - subscription: { - endpoint: 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX', - keys: { - p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', - auth: 'eH_C8rq2raXqlcBVDa1gLg==', - }, - standard: standard, - }, - } - end - - let(:alerts_payload) do - { - data: { - policy: 'all', - - alerts: { - follow: true, - follow_request: false, - favourite: false, - reblog: true, - mention: false, - poll: true, - status: false, - }, - }, - } - end - let(:standard) { '1' } - - before do - sign_in(user) - - stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200) - end - - describe 'POST #create' do - it 'saves push subscriptions' do - post :create, format: :json, params: create_payload - - expect(response).to have_http_status(200) - - user.reload - - expect(created_push_subscription) - .to have_attributes( - endpoint: eq(create_payload[:subscription][:endpoint]), - key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]), - key_auth: eq(create_payload[:subscription][:keys][:auth]) - ) - .and be_standard - expect(user.session_activations.first.web_push_subscription).to eq(created_push_subscription) - end - - context 'when standard is provided as false value' do - let(:standard) { '0' } - - it 'saves push subscription with standard as false' do - post :create, format: :json, params: create_payload - - expect(created_push_subscription) - .to_not be_standard - end - end - - context 'with a user who has a session with a prior subscription' do - let!(:prior_subscription) { Fabricate(:web_push_subscription, session_activation: user.session_activations.last) } - - it 'destroys prior subscription when creating new one' do - post :create, format: :json, params: create_payload - - expect(response).to have_http_status(200) - expect { prior_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with initial data' do - it 'saves alert settings' do - post :create, format: :json, params: create_payload.merge(alerts_payload) - - expect(response).to have_http_status(200) - - expect(created_push_subscription.data['policy']).to eq 'all' - - %w(follow follow_request favourite reblog mention poll status).each do |type| - expect(created_push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) - end - end - end - end - - describe 'PUT #update' do - it 'changes alert settings' do - post :create, format: :json, params: create_payload - - expect(response).to have_http_status(200) - - alerts_payload[:id] = created_push_subscription.id - - put :update, format: :json, params: alerts_payload - - expect(created_push_subscription.data['policy']).to eq 'all' - - %w(follow follow_request favourite reblog mention poll status).each do |type| - expect(created_push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) - end - end - end - - def created_push_subscription - Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) - end -end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index cd4181a00da..ea182a047f1 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -219,16 +219,16 @@ RSpec.describe ApplicationController do it_behaves_like 'error response', 410 end - describe 'unprocessable_entity' do + describe 'unprocessable_content' do controller do - def route_unprocessable_entity - unprocessable_entity + def route_unprocessable_content + unprocessable_content end end subject do - routes.draw { get 'route_unprocessable_entity' => 'anonymous#route_unprocessable_entity' } - get 'route_unprocessable_entity' + routes.draw { get 'route_unprocessable_content' => 'anonymous#route_unprocessable_content' } + get 'route_unprocessable_content' end it_behaves_like 'error response', 422 diff --git a/spec/controllers/concerns/accountable_concern_spec.rb b/spec/controllers/concerns/accountable_concern_spec.rb index cd06d872bb5..e68090fdc2f 100644 --- a/spec/controllers/concerns/accountable_concern_spec.rb +++ b/spec/controllers/concerns/accountable_concern_spec.rb @@ -6,6 +6,7 @@ RSpec.describe AccountableConcern do let(:hoge_class) do Class.new do include AccountableConcern + attr_reader :current_account def initialize(current_account) diff --git a/spec/fabricators/username_block_fabricator.rb b/spec/fabricators/username_block_fabricator.rb new file mode 100644 index 00000000000..edca5419aba --- /dev/null +++ b/spec/fabricators/username_block_fabricator.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Fabricator(:username_block) do + username { sequence(:email) { |i| "#{i}#{Faker::Internet.username}" } } + exact false + allow_with_approval false +end diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb index c3fbff4e8bb..a056eae364d 100644 --- a/spec/helpers/home_helper_spec.rb +++ b/spec/helpers/home_helper_spec.rb @@ -41,57 +41,19 @@ RSpec.describe HomeHelper do end end - describe 'obscured_counter' do - context 'with a value of less than zero' do - let(:count) { -10 } + describe 'field_verified_class' do + subject { helper.field_verified_class(verified) } - it 'returns the correct string' do - expect(helper.obscured_counter(count)).to eq '0' - end - end - - context 'with a value of zero' do - let(:count) { 0 } - - it 'returns the correct string' do - expect(helper.obscured_counter(count)).to eq '0' - end - end - - context 'with a value of one' do - let(:count) { 1 } - - it 'returns the correct string' do - expect(helper.obscured_counter(count)).to eq '1' - end - end - - context 'with a value of more than one' do - let(:count) { 10 } - - it 'returns the correct string' do - expect(helper.obscured_counter(count)).to eq '1+' - end - end - end - - describe 'custom_field_classes' do context 'with a verified field' do - let(:field) { instance_double(Account::Field, verified?: true) } + let(:verified) { true } - it 'returns verified string' do - result = helper.custom_field_classes(field) - expect(result).to eq 'verified' - end + it { is_expected.to eq('verified') } end context 'with a non-verified field' do - let(:field) { instance_double(Account::Field, verified?: false) } + let(:verified) { false } - it 'returns verified string' do - result = helper.custom_field_classes(field) - expect(result).to eq 'emojify' - end + it { is_expected.to eq('emojify') } end end diff --git a/spec/helpers/json_ld_helper_spec.rb b/spec/helpers/json_ld_helper_spec.rb index d76c5167a7d..f216588d978 100644 --- a/spec/helpers/json_ld_helper_spec.rb +++ b/spec/helpers/json_ld_helper_spec.rb @@ -180,6 +180,14 @@ RSpec.describe JsonLdHelper do expect(compacted.dig('object', 'tag', 0, 'href')).to eq ['foo'] expect(safe_for_forwarding?(json, compacted)).to be true end + + context 'when array size mismatch exists' do + subject { helper.patch_for_forwarding!(json, alternate) } + + let(:alternate) { json.merge('to' => %w(one two three)) } + + it { is_expected.to be_nil } + end end describe 'safe_for_forwarding?' do diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb index 6d7c05e6165..615287389c3 100644 --- a/spec/lib/activitypub/activity/accept_spec.rb +++ b/spec/lib/activitypub/activity/accept_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Accept do - let(:sender) { Fabricate(:account) } + let(:sender) { Fabricate(:account, domain: 'example.com') } let(:recipient) { Fabricate(:account) } describe '#perform' do @@ -48,5 +48,128 @@ RSpec.describe ActivityPub::Activity::Accept do end end end + + context 'with a QuoteRequest' do + let(:status) { Fabricate(:status, account: recipient) } + let(:quoted_status) { Fabricate(:status, account: sender) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status) } + let(:approval_uri) { "https://#{sender.domain}/approvals/1" } + + let(:json) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest', + }, + ], + id: 'foo', + type: 'Accept', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: { + id: quote.activity_uri, + type: 'QuoteRequest', + actor: ActivityPub::TagManager.instance.uri_for(recipient), + object: ActivityPub::TagManager.instance.uri_for(quoted_status), + instrument: ActivityPub::TagManager.instance.uri_for(status), + }, + result: approval_uri, + }.with_indifferent_access + end + + it 'marks the quote as approved and distribute an update' do + expect { subject.perform } + .to change { quote.reload.accepted? }.from(false).to(true) + .and change { quote.reload.approval_uri }.to(approval_uri) + expect(DistributionWorker) + .to have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to have_enqueued_sidekiq_job(status.id, { 'updated_at' => be_a(String) }) + end + + context 'when the quoted status is not from the sender of the Accept' do + let(:quoted_status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) } + + it 'does not mark the quote as approved and does not distribute an update' do + expect { subject.perform } + .to not_change { quote.reload.accepted? }.from(false) + .and not_change { quote.reload.approval_uri }.from(nil) + expect(DistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, anything) + end + end + + context 'when the quoting status is from an unrelated user' do + let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'foobar.com')) } + + it 'does not mark the quote as approved and does not distribute an update' do + expect { subject.perform } + .to not_change { quote.reload.accepted? }.from(false) + .and not_change { quote.reload.approval_uri }.from(nil) + expect(DistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, anything) + end + end + + context 'when approval_uri is missing' do + let(:approval_uri) { nil } + + it 'does not mark the quote as approved and does not distribute an update' do + expect { subject.perform } + .to not_change { quote.reload.accepted? }.from(false) + .and not_change { quote.reload.approval_uri }.from(nil) + expect(DistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, anything) + end + end + + context 'when the QuoteRequest is referenced by its identifier' do + let(:json) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest', + }, + ], + id: 'foo', + type: 'Accept', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: quote.activity_uri, + result: approval_uri, + }.with_indifferent_access + end + + it 'marks the quote as approved and distribute an update' do + expect { subject.perform } + .to change { quote.reload.accepted? }.from(false).to(true) + .and change { quote.reload.approval_uri }.to(approval_uri) + expect(DistributionWorker) + .to have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to have_enqueued_sidekiq_job(status.id, { 'updated_at' => be_a(String) }) + end + + context 'when approval_uri is missing' do + let(:approval_uri) { nil } + + it 'does not mark the quote as approved and does not distribute an update' do + expect { subject.perform } + .to not_change { quote.reload.accepted? }.from(false) + .and not_change { quote.reload.approval_uri }.from(nil) + expect(DistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, { 'update' => true }) + expect(ActivityPub::StatusUpdateDistributionWorker) + .to_not have_enqueued_sidekiq_job(status.id, anything) + end + end + end + end end end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 74c9f107187..cdd5cb3194d 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -888,7 +888,7 @@ RSpec.describe ActivityPub::Activity::Create do end context 'with an unverifiable quote of a known post' do - let(:quoted_status) { Fabricate(:status) } + let(:quoted_status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) } let(:object_json) do build_object( diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index dac0b438cbd..64627cbdfbe 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -2,12 +2,13 @@ require 'rails_helper' -RSpec.describe ActivityPub::Activity::QuoteRequest do +RSpec.describe ActivityPub::Activity::QuoteRequest, feature: :outgoing_quotes do let(:sender) { Fabricate(:account, domain: 'example.com') } let(:recipient) { Fabricate(:account) } let(:quoted_post) { Fabricate(:status, account: recipient) } let(:request_uri) { 'https://example.com/missing-ui' } let(:quoted_uri) { ActivityPub::TagManager.instance.uri_for(quoted_post) } + let(:instrument) { 'https://example.com/unknown-status' } let(:json) do { @@ -21,8 +22,30 @@ RSpec.describe ActivityPub::Activity::QuoteRequest do type: 'QuoteRequest', actor: ActivityPub::TagManager.instance.uri_for(sender), object: quoted_uri, - instrument: 'https://example.com/unknown-status', - }.with_indifferent_access + instrument: instrument, + }.deep_stringify_keys + end + + let(:status_json) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'https://example.com/unknown-status', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + quote: ActivityPub::TagManager.instance.uri_for(quoted_post), + attributedTo: ActivityPub::TagManager.instance.uri_for(sender), + }.deep_stringify_keys end describe '#perform' do @@ -47,5 +70,53 @@ RSpec.describe ActivityPub::Activity::QuoteRequest do end, recipient.id, sender.inbox_url) end end + + context 'when trying to quote an unquotable local status with an inlined instrument' do + let(:instrument) { status_json.without('@context') } + + it 'sends a Reject activity' do + expect { subject.perform } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker) + .with(satisfying do |body| + outgoing_json = Oj.load(body) + outgoing_json['type'] == 'Reject' && json['instrument']['id'] == outgoing_json['object']['instrument'] && %w(type id actor object).all? { |key| json[key] == outgoing_json['object'][key] } + end, recipient.id, sender.inbox_url) + end + end + + context 'when trying to quote a quotable local status' do + before do + stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: Oj.dump(status_json), headers: { 'Content-Type': 'application/activity+json' }) + quoted_post.update(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + end + + it 'accepts the quote and sends an Accept activity' do + expect { subject.perform } + .to change { quoted_post.reload.quotes.accepted.count }.by(1) + .and enqueue_sidekiq_job(ActivityPub::DeliveryWorker) + .with(satisfying do |body| + outgoing_json = Oj.load(body) + outgoing_json['type'] == 'Accept' && %w(type id actor object instrument).all? { |key| json[key] == outgoing_json['object'][key] } + end, recipient.id, sender.inbox_url) + end + end + + context 'when trying to quote a quotable local status with an inlined instrument' do + let(:instrument) { status_json.without('@context') } + + before do + quoted_post.update(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + end + + it 'accepts the quote and sends an Accept activity' do + expect { subject.perform } + .to change { quoted_post.reload.quotes.accepted.count }.by(1) + .and enqueue_sidekiq_job(ActivityPub::DeliveryWorker) + .with(satisfying do |body| + outgoing_json = Oj.load(body) + outgoing_json['type'] == 'Accept' && json['instrument']['id'] == outgoing_json['object']['instrument'] && %w(type id actor object).all? { |key| json[key] == outgoing_json['object'][key] } + end, recipient.id, sender.inbox_url) + end + end end end diff --git a/spec/lib/activitypub/activity/reject_spec.rb b/spec/lib/activitypub/activity/reject_spec.rb index 1afb0cd4033..ee8557f1239 100644 --- a/spec/lib/activitypub/activity/reject_spec.rb +++ b/spec/lib/activitypub/activity/reject_spec.rb @@ -125,5 +125,27 @@ RSpec.describe ActivityPub::Activity::Reject do expect(relay.reload.rejected?).to be true end end + + context 'with a QuoteRequest' do + let(:status) { Fabricate(:status, account: recipient) } + let(:quoted_status) { Fabricate(:status, account: sender) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'https://abc-123/456') } + let(:approval_uri) { "https://#{sender.domain}/approvals/1" } + + let(:object_json) do + { + id: 'https://abc-123/456', + type: 'QuoteRequest', + actor: ActivityPub::TagManager.instance.uri_for(recipient), + object: ActivityPub::TagManager.instance.uri_for(quoted_status), + instrument: ActivityPub::TagManager.instance.uri_for(status), + }.with_indifferent_access + end + + it 'marks the quote as rejected' do + expect { subject.perform } + .to change { quote.reload.rejected? }.from(false).to(true) + end + end end end diff --git a/spec/lib/delivery_failure_tracker_spec.rb b/spec/lib/delivery_failure_tracker_spec.rb index 34912c8133e..6935dbccb43 100644 --- a/spec/lib/delivery_failure_tracker_spec.rb +++ b/spec/lib/delivery_failure_tracker_spec.rb @@ -3,37 +3,101 @@ require 'rails_helper' RSpec.describe DeliveryFailureTracker do - subject { described_class.new('http://example.com/inbox') } + context 'with the default resolution of :days' do + subject { described_class.new('http://example.com/inbox') } - describe '#track_success!' do - before do - subject.track_failure! - subject.track_success! + describe '#track_success!' do + before do + track_failure(7, :days) + subject.track_success! + end + + it 'marks URL as available again' do + expect(described_class.available?('http://example.com/inbox')).to be true + end + + it 'resets days to 0' do + expect(subject.days).to be_zero + end end - it 'marks URL as available again' do - expect(described_class.available?('http://example.com/inbox')).to be true + describe '#track_failure!' do + it 'marks URL as unavailable after 7 days of being called' do + track_failure(7, :days) + + expect(subject.days).to eq 7 + expect(described_class.available?('http://example.com/inbox')).to be false + end + + it 'repeated calls on the same day do not count' do + subject.track_failure! + subject.track_failure! + + expect(subject.days).to eq 1 + end end - it 'resets days to 0' do - expect(subject.days).to be_zero + describe '#exhausted_deliveries_days' do + it 'returns the days on which failures were recorded' do + track_failure(3, :days) + + expect(subject.exhausted_deliveries_days).to contain_exactly(3.days.ago.to_date, 2.days.ago.to_date, Date.yesterday) + end end end - describe '#track_failure!' do - it 'marks URL as unavailable after 7 days of being called' do - 6.times { |i| redis.sadd('exhausted_deliveries:example.com', i) } - subject.track_failure! + context 'with a resolution of :minutes' do + subject { described_class.new('http://example.com/inbox', resolution: :minutes) } - expect(subject.days).to eq 7 - expect(described_class.available?('http://example.com/inbox')).to be false + describe '#track_success!' do + before do + track_failure(5, :minutes) + subject.track_success! + end + + it 'marks URL as available again' do + expect(described_class.available?('http://example.com/inbox')).to be true + end + + it 'resets failures to 0' do + expect(subject.failures).to be_zero + end end - it 'repeated calls on the same day do not count' do - subject.track_failure! - subject.track_failure! + describe '#track_failure!' do + it 'marks URL as unavailable after 5 minutes of being called' do + track_failure(5, :minutes) - expect(subject.days).to eq 1 + expect(subject.failures).to eq 5 + expect(described_class.available?('http://example.com/inbox')).to be false + end + + it 'repeated calls within the same minute do not count' do + freeze_time + subject.track_failure! + subject.track_failure! + + expect(subject.failures).to eq 1 + end + end + + describe '#exhausted_deliveries_days' do + it 'returns the days on which failures were recorded' do + # Make sure this does not accidentally span two days when run + # around midnight + travel_to Time.zone.now.change(hour: 10) + track_failure(3, :minutes) + + expect(subject.exhausted_deliveries_days).to contain_exactly(Time.zone.today) + end + end + + describe '#days' do + it 'raises due to wrong resolution' do + assert_raises TypeError do + subject.days + end + end end end @@ -60,4 +124,12 @@ RSpec.describe DeliveryFailureTracker do expect(described_class.available?('http://foo.bar/inbox')).to be true end end + + def track_failure(times, unit) + times.times do + travel_to 1.send(unit).ago + subject.track_failure! + end + travel_back + end end diff --git a/spec/lib/fasp/request_spec.rb b/spec/lib/fasp/request_spec.rb index 9b354c8f44b..380b7951242 100644 --- a/spec/lib/fasp/request_spec.rb +++ b/spec/lib/fasp/request_spec.rb @@ -27,6 +27,14 @@ RSpec.describe Fasp::Request do 'Signature-Input' => /.+/, }) end + + it 'tracks that a successful connection was made' do + provider.delivery_failure_tracker.track_failure! + + expect do + subject.send(method, '/test_path') + end.to change(provider.delivery_failure_tracker, :failures).from(1).to(0) + end end context 'when the response is not signed' do @@ -55,6 +63,21 @@ RSpec.describe Fasp::Request do end end end + + context 'when the request raises an error' do + before do + stub_request(method, 'https://reqprov.example.com/fasp/test_path') + .to_raise(HTTP::ConnectionError) + end + + it "records the failure using the provider's delivery failure tracker" do + expect do + subject.send(method, '/test_path') + end.to raise_error(HTTP::ConnectionError) + + expect(provider.delivery_failure_tracker.failures).to eq 1 + end + end end describe '#get' do diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index e56393da1dd..f450997976a 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -28,6 +28,18 @@ RSpec.describe StatusCacheHydrator do end end + context 'when handling a status with a quote policy', feature: :outgoing_quotes do + let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } + + before do + account.follow!(status.account) + end + + it 'renders the same attributes as a full render' do + expect(subject).to eql(compare_to_hash) + end + end + context 'when handling a filtered status' do let(:status) { Fabricate(:status, text: 'this toot is about that banned word') } diff --git a/spec/lib/webfinger_spec.rb b/spec/lib/webfinger_spec.rb index 5015deac7ff..e214a03536b 100644 --- a/spec/lib/webfinger_spec.rb +++ b/spec/lib/webfinger_spec.rb @@ -4,15 +4,15 @@ require 'rails_helper' RSpec.describe Webfinger do describe 'self link' do + subject { described_class.new('acct:alice@example.com').perform } + context 'when self link is specified with type application/activity+json' do let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice', type: 'application/activity+json' }] } } it 'correctly parses the response' do stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - response = described_class.new('acct:alice@example.com').perform - - expect(response.self_link_href).to eq 'https://example.com/alice' + expect(subject.self_link_href).to eq 'https://example.com/alice' end end @@ -22,9 +22,7 @@ RSpec.describe Webfinger do it 'correctly parses the response' do stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - response = described_class.new('acct:alice@example.com').perform - - expect(response.self_link_href).to eq 'https://example.com/alice' + expect(subject.self_link_href).to eq 'https://example.com/alice' end end @@ -34,7 +32,45 @@ RSpec.describe Webfinger do it 'raises an error' do stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) - expect { described_class.new('acct:alice@example.com').perform }.to raise_error(Webfinger::Error) + expect { subject } + .to raise_error(Webfinger::Error) + end + end + + context 'when webfinger fails and host meta is used' do + before { stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(status: 404) } + + context 'when host meta succeeds' do + let(:host_meta) do + <<~XML + + + + + XML + end + let(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice-from-NS', type: 'application/activity+json' }] } } + + before do + stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(body: host_meta, headers: { 'Content-Type': 'application/jrd+json' }) + stub_request(:get, 'https://example.com/.well-known/nonStandardWebfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) + end + + it 'uses host meta details' do + expect(subject.self_link_href) + .to eq 'https://example.com/alice-from-NS' + end + end + + context 'when host meta fails' do + before do + stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(status: 500) + end + + it 'raises error' do + expect { subject } + .to raise_error(Webfinger::Error) + end end end end diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index 25eb4ada263..b88277367dd 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -51,6 +51,27 @@ RSpec.describe NotificationMailer do it_behaves_like 'delivery without status' end + describe 'quote' do + let(:quote) { Fabricate(:quote, state: :accepted, status: foreign_status, quoted_status: own_status) } + let(:notification) { Notification.create!(account: receiver.account, activity: quote) } + let(:mail) { prepared_mailer_for(own_status.account).quote } + + it_behaves_like 'localized subject', 'notification_mailer.quote.subject', name: 'bob' + + it 'renders the email' do + expect(mail) + .to be_present + .and(have_subject('bob quoted your post')) + .and(have_body_text('Your post was quoted by bob')) + .and(have_body_text('The body of the foreign status')) + .and have_thread_headers + .and have_standard_headers('quote').for(receiver) + end + + it_behaves_like 'delivery to non functional user' + it_behaves_like 'delivery without status' + end + describe 'follow' do let(:follow) { sender.follow!(receiver.account) } let(:notification) { Notification.create!(account: receiver.account, activity: follow) } diff --git a/spec/mailers/previews/notification_mailer_preview.rb b/spec/mailers/previews/notification_mailer_preview.rb index a63c20c27c5..ae2d6802bcf 100644 --- a/spec/mailers/previews/notification_mailer_preview.rb +++ b/spec/mailers/previews/notification_mailer_preview.rb @@ -33,6 +33,12 @@ class NotificationMailerPreview < ActionMailer::Preview mailer_for(activity.reblog.account, activity).reblog end + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/quote + def quote + activity = Quote.first + mailer_for(activity.quoted_account, activity).quote + end + private def mailer_for(account, activity) diff --git a/spec/models/account_suggestions/friends_of_friends_source_spec.rb b/spec/models/account_suggestions/friends_of_friends_source_spec.rb index 9daaa233bfd..af1e6e98896 100644 --- a/spec/models/account_suggestions/friends_of_friends_source_spec.rb +++ b/spec/models/account_suggestions/friends_of_friends_source_spec.rb @@ -16,10 +16,12 @@ RSpec.describe AccountSuggestions::FriendsOfFriendsSource do let!(:jerk) { Fabricate(:account, discoverable: true, hide_collections: false) } let!(:larry) { Fabricate(:account, discoverable: true, hide_collections: false) } let!(:morty) { Fabricate(:account, discoverable: true, hide_collections: false, memorial: true) } + let!(:joyce) { Fabricate(:account, discoverable: true, hide_collections: false) } context 'with follows and blocks' do before do bob.block!(jerk) + bob.request_follow!(joyce) FollowRecommendationMute.create!(account: bob, target_account: neil) # bob follows eugen, alice and larry @@ -28,8 +30,8 @@ RSpec.describe AccountSuggestions::FriendsOfFriendsSource do # alice follows eve and mallory [john, mallory].each { |account| alice.follow!(account) } - # eugen follows eve, john, jerk, larry, neil and morty - [eve, mallory, jerk, larry, neil, morty].each { |account| eugen.follow!(account) } + # eugen follows eve, john, jerk, larry, neil, morty and joyce + [eve, mallory, jerk, larry, neil, morty, joyce].each { |account| eugen.follow!(account) } end it 'returns eligible accounts', :aggregate_failures do @@ -55,6 +57,9 @@ RSpec.describe AccountSuggestions::FriendsOfFriendsSource do # morty is not included because his account is in memoriam expect(results).to_not include([morty.id, :friends_of_friends]) + + # joyce is not included because there is already a pending follow request + expect(results).to_not include([joyce.id, :friends_of_friends]) end end diff --git a/spec/models/announcement_reaction_spec.rb b/spec/models/announcement_reaction_spec.rb new file mode 100644 index 00000000000..e02a8dcd069 --- /dev/null +++ b/spec/models/announcement_reaction_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AnnouncementReaction do + describe 'Associations' do + it { is_expected.to belong_to(:account) } + it { is_expected.to belong_to(:announcement).inverse_of(:announcement_reactions) } + it { is_expected.to belong_to(:custom_emoji).optional } + end + + describe 'Validations' do + subject { Fabricate.build :announcement_reaction } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_values('😀').for(:name) } + it { is_expected.to_not allow_values('INVALID').for(:name) } + + context 'when reaction limit is reached' do + subject { Fabricate.build :announcement_reaction, announcement: announcement_reaction.announcement } + + let(:announcement_reaction) { Fabricate :announcement_reaction, name: '😊' } + + before { stub_const 'ReactionValidator::LIMIT', 1 } + + it { is_expected.to_not allow_values('😀').for(:name).against(:base) } + end + end + + describe 'Callbacks' do + describe 'Setting custom emoji association' do + subject { Fabricate.build :announcement_reaction, name: } + + context 'when name is missing' do + let(:name) { '' } + + it 'does not set association' do + expect { subject.valid? } + .to not_change(subject, :custom_emoji).from(be_blank) + end + end + + context 'when name matches a custom emoji shortcode' do + let(:name) { 'custom' } + let!(:custom_emoji) { Fabricate :custom_emoji, shortcode: 'custom' } + + it 'sets association' do + expect { subject.valid? } + .to change(subject, :custom_emoji).from(be_blank).to(custom_emoji) + end + end + + context 'when name does not match a custom emoji' do + let(:name) { 'custom' } + + it 'does not set association' do + expect { subject.valid? } + .to not_change(subject, :custom_emoji).from(be_blank) + end + end + end + end +end diff --git a/spec/models/canonical_email_block_spec.rb b/spec/models/canonical_email_block_spec.rb index c63483f968f..f531fd214e0 100644 --- a/spec/models/canonical_email_block_spec.rb +++ b/spec/models/canonical_email_block_spec.rb @@ -3,6 +3,44 @@ require 'rails_helper' RSpec.describe CanonicalEmailBlock do + describe 'Associations' do + it { is_expected.to belong_to(:reference_account).class_name('Account').optional } + end + + describe 'Normalizations' do + describe 'email' do + it { is_expected.to normalize(:email).from('TEST@HOST.EXAMPLE').to('test@host.example') } + it { is_expected.to normalize(:email).from('test+more@host.example').to('test@host.example') } + it { is_expected.to normalize(:email).from('test.user@host.example').to('testuser@host.example') } + end + end + + describe 'Scopes' do + describe '.matching_email' do + subject { described_class.matching_email(email) } + + let!(:block) { Fabricate :canonical_email_block, email: 'test@example.com' } + + context 'when email is exact match' do + let(:email) { 'test@example.com' } + + it { is_expected.to contain_exactly(block) } + end + + context 'when email does not match' do + let(:email) { 'test@example.ORG' } + + it { is_expected.to be_empty } + end + + context 'when email is different but normalizes to same hash' do + let(:email) { 'te.st+more@EXAMPLE.com' } + + it { is_expected.to contain_exactly(block) } + end + end + end + describe '#email=' do let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' } diff --git a/spec/models/concerns/status/interaction_policy_concern_spec.rb b/spec/models/concerns/status/interaction_policy_concern_spec.rb new file mode 100644 index 00000000000..af42f2bba3d --- /dev/null +++ b/spec/models/concerns/status/interaction_policy_concern_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Status::InteractionPolicyConcern do + let(:status) { Fabricate(:status, quote_approval_policy: (0b0101 << 16) | 0b0010) } + + describe '#quote_policy_as_keys' do + it 'returns the expected values' do + expect(status.quote_policy_as_keys(:automatic)).to eq ['unknown', 'followers'] + expect(status.quote_policy_as_keys(:manual)).to eq ['public'] + end + end + + describe '#quote_policy_for_account' do + let(:account) { Fabricate(:account) } + + context 'when the account is not following the user' do + it 'returns :manual because of the public entry in the manual policy' do + expect(status.quote_policy_for_account(account)).to eq :manual + end + end + + context 'when the account is following the user' do + before do + account.follow!(status.account) + end + + it 'returns :automatic because of the followers entry in the automatic policy' do + expect(status.quote_policy_for_account(account)).to eq :automatic + end + end + + context 'when the account falls into the unknown bucket' do + let(:status) { Fabricate(:status, quote_approval_policy: (0b0001 << 16) | 0b0100) } + + it 'returns :automatic because of the followers entry in the automatic policy' do + expect(status.quote_policy_for_account(account)).to eq :unknown + end + end + end +end diff --git a/spec/models/fasp/provider_spec.rb b/spec/models/fasp/provider_spec.rb index 52df4638fdc..9fd2c4c2348 100644 --- a/spec/models/fasp/provider_spec.rb +++ b/spec/models/fasp/provider_spec.rb @@ -206,4 +206,12 @@ RSpec.describe Fasp::Provider do end end end + + describe '#delivery_failure_tracker' do + subject { Fabricate(:fasp_provider) } + + it 'returns a `DeliverFailureTracker` instance' do + expect(subject.delivery_failure_tracker).to be_a(DeliveryFailureTracker) + end + end end diff --git a/spec/models/form/import_spec.rb b/spec/models/form/import_spec.rb index dd8fd35a05f..d682e13ecb9 100644 --- a/spec/models/form/import_spec.rb +++ b/spec/models/form/import_spec.rb @@ -258,13 +258,13 @@ RSpec.describe Form::Import do end end - it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'blocking', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'blocking', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'muting', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) - it_behaves_like 'on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', (%w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } }) - it_behaves_like 'on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', (%w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } }) + it_behaves_like('on successful import', 'following', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'following', 'overwrite', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'blocking', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'blocking', 'overwrite', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'muting', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } }) + it_behaves_like('on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', %w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } }) + it_behaves_like('on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', %w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } }) it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [ { 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil }, diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 48c273d3ecb..e2d91835ec7 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -28,4 +28,26 @@ RSpec.describe List do end end end + + describe 'Scopes' do + describe '.with_list_account' do + let(:alice) { Fabricate :account } + let(:bob) { Fabricate :account } + let(:list) { Fabricate :list } + let(:other_list) { Fabricate :list } + + before do + Fabricate :follow, account: list.account, target_account: alice + Fabricate :follow, account: other_list.account, target_account: bob + Fabricate :list_account, list: list, account: alice + Fabricate :list_account, list: other_list, account: bob + end + + it 'returns lists connected to the account' do + expect(described_class.with_list_account(alice)) + .to include(list) + .and not_include(other_list) + end + end + end end diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 910eac4e9a4..a712cdde1dc 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -313,6 +313,12 @@ RSpec.describe MediaAttachment, :attachment_processing do end end + describe '.combined_media_file_size' do + subject { described_class.combined_media_file_size } + + it { is_expected.to be_an(Arel::Nodes::Grouping) } + end + private def media_metadata diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 0831ac34b8b..18378c000d2 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Tag do subject { Fabricate :tag, name: 'original' } it { is_expected.to_not allow_value('changed').for(:name).with_message(previous_name_error_message) } + it { is_expected.to allow_value('ORIGINAL').for(:name) } end end @@ -31,6 +32,7 @@ RSpec.describe Tag do subject { Fabricate :tag, name: 'original', display_name: 'OriginalDisplayName' } it { is_expected.to_not allow_value('ChangedDisplayName').for(:display_name).with_message(previous_name_error_message) } + it { is_expected.to allow_value('ORIGINAL').for(:display_name) } end end diff --git a/spec/models/trends/statuses_spec.rb b/spec/models/trends/statuses_spec.rb index 39839010427..54b227dad0c 100644 --- a/spec/models/trends/statuses_spec.rb +++ b/spec/models/trends/statuses_spec.rb @@ -111,12 +111,16 @@ RSpec.describe Trends::Statuses do let!(:yesterday) { today - 1.day } let!(:status_foo) { Fabricate(:status, text: 'Foo', language: 'en', trendable: true, created_at: yesterday) } - let!(:status_bar) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today) } + let!(:status_bar) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today, quote: Quote.new(state: :accepted, quoted_status: status_foo)) } let!(:status_baz) { Fabricate(:status, text: 'Baz', language: 'en', trendable: true, created_at: today) } + let!(:untrendable) { Fabricate(:status, text: 'Untrendable', language: 'en', trendable: true, visibility: :unlisted) } + let!(:untrendable_quote) { Fabricate(:status, text: 'Untrendable quote!', language: 'en', trendable: true, created_at: today, quote: Quote.new(state: :accepted, quoted_status: untrendable)) } before do default_threshold_value.times { reblog(status_foo, today) } default_threshold_value.times { reblog(status_bar, today) } + default_threshold_value.times { reblog(untrendable, today) } + default_threshold_value.times { reblog(untrendable_quote, today) } (default_threshold_value - 1).times { reblog(status_baz, today) } end @@ -129,7 +133,7 @@ RSpec.describe Trends::Statuses do results = subject.query.limit(10).to_a expect(results).to eq [status_bar, status_foo] - expect(results).to_not include(status_baz) + expect(results).to_not include(status_baz, untrendable, untrendable_quote) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c71b7a600ba..a9ab15a956e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -10,6 +10,8 @@ RSpec.describe User do let(:account) { Fabricate(:account, username: 'alice') } it_behaves_like 'two_factor_backupable' + it_behaves_like 'User::Activity' + it_behaves_like 'User::Confirmation' describe 'otp_secret' do it 'encrypts the saved value' do @@ -65,34 +67,6 @@ RSpec.describe User do end end - describe 'confirmed' do - it 'returns an array of users who are confirmed' do - Fabricate(:user, confirmed_at: nil) - confirmed_user = Fabricate(:user, confirmed_at: Time.zone.now) - expect(described_class.confirmed).to contain_exactly(confirmed_user) - end - end - - describe 'signed_in_recently' do - it 'returns a relation of users who have signed in during the recent period' do - recent_sign_in_user = Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) - Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago) - - expect(described_class.signed_in_recently) - .to contain_exactly(recent_sign_in_user) - end - end - - describe 'not_signed_in_recently' do - it 'returns a relation of users who have not signed in during the recent period' do - no_recent_sign_in_user = Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago) - Fabricate(:user, current_sign_in_at: within_duration_window_days.ago) - - expect(described_class.not_signed_in_recently) - .to contain_exactly(no_recent_sign_in_user) - end - end - describe 'account_not_suspended' do it 'returns with linked accounts that are not suspended' do suspended_account = Fabricate(:account, suspended_at: 10.days.ago) @@ -127,14 +101,6 @@ RSpec.describe User do expect(described_class.matches_ip('2160:2160::/32')).to contain_exactly(user1) end end - - def exceed_duration_window_days - described_class::ACTIVE_DURATION + 2.days - end - - def within_duration_window_days - described_class::ACTIVE_DURATION - 2.days - end end describe 'email domains denylist integration' do @@ -197,12 +163,13 @@ RSpec.describe User do describe '#update_sign_in!' do context 'with an existing user' do - let!(:user) { Fabricate :user, last_sign_in_at: 10.days.ago, current_sign_in_at: 1.hour.ago, sign_in_count: 123 } + let!(:user) { Fabricate :user, last_sign_in_at: 10.days.ago, current_sign_in_at:, sign_in_count: 123 } + let(:current_sign_in_at) { 1.hour.ago } context 'with new sign in false' do it 'updates timestamps but not counts' do expect { user.update_sign_in!(new_sign_in: false) } - .to change(user, :last_sign_in_at) + .to change(user, :last_sign_in_at).to(current_sign_in_at) .and change(user, :current_sign_in_at) .and not_change(user, :sign_in_count) end @@ -211,11 +178,22 @@ RSpec.describe User do context 'with new sign in true' do it 'updates timestamps and counts' do expect { user.update_sign_in!(new_sign_in: true) } - .to change(user, :last_sign_in_at) + .to change(user, :last_sign_in_at).to(current_sign_in_at) .and change(user, :current_sign_in_at) .and change(user, :sign_in_count).by(1) end end + + context 'when the user does not have a current_sign_in_at value' do + let(:current_sign_in_at) { nil } + + before { travel_to(1.minute.ago) } + + it 'updates last sign in to now' do + expect { user.update_sign_in! } + .to change(user, :last_sign_in_at).to(Time.now.utc) + end + end end context 'with a new user' do @@ -228,79 +206,6 @@ RSpec.describe User do end end - describe '#confirmed?' do - it 'returns true when a confirmed_at is set' do - user = Fabricate.build(:user, confirmed_at: Time.now.utc) - expect(user.confirmed?).to be true - end - - it 'returns false if a confirmed_at is nil' do - user = Fabricate.build(:user, confirmed_at: nil) - expect(user.confirmed?).to be false - end - end - - describe '#confirm' do - subject { user.confirm } - - let(:new_email) { 'new-email@example.com' } - - before do - allow(TriggerWebhookWorker).to receive(:perform_async) - end - - context 'when the user is already confirmed' do - let!(:user) { Fabricate(:user, confirmed_at: Time.now.utc, approved: true, unconfirmed_email: new_email) } - - it 'sets email to unconfirmed_email and does not trigger web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) - end - end - - context 'when the user is a new user' do - let(:user) { Fabricate(:user, confirmed_at: nil, unconfirmed_email: new_email) } - - context 'when the user is already approved' do - before do - Setting.registrations_mode = 'approved' - user.approve! - end - - it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once - end - end - - context 'when the user does not require explicit approval' do - before do - Setting.registrations_mode = 'open' - end - - it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once - end - end - - context 'when the user requires explicit approval but is not approved' do - before do - Setting.registrations_mode = 'approved' - end - - it 'sets email to unconfirmed_email and does not trigger web hook' do - expect { subject }.to change { user.reload.email }.to(new_email) - - expect(TriggerWebhookWorker).to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) - end - end - end - end - describe '#approve!' do subject { user.approve! } diff --git a/spec/models/username_block_spec.rb b/spec/models/username_block_spec.rb new file mode 100644 index 00000000000..72dbe028bdf --- /dev/null +++ b/spec/models/username_block_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UsernameBlock do + describe '.matches?' do + context 'when there is an exact block' do + before do + Fabricate(:username_block, username: 'carriage', exact: true) + end + + it 'returns true on exact match' do + expect(described_class.matches?('carriage')).to be true + end + + it 'returns true on case insensitive match' do + expect(described_class.matches?('CaRRiagE')).to be true + end + + it 'returns true on homoglyph match' do + expect(described_class.matches?('c4rr14g3')).to be true + end + + it 'returns false on partial match' do + expect(described_class.matches?('foo_carriage')).to be false + end + + it 'returns false on no match' do + expect(described_class.matches?('foo')).to be false + end + end + + context 'when there is a partial block' do + before do + Fabricate(:username_block, username: 'carriage', exact: false) + end + + it 'returns true on exact match' do + expect(described_class.matches?('carriage')).to be true + end + + it 'returns true on case insensitive match' do + expect(described_class.matches?('CaRRiagE')).to be true + end + + it 'returns true on homoglyph match' do + expect(described_class.matches?('c4rr14g3')).to be true + end + + it 'returns true on suffix match' do + expect(described_class.matches?('foo_carriage')).to be true + end + + it 'returns true on prefix match' do + expect(described_class.matches?('carriage_foo')).to be true + end + + it 'returns false on no match' do + expect(described_class.matches?('foo')).to be false + end + end + end +end diff --git a/spec/models/worker_batch_spec.rb b/spec/models/worker_batch_spec.rb index b58dc48618a..b204decb89e 100644 --- a/spec/models/worker_batch_spec.rb +++ b/spec/models/worker_batch_spec.rb @@ -42,14 +42,6 @@ RSpec.describe WorkerBatch do it 'does not persist the job IDs' do expect(subject.jobs).to eq [] end - - context 'when async refresh is connected' do - let(:async_refresh) { AsyncRefresh.new(async_refresh_key) } - - it 'immediately marks the async refresh as finished' do - expect(async_refresh.reload.finished?).to be true - end - end end context 'when called with an array of job IDs' do @@ -62,7 +54,7 @@ RSpec.describe WorkerBatch do end it 'persists the job IDs' do - expect(subject.jobs).to eq %w(foo bar) + expect(subject.jobs).to contain_exactly('foo', 'bar') end end end @@ -71,11 +63,11 @@ RSpec.describe WorkerBatch do before do subject.connect(async_refresh_key, threshold: 0.5) if async_refresh.present? subject.add_jobs(%w(foo bar baz)) - subject.remove_job('foo') + subject.remove_job('foo', increment: true) end it 'removes the job from pending jobs' do - expect(subject.jobs).to eq %w(bar baz) + expect(subject.jobs).to contain_exactly('bar', 'baz') end it 'decrements the number of pending jobs' do diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index 69c0bad026b..af6958b68dc 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -86,6 +86,92 @@ RSpec.describe StatusPolicy, type: :model do end end + context 'with the permission of quote?' do + permissions :quote? do + it 'grants access when direct and account is viewer' do + status.visibility = :direct + + expect(subject).to permit(status.account, status) + end + + it 'does not grant access access when direct and viewer is mentioned but not explicitly allowed' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: bob)] + + expect(subject).to_not permit(bob, status) + end + + it 'does not grant access access when direct and viewer is mentioned but not explicitly allowed and mentions are loaded' do + status.visibility = :direct + status.mentions = [Fabricate(:mention, account: bob)] + status.active_mentions.load + + expect(subject).to_not permit(bob, status) + end + + it 'denies access when direct and viewer is not mentioned' do + viewer = Fabricate(:account) + status.visibility = :direct + + expect(subject).to_not permit(viewer, status) + end + + it 'denies access when private and viewer is not mentioned' do + viewer = Fabricate(:account) + status.visibility = :private + + expect(subject).to_not permit(viewer, status) + end + + it 'grants access when private and viewer is mentioned but not otherwise allowed' do + status.visibility = :private + status.mentions = [Fabricate(:mention, account: bob)] + + expect(subject).to_not permit(bob, status) + end + + it 'denies access when private and non-viewer is mentioned' do + viewer = Fabricate(:account) + status.visibility = :private + status.mentions = [Fabricate(:mention, account: bob)] + + expect(subject).to_not permit(viewer, status) + end + + it 'denies access when private and account is following viewer' do + follow = Fabricate(:follow) + status.visibility = :private + status.account = follow.target_account + + expect(subject).to_not permit(follow.account, status) + end + + it 'denies access when public but policy does not allow anyone' do + viewer = Fabricate(:account) + expect(subject).to_not permit(viewer, status) + end + + it 'grants access when public and policy allows everyone' do + status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] + viewer = Fabricate(:account) + expect(subject).to permit(viewer, status) + end + + it 'denies access when public and policy allows followers but viewer is not one' do + status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] + viewer = Fabricate(:account) + expect(subject).to_not permit(viewer, status) + end + + it 'grants access when public and policy allows followers and viewer is one' do + status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] + viewer = Fabricate(:account) + viewer.follow!(status.account) + expect(subject).to permit(viewer, status) + end + end + end + context 'with the permission of reblog?' do permissions :reblog? do it 'denies access when private' do diff --git a/spec/requests/activitypub/quote_authorizations_spec.rb b/spec/requests/activitypub/quote_authorizations_spec.rb new file mode 100644 index 00000000000..98daa3a79b7 --- /dev/null +++ b/spec/requests/activitypub/quote_authorizations_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'ActivityPub QuoteAuthorization endpoint' do + let(:account) { Fabricate(:account, domain: nil) } + let(:status) { Fabricate :status, account: account } + let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } + + before { Fabricate :favourite, status: status } + + describe 'GET /accounts/:account_username/quote_authorizations/:quote_id' do + context 'with an accepted quote' do + it 'returns http success and activity json' do + get account_quote_authorization_url(quote.quoted_account, quote) + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + + expect(response.parsed_body) + .to include(type: 'QuoteAuthorization') + end + end + + context 'with an incorrect quote authorization URL' do + it 'returns http not found' do + get account_quote_authorization_url(quote.account, quote) + + expect(response) + .to have_http_status(404) + end + end + + context 'with a rejected quote' do + before do + quote.reject! + end + + it 'returns http not found' do + get account_quote_authorization_url(quote.quoted_account, quote) + + expect(response) + .to have_http_status(404) + end + end + end +end diff --git a/spec/requests/admin/username_blocks_spec.rb b/spec/requests/admin/username_blocks_spec.rb new file mode 100644 index 00000000000..6e17ca2d470 --- /dev/null +++ b/spec/requests/admin/username_blocks_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Admin Username Blocks' do + describe 'GET /admin/username_blocks' do + before { sign_in Fabricate(:admin_user) } + + it 'returns http success' do + get admin_username_blocks_path + + expect(response) + .to have_http_status(200) + end + end + + describe 'POST /admin/username_blocks' do + before { sign_in Fabricate(:admin_user) } + + it 'gracefully handles invalid nested params' do + post admin_username_blocks_path(username_block: 'invalid') + + expect(response) + .to have_http_status(400) + end + + it 'creates a username block' do + post admin_username_blocks_path(username_block: { username: 'banana', comparison: 'contains', allow_with_approval: '0' }) + + expect(response) + .to redirect_to(admin_username_blocks_path) + expect(UsernameBlock.find_by(username: 'banana')) + .to_not be_nil + end + end + + describe 'POST /admin/username_blocks/batch' do + before { sign_in Fabricate(:admin_user) } + + let(:username_blocks) { Fabricate.times(2, :username_block) } + + it 'gracefully handles invalid nested params' do + post batch_admin_username_blocks_path(form_username_block_batch: 'invalid') + + expect(response) + .to redirect_to(admin_username_blocks_path) + end + + it 'deletes selected username blocks' do + post batch_admin_username_blocks_path(form_username_block_batch: { username_block_ids: username_blocks.map(&:id) }, delete: '1') + + expect(response) + .to redirect_to(admin_username_blocks_path) + expect(UsernameBlock.where(id: username_blocks.map(&:id))) + .to be_empty + end + end + + describe 'GET /admin/username_blocks/new' do + before { sign_in Fabricate(:admin_user) } + + it 'returns http success' do + get new_admin_username_block_path + + expect(response) + .to have_http_status(200) + end + end + + describe 'GET /admin/username_blocks/:id/edit' do + before { sign_in Fabricate(:admin_user) } + + let(:username_block) { Fabricate(:username_block) } + + it 'returns http success' do + get edit_admin_username_block_path(username_block) + + expect(response) + .to have_http_status(200) + end + end + + describe 'PUT /admin/username_blocks/:id' do + before { sign_in Fabricate(:admin_user) } + + let(:username_block) { Fabricate(:username_block, username: 'banana') } + + it 'updates username block' do + put admin_username_block_path(username_block, username_block: { username: 'bebebe' }) + + expect(response) + .to redirect_to(admin_username_blocks_path) + expect(username_block.reload.username) + .to eq 'bebebe' + end + end +end diff --git a/spec/requests/api/v1/push/subscriptions_spec.rb b/spec/requests/api/v1/push/subscriptions_spec.rb index 359de9d95c0..bccbda6fa23 100644 --- a/spec/requests/api/v1/push/subscriptions_spec.rb +++ b/spec/requests/api/v1/push/subscriptions_spec.rb @@ -166,17 +166,30 @@ RSpec.describe 'API V1 Push Subscriptions' do describe 'GET /api/v1/push/subscription' do subject { get '/api/v1/push/subscription', headers: headers } - before { create_subscription_with_token } + context 'with a subscription' do + before { create_subscription_with_token } - it 'shows subscription details' do - subject + it 'shows subscription details' do + subject - expect(response) - .to have_http_status(200) - expect(response.content_type) - .to start_with('application/json') - expect(response.parsed_body) - .to include(endpoint: endpoint) + expect(response) + .to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body) + .to include(endpoint: endpoint) + end + end + + context 'without a subscription' do + it 'returns not found' do + subject + + expect(response) + .to have_http_status(404) + expect(response.content_type) + .to start_with('application/json') + end end end diff --git a/spec/requests/api/v1/statuses/quotes_spec.rb b/spec/requests/api/v1/statuses/quotes_spec.rb new file mode 100644 index 00000000000..9456556ce99 --- /dev/null +++ b/spec/requests/api/v1/statuses/quotes_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'API V1 Statuses Quotes' do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + + describe 'GET /api/v1/statuses/:status_id/quotes' do + subject do + get "/api/v1/statuses/#{status.id}/quotes", headers: headers, params: { limit: 2 } + end + + let(:scopes) { 'read:statuses' } + + let(:status) { Fabricate(:status, account: user.account) } + let!(:accepted_quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } + let!(:rejected_quote) { Fabricate(:quote, quoted_status: status, state: :rejected) } + let!(:pending_quote) { Fabricate(:quote, quoted_status: status, state: :pending) } + let!(:another_accepted_quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } + + context 'with an OAuth token' do + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + it_behaves_like 'forbidden for wrong scope', 'write write:statuses' + + it 'returns http success and statuses quoting this post' do + subject + + expect(response) + .to have_http_status(200) + .and include_pagination_headers( + prev: api_v1_status_quotes_url(limit: 2, since_id: another_accepted_quote.id), + next: api_v1_status_quotes_url(limit: 2, max_id: accepted_quote.id) + ) + expect(response.content_type) + .to start_with('application/json') + + expect(response.parsed_body) + .to contain_exactly( + include(id: accepted_quote.status.id.to_s), + include(id: another_accepted_quote.status.id.to_s) + ) + + expect(response.parsed_body) + .to_not include( + include(id: rejected_quote.status.id.to_s), + include(id: pending_quote.status.id.to_s) + ) + end + + context 'with a different user than the post owner' do + let(:status) { Fabricate(:status) } + + it 'returns http forbidden' do + subject + + expect(response).to have_http_status(403) + expect(response.content_type) + .to start_with('application/json') + end + end + end + + context 'without an OAuth token' do + let(:headers) { {} } + + it 'returns http unauthorized' do + subject + + expect(response).to have_http_status(401) + expect(response.content_type) + .to start_with('application/json') + end + end + end + + describe 'POST /api/v1/statuses/:status_id/quotes/:id/revoke' do + subject do + post "/api/v1/statuses/#{status.id}/quotes/#{quote.status.id}/revoke", headers: headers + end + + let(:scopes) { 'write:statuses' } + + let(:status) { Fabricate(:status, account: user.account) } + let!(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } + + context 'with an OAuth token' do + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + it_behaves_like 'forbidden for wrong scope', 'read read:statuses' + + context 'with a different user than the post owner' do + let(:status) { Fabricate(:status) } + + it 'returns http forbidden' do + subject + + expect(response).to have_http_status(403) + expect(response.content_type) + .to start_with('application/json') + end + end + + it 'revokes the quote and returns HTTP success' do + expect { subject } + .to change { quote.reload.state }.from('accepted').to('revoked') + + expect(response) + .to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body) + .to match( + a_hash_including(id: quote.status.id.to_s, quote: a_hash_including(state: 'revoked')) + ) + end + end + + context 'without an OAuth token' do + let(:headers) { {} } + + it 'returns http unauthorized' do + subject + + expect(response).to have_http_status(401) + expect(response.content_type) + .to start_with('application/json') + end + end + end +end diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index 285fa936552..ba18623302e 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -158,6 +158,52 @@ RSpec.describe '/api/v1/statuses' do end end + context 'with a quote policy', feature: :outgoing_quotes do + let(:quoted_status) { Fabricate(:status, account: user.account) } + let(:params) do + { + status: 'Hello world, this is a self-quote', + quote_approval_policy: 'followers', + } + end + + it 'returns post with appropriate quote policy, as well as rate limit headers', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body[:quote_approval]).to include({ + automatic: ['followers'], + manual: [], + current_user: 'automatic', + }) + expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s + expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s + end + end + + context 'with a self-quote post', feature: :outgoing_quotes do + let(:quoted_status) { Fabricate(:status, account: user.account) } + let(:params) do + { + status: 'Hello world, this is a self-quote', + quoted_status_id: quoted_status.id, + } + end + + it 'returns a quote post, as well as rate limit headers', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body[:quote]).to be_present + expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s + expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s + end + end + context 'with a safeguard' do let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob') } @@ -286,9 +332,10 @@ RSpec.describe '/api/v1/statuses' do describe 'PUT /api/v1/statuses/:id' do subject do - put "/api/v1/statuses/#{status.id}", headers: headers, params: { status: 'I am updated' } + put "/api/v1/statuses/#{status.id}", headers: headers, params: params end + let(:params) { { status: 'I am updated' } } let(:scopes) { 'write:statuses' } let(:status) { Fabricate(:status, account: user.account) } @@ -302,6 +349,19 @@ RSpec.describe '/api/v1/statuses' do .to start_with('application/json') expect(status.reload.text).to eq 'I am updated' end + + context 'when updating only the quote policy' do + let(:params) { { status: status.text, quote_approval_policy: 'public' } } + + it 'updates the status', :aggregate_failures, feature: :outgoing_quotes do + expect { subject } + .to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + end + end end end diff --git a/spec/requests/api/v2/search_spec.rb b/spec/requests/api/v2/search_spec.rb index 9c9f37e0988..6beab4c8c7d 100644 --- a/spec/requests/api/v2/search_spec.rb +++ b/spec/requests/api/v2/search_spec.rb @@ -98,7 +98,7 @@ RSpec.describe 'Search API' do context 'when search raises syntax error' do before { allow(Search).to receive(:new).and_raise(Mastodon::SyntaxError) } - it 'returns http unprocessable_entity' do + it 'returns http unprocessable_content' do get '/api/v2/search', headers: headers, params: params expect(response).to have_http_status(422) diff --git a/spec/requests/api/web/push_subscriptions_spec.rb b/spec/requests/api/web/push_subscriptions_spec.rb index 42545b3d6e2..05e6f28d1ff 100644 --- a/spec/requests/api/web/push_subscriptions_spec.rb +++ b/spec/requests/api/web/push_subscriptions_spec.rb @@ -3,6 +3,39 @@ require 'rails_helper' RSpec.describe 'API Web Push Subscriptions' do + let(:create_payload) do + { + subscription: { + endpoint: 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX', + keys: { + p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', + auth: 'eH_C8rq2raXqlcBVDa1gLg==', + }, + standard: standard, + }, + } + end + + let(:alerts_payload) do + { + data: { + policy: 'all', + + alerts: { + follow: true, + follow_request: false, + favourite: false, + reblog: true, + mention: false, + poll: true, + status: false, + quote: true, + }, + }, + } + end + let(:standard) { '1' } + describe 'DELETE /api/web/push_subscriptions/:id' do subject { delete api_web_push_subscription_path(token) } @@ -54,7 +87,9 @@ RSpec.describe 'API Web Push Subscriptions' do end describe 'POST /api/web/push_subscriptions' do - before { sign_in Fabricate :user } + before { sign_in(user) } + + let(:user) { Fabricate :user } it 'gracefully handles invalid nested params' do post api_web_push_subscriptions_path, params: { subscription: 'invalid' } @@ -62,6 +97,69 @@ RSpec.describe 'API Web Push Subscriptions' do expect(response) .to have_http_status(400) end + + it 'saves push subscriptions with valid params' do + post api_web_push_subscriptions_path, params: create_payload + expect(response) + .to have_http_status(200) + + expect(created_push_subscription) + .to have_attributes( + endpoint: eq(create_payload[:subscription][:endpoint]), + key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]), + key_auth: eq(create_payload[:subscription][:keys][:auth]) + ) + .and be_standard + expect(user.session_activations.first.web_push_subscription) + .to eq(created_push_subscription) + end + + context 'when standard is provided as false value' do + let(:standard) { '0' } + + it 'saves push subscription with standard as false' do + post api_web_push_subscriptions_path, params: create_payload + + expect(created_push_subscription) + .to_not be_standard + end + end + + context 'with a user who has a session with a prior subscription' do + before do + # Trigger creation of a `SessionActivation` for the user so that the + # prior_subscription setup and verification works as expected + get about_path + end + + let!(:prior_subscription) { Fabricate(:web_push_subscription, user:, session_activation: user.session_activations.last) } + + it 'destroys prior subscription when creating new one' do + post api_web_push_subscriptions_path, params: create_payload + + expect(response) + .to have_http_status(200) + expect { prior_subscription.reload } + .to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with initial data' do + it 'saves alert settings' do + post api_web_push_subscriptions_path, params: create_payload.merge(alerts_payload) + + expect(response) + .to have_http_status(200) + + expect(created_push_subscription.data['policy']) + .to eq 'all' + + alert_types.each do |type| + expect(created_push_subscription.data['alerts'][type]) + .to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) + end + end + end end describe 'PUT /api/web/push_subscriptions' do @@ -75,5 +173,30 @@ RSpec.describe 'API Web Push Subscriptions' do expect(response) .to have_http_status(400) end + + it 'changes existing alert settings' do + # Create record this way to correctly associate a `SessionActivation` + # during full POST->create cycle + post api_web_push_subscriptions_path params: create_payload + expect(response) + .to have_http_status(200) + + put api_web_push_subscription_path(created_push_subscription), params: alerts_payload + expect(created_push_subscription.data['policy']) + .to eq 'all' + alert_types.each do |type| + expect(created_push_subscription.data['alerts'][type]) + .to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) + end + end + end + + def created_push_subscription + Web::PushSubscription + .find_by(endpoint: create_payload[:subscription][:endpoint]) + end + + def alert_types + Notification::LEGACY_TYPE_CLASS_MAP.values.map(&:to_s) end end diff --git a/spec/routing/accounts_routing_spec.rb b/spec/routing/accounts_routing_spec.rb index 8ff711a681e..bb0bf082bde 100644 --- a/spec/routing/accounts_routing_spec.rb +++ b/spec/routing/accounts_routing_spec.rb @@ -49,6 +49,7 @@ RSpec.describe 'Routes under accounts/' do context 'with local username encoded at' do include RSpec::Rails::RequestExampleGroup + let(:username) { 'alice' } it 'routes /%40:username' do @@ -140,6 +141,7 @@ RSpec.describe 'Routes under accounts/' do context 'with remote username encoded at' do include RSpec::Rails::RequestExampleGroup + let(:username) { 'alice%40example.com' } let(:username_decoded) { 'alice@example.com' } diff --git a/spec/serializers/activitypub/accept_quote_request_serializer_spec.rb b/spec/serializers/activitypub/accept_quote_request_serializer_spec.rb new file mode 100644 index 00000000000..986d9112b79 --- /dev/null +++ b/spec/serializers/activitypub/accept_quote_request_serializer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::AcceptQuoteRequestSerializer do + subject { serialized_record_json(record, described_class, adapter: ActivityPub::Adapter) } + + describe 'serializing an object' do + let(:record) { Fabricate(:quote, state: :accepted) } + + it 'returns expected attributes' do + expect(subject.deep_symbolize_keys) + .to include( + actor: eq(ActivityPub::TagManager.instance.uri_for(record.quoted_account)), + id: match("#accepts/quote_requests/#{record.id}"), + object: include( + type: 'QuoteRequest', + instrument: ActivityPub::TagManager.instance.uri_for(record.status), + object: ActivityPub::TagManager.instance.uri_for(record.quoted_status) + ), + type: 'Accept' + ) + end + end +end diff --git a/spec/serializers/activitypub/delete_quote_authorization_serializer_spec.rb b/spec/serializers/activitypub/delete_quote_authorization_serializer_spec.rb new file mode 100644 index 00000000000..48e3a4ddf73 --- /dev/null +++ b/spec/serializers/activitypub/delete_quote_authorization_serializer_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::DeleteQuoteAuthorizationSerializer do + subject { serialized_record_json(quote, described_class, adapter: ActivityPub::Adapter) } + + describe 'serializing an object' do + let(:status) { Fabricate(:status) } + let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } + + it 'returns expected attributes' do + expect(subject.deep_symbolize_keys) + .to include( + actor: eq(ActivityPub::TagManager.instance.uri_for(status.account)), + object: ActivityPub::TagManager.instance.approval_uri_for(quote, check_approval: false), + type: 'Delete' + ) + end + end +end diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index a6976193b20..d5d02a0d495 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -41,4 +41,34 @@ RSpec.describe ActivityPub::NoteSerializer do .and(not_include(reply_by_other_first.uri)) # Replies from others .and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility end + + context 'with a quote' do + let(:quoted_status) { Fabricate(:status) } + let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, state: :accepted) } + + it 'has the expected shape' do + expect(subject).to include({ + 'type' => 'Note', + 'quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status), + 'quoteUri' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status), + '_misskey_quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status), + 'quoteAuthorization' => ActivityPub::TagManager.instance.approval_uri_for(quote), + }) + end + end + + context 'with a quote policy', feature: :outgoing_quotes do + let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } + + it 'has the expected shape' do + expect(subject).to include({ + 'type' => 'Note', + 'interactionPolicy' => a_hash_including( + 'canQuote' => a_hash_including( + 'automaticApproval' => [ActivityPub::TagManager.instance.followers_uri_for(parent.account)] + ) + ), + }) + end + end end diff --git a/spec/serializers/activitypub/quote_authorization_serializer_spec.rb b/spec/serializers/activitypub/quote_authorization_serializer_spec.rb new file mode 100644 index 00000000000..6a157756934 --- /dev/null +++ b/spec/serializers/activitypub/quote_authorization_serializer_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::QuoteAuthorizationSerializer do + subject { serialized_record_json(quote, described_class, adapter: ActivityPub::Adapter) } + + describe 'serializing an object' do + let(:status) { Fabricate(:status) } + let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } + + it 'returns expected attributes' do + expect(subject.deep_symbolize_keys) + .to include( + attributedTo: eq(ActivityPub::TagManager.instance.uri_for(status.account)), + interactionTarget: ActivityPub::TagManager.instance.uri_for(status), + interactingObject: ActivityPub::TagManager.instance.uri_for(quote.status), + type: 'QuoteAuthorization' + ) + end + end +end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index a7e1b923832..74b8cef413a 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -564,6 +564,80 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end end + context 'when an approved quote of a local post gets updated through an explicit update' do + let(:quoted_account) { Fabricate(:account) } + let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted) } + let(:approval_uri) { ActivityPub::TagManager.instance.approval_uri_for(quote) } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + quoteAuthorization: approval_uri, + } + end + + it 'updates the quote post without changing the quote status' do + expect { subject.call(status, json, json) } + .to not_change(quote, :approval_uri) + .and not_change(quote, :state).from('accepted') + .and change(status, :text).from('Hello world').to('Hello universe') + end + end + + context 'when an unapproved quote of a local post gets updated through an explicit update and claims approval' do + let(:quoted_account) { Fabricate(:account) } + let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: 0) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, state: :rejected) } + let(:approval_uri) { ActivityPub::TagManager.instance.approval_uri_for(quote) } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + quoteAuthorization: approval_uri, + } + end + + it 'updates the quote post without changing the quote status' do + expect { subject.call(status, json, json) } + .to not_change(quote, :approval_uri) + .and not_change(quote, :state).from('rejected') + .and change(status, :text).from('Hello world').to('Hello universe') + end + end + context 'when the status has an existing verified quote and removes an approval link through an explicit update' do let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } let(:quoted_status) { Fabricate(:status, account: quoted_account) } diff --git a/spec/services/activitypub/verify_quote_service_spec.rb b/spec/services/activitypub/verify_quote_service_spec.rb index ae4ffae9bb2..94b9e33ed3b 100644 --- a/spec/services/activitypub/verify_quote_service_spec.rb +++ b/spec/services/activitypub/verify_quote_service_spec.rb @@ -267,9 +267,9 @@ RSpec.describe ActivityPub::VerifyQuoteService do quoted_status.mentions << Mention.new(account: account) end - it 'updates the status' do + it 'does not the status' do expect { subject.call(quote) } - .to change(quote, :state).to('accepted') + .to_not change(quote, :state).from('pending') end end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 8836b9e0a63..64bb5e32e21 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -160,6 +160,12 @@ RSpec.describe PostStatusService do expect(status.language).to eq 'en' end + it 'creates a status with the quote approval policy set' do + status = create_status_with_options(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) + + expect(status.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) + end + it 'processes mentions' do mention_service = instance_double(ProcessMentionsService) allow(mention_service).to receive(:call) @@ -291,6 +297,14 @@ RSpec.describe PostStatusService do ) end + it 'correctly requests a quote for remote posts' do + account = Fabricate(:account) + quoted_status = Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) + + expect { subject.call(account, text: 'test', quoted_status: quoted_status) } + .to enqueue_sidekiq_job(ActivityPub::QuoteRequestWorker) + end + it 'returns existing status when used twice with idempotency key' do account = Fabricate(:account) status1 = subject.call(account, text: 'test', idempotency: 'meepmeep') diff --git a/spec/services/revoke_quote_service_spec.rb b/spec/services/revoke_quote_service_spec.rb new file mode 100644 index 00000000000..c1dbcfda54e --- /dev/null +++ b/spec/services/revoke_quote_service_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RevokeQuoteService do + subject { described_class.new } + + let!(:alice) { Fabricate(:account) } + let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } + + let(:status) { Fabricate(:status, account: alice) } + + let(:quote) { Fabricate(:quote, quoted_status: status, state: :accepted) } + + before do + hank.follow!(alice) + end + + context 'with an accepted quote' do + it 'revokes the quote and sends a Delete activity' do + expect { described_class.new.call(quote) } + .to change { quote.reload.state }.from('accepted').to('revoked') + .and enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(/Delete/, alice.id, hank.inbox_url) + end + end +end diff --git a/spec/support/examples/models/concerns/user/activity.rb b/spec/support/examples/models/concerns/user/activity.rb new file mode 100644 index 00000000000..7e647b694a9 --- /dev/null +++ b/spec/support/examples/models/concerns/user/activity.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.shared_examples 'User::Activity' do + before { stub_const 'User::ACTIVE_DURATION', 7.days } + + describe 'Scopes' do + let!(:recent_sign_in_user) { Fabricate(:user, current_sign_in_at: 2.days.ago) } + let!(:no_recent_sign_in_user) { Fabricate(:user, current_sign_in_at: 10.days.ago) } + + describe '.signed_in_recently' do + it 'returns users who have signed in during the recent period' do + expect(described_class.signed_in_recently) + .to contain_exactly(recent_sign_in_user) + end + end + + describe '.not_signed_in_recently' do + it 'returns users who have not signed in during the recent period' do + expect(described_class.not_signed_in_recently) + .to contain_exactly(no_recent_sign_in_user) + end + end + end + + describe '#signed_in_recently?' do + subject { Fabricate.build :user, current_sign_in_at: } + + context 'when current_sign_in_at is nil' do + let(:current_sign_in_at) { nil } + + it { is_expected.to_not be_signed_in_recently } + end + + context 'when current_sign_in_at is before the threshold' do + let(:current_sign_in_at) { 10.days.ago } + + it { is_expected.to_not be_signed_in_recently } + end + + context 'when current_sign_in_at is after the threshold' do + let(:current_sign_in_at) { 2.days.ago } + + it { is_expected.to be_signed_in_recently } + end + end +end diff --git a/spec/support/examples/models/concerns/user/confirmation.rb b/spec/support/examples/models/concerns/user/confirmation.rb new file mode 100644 index 00000000000..4edc402f950 --- /dev/null +++ b/spec/support/examples/models/concerns/user/confirmation.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.shared_examples 'User::Confirmation' do + describe 'Scopes' do + let!(:unconfirmed_user) { Fabricate :user, confirmed_at: nil } + let!(:confirmed_user) { Fabricate :user, confirmed_at: Time.now.utc } + + describe '.confirmed' do + it 'returns users who are confirmed' do + expect(described_class.confirmed) + .to contain_exactly(confirmed_user) + end + end + + describe '.unconfirmed' do + it 'returns users who are not confirmed' do + expect(described_class.unconfirmed) + .to contain_exactly(unconfirmed_user) + end + end + end + + describe '#confirmed?' do + subject { Fabricate.build(:user, confirmed_at:) } + + context 'when confirmed_at is set' do + let(:confirmed_at) { Time.now.utc } + + it { is_expected.to be_confirmed } + end + + context 'when confirmed_at is not set' do + let(:confirmed_at) { nil } + + it { is_expected.to_not be_confirmed } + end + end + + describe '#unconfirmed?' do + subject { Fabricate.build(:user, confirmed_at:) } + + context 'when confirmed_at is set' do + let(:confirmed_at) { Time.now.utc } + + it { is_expected.to_not be_unconfirmed } + end + + context 'when confirmed_at is not set' do + let(:confirmed_at) { nil } + + it { is_expected.to be_unconfirmed } + end + end + + describe '#confirm' do + subject { user.confirm } + + let(:new_email) { 'new-email@host.example' } + + before { allow(TriggerWebhookWorker).to receive(:perform_async) } + + context 'when the user is already confirmed' do + let!(:user) { Fabricate(:user, confirmed_at: Time.now.utc, approved: true, unconfirmed_email: new_email) } + + it 'sets email to unconfirmed_email and does not trigger web hook' do + expect { subject } + .to change { user.reload.email }.to(new_email) + expect(TriggerWebhookWorker) + .to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) + end + end + + context 'when the user is a new user' do + let(:user) { Fabricate(:user, confirmed_at: nil, unconfirmed_email: new_email) } + + context 'when the user does not require explicit approval' do + before { Setting.registrations_mode = 'open' } + + it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do + expect { subject } + .to change { user.reload.email }.to(new_email) + expect(TriggerWebhookWorker) + .to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once + end + end + + context 'when registrations mode is approved' do + before { Setting.registrations_mode = 'approved' } + + context 'when the user is already approved' do + before { user.approve! } + + it 'sets email to unconfirmed_email and triggers `account.approved` web hook' do + expect { subject } + .to change { user.reload.email }.to(new_email) + expect(TriggerWebhookWorker) + .to have_received(:perform_async).with('account.approved', 'Account', user.account_id).once + end + end + + context 'when the user is not approved' do + it 'sets email to unconfirmed_email and does not trigger web hook' do + expect { subject } + .to change { user.reload.email }.to(new_email) + expect(TriggerWebhookWorker) + .to_not have_received(:perform_async).with('account.approved', 'Account', user.account_id) + end + end + end + end + end +end diff --git a/spec/system/admin/domain_blocks_spec.rb b/spec/system/admin/domain_blocks_spec.rb index 56a5d119844..c309e39a4fb 100644 --- a/spec/system/admin/domain_blocks_spec.rb +++ b/spec/system/admin/domain_blocks_spec.rb @@ -57,6 +57,30 @@ RSpec.describe 'blocking domains through the moderation interface' do end end + context 'when suspending an already suspended domain and using a lower severity' do + before { Fabricate :domain_block, domain: 'example.com', severity: 'silence' } + + it 'warns about downgrade and does not update' do + visit new_admin_domain_block_path + + submit_domain_block('example.com', 'noop') + + expect(page) + .to have_content(/You have already imposed stricter limits on example.com/) + end + end + + context 'when failing to provide a domain value' do + it 'provides an error about the missing value' do + visit new_admin_domain_block_path + + submit_domain_block('', 'noop') + + expect(page) + .to have_content(/review the error below/) + end + end + context 'when suspending a subdomain of an already-silenced domain' do it 'presents a confirmation screen before suspending the domain' do domain_block = Fabricate(:domain_block, domain: 'example.com', severity: 'silence') diff --git a/spec/validators/existing_username_validator_spec.rb b/spec/validators/existing_username_validator_spec.rb index 25ecb1fbcde..ab5be524534 100644 --- a/spec/validators/existing_username_validator_spec.rb +++ b/spec/validators/existing_username_validator_spec.rb @@ -6,6 +6,7 @@ RSpec.describe ExistingUsernameValidator do let(:record_class) do Class.new do include ActiveModel::Validations + attr_accessor :contact, :friends def self.name diff --git a/spec/validators/language_validator_spec.rb b/spec/validators/language_validator_spec.rb index 19e55f34672..d19b33f27f8 100644 --- a/spec/validators/language_validator_spec.rb +++ b/spec/validators/language_validator_spec.rb @@ -6,6 +6,7 @@ RSpec.describe LanguageValidator do let(:record_class) do Class.new do include ActiveModel::Validations + attr_accessor :locale validates :locale, language: true diff --git a/spec/validators/unreserved_username_validator_spec.rb b/spec/validators/unreserved_username_validator_spec.rb index ad1092109db..55dca7db844 100644 --- a/spec/validators/unreserved_username_validator_spec.rb +++ b/spec/validators/unreserved_username_validator_spec.rb @@ -6,11 +6,17 @@ RSpec.describe UnreservedUsernameValidator do let(:record_class) do Class.new do include ActiveModel::Validations + attr_accessor :username validates_with UnreservedUsernameValidator + + def self.name + 'Foo' + end end end + let(:record) { record_class.new } describe '#validate' do @@ -113,7 +119,7 @@ RSpec.describe UnreservedUsernameValidator do end def stub_reserved_usernames(value) - allow(Setting).to receive(:[]).with('reserved_usernames').and_return(value) + value&.each { |str| Fabricate(:username_block, username: str, exact: true) } end end end diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index 2297dddaa01..55c0347d18e 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -6,6 +6,7 @@ RSpec.describe URLValidator do let(:record_class) do Class.new do include ActiveModel::Validations + attr_accessor :profile validates :profile, url: true diff --git a/spec/workers/activitypub/quote_request_worker_spec.rb b/spec/workers/activitypub/quote_request_worker_spec.rb new file mode 100644 index 00000000000..b0e10aeffc3 --- /dev/null +++ b/spec/workers/activitypub/quote_request_worker_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::QuoteRequestWorker do + subject { described_class.new } + + let(:quoted_account) { Fabricate(:account, inbox_url: 'http://example.com', domain: 'example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:status) { Fabricate(:status, text: 'foo') } + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'TODO') } # TODO: activity URI + + describe '#perform' do + it 'sends the expected QuoteRequest activity' do + subject.perform(quote.id) + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(match_object_shape, quote.account_id, 'http://example.com', {}) + end + + def match_object_shape + match_json_values( + type: 'QuoteRequest', + actor: ActivityPub::TagManager.instance.uri_for(quote.account), + object: ActivityPub::TagManager.instance.uri_for(quoted_status), + instrument: a_hash_including( + id: ActivityPub::TagManager.instance.uri_for(status) + ) + ) + end + end +end diff --git a/spec/workers/activitypub/status_update_distribution_worker_spec.rb b/spec/workers/activitypub/status_update_distribution_worker_spec.rb index e9a70d11d19..58d11db41cb 100644 --- a/spec/workers/activitypub/status_update_distribution_worker_spec.rb +++ b/spec/workers/activitypub/status_update_distribution_worker_spec.rb @@ -9,36 +9,64 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com', domain: 'example.com') } describe '#perform' do - before do - follower.follow!(status.account) - - status.snapshot! - status.text = 'bar' - status.edited_at = Time.now.utc - status.snapshot! - status.save! - end - - context 'with public status' do + context 'with an explicitly edited status' do before do - status.update(visibility: :public) + follower.follow!(status.account) + + status.snapshot! + status.text = 'bar' + status.edited_at = Time.now.utc + status.snapshot! + status.save! end - it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) + context 'with public status' do + before do + status.update(visibility: :public) + end + + it 'delivers to followers' do + expect { subject.perform(status.id) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) + end + end + + context 'with private status' do + before do + status.update(visibility: :private) + end + + it 'delivers to followers' do + expect { subject.perform(status.id) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) end end end - context 'with private status' do + context 'with an implicitly edited status' do before do - status.update(visibility: :private) + follower.follow!(status.account) end - it 'delivers to followers' do - expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do - subject.perform(status.id) + context 'with public status' do + before do + status.update(visibility: :public) + end + + it 'delivers to followers' do + expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) + end + end + + context 'with private status' do + before do + status.update(visibility: :private) + end + + it 'delivers to followers' do + expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) } + .to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything) end end end diff --git a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb index 55b66629e06..1cd01eb3b90 100644 --- a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb @@ -112,7 +112,7 @@ RSpec.describe Scheduler::AccountsStatusesCleanupScheduler do expect { subject.perform } .to change(Status, :count).by(-subject.compute_budget) # Cleanable statuses - .and (not_change { account_bob.statuses.count }) # No cleanup policy for account + .and not_change { account_bob.statuses.count } # No cleanup policy for account .and(not_change { account_dave.statuses.count }) # Disabled cleanup policy end diff --git a/vite.config.mts b/vite.config.mts index 7f93157b7e1..30c0741aaa6 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -65,6 +65,11 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { // but it needs to be scoped to the whole domain 'Service-Worker-Allowed': '/', }, + hmr: { + // Forcing the protocol to be insecure helps if you are proxying your dev server with SSL, + // because Vite still tries to connect to localhost. + protocol: 'ws', + }, port: 3036, }, build: { @@ -184,29 +189,30 @@ async function findEntrypoints() { const entrypoints: Record = {}; // First, JS entrypoints - const jsEntrypoints = await readdir(path.resolve(jsRoot, 'entrypoints'), { + const jsEntrypointsDir = path.resolve(jsRoot, 'entrypoints'); + const jsEntrypoints = await readdir(jsEntrypointsDir, { withFileTypes: true, }); const jsExtTest = /\.[jt]sx?$/; for (const file of jsEntrypoints) { if (file.isFile() && jsExtTest.test(file.name)) { entrypoints[file.name.replace(jsExtTest, '')] = path.resolve( - file.parentPath, + jsEntrypointsDir, file.name, ); } } // Next, SCSS entrypoints - const scssEntrypoints = await readdir( - path.resolve(jsRoot, 'styles/entrypoints'), - { withFileTypes: true }, - ); + const scssEntrypointsDir = path.resolve(jsRoot, 'styles/entrypoints'); + const scssEntrypoints = await readdir(scssEntrypointsDir, { + withFileTypes: true, + }); const scssExtTest = /\.s?css$/; for (const file of scssEntrypoints) { if (file.isFile() && scssExtTest.test(file.name)) { entrypoints[file.name.replace(scssExtTest, '')] = path.resolve( - file.parentPath, + scssEntrypointsDir, file.name, ); } diff --git a/vitest.config.mts b/vitest.config.mts index 7df462ed6db..b129c293f4c 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -49,6 +49,7 @@ const legacyTests: TestProjectInlineConfiguration = { 'tmp/**', ], globals: true, + setupFiles: ['fake-indexeddb/auto'], }, }; diff --git a/yarn.lock b/yarn.lock index 67739e74d4c..26f7cd409e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -73,39 +73,39 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/core@npm:7.27.4" +"@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/core@npm:7.28.0" dependencies: "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.27.3" + "@babel/generator": "npm:^7.28.0" "@babel/helper-compilation-targets": "npm:^7.27.2" "@babel/helper-module-transforms": "npm:^7.27.3" - "@babel/helpers": "npm:^7.27.4" - "@babel/parser": "npm:^7.27.4" + "@babel/helpers": "npm:^7.27.6" + "@babel/parser": "npm:^7.28.0" "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.27.4" - "@babel/types": "npm:^7.27.3" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.0" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10c0/d2d17b106a8d91d3eda754bb3f26b53a12eb7646df73c2b2d2e9b08d90529186bc69e3823f70a96ec6e5719dc2372fb54e14ad499da47ceeb172d2f7008787b5 + checksum: 10c0/423302e7c721e73b1c096217880272e02020dfb697a55ccca60ad01bba90037015f84d0c20c6ce297cf33a19bb704bc5c2b3d3095f5284dfa592bd1de0b9e8c3 languageName: node linkType: hard -"@babel/generator@npm:^7.27.3": - version: 7.27.3 - resolution: "@babel/generator@npm:7.27.3" +"@babel/generator@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/generator@npm:7.28.0" dependencies: - "@babel/parser": "npm:^7.27.3" - "@babel/types": "npm:^7.27.3" - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.25" + "@babel/parser": "npm:^7.28.0" + "@babel/types": "npm:^7.28.0" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" jsesc: "npm:^3.0.2" - checksum: 10c0/341622e17c61d008fc746b655ab95ef7febb543df8efb4148f57cf06e60ade1abe091ed7d6811df17b064d04d64f69bb7f35ab0654137116d55c54a73145a61a + checksum: 10c0/1b3d122268ea3df50fde707ad864d9a55c72621357d5cebb972db3dd76859c45810c56e16ad23123f18f80cc2692f5a015d2858361300f0f224a05dc43d36a92 languageName: node linkType: hard @@ -176,6 +176,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-globals@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/helper-globals@npm:7.28.0" + checksum: 10c0/5a0cd0c0e8c764b5f27f2095e4243e8af6fa145daea2b41b53c0c1414fe6ff139e3640f4e2207ae2b3d2153a1abd346f901c26c290ee7cb3881dd922d4ee9232 + languageName: node + linkType: hard + "@babel/helper-member-expression-to-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-member-expression-to-functions@npm:7.27.1" @@ -293,24 +300,24 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/helpers@npm:7.27.4" +"@babel/helpers@npm:^7.27.6": + version: 7.27.6 + resolution: "@babel/helpers@npm:7.27.6" dependencies: "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.27.3" - checksum: 10c0/3463551420926b3f403c1a30d66ac67bba5c4f73539a8ccb71544da129c4709ac37c57fac740ed8a261b3e6bbbf353b05e03b36ea1a6bf1081604b2a94ca43c1 + "@babel/types": "npm:^7.27.6" + checksum: 10c0/448bac96ef8b0f21f2294a826df9de6bf4026fd023f8a6bb6c782fe3e61946801ca24381490b8e58d861fee75cd695a1882921afbf1f53b0275ee68c938bd6d3 languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.3, @babel/parser@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/parser@npm:7.27.4" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/parser@npm:7.28.0" dependencies: - "@babel/types": "npm:^7.27.3" + "@babel/types": "npm:^7.28.0" bin: parser: ./bin/babel-parser.js - checksum: 10c0/d1bf17e7508585235e2a76594ba81828e48851877112bb8abbecd7161a31fb66654e993e458ddaedb18a3d5fa31970e5f3feca5ae2900f51e6d8d3d35da70dbf + checksum: 10c0/c2ef81d598990fa949d1d388429df327420357cb5200271d0d0a2784f1e6d54afc8301eb8bdf96d8f6c77781e402da93c7dc07980fcc136ac5b9d5f1fce701b5 languageName: node linkType: hard @@ -1157,28 +1164,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.27.4": - version: 7.27.4 - resolution: "@babel/traverse@npm:7.27.4" +"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/traverse@npm:7.28.0" dependencies: "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.27.3" - "@babel/parser": "npm:^7.27.4" + "@babel/generator": "npm:^7.28.0" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.0" "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.27.3" + "@babel/types": "npm:^7.28.0" debug: "npm:^4.3.1" - globals: "npm:^11.1.0" - checksum: 10c0/6de8aa2a0637a6ee6d205bf48b9e923928a02415771fdec60085ed754dcdf605e450bb3315c2552fa51c31a4662275b45d5ae4ad527ce55a7db9acebdbbbb8ed + checksum: 10c0/32794402457827ac558173bcebdcc0e3a18fa339b7c41ca35621f9f645f044534d91bb923ff385f5f960f2e495f56ce18d6c7b0d064d2f0ccb55b285fa6bc7b9 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.4.4": - version: 7.27.3 - resolution: "@babel/types@npm:7.27.3" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.6, @babel/types@npm:^7.28.0, @babel/types@npm:^7.4.4": + version: 7.28.1 + resolution: "@babel/types@npm:7.28.1" dependencies: "@babel/helper-string-parser": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10c0/bafdfc98e722a6b91a783b6f24388f478fd775f0c0652e92220e08be2cc33e02d42088542f1953ac5e5ece2ac052172b3dadedf12bec9aae57899e92fb9a9757 + checksum: 10c0/5e99b346c11ee42ffb0cadc28159fe0b184d865a2cc1593df79b199772a534f6453969b4942aa5e4a55a3081863096e1cc3fc1c724d826926dc787cf229b845d languageName: node linkType: hard @@ -1787,31 +1794,31 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.3.1": - version: 1.3.1 - resolution: "@emnapi/core@npm:1.3.1" +"@emnapi/core@npm:^1.4.3": + version: 1.4.5 + resolution: "@emnapi/core@npm:1.4.5" dependencies: - "@emnapi/wasi-threads": "npm:1.0.1" + "@emnapi/wasi-threads": "npm:1.0.4" tslib: "npm:^2.4.0" - checksum: 10c0/d3be1044ad704e2c486641bc18908523490f28c7d38bd12d9c1d4ce37d39dae6c4aecd2f2eaf44c6e3bd90eaf04e0591acc440b1b038cdf43cce078a355a0ea0 + checksum: 10c0/da4a57f65f325d720d0e0d1a9c6618b90c4c43a5027834a110476984e1d47c95ebaed4d316b5dddb9c0ed9a493ffeb97d1934f9677035f336d8a36c1f3b2818f languageName: node linkType: hard -"@emnapi/runtime@npm:^1.3.1": - version: 1.3.1 - resolution: "@emnapi/runtime@npm:1.3.1" +"@emnapi/runtime@npm:^1.4.3": + version: 1.4.5 + resolution: "@emnapi/runtime@npm:1.4.5" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/060ffede50f1b619c15083312b80a9e62a5b0c87aa8c1b54854c49766c9d69f8d1d3d87bd963a647071263a320db41b25eaa50b74d6a80dcc763c23dbeaafd6c + checksum: 10c0/37a0278be5ac81e918efe36f1449875cbafba947039c53c65a1f8fc238001b866446fc66041513b286baaff5d6f9bec667f5164b3ca481373a8d9cb65bfc984b languageName: node linkType: hard -"@emnapi/wasi-threads@npm:1.0.1": - version: 1.0.1 - resolution: "@emnapi/wasi-threads@npm:1.0.1" +"@emnapi/wasi-threads@npm:1.0.4": + version: 1.0.4 + resolution: "@emnapi/wasi-threads@npm:1.0.4" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/1e0c8036b8d53e9b07cc9acf021705ef6c86ab6b13e1acda7fffaf541a2d3565072afb92597419173ced9ea14f6bf32fce149106e669b5902b825e8b499e5c6c + checksum: 10c0/2c91a53e62f875800baf035c4d42c9c0d18e5afd9a31ca2aac8b435aeaeaeaac386b5b3d0d0e70aa7a5a9852bbe05106b1f680cd82cce03145c703b423d41313 languageName: node linkType: hard @@ -1932,6 +1939,13 @@ __metadata: languageName: node linkType: hard +"@epic-web/invariant@npm:^1.0.0": + version: 1.0.0 + resolution: "@epic-web/invariant@npm:1.0.0" + checksum: 10c0/72dbeb026e4e4eb3bc9c65739b91408ca77ab7d603a2494fa2eff3790ec22892c4caba751cffdf30f5ccf0e7ba79c1e9c96cf0a357404b9432bf1365baac23ca + languageName: node + linkType: hard + "@es-joy/jsdoccomment@npm:~0.52.0": version: 0.52.0 resolution: "@es-joy/jsdoccomment@npm:0.52.0" @@ -2138,30 +2152,30 @@ __metadata: languageName: node linkType: hard -"@eslint/config-array@npm:^0.19.2": - version: 0.19.2 - resolution: "@eslint/config-array@npm:0.19.2" +"@eslint/config-array@npm:^0.21.0": + version: 0.21.0 + resolution: "@eslint/config-array@npm:0.21.0" dependencies: "@eslint/object-schema": "npm:^2.1.6" debug: "npm:^4.3.1" minimatch: "npm:^3.1.2" - checksum: 10c0/dd68da9abb32d336233ac4fe0db1e15a0a8d794b6e69abb9e57545d746a97f6f542496ff9db0d7e27fab1438546250d810d90b1904ac67677215b8d8e7573f3d + checksum: 10c0/0ea801139166c4aa56465b309af512ef9b2d3c68f9198751bbc3e21894fe70f25fbf26e1b0e9fffff41857bc21bfddeee58649ae6d79aadcd747db0c5dca771f languageName: node linkType: hard -"@eslint/config-helpers@npm:^0.2.0": - version: 0.2.0 - resolution: "@eslint/config-helpers@npm:0.2.0" - checksum: 10c0/743a64653e13177029108f57ab47460ded08e3412c86216a14b7e8ab2dc79c2b64be45bf55c5ef29f83692a707dc34cf1e9217e4b8b4b272a0d9b691fdaf6a2a +"@eslint/config-helpers@npm:^0.3.0": + version: 0.3.0 + resolution: "@eslint/config-helpers@npm:0.3.0" + checksum: 10c0/013ae7b189eeae8b30cc2ee87bc5c9c091a9cd615579003290eb28bebad5d78806a478e74ba10b3fe08ed66975b52af7d2cd4b4b43990376412b14e5664878c8 languageName: node linkType: hard -"@eslint/core@npm:^0.12.0": - version: 0.12.0 - resolution: "@eslint/core@npm:0.12.0" +"@eslint/core@npm:^0.15.0, @eslint/core@npm:^0.15.1": + version: 0.15.1 + resolution: "@eslint/core@npm:0.15.1" dependencies: "@types/json-schema": "npm:^7.0.15" - checksum: 10c0/d032af81195bb28dd800c2b9617548c6c2a09b9490da3c5537fd2a1201501666d06492278bb92cfccac1f7ac249e58601dd87f813ec0d6a423ef0880434fa0c3 + checksum: 10c0/abaf641940776638b8c15a38d99ce0dac551a8939310ec81b9acd15836a574cf362588eaab03ab11919bc2a0f9648b19ea8dee33bf12675eb5b6fd38bda6f25e languageName: node linkType: hard @@ -2182,10 +2196,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.23.0, @eslint/js@npm:^9.23.0": - version: 9.23.0 - resolution: "@eslint/js@npm:9.23.0" - checksum: 10c0/4e70869372b6325389e0ab51cac6d3062689807d1cef2c3434857571422ce11dde3c62777af85c382b9f94d937127598d605d2086787f08611351bf99faded81 +"@eslint/js@npm:9.32.0, @eslint/js@npm:^9.23.0": + version: 9.32.0 + resolution: "@eslint/js@npm:9.32.0" + checksum: 10c0/f71e8f9146638d11fb15238279feff98801120a4d4130f1c587c4f09b024ff5ec01af1ba88e97ba6b7013488868898a668f77091300cc3d4394c7a8ed32d2667 languageName: node linkType: hard @@ -2196,13 +2210,13 @@ __metadata: languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.2.7": - version: 0.2.7 - resolution: "@eslint/plugin-kit@npm:0.2.7" +"@eslint/plugin-kit@npm:^0.3.4": + version: 0.3.4 + resolution: "@eslint/plugin-kit@npm:0.3.4" dependencies: - "@eslint/core": "npm:^0.12.0" + "@eslint/core": "npm:^0.15.1" levn: "npm:^0.4.1" - checksum: 10c0/0a1aff1ad63e72aca923217e556c6dfd67d7cd121870eb7686355d7d1475d569773528a8b2111b9176f3d91d2ea81f7413c34600e8e5b73d59e005d70780b633 + checksum: 10c0/64331ca100f62a0115d10419a28059d0f377e390192163b867b9019517433d5073d10b4ec21f754fa01faf832aceb34178745924baab2957486f8bf95fd628d2 languageName: node linkType: hard @@ -2348,26 +2362,6 @@ __metadata: languageName: node linkType: hard -"@formatjs/ts-transformer@npm:3.13.34": - version: 3.13.34 - resolution: "@formatjs/ts-transformer@npm:3.13.34" - dependencies: - "@formatjs/icu-messageformat-parser": "npm:2.11.2" - "@types/json-stable-stringify": "npm:^1.1.0" - "@types/node": "npm:^22.0.0" - chalk: "npm:^4.1.2" - json-stable-stringify: "npm:^1.1.1" - tslib: "npm:^2.8.0" - typescript: "npm:^5.6.0" - peerDependencies: - ts-jest: ^29 - peerDependenciesMeta: - ts-jest: - optional: true - checksum: 10c0/2e53af5a53cab71be0ba2fc16ba856c95bf336c063cc835486cd3a68d01013c5c08026b34667f4bdf99422e74faa8eb1f26d5fe8006f3a1ae9c77e065599362e - languageName: node - linkType: hard - "@formatjs/ts-transformer@npm:3.14.0": version: 3.14.0 resolution: "@formatjs/ts-transformer@npm:3.14.0" @@ -2497,10 +2491,10 @@ __metadata: languageName: node linkType: hard -"@ioredis/commands@npm:^1.1.1": - version: 1.2.0 - resolution: "@ioredis/commands@npm:1.2.0" - checksum: 10c0/a5d3c29dd84d8a28b7c67a441ac1715cbd7337a7b88649c0f17c345d89aa218578d2b360760017c48149ef8a70f44b051af9ac0921a0622c2b479614c4f65b36 +"@ioredis/commands@npm:^1.3.0": + version: 1.3.0 + resolution: "@ioredis/commands@npm:1.3.0" + checksum: 10c0/5ab990a8f69c20daf3d7d64307aa9f13ee727c92ab4c7664a6943bb500227667a0c368892e9c4913f06416377db47dba78d58627fe723da476d25f2c04a6d5aa languageName: node linkType: hard @@ -2542,14 +2536,13 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.5": - version: 0.3.5 - resolution: "@jridgewell/gen-mapping@npm:0.3.5" +"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.12 + resolution: "@jridgewell/gen-mapping@npm:0.3.12" dependencies: - "@jridgewell/set-array": "npm:^1.2.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/sourcemap-codec": "npm:^1.5.0" "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10c0/1be4fd4a6b0f41337c4f5fdf4afc3bd19e39c3691924817108b82ffcb9c9e609c273f936932b9fba4b3a298ce2eb06d9bff4eb1cc3bd81c4f4ee1b4917e25feb + checksum: 10c0/32f771ae2467e4d440be609581f7338d786d3d621bac3469e943b9d6d116c23c4becb36f84898a92bbf2f3c0511365c54a945a3b86a83141547a2a360a5ec0c7 languageName: node linkType: hard @@ -2560,13 +2553,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.2.1": - version: 1.2.1 - resolution: "@jridgewell/set-array@npm:1.2.1" - checksum: 10c0/2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 - languageName: node - linkType: hard - "@jridgewell/source-map@npm:^0.3.3": version: 0.3.6 resolution: "@jridgewell/source-map@npm:0.3.6" @@ -2577,20 +2563,20 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": +"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.0 resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": - version: 0.3.25 - resolution: "@jridgewell/trace-mapping@npm:0.3.25" +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28": + version: 0.3.29 + resolution: "@jridgewell/trace-mapping@npm:0.3.29" dependencies: "@jridgewell/resolve-uri": "npm:^3.1.0" "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + checksum: 10c0/fb547ba31658c4d74eb17e7389f4908bf7c44cef47acb4c5baa57289daf68e6fe53c639f41f751b3923aca67010501264f70e7b49978ad1f040294b22c37b333 languageName: node linkType: hard @@ -2626,6 +2612,7 @@ __metadata: "@storybook/react-vite": "npm:^9.0.4" "@testing-library/dom": "npm:^10.2.0" "@testing-library/react": "npm:^16.0.0" + "@types/debug": "npm:^4" "@types/emoji-mart": "npm:3.0.14" "@types/escape-html": "npm:^1.0.2" "@types/hoist-non-react-statics": "npm:^3.3.1" @@ -2666,7 +2653,8 @@ __metadata: cocoon-js-vanilla: "npm:^1.5.1" color-blend: "npm:^4.0.0" core-js: "npm:^3.30.2" - cross-env: "npm:^7.0.3" + cross-env: "npm:^10.0.0" + debug: "npm:^4.4.1" detect-passive-events: "npm:^2.0.3" emoji-mart: "npm:emoji-mart-lazyload@latest" emojibase: "npm:^16.0.0" @@ -2676,13 +2664,14 @@ __metadata: eslint: "npm:^9.23.0" eslint-import-resolver-typescript: "npm:^4.2.5" eslint-plugin-formatjs: "npm:^5.3.1" - eslint-plugin-import: "npm:~2.31.0" - eslint-plugin-jsdoc: "npm:^51.0.0" + eslint-plugin-import: "npm:~2.32.0" + eslint-plugin-jsdoc: "npm:^52.0.0" eslint-plugin-jsx-a11y: "npm:~6.10.2" eslint-plugin-promise: "npm:~7.2.1" eslint-plugin-react: "npm:^7.37.4" eslint-plugin-react-hooks: "npm:^5.2.0" eslint-plugin-storybook: "npm:^9.0.4" + fake-indexeddb: "npm:^6.0.1" fast-glob: "npm:^3.3.3" fuzzysort: "npm:^3.0.0" globals: "npm:^16.0.0" @@ -2827,14 +2816,14 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^0.2.7": - version: 0.2.7 - resolution: "@napi-rs/wasm-runtime@npm:0.2.7" +"@napi-rs/wasm-runtime@npm:^0.2.11": + version: 0.2.12 + resolution: "@napi-rs/wasm-runtime@npm:0.2.12" dependencies: - "@emnapi/core": "npm:^1.3.1" - "@emnapi/runtime": "npm:^1.3.1" - "@tybys/wasm-util": "npm:^0.9.0" - checksum: 10c0/04a5edd79144bfa4e821a373fb6d4939f10c578c5f3633b5e67a57d0f5e36a593f595834d26654ea757bba7cd80b6c42d0d1405d6a8460c5d774e8cd5c9548a4 + "@emnapi/core": "npm:^1.4.3" + "@emnapi/runtime": "npm:^1.4.3" + "@tybys/wasm-util": "npm:^0.10.0" + checksum: 10c0/6d07922c0613aab30c6a497f4df297ca7c54e5b480e00035e0209b872d5c6aab7162fc49477267556109c2c7ed1eb9c65a174e27e9b87568106a87b0a6e3ca7d languageName: node linkType: hard @@ -3211,10 +3200,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-beta.19": - version: 1.0.0-beta.19 - resolution: "@rolldown/pluginutils@npm:1.0.0-beta.19" - checksum: 10c0/e4205df56e6231a347ac601d044af365639741d51b5bea4e91ecc37e19e9777cb79d1daa924b8709ddf1f743ed6922e4e68e2445126434c4d420d9f4416f4feb +"@rolldown/pluginutils@npm:1.0.0-beta.27": + version: 1.0.0-beta.27 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27" + checksum: 10c0/9658f235b345201d4f6bfb1f32da9754ca164f892d1cb68154fe5f53c1df42bd675ecd409836dff46884a7847d6c00bdc38af870f7c81e05bba5c2645eb4ab9c languageName: node linkType: hard @@ -3815,12 +3804,12 @@ __metadata: languageName: node linkType: hard -"@tybys/wasm-util@npm:^0.9.0": - version: 0.9.0 - resolution: "@tybys/wasm-util@npm:0.9.0" +"@tybys/wasm-util@npm:^0.10.0": + version: 0.10.0 + resolution: "@tybys/wasm-util@npm:0.10.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/f9fde5c554455019f33af6c8215f1a1435028803dc2a2825b077d812bed4209a1a64444a4ca0ce2ea7e1175c8d88e2f9173a36a33c199e8a5c671aa31de8242d + checksum: 10c0/044feba55c1e2af703aa4946139969badb183ce1a659a75ed60bc195a90e73a3f3fc53bcd643497c9954597763ddb051fec62f80962b2ca6fc716ba897dc696e languageName: node linkType: hard @@ -3917,11 +3906,20 @@ __metadata: linkType: hard "@types/cors@npm:^2.8.16": - version: 2.8.18 - resolution: "@types/cors@npm:2.8.18" + version: 2.8.19 + resolution: "@types/cors@npm:2.8.19" dependencies: "@types/node": "npm:*" - checksum: 10c0/9dd1075de0e3a40c304826668960c797e67e597a734fb8e8ab404561f31ef2bd553ef5500eb86da7e91a344bee038a59931d2fbf182fbce09f13816f51fdd80e + checksum: 10c0/b5dd407040db7d8aa1bd36e79e5f3f32292f6b075abc287529e9f48df1a25fda3e3799ba30b4656667ffb931d3b75690c1d6ca71e39f7337ea6dfda8581916d0 + languageName: node + linkType: hard + +"@types/debug@npm:^4": + version: 4.1.12 + resolution: "@types/debug@npm:4.1.12" + dependencies: + "@types/ms": "npm:*" + checksum: 10c0/5dcd465edbb5a7f226e9a5efd1f399c6172407ef5840686b73e3608ce135eeca54ae8037dcd9f16bdb2768ac74925b820a8b9ecc588a58ca09eca6acabe33e2f languageName: node linkType: hard @@ -3999,14 +3997,14 @@ __metadata: linkType: hard "@types/express@npm:^4.17.17": - version: 4.17.22 - resolution: "@types/express@npm:4.17.22" + version: 4.17.23 + resolution: "@types/express@npm:4.17.23" dependencies: "@types/body-parser": "npm:*" "@types/express-serve-static-core": "npm:^4.17.33" "@types/qs": "npm:*" "@types/serve-static": "npm:*" - checksum: 10c0/15c10a5ebb40a0356baa95ed374a2150d862786c9fccbdd724df12acc9c8cb08fbe1d34b446b1bcef2dbe5305cb3013fb39fba791baa54ef6df8056482776abb + checksum: 10c0/60490cd4f73085007247e7d4fafad0a7abdafa34fa3caba2757512564ca5e094ece7459f0f324030a63d513f967bb86579a8682af76ae2fd718e889b0a2a4fe8 languageName: node linkType: hard @@ -4018,12 +4016,13 @@ __metadata: linkType: hard "@types/hoist-non-react-statics@npm:^3.3.1": - version: 3.3.6 - resolution: "@types/hoist-non-react-statics@npm:3.3.6" + version: 3.3.7 + resolution: "@types/hoist-non-react-statics@npm:3.3.7" dependencies: - "@types/react": "npm:*" hoist-non-react-statics: "npm:^3.3.0" - checksum: 10c0/149a4c217d81f21f8a1e152160a59d5b99b6a9aa6d354385d5f5bc02760cbf1e170a8442ba92eb653befff44b0c5bc2234bb77ce33e0d11a65f779e8bab5c321 + peerDependencies: + "@types/react": "*" + checksum: 10c0/ed8f4e88338f7d021d0f956adf6089d2a12b2e254a03c05292324f2e986d2376eb9efdb8a4f04596823e8fca88c9d06361d20dab4a2a00dc935fb36ac911de55 languageName: node linkType: hard @@ -4079,9 +4078,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.195": - version: 4.17.17 - resolution: "@types/lodash@npm:4.17.17" - checksum: 10c0/8e75df02a15f04d4322c5a503e4efd0e7a92470570ce80f17e9f11ce2b1f1a7c994009c9bcff39f07e0f9ffd8ccaff09b3598997c404b801abd5a7eee5a639dc + version: 4.17.20 + resolution: "@types/lodash@npm:4.17.20" + checksum: 10c0/98cdd0faae22cbb8079a01a3bb65aa8f8c41143367486c1cbf5adc83f16c9272a2a5d2c1f541f61d0d73da543c16ee1d21cf2ef86cb93cd0cc0ac3bced6dd88f languageName: node linkType: hard @@ -4106,6 +4105,13 @@ __metadata: languageName: node linkType: hard +"@types/ms@npm:*": + version: 2.1.0 + resolution: "@types/ms@npm:2.1.0" + checksum: 10c0/5ce692ffe1549e1b827d99ef8ff71187457e0eb44adbae38fdf7b9a74bae8d20642ee963c14516db1d35fa2652e65f47680fdf679dcbde52bbfadd021f497225 + languageName: node + linkType: hard + "@types/node@npm:*, @types/node@npm:^22.0.0": version: 22.13.14 resolution: "@types/node@npm:22.13.14" @@ -4130,13 +4136,13 @@ __metadata: linkType: hard "@types/pg@npm:^8.6.6": - version: 8.15.4 - resolution: "@types/pg@npm:8.15.4" + version: 8.15.5 + resolution: "@types/pg@npm:8.15.5" dependencies: "@types/node": "npm:*" pg-protocol: "npm:*" pg-types: "npm:^2.2.0" - checksum: 10c0/7f9295cb2d934681bba84f7caad529c3b100d87e83ad0732c7fe496f4f79e42a795097321db54e010fcff22cb5e410cf683b4c9941907ee4564c822242816e91 + checksum: 10c0/19a3cc1811918753f8c827733648c3a85c7b0355bf207c44eb1a3b79b2e6a0d85cb5457ec550d860fc9be7e88c7587a3600958ec8c61fa1ad573061c63af93f0 languageName: node linkType: hard @@ -4148,9 +4154,9 @@ __metadata: linkType: hard "@types/prop-types@npm:*, @types/prop-types@npm:^15.7.5": - version: 15.7.14 - resolution: "@types/prop-types@npm:15.7.14" - checksum: 10c0/1ec775160bfab90b67a782d735952158c7e702ca4502968aa82565bd8e452c2de8601c8dfe349733073c31179116cf7340710160d3836aa8a1ef76d1532893b1 + version: 15.7.15 + resolution: "@types/prop-types@npm:15.7.15" + checksum: 10c0/b59aad1ad19bf1733cf524fd4e618196c6c7690f48ee70a327eb450a42aab8e8a063fbe59ca0a5701aebe2d92d582292c0fb845ea57474f6a15f6994b0e260b2 languageName: node linkType: hard @@ -4389,145 +4395,106 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.29.1": - version: 8.29.1 - resolution: "@typescript-eslint/eslint-plugin@npm:8.29.1" +"@typescript-eslint/eslint-plugin@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.38.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.29.1" - "@typescript-eslint/type-utils": "npm:8.29.1" - "@typescript-eslint/utils": "npm:8.29.1" - "@typescript-eslint/visitor-keys": "npm:8.29.1" + "@typescript-eslint/scope-manager": "npm:8.38.0" + "@typescript-eslint/type-utils": "npm:8.38.0" + "@typescript-eslint/utils": "npm:8.38.0" + "@typescript-eslint/visitor-keys": "npm:8.38.0" graphemer: "npm:^1.4.0" - ignore: "npm:^5.3.1" + ignore: "npm:^7.0.0" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^2.0.1" + ts-api-utils: "npm:^2.1.0" peerDependencies: - "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 + "@typescript-eslint/parser": ^8.38.0 eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/a3ed7556edcac374cab622862f2f9adedc91ca305d6937db6869a0253d675858c296cb5413980e8404fc39737117faaf35b00c6804664b3c542bdc417502532f + checksum: 10c0/199b82e9f0136baecf515df7c31bfed926a7c6d4e6298f64ee1a77c8bdd7a8cb92a2ea55a5a345c9f2948a02f7be6d72530efbe803afa1892b593fbd529d0c27 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.29.1": - version: 8.29.1 - resolution: "@typescript-eslint/parser@npm:8.29.1" +"@typescript-eslint/parser@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/parser@npm:8.38.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.29.1" - "@typescript-eslint/types": "npm:8.29.1" - "@typescript-eslint/typescript-estree": "npm:8.29.1" - "@typescript-eslint/visitor-keys": "npm:8.29.1" + "@typescript-eslint/scope-manager": "npm:8.38.0" + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/typescript-estree": "npm:8.38.0" + "@typescript-eslint/visitor-keys": "npm:8.38.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/af3570ff58c42c2014e5c117bebf91120737fb139d94415ca2711844990e95252c3006ccc699871fe3f592cc1a3f4ebfdc9dd5f6cb29b6b128c2524fcf311b75 + checksum: 10c0/5580c2a328f0c15f85e4a0961a07584013cc0aca85fe868486187f7c92e9e3f6602c6e3dab917b092b94cd492ed40827c6f5fea42730bef88eb17592c947adf4 languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.33.0": - version: 8.33.0 - resolution: "@typescript-eslint/project-service@npm:8.33.0" +"@typescript-eslint/project-service@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/project-service@npm:8.38.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.33.0" - "@typescript-eslint/types": "npm:^8.33.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.38.0" + "@typescript-eslint/types": "npm:^8.38.0" debug: "npm:^4.3.4" - checksum: 10c0/a863d9e3be5ffb53c9d57b25b7a35149dae01afd942dd7fc36bd72a4230676ae12d0f37a789cddaf1baf71e3b35f09436bebbd081336e667b4181b48d0afe8f5 - languageName: node - linkType: hard - -"@typescript-eslint/scope-manager@npm:8.29.1": - version: 8.29.1 - resolution: "@typescript-eslint/scope-manager@npm:8.29.1" - dependencies: - "@typescript-eslint/types": "npm:8.29.1" - "@typescript-eslint/visitor-keys": "npm:8.29.1" - checksum: 10c0/8b87a04f01ebc13075e352509bca8f31c757c3220857fa473ac155f6bdf7f30fe82765d0c3d8e790f7fad394a32765bd9f716b97c08e17581d358c76086d51af - languageName: node - linkType: hard - -"@typescript-eslint/scope-manager@npm:8.33.0": - version: 8.33.0 - resolution: "@typescript-eslint/scope-manager@npm:8.33.0" - dependencies: - "@typescript-eslint/types": "npm:8.33.0" - "@typescript-eslint/visitor-keys": "npm:8.33.0" - checksum: 10c0/eb259add242ce40642e7272b414c92ae9407d97cb304981f17f0de0846d5c4ab47d41816ef13da3d3976fe0b7a74df291525be27e4fe4f0ab5d35e86d340faa0 - languageName: node - linkType: hard - -"@typescript-eslint/tsconfig-utils@npm:8.33.0, @typescript-eslint/tsconfig-utils@npm:^8.33.0": - version: 8.33.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.33.0" peerDependencies: typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/6e9a8e73e65b925f908f31e00be4f1b8d7e89f45d97fa703f468115943c297fc2cc6f9daa0c12b9607f39186f033ac244515f11710df7e1df8302c815ed57389 + checksum: 10c0/87d2f55521e289bbcdc666b1f4587ee2d43039cee927310b05abaa534b528dfb1b5565c1545bb4996d7fbdf9d5a3b0aa0e6c93a8f1289e3fcfd60d246364a884 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.29.1": - version: 8.29.1 - resolution: "@typescript-eslint/type-utils@npm:8.29.1" +"@typescript-eslint/scope-manager@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/scope-manager@npm:8.38.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.29.1" - "@typescript-eslint/utils": "npm:8.29.1" + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/visitor-keys": "npm:8.38.0" + checksum: 10c0/ceaf489ea1f005afb187932a7ee363dfe1e0f7cc3db921283991e20e4c756411a5e25afbec72edd2095d6a4384f73591f4c750cf65b5eaa650c90f64ef9fe809 + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.38.0, @typescript-eslint/tsconfig-utils@npm:^8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.38.0" + peerDependencies: + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/1a90da16bf1f7cfbd0303640a8ead64a0080f2b1d5969994bdac3b80abfa1177f0c6fbf61250bae082e72cf5014308f2f5cc98edd6510202f13420a7ffd07a84 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/type-utils@npm:8.38.0" + dependencies: + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/typescript-estree": "npm:8.38.0" + "@typescript-eslint/utils": "npm:8.38.0" debug: "npm:^4.3.4" - ts-api-utils: "npm:^2.0.1" + ts-api-utils: "npm:^2.1.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/72cc01dbac866b0a7c7b1f637ad03ffd22f6d3617f70f06f485cf3096fddfc821fdc56de1a072cc6af70250c63698a3e5a910f67fe46eea941955b6e0da1b2bd + checksum: 10c0/27795c4bd0be395dda3424e57d746639c579b7522af1c17731b915298a6378fd78869e8e141526064b6047db2c86ba06444469ace19c98cda5779d06f4abd37c languageName: node linkType: hard -"@typescript-eslint/types@npm:8.29.1": - version: 8.29.1 - resolution: "@typescript-eslint/types@npm:8.29.1" - checksum: 10c0/bbcb9e4f38df4485092b51ac6bb62d65f321d914ab58dc0ff1eaa7787dc0b4a39e237c2617b9f2c2bcb91a343f30de523e3544f69affa1ee4287a3ef2fc10ce7 +"@typescript-eslint/types@npm:8.38.0, @typescript-eslint/types@npm:^8.34.1, @typescript-eslint/types@npm:^8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/types@npm:8.38.0" + checksum: 10c0/f0ac0060c98c0f3d1871f107177b6ae25a0f1846ca8bd8cfc7e1f1dd0ddce293cd8ac4a5764d6a767de3503d5d01defcd68c758cb7ba6de52f82b209a918d0d2 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.33.0": - version: 8.33.0 - resolution: "@typescript-eslint/types@npm:8.33.0" - checksum: 10c0/348b64eb408719d7711a433fc9716e0c2aab8b3f3676f5a1cc2e00269044132282cf655deb6d0dd9817544116909513de3b709005352d186949d1014fad1a3cb - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:^8.33.0, @typescript-eslint/types@npm:^8.34.1": - version: 8.36.0 - resolution: "@typescript-eslint/types@npm:8.36.0" - checksum: 10c0/cacb941a0caad6ab556c416051b97ec33b364b7c8e0703e2729ae43f12daf02b42eef12011705329107752e3f1685ca82cfffe181d637f85907293cb634bee31 - languageName: node - linkType: hard - -"@typescript-eslint/typescript-estree@npm:8.29.1": - version: 8.29.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.29.1" +"@typescript-eslint/typescript-estree@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.38.0" dependencies: - "@typescript-eslint/types": "npm:8.29.1" - "@typescript-eslint/visitor-keys": "npm:8.29.1" - debug: "npm:^4.3.4" - fast-glob: "npm:^3.3.2" - is-glob: "npm:^4.0.3" - minimatch: "npm:^9.0.4" - semver: "npm:^7.6.0" - ts-api-utils: "npm:^2.0.1" - peerDependencies: - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/33c46c667d9262e5625d5d0064338711b342e62c5675ded6811a2cb13ee5de2f71b90e9d0be5cb338b11b1a329c376a6bbf6c3d24fa8fb457b2eefc9f3298513 - languageName: node - linkType: hard - -"@typescript-eslint/typescript-estree@npm:8.33.0": - version: 8.33.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.33.0" - dependencies: - "@typescript-eslint/project-service": "npm:8.33.0" - "@typescript-eslint/tsconfig-utils": "npm:8.33.0" - "@typescript-eslint/types": "npm:8.33.0" - "@typescript-eslint/visitor-keys": "npm:8.33.0" + "@typescript-eslint/project-service": "npm:8.38.0" + "@typescript-eslint/tsconfig-utils": "npm:8.38.0" + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/visitor-keys": "npm:8.38.0" debug: "npm:^4.3.4" fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" @@ -4536,163 +4503,166 @@ __metadata: ts-api-utils: "npm:^2.1.0" peerDependencies: typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/677b12b2e5780ffaef508bddbf8712fe2c3413f3d14fd8fd0cfbe22952a81c6642b3cc26984cf27fdfc3dd2457ae5f8aa04437d3b0ae32987a1895f9648ca7b2 + checksum: 10c0/00a00f6549877f4ae5c2847fa5ac52bf42cbd59a87533856c359e2746e448ed150b27a6137c92fd50c06e6a4b39e386d6b738fac97d80d05596e81ce55933230 languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.29.1": - version: 8.29.1 - resolution: "@typescript-eslint/utils@npm:8.29.1" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.29.1" - "@typescript-eslint/types": "npm:8.29.1" - "@typescript-eslint/typescript-estree": "npm:8.29.1" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/1b2704b769b0c9353cf26a320ecf9775ba51c94c7c30e2af80ca31f4cb230f319762ab06535fcb26b6963144bbeaa53233b34965907c506283861b013f5b95fc - languageName: node - linkType: hard - -"@typescript-eslint/utils@npm:^8.27.0, @typescript-eslint/utils@npm:^8.8.1": - version: 8.33.0 - resolution: "@typescript-eslint/utils@npm:8.33.0" +"@typescript-eslint/utils@npm:8.38.0, @typescript-eslint/utils@npm:^8.27.0, @typescript-eslint/utils@npm:^8.8.1": + version: 8.38.0 + resolution: "@typescript-eslint/utils@npm:8.38.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.7.0" - "@typescript-eslint/scope-manager": "npm:8.33.0" - "@typescript-eslint/types": "npm:8.33.0" - "@typescript-eslint/typescript-estree": "npm:8.33.0" + "@typescript-eslint/scope-manager": "npm:8.38.0" + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/typescript-estree": "npm:8.38.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/a0adb9e13d8f8d8f86ae2e905f3305ad60732e760364b291de66a857a551485d37c23e923299078a47f75d3cca643e1f2aefa010a0beb4cb0d08d0507c1038e1 + checksum: 10c0/e97a45bf44f315f9ed8c2988429e18c88e3369c9ee3227ee86446d2d49f7325abebbbc9ce801e178f676baa986d3e1fd4b5391f1640c6eb8944c123423ae43bb languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.29.1": - version: 8.29.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.29.1" +"@typescript-eslint/visitor-keys@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.38.0" dependencies: - "@typescript-eslint/types": "npm:8.29.1" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10c0/0c12e83c84a754161c89e594a96454799669979c7021a8936515ec574a1fa1d6e3119e0eacf502e07a0fa7254974558ea7a48901c8bfed3c46579a61b655e4f5 + "@typescript-eslint/types": "npm:8.38.0" + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10c0/071a756e383f41a6c9e51d78c8c64bd41cd5af68b0faef5fbaec4fa5dbd65ec9e4cd610c2e2cdbe9e2facc362995f202850622b78e821609a277b5b601a1d4ec languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.33.0": - version: 8.33.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.33.0" - dependencies: - "@typescript-eslint/types": "npm:8.33.0" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10c0/41660f241e78314f69d251792f369ef1eeeab3b40fe4ab11b794d402c95bcb82b61d3e91763e7ab9b0f22011a7ac9c8f9dfd91734d61c9f4eaf4f7660555b53b +"@unrs/resolver-binding-android-arm-eabi@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.11.1" + conditions: os=android & cpu=arm languageName: node linkType: hard -"@unrs/resolver-binding-darwin-arm64@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-darwin-arm64@npm:1.3.2" +"@unrs/resolver-binding-android-arm64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-android-arm64@npm:1.11.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-darwin-arm64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-darwin-arm64@npm:1.11.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@unrs/resolver-binding-darwin-x64@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-darwin-x64@npm:1.3.2" +"@unrs/resolver-binding-darwin-x64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-darwin-x64@npm:1.11.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@unrs/resolver-binding-freebsd-x64@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-freebsd-x64@npm:1.3.2" +"@unrs/resolver-binding-freebsd-x64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-freebsd-x64@npm:1.11.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.3.2" +"@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.11.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@unrs/resolver-binding-linux-arm-musleabihf@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-linux-arm-musleabihf@npm:1.3.2" +"@unrs/resolver-binding-linux-arm-musleabihf@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm-musleabihf@npm:1.11.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@unrs/resolver-binding-linux-arm64-gnu@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-linux-arm64-gnu@npm:1.3.2" +"@unrs/resolver-binding-linux-arm64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm64-gnu@npm:1.11.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@unrs/resolver-binding-linux-arm64-musl@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-linux-arm64-musl@npm:1.3.2" +"@unrs/resolver-binding-linux-arm64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm64-musl@npm:1.11.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@unrs/resolver-binding-linux-ppc64-gnu@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-linux-ppc64-gnu@npm:1.3.2" +"@unrs/resolver-binding-linux-ppc64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-ppc64-gnu@npm:1.11.1" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@unrs/resolver-binding-linux-s390x-gnu@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-linux-s390x-gnu@npm:1.3.2" +"@unrs/resolver-binding-linux-riscv64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-riscv64-gnu@npm:1.11.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-riscv64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-riscv64-musl@npm:1.11.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-s390x-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-s390x-gnu@npm:1.11.1" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@unrs/resolver-binding-linux-x64-gnu@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-linux-x64-gnu@npm:1.3.2" +"@unrs/resolver-binding-linux-x64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-x64-gnu@npm:1.11.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@unrs/resolver-binding-linux-x64-musl@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-linux-x64-musl@npm:1.3.2" +"@unrs/resolver-binding-linux-x64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-x64-musl@npm:1.11.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@unrs/resolver-binding-wasm32-wasi@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-wasm32-wasi@npm:1.3.2" +"@unrs/resolver-binding-wasm32-wasi@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-wasm32-wasi@npm:1.11.1" dependencies: - "@napi-rs/wasm-runtime": "npm:^0.2.7" + "@napi-rs/wasm-runtime": "npm:^0.2.11" conditions: cpu=wasm32 languageName: node linkType: hard -"@unrs/resolver-binding-win32-arm64-msvc@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-win32-arm64-msvc@npm:1.3.2" +"@unrs/resolver-binding-win32-arm64-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-arm64-msvc@npm:1.11.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@unrs/resolver-binding-win32-ia32-msvc@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-win32-ia32-msvc@npm:1.3.2" +"@unrs/resolver-binding-win32-ia32-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-ia32-msvc@npm:1.11.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@unrs/resolver-binding-win32-x64-msvc@npm:1.3.2": - version: 1.3.2 - resolution: "@unrs/resolver-binding-win32-x64-msvc@npm:1.3.2" +"@unrs/resolver-binding-win32-x64-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-x64-msvc@npm:1.11.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4735,18 +4705,18 @@ __metadata: linkType: hard "@vitejs/plugin-react@npm:^4.2.1": - version: 4.6.0 - resolution: "@vitejs/plugin-react@npm:4.6.0" + version: 4.7.0 + resolution: "@vitejs/plugin-react@npm:4.7.0" dependencies: - "@babel/core": "npm:^7.27.4" + "@babel/core": "npm:^7.28.0" "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" - "@rolldown/pluginutils": "npm:1.0.0-beta.19" + "@rolldown/pluginutils": "npm:1.0.0-beta.27" "@types/babel__core": "npm:^7.20.5" react-refresh: "npm:^0.17.0" peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - checksum: 10c0/73b8f271978a0337debb255afd1667f49c2018c118962a8613120383375c4038255a5315cee2ef210dc7fd07cd30d5b12271077ad47db29980f8156b8a49be2c + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10c0/692f23960972879485d647713663ec299c478222c96567d60285acf7c7dc5c178e71abfe9d2eefddef1eeb01514dacbc2ed68aad84628debf9c7116134734253 languageName: node linkType: hard @@ -5152,17 +5122,19 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": - version: 3.1.8 - resolution: "array-includes@npm:3.1.8" +"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9": + version: 3.1.9 + resolution: "array-includes@npm:3.1.9" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-object-atoms: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.4" - is-string: "npm:^1.0.7" - checksum: 10c0/5b1004d203e85873b96ddc493f090c9672fd6c80d7a60b798da8a14bff8a670ff95db5aafc9abc14a211943f05220dacf8ea17638ae0af1a6a47b8c0b48ce370 + es-abstract: "npm:^1.24.0" + es-object-atoms: "npm:^1.1.1" + get-intrinsic: "npm:^1.3.0" + is-string: "npm:^1.1.1" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/0235fa69078abeac05ac4250699c44996bc6f774a9cbe45db48674ce6bd142f09b327d31482ff75cf03344db4ea03eae23edb862d59378b484b47ed842574856 languageName: node linkType: hard @@ -5187,29 +5159,30 @@ __metadata: languageName: node linkType: hard -"array.prototype.findlastindex@npm:^1.2.5": - version: 1.2.5 - resolution: "array.prototype.findlastindex@npm:1.2.5" +"array.prototype.findlastindex@npm:^1.2.6": + version: 1.2.6 + resolution: "array.prototype.findlastindex@npm:1.2.6" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" + es-abstract: "npm:^1.23.9" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10c0/962189487728b034f3134802b421b5f39e42ee2356d13b42d2ddb0e52057ffdcc170b9524867f4f0611a6f638f4c19b31e14606e8bcbda67799e26685b195aa3 + es-object-atoms: "npm:^1.1.1" + es-shim-unscopables: "npm:^1.1.0" + checksum: 10c0/82559310d2e57ec5f8fc53d7df420e3abf0ba497935de0a5570586035478ba7d07618cb18e2d4ada2da514c8fb98a034aaf5c06caa0a57e2f7f4c4adedef5956 languageName: node linkType: hard -"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": - version: 1.3.2 - resolution: "array.prototype.flat@npm:1.3.2" +"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.3": + version: 1.3.3 + resolution: "array.prototype.flat@npm:1.3.3" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - es-shim-unscopables: "npm:^1.0.0" - checksum: 10c0/a578ed836a786efbb6c2db0899ae80781b476200617f65a44846cb1ed8bd8b24c8821b83703375d8af639c689497b7b07277060024b9919db94ac3e10dc8a49b + call-bind: "npm:^1.0.8" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.5" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10c0/d90e04dfbc43bb96b3d2248576753d1fb2298d2d972e29ca7ad5ec621f0d9e16ff8074dae647eac4f31f4fb7d3f561a7ac005fb01a71f51705a13b5af06a7d8a languageName: node linkType: hard @@ -5677,7 +5650,7 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.2, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": +"call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": version: 1.0.8 resolution: "call-bind@npm:1.0.8" dependencies: @@ -5689,7 +5662,7 @@ __metadata: languageName: node linkType: hard -"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3": +"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3, call-bound@npm:^1.0.4": version: 1.0.4 resolution: "call-bound@npm:1.0.4" dependencies: @@ -6105,19 +6078,20 @@ __metadata: languageName: node linkType: hard -"cross-env@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-env@npm:7.0.3" +"cross-env@npm:^10.0.0": + version: 10.0.0 + resolution: "cross-env@npm:10.0.0" dependencies: - cross-spawn: "npm:^7.0.1" + "@epic-web/invariant": "npm:^1.0.0" + cross-spawn: "npm:^7.0.6" bin: - cross-env: src/bin/cross-env.js - cross-env-shell: src/bin/cross-env-shell.js - checksum: 10c0/f3765c25746c69fcca369655c442c6c886e54ccf3ab8c16847d5ad0e91e2f337d36eedc6599c1227904bf2a228d721e690324446876115bc8e7b32a866735ecf + cross-env: dist/bin/cross-env.js + cross-env-shell: dist/bin/cross-env-shell.js + checksum: 10c0/d16ffc3734106577d57b6253d81ab50294623bd59f96e161033eaf99c1c308ffbaba8463c23a6c0f72e841eff467cb7007a0a551f27554fcf2bbf6598cd828f9 languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -6358,7 +6332,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -6684,26 +6658,26 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.17.5, es-abstract@npm:^1.22.1, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9": - version: 1.23.9 - resolution: "es-abstract@npm:1.23.9" +"es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": + version: 1.24.0 + resolution: "es-abstract@npm:1.24.0" dependencies: array-buffer-byte-length: "npm:^1.0.2" arraybuffer.prototype.slice: "npm:^1.0.4" available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" + call-bound: "npm:^1.0.4" data-view-buffer: "npm:^1.0.2" data-view-byte-length: "npm:^1.0.2" data-view-byte-offset: "npm:^1.0.1" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" + es-object-atoms: "npm:^1.1.1" es-set-tostringtag: "npm:^2.1.0" es-to-primitive: "npm:^1.3.0" function.prototype.name: "npm:^1.1.8" - get-intrinsic: "npm:^1.2.7" - get-proto: "npm:^1.0.0" + get-intrinsic: "npm:^1.3.0" + get-proto: "npm:^1.0.1" get-symbol-description: "npm:^1.1.0" globalthis: "npm:^1.0.4" gopd: "npm:^1.2.0" @@ -6715,21 +6689,24 @@ __metadata: is-array-buffer: "npm:^3.0.5" is-callable: "npm:^1.2.7" is-data-view: "npm:^1.0.2" + is-negative-zero: "npm:^2.0.3" is-regex: "npm:^1.2.1" + is-set: "npm:^2.0.3" is-shared-array-buffer: "npm:^1.0.4" is-string: "npm:^1.1.1" is-typed-array: "npm:^1.1.15" - is-weakref: "npm:^1.1.0" + is-weakref: "npm:^1.1.1" math-intrinsics: "npm:^1.1.0" - object-inspect: "npm:^1.13.3" + object-inspect: "npm:^1.13.4" object-keys: "npm:^1.1.1" object.assign: "npm:^4.1.7" own-keys: "npm:^1.0.1" - regexp.prototype.flags: "npm:^1.5.3" + regexp.prototype.flags: "npm:^1.5.4" safe-array-concat: "npm:^1.1.3" safe-push-apply: "npm:^1.0.0" safe-regex-test: "npm:^1.1.0" set-proto: "npm:^1.0.0" + stop-iteration-iterator: "npm:^1.1.0" string.prototype.trim: "npm:^1.2.10" string.prototype.trimend: "npm:^1.0.9" string.prototype.trimstart: "npm:^1.0.8" @@ -6738,8 +6715,8 @@ __metadata: typed-array-byte-offset: "npm:^1.0.4" typed-array-length: "npm:^1.0.7" unbox-primitive: "npm:^1.1.0" - which-typed-array: "npm:^1.1.18" - checksum: 10c0/1de229c9e08fe13c17fe5abaec8221545dfcd57e51f64909599a6ae896df84b8fd2f7d16c60cb00d7bf495b9298ca3581aded19939d4b7276854a4b066f8422b + which-typed-array: "npm:^1.1.19" + checksum: 10c0/b256e897be32df5d382786ce8cce29a1dd8c97efbab77a26609bd70f2ed29fbcfc7a31758cb07488d532e7ccccdfca76c1118f2afe5a424cdc05ca007867c318 languageName: node linkType: hard @@ -6809,12 +6786,12 @@ __metadata: languageName: node linkType: hard -"es-shim-unscopables@npm:^1.0.0, es-shim-unscopables@npm:^1.0.2": - version: 1.0.2 - resolution: "es-shim-unscopables@npm:1.0.2" +"es-shim-unscopables@npm:^1.0.2, es-shim-unscopables@npm:^1.1.0": + version: 1.1.0 + resolution: "es-shim-unscopables@npm:1.1.0" dependencies: - hasown: "npm:^2.0.0" - checksum: 10c0/f495af7b4b7601a4c0cfb893581c352636e5c08654d129590386a33a0432cf13a7bdc7b6493801cadd990d838e2839b9013d1de3b880440cb537825e834fe783 + hasown: "npm:^2.0.2" + checksum: 10c0/1b9702c8a1823fc3ef39035a4e958802cf294dd21e917397c561d0b3e195f383b978359816b1732d02b255ccf63e1e4815da0065b95db8d7c992037be3bbbcdb languageName: node linkType: hard @@ -6947,6 +6924,21 @@ __metadata: languageName: node linkType: hard +"eslint-import-context@npm:^0.1.8": + version: 0.1.9 + resolution: "eslint-import-context@npm:0.1.9" + dependencies: + get-tsconfig: "npm:^4.10.1" + stable-hash-x: "npm:^0.2.0" + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + checksum: 10c0/07851103443b70af681c5988e2702e681ff9b956e055e11d4bd9b2322847fa0d9e8da50c18fc7cb1165106b043f34fbd0384d7011c239465c4645c52132e56f3 + languageName: node + linkType: hard + "eslint-import-resolver-node@npm:^0.3.9": version: 0.3.9 resolution: "eslint-import-resolver-node@npm:0.3.9" @@ -6959,15 +6951,16 @@ __metadata: linkType: hard "eslint-import-resolver-typescript@npm:^4.2.5": - version: 4.2.5 - resolution: "eslint-import-resolver-typescript@npm:4.2.5" + version: 4.4.4 + resolution: "eslint-import-resolver-typescript@npm:4.4.4" dependencies: - debug: "npm:^4.4.0" - get-tsconfig: "npm:^4.10.0" + debug: "npm:^4.4.1" + eslint-import-context: "npm:^0.1.8" + get-tsconfig: "npm:^4.10.1" is-bun-module: "npm:^2.0.0" - stable-hash: "npm:^0.0.5" - tinyglobby: "npm:^0.2.12" - unrs-resolver: "npm:^1.3.2" + stable-hash-x: "npm:^0.2.0" + tinyglobby: "npm:^0.2.14" + unrs-resolver: "npm:^1.7.11" peerDependencies: eslint: "*" eslint-plugin-import: "*" @@ -6977,28 +6970,28 @@ __metadata: optional: true eslint-plugin-import-x: optional: true - checksum: 10c0/9134c4dd6e8b3cf1356d6bff68939153c81255c0ac7f694e829c3c7f5e785936591cfe43209d866c8a3b379d3a8dcd203651ec49bd99361fcb54dc0c2b9ce8fc + checksum: 10c0/3bf8ad77c21660f77a0e455555ab179420f68ae7a132906c85a217ccce51cb6680cf70027cab32a358d193e5b9e476f6ba2e595585242aa97d4f6435ca22104e languageName: node linkType: hard -"eslint-module-utils@npm:^2.12.0": - version: 2.12.0 - resolution: "eslint-module-utils@npm:2.12.0" +"eslint-module-utils@npm:^2.12.1": + version: 2.12.1 + resolution: "eslint-module-utils@npm:2.12.1" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10c0/4d8b46dcd525d71276f9be9ffac1d2be61c9d54cc53c992e6333cf957840dee09381842b1acbbb15fc6b255ebab99cd481c5007ab438e5455a14abe1a0468558 + checksum: 10c0/6f4efbe7a91ae49bf67b4ab3644cb60bc5bd7db4cb5521de1b65be0847ffd3fb6bce0dd68f0995e1b312d137f768e2a1f842ee26fe73621afa05f850628fdc40 languageName: node linkType: hard "eslint-plugin-formatjs@npm:^5.3.1": - version: 5.3.1 - resolution: "eslint-plugin-formatjs@npm:5.3.1" + version: 5.4.0 + resolution: "eslint-plugin-formatjs@npm:5.4.0" dependencies: "@formatjs/icu-messageformat-parser": "npm:2.11.2" - "@formatjs/ts-transformer": "npm:3.13.34" + "@formatjs/ts-transformer": "npm:3.14.0" "@types/eslint": "npm:^9.6.1" "@types/picomatch": "npm:^3" "@typescript-eslint/utils": "npm:^8.27.0" @@ -7008,42 +7001,42 @@ __metadata: unicode-emoji-utils: "npm:^1.2.0" peerDependencies: eslint: ^9.23.0 - checksum: 10c0/fb8ba06e0718cd098f2393aea04eb4a6037ca3ead1c9450bd38926d0adecba4cefdebfb661c56c36685e0f003331520c3330544c45803f397b827713ab5e1d7d + checksum: 10c0/5c74a53988df68ffed4e68bb58a4ee75cdcd92b7d94f699e2edbcdd8c2c45930f500c7211da0a4616714d7d83bbbdc105328e5aacf0c9c7582a78fdfc9fa2b55 languageName: node linkType: hard -"eslint-plugin-import@npm:~2.31.0": - version: 2.31.0 - resolution: "eslint-plugin-import@npm:2.31.0" +"eslint-plugin-import@npm:~2.32.0": + version: 2.32.0 + resolution: "eslint-plugin-import@npm:2.32.0" dependencies: "@rtsao/scc": "npm:^1.1.0" - array-includes: "npm:^3.1.8" - array.prototype.findlastindex: "npm:^1.2.5" - array.prototype.flat: "npm:^1.3.2" - array.prototype.flatmap: "npm:^1.3.2" + array-includes: "npm:^3.1.9" + array.prototype.findlastindex: "npm:^1.2.6" + array.prototype.flat: "npm:^1.3.3" + array.prototype.flatmap: "npm:^1.3.3" debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.12.0" + eslint-module-utils: "npm:^2.12.1" hasown: "npm:^2.0.2" - is-core-module: "npm:^2.15.1" + is-core-module: "npm:^2.16.1" is-glob: "npm:^4.0.3" minimatch: "npm:^3.1.2" object.fromentries: "npm:^2.0.8" object.groupby: "npm:^1.0.3" - object.values: "npm:^1.2.0" + object.values: "npm:^1.2.1" semver: "npm:^6.3.1" - string.prototype.trimend: "npm:^1.0.8" + string.prototype.trimend: "npm:^1.0.9" tsconfig-paths: "npm:^3.15.0" peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - checksum: 10c0/e21d116ddd1900e091ad120b3eb68c5dd5437fe2c930f1211781cd38b246f090a6b74d5f3800b8255a0ed29782591521ad44eb21c5534960a8f1fb4040fd913a + checksum: 10c0/bfb1b8fc8800398e62ddfefbf3638d185286edfed26dfe00875cc2846d954491b4f5112457831588b757fa789384e1ae585f812614c4797f0499fa234fd4a48b languageName: node linkType: hard -"eslint-plugin-jsdoc@npm:^51.0.0": - version: 51.3.4 - resolution: "eslint-plugin-jsdoc@npm:51.3.4" +"eslint-plugin-jsdoc@npm:^52.0.0": + version: 52.0.2 + resolution: "eslint-plugin-jsdoc@npm:52.0.2" dependencies: "@es-joy/jsdoccomment": "npm:~0.52.0" are-docs-informative: "npm:^0.0.2" @@ -7057,7 +7050,7 @@ __metadata: spdx-expression-parse: "npm:^4.0.0" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10c0/59e5aa972bdd1bd4e2ca2796ed4455dff1069044abc028621e107aa4b0cbb62ce09554c8e7c2ff3a44a1cbd551e54b6970adc420ba3a89adc6236b94310a81ff + checksum: 10c0/e36d8c75a5a100f71f4f5287ce12ffdf15474194b2877dbe1e06c3c24a1c0c0c7f13c2d92cb289a80515b6fe1e43c4c0b19814b5c5b22a46ac2ca7df23ab55ad languageName: node linkType: hard @@ -7107,8 +7100,8 @@ __metadata: linkType: hard "eslint-plugin-react@npm:^7.37.4": - version: 7.37.4 - resolution: "eslint-plugin-react@npm:7.37.4" + version: 7.37.5 + resolution: "eslint-plugin-react@npm:7.37.5" dependencies: array-includes: "npm:^3.1.8" array.prototype.findlast: "npm:^1.2.5" @@ -7120,7 +7113,7 @@ __metadata: hasown: "npm:^2.0.2" jsx-ast-utils: "npm:^2.4.1 || ^3.0.0" minimatch: "npm:^3.1.2" - object.entries: "npm:^1.1.8" + object.entries: "npm:^1.1.9" object.fromentries: "npm:^2.0.8" object.values: "npm:^1.2.1" prop-types: "npm:^15.8.1" @@ -7130,29 +7123,29 @@ __metadata: string.prototype.repeat: "npm:^1.0.0" peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - checksum: 10c0/4acbbdb19669dfa9a162ed8847c3ad1918f6aea1ceb675ee320b5d903b4e463fdef25e15233295b6d0a726fef2ea8b015c527da769c7690932ddc52d5b82ba12 + checksum: 10c0/c850bfd556291d4d9234f5ca38db1436924a1013627c8ab1853f77cac73ec19b020e861e6c7b783436a48b6ffcdfba4547598235a37ad4611b6739f65fd8ad57 languageName: node linkType: hard "eslint-plugin-storybook@npm:^9.0.4": - version: 9.0.4 - resolution: "eslint-plugin-storybook@npm:9.0.4" + version: 9.1.1 + resolution: "eslint-plugin-storybook@npm:9.1.1" dependencies: "@typescript-eslint/utils": "npm:^8.8.1" peerDependencies: eslint: ">=8" - storybook: ^9.0.4 - checksum: 10c0/b5dbcd15feab63d71f4bd5da26306043339620ddf64bb623de3a7542ee81828b4137af93e199c3e49fb0e5a76d582a21fb580626011ae2340dd6fc684f438358 + storybook: ^9.1.1 + checksum: 10c0/4cf80aa078633021b153a3a5b790a39c9919b5fa7203727c15d8ae066e75d6e134d7d718e66a6a5db9815275f32942a2deae1979aeb36be2543572507faced2c languageName: node linkType: hard -"eslint-scope@npm:^8.3.0": - version: 8.3.0 - resolution: "eslint-scope@npm:8.3.0" +"eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10c0/23bf54345573201fdf06d29efa345ab508b355492f6c6cc9e2b9f6d02b896f369b6dd5315205be94b8853809776c4d13353b85c6b531997b164ff6c3328ecf5b + checksum: 10c0/407f6c600204d0f3705bd557f81bd0189e69cd7996f408f8971ab5779c0af733d1af2f1412066b40ee1588b085874fc37a2333986c6521669cdbdd36ca5058e0 languageName: node linkType: hard @@ -7163,7 +7156,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.2.0, eslint-visitor-keys@npm:^4.2.1": +"eslint-visitor-keys@npm:^4.2.1": version: 4.2.1 resolution: "eslint-visitor-keys@npm:4.2.1" checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43 @@ -7171,17 +7164,17 @@ __metadata: linkType: hard "eslint@npm:^9.23.0": - version: 9.23.0 - resolution: "eslint@npm:9.23.0" + version: 9.32.0 + resolution: "eslint@npm:9.32.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.19.2" - "@eslint/config-helpers": "npm:^0.2.0" - "@eslint/core": "npm:^0.12.0" + "@eslint/config-array": "npm:^0.21.0" + "@eslint/config-helpers": "npm:^0.3.0" + "@eslint/core": "npm:^0.15.0" "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.23.0" - "@eslint/plugin-kit": "npm:^0.2.7" + "@eslint/js": "npm:9.32.0" + "@eslint/plugin-kit": "npm:^0.3.4" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" @@ -7192,9 +7185,9 @@ __metadata: cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^8.3.0" - eslint-visitor-keys: "npm:^4.2.0" - espree: "npm:^10.3.0" + eslint-scope: "npm:^8.4.0" + eslint-visitor-keys: "npm:^4.2.1" + espree: "npm:^10.4.0" esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" @@ -7216,11 +7209,11 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/9616c308dfa8d09db8ae51019c87d5d05933742214531b077bd6ab618baab3bec7938256c14dcad4dc47f5ba93feb0bc5e089f68799f076374ddea21b6a9be45 + checksum: 10c0/e8a23924ec5f8b62e95483002ca25db74e25c23bd9c6d98a9f656ee32f820169bee3bfdf548ec728b16694f198b3db857d85a49210ee4a035242711d08fdc602 languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.3.0, espree@npm:^10.4.0": +"espree@npm:^10.0.1, espree@npm:^10.4.0": version: 10.4.0 resolution: "espree@npm:10.4.0" dependencies: @@ -7363,6 +7356,13 @@ __metadata: languageName: node linkType: hard +"fake-indexeddb@npm:^6.0.1": + version: 6.0.1 + resolution: "fake-indexeddb@npm:6.0.1" + checksum: 10c0/60f4ccdfd5ecb37bb98019056c688366847840cce7146e0005c5ca54823238455403b0a8803b898a11cf80f6147b1bb553457c6af427a644a6e64566cdbe42ec + languageName: node + linkType: hard + "fast-copy@npm:^3.0.2": version: 3.0.2 resolution: "fast-copy@npm:3.0.2" @@ -7566,12 +7566,12 @@ __metadata: languageName: node linkType: hard -"for-each@npm:^0.3.3": - version: 0.3.3 - resolution: "for-each@npm:0.3.3" +"for-each@npm:^0.3.3, for-each@npm:^0.3.5": + version: 0.3.5 + resolution: "for-each@npm:0.3.5" dependencies: - is-callable: "npm:^1.1.3" - checksum: 10c0/22330d8a2db728dbf003ec9182c2d421fbcd2969b02b4f97ec288721cda63eb28f2c08585ddccd0f77cb2930af8d958005c9e72f47141dc51816127a118f39aa + is-callable: "npm:^1.2.7" + checksum: 10c0/0e0b50f6a843a282637d43674d1fb278dda1dd85f4f99b640024cfb10b85058aac0cc781bf689d5fe50b4b7f638e91e548560723a4e76e04fe96ae35ef039cee languageName: node linkType: hard @@ -7807,12 +7807,12 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.10.0": - version: 4.10.0 - resolution: "get-tsconfig@npm:4.10.0" +"get-tsconfig@npm:^4.10.1": + version: 4.10.1 + resolution: "get-tsconfig@npm:4.10.1" dependencies: resolve-pkg-maps: "npm:^1.0.0" - checksum: 10c0/c9b5572c5118923c491c04285c73bd55b19e214992af957c502a3be0fc0043bb421386ffd45ca3433c0a7fba81221ca300479e8393960acf15d0ed4563f38a86 + checksum: 10c0/7f8e3dabc6a49b747920a800fb88e1952fef871cdf51b79e98db48275a5de6cdaf499c55ee67df5fa6fe7ce65f0063e26de0f2e53049b408c585aa74d39ffa21 languageName: node linkType: hard @@ -7899,9 +7899,9 @@ __metadata: linkType: hard "globals@npm:^16.0.0": - version: 16.0.0 - resolution: "globals@npm:16.0.0" - checksum: 10c0/8906d5f01838df64a81d6c2a7b7214312e2216cf65c5ed1546dc9a7d0febddf55ffa906cf04efd5b01eec2534d6f14859a89535d1a68241832810e41ef3fd5bb + version: 16.3.0 + resolution: "globals@npm:16.3.0" + checksum: 10c0/c62dc20357d1c0bf2be4545d6c4141265d1a229bf1c3294955efb5b5ef611145391895e3f2729f8603809e81b30b516c33e6c2597573844449978606aad6eb38 languageName: node linkType: hard @@ -8019,7 +8019,7 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.0, hasown@npm:^2.0.2": +"hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" dependencies: @@ -8197,17 +8197,17 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0, ignore@npm:^5.3.1": +"ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 languageName: node linkType: hard -"ignore@npm:^7.0.3": - version: 7.0.4 - resolution: "ignore@npm:7.0.4" - checksum: 10c0/90e1f69ce352b9555caecd9cbfd07abe7626d312a6f90efbbb52c7edca6ea8df065d66303863b30154ab1502afb2da8bc59d5b04e1719a52ef75bbf675c488eb +"ignore@npm:^7.0.0, ignore@npm:^7.0.3": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10c0/ae00db89fe873064a093b8999fe4cc284b13ef2a178636211842cceb650b9c3e390d3339191acb145d81ed5379d2074840cf0c33a20bdbd6f32821f79eb4ad5d languageName: node linkType: hard @@ -8327,10 +8327,10 @@ __metadata: linkType: hard "ioredis@npm:^5.3.2": - version: 5.6.1 - resolution: "ioredis@npm:5.6.1" + version: 5.7.0 + resolution: "ioredis@npm:5.7.0" dependencies: - "@ioredis/commands": "npm:^1.1.1" + "@ioredis/commands": "npm:^1.3.0" cluster-key-slot: "npm:^1.1.0" debug: "npm:^4.3.4" denque: "npm:^2.1.0" @@ -8339,7 +8339,7 @@ __metadata: redis-errors: "npm:^1.2.0" redis-parser: "npm:^3.0.0" standard-as-callback: "npm:^2.1.0" - checksum: 10c0/26ae49cf448e807e454a9bdea5a9dfdcf669e2fdbf2df341900a0fb693c5662fea7e39db3227ce8972d1bda0ba7da9b7410e5163b12d8878a579548d847220ac + checksum: 10c0/c63c521a953bfaf29f8c8871b122af38e439328336fa238f83bfbb066556f64daf69ed7a4ec01fc7b9ee1f0862059dd188b8c684150125d362d36642399b30ee languageName: node linkType: hard @@ -8421,14 +8421,14 @@ __metadata: languageName: node linkType: hard -"is-callable@npm:^1.1.3, is-callable@npm:^1.2.7": +"is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" checksum: 10c0/ceebaeb9d92e8adee604076971dd6000d38d6afc40bb843ea8e45c5579b57671c3f3b50d7f04869618242c6cee08d1b67806a8cb8edaaaf7c0748b3720d6066f languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -8545,6 +8545,13 @@ __metadata: languageName: node linkType: hard +"is-negative-zero@npm:^2.0.3": + version: 2.0.3 + resolution: "is-negative-zero@npm:2.0.3" + checksum: 10c0/bcdcf6b8b9714063ffcfa9929c575ac69bfdabb8f4574ff557dfc086df2836cf07e3906f5bbc4f2a5c12f8f3ba56af640c843cdfc74da8caed86c7c7d66fd08e + languageName: node + linkType: hard + "is-node-process@npm:^1.0.1, is-node-process@npm:^1.2.0": version: 1.2.0 resolution: "is-node-process@npm:1.2.0" @@ -8632,7 +8639,7 @@ __metadata: languageName: node linkType: hard -"is-string@npm:^1.0.7, is-string@npm:^1.1.1": +"is-string@npm:^1.1.1": version: 1.1.1 resolution: "is-string@npm:1.1.1" dependencies: @@ -8676,7 +8683,7 @@ __metadata: languageName: node linkType: hard -"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0": +"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.1": version: 1.1.1 resolution: "is-weakref@npm:1.1.1" dependencies: @@ -9671,6 +9678,15 @@ __metadata: languageName: node linkType: hard +"napi-postinstall@npm:^0.3.0": + version: 0.3.2 + resolution: "napi-postinstall@npm:0.3.2" + bin: + napi-postinstall: lib/cli.js + checksum: 10c0/77c67eb9871d24afe7bad30e6115c441d099d6a0e42dc1c49c4a722ff682425e08dc6dd2b03eca10db9b547e724c38fb51325c35039e7ac10dcb714bb88d7326 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -9795,7 +9811,7 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.13.3": +"object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 @@ -9823,14 +9839,15 @@ __metadata: languageName: node linkType: hard -"object.entries@npm:^1.1.8": - version: 1.1.8 - resolution: "object.entries@npm:1.1.8" +"object.entries@npm:^1.1.9": + version: 1.1.9 + resolution: "object.entries@npm:1.1.9" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10c0/db9ea979d2956a3bc26c262da4a4d212d36f374652cc4c13efdd069c1a519c16571c137e2893d1c46e1cb0e15c88fd6419eaf410c945f329f09835487d7e65d3 + es-object-atoms: "npm:^1.1.1" + checksum: 10c0/d4b8c1e586650407da03370845f029aa14076caca4e4d4afadbc69cfb5b78035fd3ee7be417141abdb0258fa142e59b11923b4c44d8b1255b28f5ffcc50da7db languageName: node linkType: hard @@ -9857,7 +9874,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1": +"object.values@npm:^1.1.6, object.values@npm:^1.2.1": version: 1.2.1 resolution: "object.values@npm:1.2.1" dependencies: @@ -11588,7 +11605,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.3": +"regexp.prototype.flags@npm:^1.5.3, regexp.prototype.flags@npm:^1.5.4": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: @@ -12430,10 +12447,10 @@ __metadata: languageName: node linkType: hard -"stable-hash@npm:^0.0.5": - version: 0.0.5 - resolution: "stable-hash@npm:0.0.5" - checksum: 10c0/ca670cb6d172f1c834950e4ec661e2055885df32fee3ebf3647c5df94993b7c2666a5dbc1c9a62ee11fc5c24928579ec5e81bb5ad31971d355d5a341aab493b3 +"stable-hash-x@npm:^0.2.0": + version: 0.2.0 + resolution: "stable-hash-x@npm:0.2.0" + checksum: 10c0/c757df58366ee4bb266a9486b8932eab7c1ba730469eaf4b68d2dee404814e9f84089c44c9b5205f8c7d99a0ab036cce2af69139ce5ed44b635923c011a8aea8 languageName: node linkType: hard @@ -12509,6 +12526,16 @@ __metadata: languageName: node linkType: hard +"stop-iteration-iterator@npm:^1.1.0": + version: 1.1.0 + resolution: "stop-iteration-iterator@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + internal-slot: "npm:^1.1.0" + checksum: 10c0/de4e45706bb4c0354a4b1122a2b8cc45a639e86206807ce0baf390ee9218d3ef181923fa4d2b67443367c491aa255c5fbaa64bb74648e3c5b48299928af86c09 + languageName: node + linkType: hard + "storybook@npm:^9.0.4": version: 9.0.4 resolution: "storybook@npm:9.0.4" @@ -12639,7 +12666,7 @@ __metadata: languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9": +"string.prototype.trimend@npm:^1.0.9": version: 1.0.9 resolution: "string.prototype.trimend@npm:1.0.9" dependencies: @@ -13086,7 +13113,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14": +"tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" dependencies: @@ -13218,7 +13245,7 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^2.0.1, ts-api-utils@npm:^2.1.0": +"ts-api-utils@npm:^2.1.0": version: 2.1.0 resolution: "ts-api-utils@npm:2.1.0" peerDependencies: @@ -13391,16 +13418,17 @@ __metadata: linkType: hard "typescript-eslint@npm:^8.28.0, typescript-eslint@npm:^8.29.1": - version: 8.29.1 - resolution: "typescript-eslint@npm:8.29.1" + version: 8.38.0 + resolution: "typescript-eslint@npm:8.38.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.29.1" - "@typescript-eslint/parser": "npm:8.29.1" - "@typescript-eslint/utils": "npm:8.29.1" + "@typescript-eslint/eslint-plugin": "npm:8.38.0" + "@typescript-eslint/parser": "npm:8.38.0" + "@typescript-eslint/typescript-estree": "npm:8.38.0" + "@typescript-eslint/utils": "npm:8.38.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" - checksum: 10c0/31319c891d224ec8d7cf96ad7e6c84480b3d17d4c46c5beccca06fc7891f41eabd5593e44867e69dbfb79459f5545c2cc2e985c950bdd7b4e7c3bb1ec8941030 + checksum: 10c0/486b9862ee08f7827d808a2264ce03b58087b11c4c646c0da3533c192a67ae3fcb4e68d7a1e69d0f35a1edc274371a903a50ecfe74012d5eaa896cb9d5a81e0b languageName: node linkType: hard @@ -13575,26 +13603,35 @@ __metadata: languageName: node linkType: hard -"unrs-resolver@npm:^1.3.2": - version: 1.3.2 - resolution: "unrs-resolver@npm:1.3.2" +"unrs-resolver@npm:^1.7.11": + version: 1.11.1 + resolution: "unrs-resolver@npm:1.11.1" dependencies: - "@unrs/resolver-binding-darwin-arm64": "npm:1.3.2" - "@unrs/resolver-binding-darwin-x64": "npm:1.3.2" - "@unrs/resolver-binding-freebsd-x64": "npm:1.3.2" - "@unrs/resolver-binding-linux-arm-gnueabihf": "npm:1.3.2" - "@unrs/resolver-binding-linux-arm-musleabihf": "npm:1.3.2" - "@unrs/resolver-binding-linux-arm64-gnu": "npm:1.3.2" - "@unrs/resolver-binding-linux-arm64-musl": "npm:1.3.2" - "@unrs/resolver-binding-linux-ppc64-gnu": "npm:1.3.2" - "@unrs/resolver-binding-linux-s390x-gnu": "npm:1.3.2" - "@unrs/resolver-binding-linux-x64-gnu": "npm:1.3.2" - "@unrs/resolver-binding-linux-x64-musl": "npm:1.3.2" - "@unrs/resolver-binding-wasm32-wasi": "npm:1.3.2" - "@unrs/resolver-binding-win32-arm64-msvc": "npm:1.3.2" - "@unrs/resolver-binding-win32-ia32-msvc": "npm:1.3.2" - "@unrs/resolver-binding-win32-x64-msvc": "npm:1.3.2" + "@unrs/resolver-binding-android-arm-eabi": "npm:1.11.1" + "@unrs/resolver-binding-android-arm64": "npm:1.11.1" + "@unrs/resolver-binding-darwin-arm64": "npm:1.11.1" + "@unrs/resolver-binding-darwin-x64": "npm:1.11.1" + "@unrs/resolver-binding-freebsd-x64": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm-gnueabihf": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm-musleabihf": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm64-musl": "npm:1.11.1" + "@unrs/resolver-binding-linux-ppc64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-riscv64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-riscv64-musl": "npm:1.11.1" + "@unrs/resolver-binding-linux-s390x-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-x64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-x64-musl": "npm:1.11.1" + "@unrs/resolver-binding-wasm32-wasi": "npm:1.11.1" + "@unrs/resolver-binding-win32-arm64-msvc": "npm:1.11.1" + "@unrs/resolver-binding-win32-ia32-msvc": "npm:1.11.1" + "@unrs/resolver-binding-win32-x64-msvc": "npm:1.11.1" + napi-postinstall: "npm:^0.3.0" dependenciesMeta: + "@unrs/resolver-binding-android-arm-eabi": + optional: true + "@unrs/resolver-binding-android-arm64": + optional: true "@unrs/resolver-binding-darwin-arm64": optional: true "@unrs/resolver-binding-darwin-x64": @@ -13611,6 +13648,10 @@ __metadata: optional: true "@unrs/resolver-binding-linux-ppc64-gnu": optional: true + "@unrs/resolver-binding-linux-riscv64-gnu": + optional: true + "@unrs/resolver-binding-linux-riscv64-musl": + optional: true "@unrs/resolver-binding-linux-s390x-gnu": optional: true "@unrs/resolver-binding-linux-x64-gnu": @@ -13625,7 +13666,7 @@ __metadata: optional: true "@unrs/resolver-binding-win32-x64-msvc": optional: true - checksum: 10c0/f9b6d18193bcaae7ef9e284a74c85d4cb3d8c833851f1b23254a947297e672826223d82798dbff818455fefeda02084340aca904300fd5060468c2f243767cc1 + checksum: 10c0/c91b112c71a33d6b24e5c708dab43ab80911f2df8ee65b87cd7a18fb5af446708e98c4b415ca262026ad8df326debcc7ca6a801b2935504d87fd6f0b9d70dce1 languageName: node linkType: hard @@ -13792,8 +13833,8 @@ __metadata: linkType: hard "vite-plugin-pwa@npm:^1.0.0": - version: 1.0.1 - resolution: "vite-plugin-pwa@npm:1.0.1" + version: 1.0.2 + resolution: "vite-plugin-pwa@npm:1.0.2" dependencies: debug: "npm:^4.3.6" pretty-bytes: "npm:^6.1.1" @@ -13808,7 +13849,7 @@ __metadata: peerDependenciesMeta: "@vite-pwa/assets-generator": optional: true - checksum: 10c0/ceca04df97877ca97eb30805207d4826bd6340796194c9015afeefeb781931bf9019a630c5a0bdaa6dffcada11ce1fdf8595ac48a08d751dff81601aa0c7db38 + checksum: 10c0/e4f2f4dfff843ee2585a0d89e74187168ba20da77cd0d127ce7ad7eebcf5a68b2bf09000afb6bb86d43a2034fea9f568cd6db2a2d4b47a72e175d999a5e07eb1 languageName: node linkType: hard @@ -14122,17 +14163,18 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18": - version: 1.1.18 - resolution: "which-typed-array@npm:1.1.18" +"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19": + version: 1.1.19 + resolution: "which-typed-array@npm:1.1.19" dependencies: available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - for-each: "npm:^0.3.3" + call-bound: "npm:^1.0.4" + for-each: "npm:^0.3.5" + get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-tostringtag: "npm:^1.0.2" - checksum: 10c0/0412f4a91880ca1a2a63056187c2e3de6b129b2b5b6c17bc3729f0f7041047ae48fb7424813e51506addb2c97320003ee18b8c57469d2cde37983ef62126143c + checksum: 10c0/702b5dc878addafe6c6300c3d0af5983b175c75fcb4f2a72dfc3dd38d93cf9e89581e4b29c854b16ea37e50a7d7fca5ae42ece5c273d8060dcd603b2404bbb3f languageName: node linkType: hard