about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/chewy/statuses_index.rb5
-rw-r--r--app/controllers/account_follow_controller.rb2
-rw-r--r--app/controllers/admin/action_logs_controller.rb14
-rw-r--r--app/controllers/admin/email_domain_blocks_controller.rb28
-rw-r--r--app/controllers/admin/site_uploads_controller.rb21
-rw-r--r--app/controllers/admin/warning_presets_controller.rb6
-rw-r--r--app/controllers/api/base_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/follower_accounts_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/following_accounts_controller.rb4
-rw-r--r--app/controllers/api/v1/accounts/identity_proofs_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/lists_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/pins_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/relationships_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/search_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts/statuses_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts_controller.rb4
-rw-r--r--app/controllers/api/v1/apps/credentials_controller.rb2
-rw-r--r--app/controllers/api/v1/blocks_controller.rb2
-rw-r--r--app/controllers/api/v1/bookmarks_controller.rb2
-rw-r--r--app/controllers/api/v1/conversations_controller.rb2
-rw-r--r--app/controllers/api/v1/custom_emojis_controller.rb2
-rw-r--r--app/controllers/api/v1/domain_blocks_controller.rb2
-rw-r--r--app/controllers/api/v1/endorsements_controller.rb2
-rw-r--r--app/controllers/api/v1/favourites_controller.rb2
-rw-r--r--app/controllers/api/v1/featured_tags/suggestions_controller.rb3
-rw-r--r--app/controllers/api/v1/filters_controller.rb2
-rw-r--r--app/controllers/api/v1/instances/activity_controller.rb2
-rw-r--r--app/controllers/api/v1/instances/peers_controller.rb2
-rw-r--r--app/controllers/api/v1/instances_controller.rb2
-rw-r--r--app/controllers/api/v1/media_controller.rb31
-rw-r--r--app/controllers/api/v1/mutes_controller.rb2
-rw-r--r--app/controllers/api/v1/notifications_controller.rb2
-rw-r--r--app/controllers/api/v1/polls/votes_controller.rb2
-rw-r--r--app/controllers/api/v1/polls_controller.rb2
-rw-r--r--app/controllers/api/v1/preferences_controller.rb2
-rw-r--r--app/controllers/api/v1/reports_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/bookmarks_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/favourites_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/mutes_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/pins_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/statuses/reblogs_controller.rb3
-rw-r--r--app/controllers/api/v1/statuses_controller.rb14
-rw-r--r--app/controllers/api/v1/streaming_controller.rb2
-rw-r--r--app/controllers/api/v1/suggestions_controller.rb2
-rw-r--r--app/controllers/api/v1/timelines/home_controller.rb2
-rw-r--r--app/controllers/api/v1/timelines/public_controller.rb2
-rw-r--r--app/controllers/api/v1/timelines/tag_controller.rb2
-rw-r--r--app/controllers/api/v1/trends_controller.rb2
-rw-r--r--app/controllers/api/v2/media_controller.rb12
-rw-r--r--app/controllers/api/v2/search_controller.rb2
-rw-r--r--app/controllers/api/web/embeds_controller.rb2
-rw-r--r--app/controllers/api/web/push_subscriptions_controller.rb2
-rw-r--r--app/controllers/api/web/settings_controller.rb2
-rw-r--r--app/controllers/application_controller.rb5
-rw-r--r--app/controllers/authorize_interactions_controller.rb2
-rw-r--r--app/controllers/concerns/rate_limit_headers.rb16
-rw-r--r--app/controllers/follower_accounts_controller.rb11
-rw-r--r--app/controllers/following_accounts_controller.rb11
-rw-r--r--app/controllers/settings/imports_controller.rb2
-rw-r--r--app/helpers/admin/action_logs_helper.rb75
-rw-r--r--app/helpers/admin/filter_helper.rb1
-rw-r--r--app/helpers/admin/settings_helper.rb11
-rw-r--r--app/javascript/core/admin.js6
-rw-r--r--app/javascript/core/public.js16
-rw-r--r--app/javascript/core/settings.js2
-rw-r--r--app/javascript/flavours/glitch/actions/accounts.js3
-rw-r--r--app/javascript/flavours/glitch/actions/alerts.js4
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js23
-rw-r--r--app/javascript/flavours/glitch/actions/identity_proofs.js1
-rw-r--r--app/javascript/flavours/glitch/actions/timelines.js1
-rw-r--r--app/javascript/flavours/glitch/components/domain.js2
-rw-r--r--app/javascript/flavours/glitch/components/intersection_observer_article.js2
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js2
-rw-r--r--app/javascript/flavours/glitch/components/poll.js28
-rw-r--r--app/javascript/flavours/glitch/containers/domain_container.js2
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js8
-rw-r--r--app/javascript/flavours/glitch/features/audio/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/blocks/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js29
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/options.js8
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/poll_form.js3
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js9
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js2
-rw-r--r--app/javascript/flavours/glitch/features/domain_blocks/index.js8
-rw-r--r--app/javascript/flavours/glitch/features/favourites/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/index.js19
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/components/announcements.js19
-rw-r--r--app/javascript/flavours/glitch/features/lists/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/reblogs/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/status/components/card.js2
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js6
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js20
-rw-r--r--app/javascript/flavours/glitch/middleware/errors.js2
-rw-r--r--app/javascript/flavours/glitch/packs/common.js2
-rw-r--r--app/javascript/flavours/glitch/packs/public.js24
-rw-r--r--app/javascript/flavours/glitch/packs/settings.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js1
-rw-r--r--app/javascript/flavours/glitch/reducers/notifications.js4
-rw-r--r--app/javascript/flavours/glitch/reducers/timelines.js4
-rw-r--r--app/javascript/flavours/glitch/selectors/index.js2
-rw-r--r--app/javascript/flavours/glitch/store/configureStore.js2
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss68
-rw-r--r--app/javascript/flavours/glitch/styles/components/accounts.scss1
-rw-r--r--app/javascript/flavours/glitch/styles/components/announcements.scss7
-rw-r--r--app/javascript/flavours/glitch/styles/components/columns.scss8
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss4
-rw-r--r--app/javascript/flavours/glitch/styles/components/emoji.scss6
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss1
-rw-r--r--app/javascript/flavours/glitch/styles/components/media.scss6
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss6
-rw-r--r--app/javascript/flavours/glitch/styles/polls.scss42
-rw-r--r--app/javascript/flavours/glitch/util/log_out.js2
-rw-r--r--app/javascript/mastodon/actions/accounts.js5
-rw-r--r--app/javascript/mastodon/actions/alerts.js4
-rw-r--r--app/javascript/mastodon/actions/compose.js23
-rw-r--r--app/javascript/mastodon/actions/identity_proofs.js1
-rw-r--r--app/javascript/mastodon/actions/timelines.js1
-rw-r--r--app/javascript/mastodon/common.js2
-rw-r--r--app/javascript/mastodon/components/column_header.js3
-rw-r--r--app/javascript/mastodon/components/domain.js2
-rw-r--r--app/javascript/mastodon/components/intersection_observer_article.js2
-rw-r--r--app/javascript/mastodon/components/media_gallery.js2
-rw-r--r--app/javascript/mastodon/components/poll.js28
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js20
-rw-r--r--app/javascript/mastodon/components/status.js8
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js4
-rw-r--r--app/javascript/mastodon/components/status_content.js14
-rw-r--r--app/javascript/mastodon/containers/domain_container.js2
-rw-r--r--app/javascript/mastodon/features/account/components/header.js8
-rw-r--r--app/javascript/mastodon/features/audio/index.js6
-rw-r--r--app/javascript/mastodon/features/blocks/index.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/action_bar.js2
-rw-r--r--app/javascript/mastodon/features/compose/components/poll_form.js3
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js8
-rw-r--r--app/javascript/mastodon/features/direct_timeline/components/conversation.js2
-rw-r--r--app/javascript/mastodon/features/domain_blocks/index.js8
-rw-r--r--app/javascript/mastodon/features/favourites/index.js2
-rw-r--r--app/javascript/mastodon/features/follow_requests/index.js19
-rw-r--r--app/javascript/mastodon/features/followers/index.js2
-rw-r--r--app/javascript/mastodon/features/following/index.js2
-rw-r--r--app/javascript/mastodon/features/getting_started/components/announcements.js19
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js8
-rw-r--r--app/javascript/mastodon/features/lists/index.js2
-rw-r--r--app/javascript/mastodon/features/mutes/index.js2
-rw-r--r--app/javascript/mastodon/features/reblogs/index.js2
-rw-r--r--app/javascript/mastodon/features/status/components/action_bar.js4
-rw-r--r--app/javascript/mastodon/features/status/components/card.js2
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/__tests__/column-test.js2
-rw-r--r--app/javascript/mastodon/features/video/index.js14
-rw-r--r--app/javascript/mastodon/locales/ast.json4
-rw-r--r--app/javascript/mastodon/locales/bg.json2
-rw-r--r--app/javascript/mastodon/locales/br.json486
-rw-r--r--app/javascript/mastodon/locales/cs.json6
-rw-r--r--app/javascript/mastodon/locales/de.json14
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json46
-rw-r--r--app/javascript/mastodon/locales/el.json12
-rw-r--r--app/javascript/mastodon/locales/en.json25
-rw-r--r--app/javascript/mastodon/locales/eo.json4
-rw-r--r--app/javascript/mastodon/locales/es-AR.json4
-rw-r--r--app/javascript/mastodon/locales/eu.json18
-rw-r--r--app/javascript/mastodon/locales/fa.json10
-rw-r--r--app/javascript/mastodon/locales/fi.json40
-rw-r--r--app/javascript/mastodon/locales/fr.json166
-rw-r--r--app/javascript/mastodon/locales/ga.json2
-rw-r--r--app/javascript/mastodon/locales/gl.json36
-rw-r--r--app/javascript/mastodon/locales/hi.json2
-rw-r--r--app/javascript/mastodon/locales/hu.json2
-rw-r--r--app/javascript/mastodon/locales/hy.json124
-rw-r--r--app/javascript/mastodon/locales/it.json4
-rw-r--r--app/javascript/mastodon/locales/ja.json24
-rw-r--r--app/javascript/mastodon/locales/kab.json124
-rw-r--r--app/javascript/mastodon/locales/kn.json2
-rw-r--r--app/javascript/mastodon/locales/ko.json12
-rw-r--r--app/javascript/mastodon/locales/lt.json2
-rw-r--r--app/javascript/mastodon/locales/lv.json2
-rw-r--r--app/javascript/mastodon/locales/mk.json2
-rw-r--r--app/javascript/mastodon/locales/ml.json2
-rw-r--r--app/javascript/mastodon/locales/mr.json2
-rw-r--r--app/javascript/mastodon/locales/ms.json2
-rw-r--r--app/javascript/mastodon/locales/nl.json32
-rw-r--r--app/javascript/mastodon/locales/nn.json16
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json20
-rw-r--r--app/javascript/mastodon/locales/pt-PT.json34
-rw-r--r--app/javascript/mastodon/locales/ru.json18
-rw-r--r--app/javascript/mastodon/locales/sk.json12
-rw-r--r--app/javascript/mastodon/locales/sr.json130
-rw-r--r--app/javascript/mastodon/locales/sv.json6
-rw-r--r--app/javascript/mastodon/locales/th.json40
-rw-r--r--app/javascript/mastodon/locales/ur.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json2
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json14
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json14
-rw-r--r--app/javascript/mastodon/middleware/errors.js2
-rw-r--r--app/javascript/mastodon/reducers/compose.js1
-rw-r--r--app/javascript/mastodon/reducers/notifications.js4
-rw-r--r--app/javascript/mastodon/reducers/timelines.js4
-rw-r--r--app/javascript/mastodon/selectors/index.js2
-rw-r--r--app/javascript/mastodon/service_worker/web_push_notifications.js2
-rw-r--r--app/javascript/mastodon/store/configureStore.js2
-rw-r--r--app/javascript/mastodon/utils/log_out.js2
-rw-r--r--app/javascript/packs/public.js24
-rw-r--r--app/javascript/styles/mastodon-light/diff.scss2
-rw-r--r--app/javascript/styles/mastodon/admin.scss68
-rw-r--r--app/javascript/styles/mastodon/components.scss39
-rw-r--r--app/javascript/styles/mastodon/polls.scss42
-rw-r--r--app/lib/activitypub/tag_manager.rb2
-rw-r--r--app/lib/entity_cache.rb4
-rw-r--r--app/lib/exceptions.rb1
-rw-r--r--app/lib/formatter.rb8
-rw-r--r--app/lib/language_detector.rb4
-rw-r--r--app/lib/rate_limiter.rb64
-rw-r--r--app/lib/sanitize_config.rb14
-rw-r--r--app/models/account.rb10
-rw-r--r--app/models/account_filter.rb27
-rw-r--r--app/models/account_warning_preset.rb3
-rw-r--r--app/models/admin/account_action.rb12
-rw-r--r--app/models/admin/action_log_filter.rb81
-rw-r--r--app/models/announcement.rb11
-rw-r--r--app/models/concerns/account_interactions.rb16
-rw-r--r--app/models/concerns/attachmentable.rb2
-rw-r--r--app/models/concerns/rate_limitable.rb36
-rw-r--r--app/models/email_domain_block.rb14
-rw-r--r--app/models/follow.rb3
-rw-r--r--app/models/follow_request.rb3
-rw-r--r--app/models/media_attachment.rb122
-rw-r--r--app/models/report.rb11
-rw-r--r--app/models/status.rb20
-rw-r--r--app/policies/settings_policy.rb4
-rw-r--r--app/serializers/rest/announcement_serializer.rb13
-rw-r--r--app/serializers/rest/media_attachment_serializer.rb4
-rw-r--r--app/services/account_search_service.rb2
-rw-r--r--app/services/activitypub/process_account_service.rb25
-rw-r--r--app/services/fetch_resource_service.rb3
-rw-r--r--app/services/follow_service.rb78
-rw-r--r--app/services/import_service.rb3
-rw-r--r--app/services/post_status_service.rb23
-rw-r--r--app/services/reblog_service.rb16
-rw-r--r--app/services/resolve_url_service.rb10
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/views/admin/account_actions/new.html.haml2
-rw-r--r--app/views/admin/accounts/_account.html.haml2
-rw-r--r--app/views/admin/accounts/index.html.haml6
-rw-r--r--app/views/admin/accounts/show.html.haml15
-rw-r--r--app/views/admin/action_logs/_action_log.html.haml6
-rw-r--r--app/views/admin/action_logs/index.html.haml21
-rw-r--r--app/views/admin/email_domain_blocks/_email_domain_block.html.haml10
-rw-r--r--app/views/admin/email_domain_blocks/new.html.haml5
-rw-r--r--app/views/admin/settings/edit.html.haml11
-rw-r--r--app/views/admin/warning_presets/_warning_preset.html.haml10
-rw-r--r--app/views/admin/warning_presets/edit.html.haml3
-rw-r--r--app/views/admin/warning_presets/index.html.haml24
-rw-r--r--app/views/errors/429.html.haml5
-rw-r--r--app/views/settings/preferences/appearance/show.html.haml5
-rw-r--r--app/views/settings/preferences/notifications/show.html.haml8
-rw-r--r--app/views/settings/preferences/other/show.html.haml5
-rw-r--r--app/views/settings/profiles/show.html.haml5
-rw-r--r--app/views/statuses/_poll.html.haml10
-rw-r--r--app/workers/activitypub/distribute_poll_update_worker.rb2
-rw-r--r--app/workers/activitypub/synchronize_featured_collection_worker.rb2
-rw-r--r--app/workers/after_remote_follow_request_worker.rb9
-rw-r--r--app/workers/after_remote_follow_worker.rb9
-rw-r--r--app/workers/backup_worker.rb8
-rw-r--r--app/workers/notification_worker.rb9
-rw-r--r--app/workers/poll_expiration_notify_worker.rb2
-rw-r--r--app/workers/post_process_media_worker.rb34
-rw-r--r--app/workers/processing_worker.rb9
-rw-r--r--app/workers/publish_scheduled_announcement_worker.rb15
-rw-r--r--app/workers/publish_scheduled_status_worker.rb2
-rw-r--r--app/workers/pubsubhubbub/confirmation_worker.rb9
-rw-r--r--app/workers/pubsubhubbub/delivery_worker.rb9
-rw-r--r--app/workers/pubsubhubbub/distribution_worker.rb9
-rw-r--r--app/workers/pubsubhubbub/raw_distribution_worker.rb9
-rw-r--r--app/workers/pubsubhubbub/subscribe_worker.rb9
-rw-r--r--app/workers/pubsubhubbub/unsubscribe_worker.rb9
-rw-r--r--app/workers/regeneration_worker.rb2
-rw-r--r--app/workers/remote_profile_update_worker.rb9
-rw-r--r--app/workers/resolve_account_worker.rb2
-rw-r--r--app/workers/salmon_worker.rb9
-rw-r--r--app/workers/scheduler/backup_cleanup_scheduler.rb2
-rw-r--r--app/workers/scheduler/doorkeeper_cleanup_scheduler.rb2
-rw-r--r--app/workers/scheduler/email_scheduler.rb2
-rw-r--r--app/workers/scheduler/feed_cleanup_scheduler.rb2
-rw-r--r--app/workers/scheduler/ip_cleanup_scheduler.rb2
-rw-r--r--app/workers/scheduler/media_cleanup_scheduler.rb2
-rw-r--r--app/workers/scheduler/pghero_scheduler.rb2
-rw-r--r--app/workers/scheduler/scheduled_statuses_scheduler.rb2
-rw-r--r--app/workers/scheduler/subscriptions_cleanup_scheduler.rb9
-rw-r--r--app/workers/scheduler/subscriptions_scheduler.rb9
-rw-r--r--app/workers/scheduler/trending_tags_scheduler.rb2
-rw-r--r--app/workers/scheduler/user_cleanup_scheduler.rb2
-rw-r--r--app/workers/verify_account_links_worker.rb2
297 files changed, 2132 insertions, 1656 deletions
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index f5735421c..bec9ed88b 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -47,6 +47,11 @@ class StatusesIndex < Chewy::Index
       data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
     end
 
+    crutch :bookmarks do |collection|
+      data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
+      data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
+    end
+
     root date_detection: false do
       field :id, type: 'long'
       field :account_id, type: 'long'
diff --git a/app/controllers/account_follow_controller.rb b/app/controllers/account_follow_controller.rb
index 185a355f8..33394074d 100644
--- a/app/controllers/account_follow_controller.rb
+++ b/app/controllers/account_follow_controller.rb
@@ -6,7 +6,7 @@ class AccountFollowController < ApplicationController
   before_action :authenticate_user!
 
   def create
-    FollowService.new.call(current_user.account, @account.acct)
+    FollowService.new.call(current_user.account, @account, with_rate_limit: true)
     redirect_to account_path(@account)
   end
 end
diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb
index e273dfeae..2d77620df 100644
--- a/app/controllers/admin/action_logs_controller.rb
+++ b/app/controllers/admin/action_logs_controller.rb
@@ -2,8 +2,18 @@
 
 module Admin
   class ActionLogsController < BaseController
-    def index
-      @action_logs = Admin::ActionLog.page(params[:page])
+    before_action :set_action_logs
+
+    def index; end
+
+    private
+
+    def set_action_logs
+      @action_logs = Admin::ActionLogFilter.new(filter_params).results.page(params[:page])
+    end
+
+    def filter_params
+      params.slice(:page, *Admin::ActionLogFilter::KEYS).permit(:page, *Admin::ActionLogFilter::KEYS)
     end
   end
 end
diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb
index 9fe85064e..c25919726 100644
--- a/app/controllers/admin/email_domain_blocks_controller.rb
+++ b/app/controllers/admin/email_domain_blocks_controller.rb
@@ -6,12 +6,12 @@ module Admin
 
     def index
       authorize :email_domain_block, :index?
-      @email_domain_blocks = EmailDomainBlock.page(params[:page])
+      @email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
     end
 
     def new
       authorize :email_domain_block, :create?
-      @email_domain_block = EmailDomainBlock.new
+      @email_domain_block = EmailDomainBlock.new(domain: params[:_domain])
     end
 
     def create
@@ -21,6 +21,28 @@ module Admin
 
       if @email_domain_block.save
         log_action :create, @email_domain_block
+
+        if @email_domain_block.with_dns_records?
+          hostnames = []
+          ips       = []
+
+          Resolv::DNS.open do |dns|
+            dns.timeouts = 1
+
+            hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
+
+            ([@email_domain_block.domain] + hostnames).uniq.each do |hostname|
+              ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
+              ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s })
+            end
+          end
+
+          (hostnames + ips).each do |hostname|
+            another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: @email_domain_block)
+            log_action :create, another_email_domain_block if another_email_domain_block.save
+          end
+        end
+
         redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
       else
         render :new
@@ -41,7 +63,7 @@ module Admin
     end
 
     def resource_params
-      params.require(:email_domain_block).permit(:domain)
+      params.require(:email_domain_block).permit(:domain, :with_dns_records)
     end
   end
 end
diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb
new file mode 100644
index 000000000..cacecedb0
--- /dev/null
+++ b/app/controllers/admin/site_uploads_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Admin
+  class SiteUploadsController < BaseController
+    before_action :set_site_upload
+
+    def destroy
+      authorize :settings, :destroy?
+
+      @site_upload.destroy!
+
+      redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
+    end
+
+    private
+
+    def set_site_upload
+      @site_upload = SiteUpload.find(params[:id])
+    end
+  end
+end
diff --git a/app/controllers/admin/warning_presets_controller.rb b/app/controllers/admin/warning_presets_controller.rb
index 37be842c5..b376f8d9b 100644
--- a/app/controllers/admin/warning_presets_controller.rb
+++ b/app/controllers/admin/warning_presets_controller.rb
@@ -7,7 +7,7 @@ module Admin
     def index
       authorize :account_warning_preset, :index?
 
-      @warning_presets = AccountWarningPreset.all
+      @warning_presets = AccountWarningPreset.alphabetic
       @warning_preset  = AccountWarningPreset.new
     end
 
@@ -19,7 +19,7 @@ module Admin
       if @warning_preset.save
         redirect_to admin_warning_presets_path
       else
-        @warning_presets = AccountWarningPreset.all
+        @warning_presets = AccountWarningPreset.alphabetic
         render :index
       end
     end
@@ -52,7 +52,7 @@ module Admin
     end
 
     def warning_preset_params
-      params.require(:account_warning_preset).permit(:text)
+      params.require(:account_warning_preset).permit(:title, :text)
     end
   end
 end
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 68bf425f4..153ade253 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -44,6 +44,10 @@ class Api::BaseController < ApplicationController
     render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
   end
 
+  rescue_from Mastodon::RateLimitExceededError do
+    render json: { error: I18n.t('errors.429') }, status: 429
+  end
+
   rescue_from ActionController::ParameterMissing do |e|
     render json: { error: e.to_s }, status: 400
   end
diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index e360b8a92..1daa1ed0d 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
   before_action :set_account
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
@@ -27,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
   end
 
   def hide_results?
-    (@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
+    (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
   end
 
   def default_accounts
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index a405b365f..6fc23cf75 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
   before_action :set_account
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
@@ -27,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
   end
 
   def hide_results?
-    (@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
+    (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
   end
 
   def default_accounts
diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb
index bea51ae11..8dad6fee9 100644
--- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb
+++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb
@@ -4,8 +4,6 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
   before_action :require_user!
   before_action :set_account
 
-  respond_to :json
-
   def index
     @proofs = @account.identity_proofs.active
     render json: @proofs, each_serializer: REST::IdentityProofSerializer
diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb
index 72392453c..ccb751f8f 100644
--- a/app/controllers/api/v1/accounts/lists_controller.rb
+++ b/app/controllers/api/v1/accounts/lists_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::Accounts::ListsController < Api::BaseController
   before_action :require_user!
   before_action :set_account
 
-  respond_to :json
-
   def index
     @lists = @account.lists.where(account: current_account)
     render json: @lists, each_serializer: REST::ListSerializer
diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb
index 0a0239c42..3915b5669 100644
--- a/app/controllers/api/v1/accounts/pins_controller.rb
+++ b/app/controllers/api/v1/accounts/pins_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController
   before_action :require_user!
   before_action :set_account
 
-  respond_to :json
-
   def create
     AccountPin.create!(account: current_account, target_account: @account)
     render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb
index ab8a0461f..1d3992a28 100644
--- a/app/controllers/api/v1/accounts/relationships_controller.rb
+++ b/app/controllers/api/v1/accounts/relationships_controller.rb
@@ -4,8 +4,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:follows' }
   before_action :require_user!
 
-  respond_to :json
-
   def index
     accounts = Account.where(id: account_ids).select('id')
     # .where doesn't guarantee that our results are in the same order
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 4217b527a..3061fcb7e 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -4,8 +4,6 @@ class Api::V1::Accounts::SearchController < Api::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
   before_action :require_user!
 
-  respond_to :json
-
   def show
     @accounts = account_search
     render json: @accounts, each_serializer: REST::AccountSerializer
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 333db9618..114ee0a82 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -6,8 +6,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
 
   after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
 
-  respond_to :json
-
   def index
     @statuses = load_statuses
     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index d68d2715f..0080faf33 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -14,7 +14,7 @@ class Api::V1::AccountsController < Api::BaseController
 
   skip_before_action :require_authenticated_user!, only: :create
 
-  respond_to :json
+  override_rate_limit_headers :follow, family: :follows
 
   def show
     render json: @account, serializer: REST::AccountSerializer
@@ -31,7 +31,7 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def follow
-    FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
+    FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
 
     options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
 
diff --git a/app/controllers/api/v1/apps/credentials_controller.rb b/app/controllers/api/v1/apps/credentials_controller.rb
index 8b63d0490..0475b2d4a 100644
--- a/app/controllers/api/v1/apps/credentials_controller.rb
+++ b/app/controllers/api/v1/apps/credentials_controller.rb
@@ -3,8 +3,6 @@
 class Api::V1::Apps::CredentialsController < Api::BaseController
   before_action -> { doorkeeper_authorize! :read }
 
-  respond_to :json
-
   def show
     render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
   end
diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb
index 4cff04cad..a2baeef90 100644
--- a/app/controllers/api/v1/blocks_controller.rb
+++ b/app/controllers/api/v1/blocks_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::BlocksController < Api::BaseController
   before_action :require_user!
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb
index e1b244e76..c15212f0a 100644
--- a/app/controllers/api/v1/bookmarks_controller.rb
+++ b/app/controllers/api/v1/bookmarks_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::BookmarksController < Api::BaseController
   before_action :require_user!
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @statuses = load_statuses
     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb
index b19f27ebf..bc8013379 100644
--- a/app/controllers/api/v1/conversations_controller.rb
+++ b/app/controllers/api/v1/conversations_controller.rb
@@ -9,8 +9,6 @@ class Api::V1::ConversationsController < Api::BaseController
   before_action :set_conversation, except: :index
   after_action :insert_pagination_headers, only: :index
 
-  respond_to :json
-
   def index
     @conversations = paginated_conversations
     render json: @conversations, each_serializer: REST::ConversationSerializer
diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb
index 4e6d5d7c6..08b3474cc 100644
--- a/app/controllers/api/v1/custom_emojis_controller.rb
+++ b/app/controllers/api/v1/custom_emojis_controller.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class Api::V1::CustomEmojisController < Api::BaseController
-  respond_to :json
-
   skip_before_action :set_cache_headers
 
   def index
diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb
index af9e7a20f..5bb02d834 100644
--- a/app/controllers/api/v1/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/domain_blocks_controller.rb
@@ -8,8 +8,6 @@ class Api::V1::DomainBlocksController < Api::BaseController
   before_action :require_user!
   after_action :insert_pagination_headers, only: :show
 
-  respond_to :json
-
   def show
     @blocks = load_domain_blocks
     render json: @blocks.map(&:domain)
diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb
index 2770c7aef..c87dbc4ce 100644
--- a/app/controllers/api/v1/endorsements_controller.rb
+++ b/app/controllers/api/v1/endorsements_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::EndorsementsController < Api::BaseController
   before_action :require_user!
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb
index db827f9d4..3e242905d 100644
--- a/app/controllers/api/v1/favourites_controller.rb
+++ b/app/controllers/api/v1/favourites_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::FavouritesController < Api::BaseController
   before_action :require_user!
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @statuses = load_statuses
     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
diff --git a/app/controllers/api/v1/featured_tags/suggestions_controller.rb b/app/controllers/api/v1/featured_tags/suggestions_controller.rb
index fb27ef88b..8c1b81a0f 100644
--- a/app/controllers/api/v1/featured_tags/suggestions_controller.rb
+++ b/app/controllers/api/v1/featured_tags/suggestions_controller.rb
@@ -2,12 +2,9 @@
 
 class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
-
   before_action :require_user!
   before_action :set_most_used_tags, only: :index
 
-  respond_to :json
-
   def index
     render json: @most_used_tags, each_serializer: REST::TagSerializer
   end
diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb
index e5ebaff4d..b0ace3af0 100644
--- a/app/controllers/api/v1/filters_controller.rb
+++ b/app/controllers/api/v1/filters_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::FiltersController < Api::BaseController
   before_action :set_filters, only: :index
   before_action :set_filter, only: [:show, :update, :destroy]
 
-  respond_to :json
-
   def index
     render json: @filters, each_serializer: REST::FilterSerializer
   end
diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb
index b30e8464c..4f6b4bcbf 100644
--- a/app/controllers/api/v1/instances/activity_controller.rb
+++ b/app/controllers/api/v1/instances/activity_controller.rb
@@ -6,8 +6,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
   skip_before_action :set_cache_headers
   skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
 
-  respond_to :json
-
   def show
     expires_in 1.day, public: true
     render_with_cache json: :activity, expires_in: 1.day
diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb
index cc00d8a6b..9fa440935 100644
--- a/app/controllers/api/v1/instances/peers_controller.rb
+++ b/app/controllers/api/v1/instances/peers_controller.rb
@@ -6,8 +6,6 @@ class Api::V1::Instances::PeersController < Api::BaseController
   skip_before_action :set_cache_headers
   skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
 
-  respond_to :json
-
   def index
     expires_in 1.day, public: true
     render_with_cache(expires_in: 1.day) { Account.remote.domains }
diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb
index c323b60b4..5b5058a7b 100644
--- a/app/controllers/api/v1/instances_controller.rb
+++ b/app/controllers/api/v1/instances_controller.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class Api::V1::InstancesController < Api::BaseController
-  respond_to :json
-
   skip_before_action :set_cache_headers
   skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
 
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index 81825db15..0bb3d0d27 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -3,27 +3,42 @@
 class Api::V1::MediaController < Api::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:media' }
   before_action :require_user!
-
-  respond_to :json
+  before_action :set_media_attachment, except: [:create]
+  before_action :check_processing, except: [:create]
 
   def create
-    @media = current_account.media_attachments.create!(media_params)
-    render json: @media, serializer: REST::MediaAttachmentSerializer
+    @media_attachment = current_account.media_attachments.create!(media_attachment_params)
+    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer
   rescue Paperclip::Errors::NotIdentifiedByImageMagickError
     render json: file_type_error, status: 422
   rescue Paperclip::Error
     render json: processing_error, status: 500
   end
 
+  def show
+    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
+  end
+
   def update
-    @media = current_account.media_attachments.where(status_id: nil).find(params[:id])
-    @media.update!(media_params)
-    render json: @media, serializer: REST::MediaAttachmentSerializer
+    @media_attachment.update!(media_attachment_params)
+    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
   end
 
   private
 
-  def media_params
+  def status_code_for_media_attachment
+    @media_attachment.not_processed? ? 206 : 200
+  end
+
+  def set_media_attachment
+    @media_attachment = current_account.media_attachments.unattached.find(params[:id])
+  end
+
+  def check_processing
+    render json: processing_error, status: 422 if @media_attachment.processing_failed?
+  end
+
+  def media_attachment_params
     params.permit(:file, :description, :focus)
   end
 
diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb
index 3b3a39943..5dc047b43 100644
--- a/app/controllers/api/v1/mutes_controller.rb
+++ b/app/controllers/api/v1/mutes_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::MutesController < Api::BaseController
   before_action :require_user!
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @data = @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index c91753ae7..9dce9b807 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -6,8 +6,6 @@ class Api::V1::NotificationsController < Api::BaseController
   before_action :require_user!
   after_action :insert_pagination_headers, only: :index
 
-  respond_to :json
-
   DEFAULT_NOTIFICATIONS_LIMIT = 15
 
   def index
diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb
index 3fa0b6a76..e1d26106a 100644
--- a/app/controllers/api/v1/polls/votes_controller.rb
+++ b/app/controllers/api/v1/polls/votes_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::Polls::VotesController < Api::BaseController
   before_action :require_user!
   before_action :set_poll
 
-  respond_to :json
-
   def create
     VoteService.new.call(current_account, @poll, vote_params[:choices])
     render json: @poll, serializer: REST::PollSerializer
diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb
index 031e6d42d..744baf7bb 100644
--- a/app/controllers/api/v1/polls_controller.rb
+++ b/app/controllers/api/v1/polls_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::PollsController < Api::BaseController
   before_action :set_poll
   before_action :refresh_poll
 
-  respond_to :json
-
   def show
     render json: @poll, serializer: REST::PollSerializer, include_results: true
   end
diff --git a/app/controllers/api/v1/preferences_controller.rb b/app/controllers/api/v1/preferences_controller.rb
index 077d39f5d..1640a8224 100644
--- a/app/controllers/api/v1/preferences_controller.rb
+++ b/app/controllers/api/v1/preferences_controller.rb
@@ -4,8 +4,6 @@ class Api::V1::PreferencesController < Api::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
   before_action :require_user!
 
-  respond_to :json
-
   def index
     render json: current_account, serializer: REST::PreferencesSerializer
   end
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index 1b0b4b05b..e10083d45 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -4,7 +4,7 @@ class Api::V1::ReportsController < Api::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create]
   before_action :require_user!
 
-  respond_to :json
+  override_rate_limit_headers :create, family: :reports
 
   def create
     @report = ReportService.new.call(
diff --git a/app/controllers/api/v1/statuses/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb
index a7f1eed00..3954af3c9 100644
--- a/app/controllers/api/v1/statuses/bookmarks_controller.rb
+++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
   before_action :require_user!
   before_action :set_status
 
-  respond_to :json
-
   def create
     current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
     render json: @status, serializer: REST::StatusSerializer
diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
index 05f4acc33..8229786d6 100644
--- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
   before_action :set_status
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb
index f18ace996..7afa822ed 100644
--- a/app/controllers/api/v1/statuses/favourites_controller.rb
+++ b/app/controllers/api/v1/statuses/favourites_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
   before_action :require_user!
   before_action :set_status
 
-  respond_to :json
-
   def create
     FavouriteService.new.call(current_account, @status)
     render json: @status, serializer: REST::StatusSerializer
diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb
index b02469b4f..43c7a525a 100644
--- a/app/controllers/api/v1/statuses/mutes_controller.rb
+++ b/app/controllers/api/v1/statuses/mutes_controller.rb
@@ -8,8 +8,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController
   before_action :set_status
   before_action :set_conversation
 
-  respond_to :json
-
   def create
     current_account.mute_conversation!(@conversation)
     @mutes_map = { @conversation.id => true }
diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb
index 4118a8ce4..51b1621b6 100644
--- a/app/controllers/api/v1/statuses/pins_controller.rb
+++ b/app/controllers/api/v1/statuses/pins_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
   before_action :require_user!
   before_action :set_status
 
-  respond_to :json
-
   def create
     StatusPin.create!(account: current_account, status: @status)
     distribute_add_activity!
diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
index fa60e7d84..6c9e49d90 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
   before_action :set_status
   after_action :insert_pagination_headers
 
-  respond_to :json
-
   def index
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb
index 67106ccbe..7fa774a4d 100644
--- a/app/controllers/api/v1/statuses/reblogs_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogs_controller.rb
@@ -7,10 +7,11 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
   before_action :require_user!
   before_action :set_reblog
 
-  respond_to :json
+  override_rate_limit_headers :create, family: :statuses
 
   def create
     @status = ReblogService.new.call(current_account, @reblog, reblog_params)
+
     render json: @status, serializer: REST::StatusSerializer
   end
 
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 486004f9c..29ae91762 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -7,8 +7,9 @@ class Api::V1::StatusesController < Api::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only:   [:create, :destroy]
   before_action :require_user!, except:  [:show, :context]
   before_action :set_status, only:       [:show, :context]
+  before_action :set_thread, only:       [:create]
 
-  respond_to :json
+  override_rate_limit_headers :create, family: :statuses
 
   # This API was originally unlimited, pagination cannot be introduced without
   # breaking backwards-compatibility. Arbitrarily high number to cover most
@@ -36,7 +37,7 @@ class Api::V1::StatusesController < Api::BaseController
   def create
     @status = PostStatusService.new.call(current_user.account,
                                          text: status_params[:status],
-                                         thread: status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
+                                         thread: @thread,
                                          media_ids: status_params[:media_ids],
                                          sensitive: status_params[:sensitive],
                                          spoiler_text: status_params[:spoiler_text],
@@ -45,7 +46,8 @@ class Api::V1::StatusesController < Api::BaseController
                                          application: doorkeeper_token.application,
                                          poll: status_params[:poll],
                                          content_type: status_params[:content_type],
-                                         idempotency: request.headers['Idempotency-Key'])
+                                         idempotency: request.headers['Idempotency-Key'],
+                                         with_rate_limit: true)
 
     render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
   end
@@ -69,6 +71,12 @@ class Api::V1::StatusesController < Api::BaseController
     raise ActiveRecord::RecordNotFound
   end
 
+  def set_thread
+    @thread = status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id])
+  rescue ActiveRecord::RecordNotFound
+    render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
+  end
+
   def status_params
     params.permit(
       :status,
diff --git a/app/controllers/api/v1/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb
index ebb17608c..7cd60615a 100644
--- a/app/controllers/api/v1/streaming_controller.rb
+++ b/app/controllers/api/v1/streaming_controller.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class Api::V1::StreamingController < Api::BaseController
-  respond_to :json
-
   def index
     if Rails.configuration.x.streaming_api_base_url != request.host
       redirect_to streaming_api_url, status: 301
diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb
index 9da2b60ae..52054160d 100644
--- a/app/controllers/api/v1/suggestions_controller.rb
+++ b/app/controllers/api/v1/suggestions_controller.rb
@@ -7,8 +7,6 @@ class Api::V1::SuggestionsController < Api::BaseController
   before_action :require_user!
   before_action :set_accounts
 
-  respond_to :json
-
   def index
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index ff5ede138..ae6dbcb8b 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -5,8 +5,6 @@ class Api::V1::Timelines::HomeController < Api::BaseController
   before_action :require_user!, only: [:show]
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
-  respond_to :json
-
   def show
     @statuses = load_statuses
 
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index ccc10f966..581befef1 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -4,8 +4,6 @@ class Api::V1::Timelines::PublicController < Api::BaseController
   before_action :require_user!, only: [:show], if: :require_auth?
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
-  respond_to :json
-
   def show
     @statuses = load_statuses
     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 9adc4ad29..2d6ad5a80 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -4,8 +4,6 @@ class Api::V1::Timelines::TagController < Api::BaseController
   before_action :load_tag
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
-  respond_to :json
-
   def show
     @statuses = load_statuses
     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
diff --git a/app/controllers/api/v1/trends_controller.rb b/app/controllers/api/v1/trends_controller.rb
index bcea9857e..c875e9041 100644
--- a/app/controllers/api/v1/trends_controller.rb
+++ b/app/controllers/api/v1/trends_controller.rb
@@ -3,8 +3,6 @@
 class Api::V1::TrendsController < Api::BaseController
   before_action :set_tags
 
-  respond_to :json
-
   def index
     render json: @tags, each_serializer: REST::TagSerializer
   end
diff --git a/app/controllers/api/v2/media_controller.rb b/app/controllers/api/v2/media_controller.rb
new file mode 100644
index 000000000..0c1baf01d
--- /dev/null
+++ b/app/controllers/api/v2/media_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class Api::V2::MediaController < Api::V1::MediaController
+  def create
+    @media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params))
+    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: 202
+  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
+    render json: file_type_error, status: 422
+  rescue Paperclip::Error
+    render json: processing_error, status: 500
+  end
+end
diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb
index 76decdb25..ddcf92200 100644
--- a/app/controllers/api/v2/search_controller.rb
+++ b/app/controllers/api/v2/search_controller.rb
@@ -8,8 +8,6 @@ class Api::V2::SearchController < Api::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:search' }
   before_action :require_user!
 
-  respond_to :json
-
   def index
     @search = Search.new(search_results)
     render json: @search, serializer: REST::SearchSerializer
diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb
index 4aa31695c..741ba910f 100644
--- a/app/controllers/api/web/embeds_controller.rb
+++ b/app/controllers/api/web/embeds_controller.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class Api::Web::EmbedsController < Api::Web::BaseController
-  respond_to :json
-
   before_action :require_user!
 
   def create
diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index f388b17e5..7916b82fa 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class Api::Web::PushSubscriptionsController < Api::Web::BaseController
-  respond_to :json
-
   before_action :require_user!
 
   def create
diff --git a/app/controllers/api/web/settings_controller.rb b/app/controllers/api/web/settings_controller.rb
index e3178bf48..3d65e46ed 100644
--- a/app/controllers/api/web/settings_controller.rb
+++ b/app/controllers/api/web/settings_controller.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class Api::Web::SettingsController < Api::Web::BaseController
-  respond_to :json
-
   before_action :require_user!
 
   def update
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index c882d40ab..63d9f91fb 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -30,6 +30,7 @@ class ApplicationController < ActionController::Base
   rescue_from Mastodon::NotPermittedError, with: :forbidden
   rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
   rescue_from Mastodon::RaceConditionError, with: :service_unavailable
+  rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
 
   before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
   before_action :require_functional!, if: :user_signed_in?
@@ -181,6 +182,10 @@ class ApplicationController < ActionController::Base
     respond_with_error(503)
   end
 
+  def too_many_requests
+    respond_with_error(429)
+  end
+
   def single_user_mode?
     @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
   end
diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb
index 20b3fa94b..f0bcac75b 100644
--- a/app/controllers/authorize_interactions_controller.rb
+++ b/app/controllers/authorize_interactions_controller.rb
@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
   end
 
   def create
-    if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource)
+    if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource, with_rate_limit: true)
       render :success
     else
       render :error
diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb
index b79c558d8..86fe58a71 100644
--- a/app/controllers/concerns/rate_limit_headers.rb
+++ b/app/controllers/concerns/rate_limit_headers.rb
@@ -3,6 +3,20 @@
 module RateLimitHeaders
   extend ActiveSupport::Concern
 
+  class_methods do
+    def override_rate_limit_headers(method_name, options = {})
+      around_action(only: method_name, if: :current_account) do |_controller, block|
+        begin
+          block.call
+        ensure
+          rate_limiter = RateLimiter.new(current_account, options)
+          rate_limit_headers = rate_limiter.to_headers
+          response.headers.merge!(rate_limit_headers) unless response.headers['X-RateLimit-Remaining'].present? && rate_limit_headers['X-RateLimit-Remaining'].to_i > response.headers['X-RateLimit-Remaining'].to_i
+        end
+      end
+    end
+  end
+
   included do
     before_action :set_rate_limit_headers, if: :rate_limited_request?
   end
@@ -44,7 +58,7 @@ module RateLimitHeaders
   end
 
   def api_throttle_data
-    most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
+    most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] - v[:count] }
     request.env['rack.attack.throttle_data'][most_limited_type]
   end
 
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index a5dfffd6d..eb223c3f7 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -29,7 +29,8 @@ class FollowerAccountsController < ApplicationController
         render json: collection_presenter,
                serializer: ActivityPub::CollectionSerializer,
                adapter: ActivityPub::Adapter,
-               content_type: 'application/activity+json'
+               content_type: 'application/activity+json',
+               fields: restrict_fields_to
       end
     end
   end
@@ -72,4 +73,12 @@ class FollowerAccountsController < ApplicationController
       )
     end
   end
+
+  def restrict_fields_to
+    if page_requested? || !@account.user_hides_network?
+      # Return all fields
+    else
+      %i(id type totalItems)
+    end
+  end
 end
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index ff23d97f9..4ddccf607 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -29,7 +29,8 @@ class FollowingAccountsController < ApplicationController
         render json: collection_presenter,
                serializer: ActivityPub::CollectionSerializer,
                adapter: ActivityPub::Adapter,
-               content_type: 'application/activity+json'
+               content_type: 'application/activity+json',
+               fields: restrict_fields_to
       end
     end
   end
@@ -72,4 +73,12 @@ class FollowingAccountsController < ApplicationController
       )
     end
   end
+
+  def restrict_fields_to
+    if page_requested? || !@account.user_hides_network?
+      # Return all fields
+    else
+      %i(id type totalItems)
+    end
+  end
 end
diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb
index 38f2e39c1..7b8c4ae23 100644
--- a/app/controllers/settings/imports_controller.rb
+++ b/app/controllers/settings/imports_controller.rb
@@ -29,6 +29,6 @@ class Settings::ImportsController < Settings::BaseController
   end
 
   def import_params
-    params.require(:import).permit(:data, :type)
+    params.require(:import).permit(:data, :type, :mode)
   end
 end
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index 6bc75aa56..88d6e4580 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -9,79 +9,8 @@ module Admin::ActionLogsHelper
     end
   end
 
-  def relevant_log_changes(log)
-    if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
-      log.recorded_changes.slice('domain')
-    elsif log.target_type == 'CustomEmoji' && log.action == :update
-      log.recorded_changes.slice('domain', 'visible_in_picker')
-    elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
-      log.recorded_changes.slice('moderator', 'admin')
-    elsif log.target_type == 'User' && [:change_email].include?(log.action)
-      log.recorded_changes.slice('email', 'unconfirmed_email')
-    elsif log.target_type == 'DomainBlock'
-      log.recorded_changes.slice('severity', 'reject_media')
-    elsif log.target_type == 'Status' && log.action == :update
-      log.recorded_changes.slice('sensitive')
-    elsif log.target_type == 'Announcement' && log.action == :update
-      log.recorded_changes.slice('text', 'starts_at', 'ends_at', 'all_day')
-    end
-  end
-
-  def log_extra_attributes(hash)
-    safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
-  end
-
-  def log_change(val)
-    return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
-    safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
-  end
-
-  def icon_for_log(log)
-    case log.target_type
-    when 'Account', 'User'
-      'user'
-    when 'CustomEmoji'
-      'file'
-    when 'Report'
-      'flag'
-    when 'DomainBlock'
-      'lock'
-    when 'DomainAllow'
-      'plus-circle'
-    when 'EmailDomainBlock'
-      'envelope'
-    when 'Status'
-      'pencil'
-    when 'AccountWarning'
-      'warning'
-    when 'Announcement'
-      'bullhorn'
-    end
-  end
-
-  def class_for_log_icon(log)
-    case log.action
-    when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
-      'positive'
-    when :create
-      opposite_verbs?(log) ? 'negative' : 'positive'
-    when :update, :reset_password, :disable_2fa, :memorialize, :change_email
-      'neutral'
-    when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
-      'negative'
-    when :destroy
-      opposite_verbs?(log) ? 'positive' : 'negative'
-    else
-      ''
-    end
-  end
-
   private
 
-  def opposite_verbs?(log)
-    %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
-  end
-
   def linkable_log_target(record)
     case record.class.name
     when 'Account'
@@ -99,7 +28,7 @@ module Admin::ActionLogsHelper
     when 'AccountWarning'
       link_to record.target_account.acct, admin_account_path(record.target_account_id)
     when 'Announcement'
-      link_to "##{record.id}", edit_admin_announcement_path(record.id)
+      link_to truncate(record.text), edit_admin_announcement_path(record.id)
     end
   end
 
@@ -118,7 +47,7 @@ module Admin::ActionLogsHelper
         I18n.t('admin.action_logs.deleted_status')
       end
     when 'Announcement'
-      "##{attributes['id']}"
+      truncate(attributes['text'])
     end
   end
 end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 6ab92939d..ba0ca9638 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -10,6 +10,7 @@ module Admin::FilterHelper
     InviteFilter::KEYS,
     RelationshipFilter::KEYS,
     AnnouncementFilter::KEYS,
+    Admin::ActionLogFilter::KEYS,
   ].flatten.freeze
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb
new file mode 100644
index 000000000..baf14ab25
--- /dev/null
+++ b/app/helpers/admin/settings_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Admin::SettingsHelper
+  def site_upload_delete_hint(hint, var)
+    upload = SiteUpload.find_by(var: var.to_s)
+    return hint unless upload
+
+    link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
+    safe_join([hint, link], '<br/>'.html_safe)
+  end
+end
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js
index e4d683dd0..f2334c254 100644
--- a/app/javascript/core/admin.js
+++ b/app/javascript/core/admin.js
@@ -1,6 +1,6 @@
 //  This file will be loaded on admin pages, regardless of theme.
 
-import { delegate } from 'rails-ujs';
+import { delegate } from '@rails/ujs';
 import ready from '../mastodon/ready';
 
 const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
@@ -32,6 +32,10 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => {
   });
 });
 
+delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
+  target.form.submit();
+});
+
 const onDomainBlockSeverityChange = (target) => {
   const rejectMediaDiv   = document.querySelector('.input.with_label.domain_block_reject_media');
   const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js
index 0f4222139..39f198fe7 100644
--- a/app/javascript/core/public.js
+++ b/app/javascript/core/public.js
@@ -3,7 +3,7 @@
 import createHistory from 'history/createBrowserHistory';
 import ready from '../mastodon/ready';
 
-const { delegate } = require('rails-ujs');
+const { delegate } = require('@rails/ujs');
 const { length } = require('stringz');
 
 delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
@@ -14,20 +14,6 @@ delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
   return false;
 });
 
-delegate(document, '.status__content__spoiler-link', 'click', function() {
-  const contentEl = this.parentNode.parentNode.querySelector('.e-content');
-
-  if (contentEl.style.display === 'block') {
-    contentEl.style.display = 'none';
-    this.parentNode.style.marginBottom = 0;
-  } else {
-    contentEl.style.display = 'block';
-    this.parentNode.style.marginBottom = null;
-  }
-
-  return false;
-});
-
 delegate(document, '.modal-button', 'click', e => {
   e.preventDefault();
 
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
index e0cb944e0..e02c91cc7 100644
--- a/app/javascript/core/settings.js
+++ b/app/javascript/core/settings.js
@@ -1,7 +1,7 @@
 //  This file will be loaded on settings pages, regardless of theme.
 
 import escapeTextContentForBrowser from 'escape-html';
-const { delegate } = require('rails-ujs');
+const { delegate } = require('@rails/ujs');
 import emojify from '../mastodon/features/emoji/emoji';
 
 delegate(document, '#account_display_name', 'input', ({ target }) => {
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index b659e4ff3..e1012a80b 100644
--- a/app/javascript/flavours/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -370,6 +370,7 @@ export function fetchFollowersFail(id, error) {
     type: FOLLOWERS_FETCH_FAIL,
     id,
     error,
+    skipNotFound: true,
   };
 };
 
@@ -456,6 +457,7 @@ export function fetchFollowingFail(id, error) {
     type: FOLLOWING_FETCH_FAIL,
     id,
     error,
+    skipNotFound: true,
   };
 };
 
@@ -545,6 +547,7 @@ export function fetchRelationshipsFail(error) {
     type: RELATIONSHIPS_FETCH_FAIL,
     error,
     skipLoading: true,
+    skipNotFound: true,
   };
 };
 
diff --git a/app/javascript/flavours/glitch/actions/alerts.js b/app/javascript/flavours/glitch/actions/alerts.js
index cd36d8007..1670f9c10 100644
--- a/app/javascript/flavours/glitch/actions/alerts.js
+++ b/app/javascript/flavours/glitch/actions/alerts.js
@@ -34,11 +34,11 @@ export function showAlert(title = messages.unexpectedTitle, message = messages.u
   };
 };
 
-export function showAlertForError(error) {
+export function showAlertForError(error, skipNotFound = false) {
   if (error.response) {
     const { data, status, statusText, headers } = error.response;
 
-    if (status === 404 || status === 410) {
+    if (skipNotFound && (status === 404 || status === 410)) {
       // Skip these errors as they are reflected in the UI
       return { type: ALERT_NOOP };
     }
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 0be746048..f98cb7bf8 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -259,12 +259,31 @@ export function uploadCompose(files) {
         // Account for disparity in size of original image and resized data
         total += file.size - f.size;
 
-        return api(getState).post('/api/v1/media', data, {
+        return api(getState).post('/api/v2/media', data, {
           onUploadProgress: function({ loaded }){
             progress[i] = loaded;
             dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
           },
-        }).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
+        }).then(({ status, data }) => {
+          // If server-side processing of the media attachment has not completed yet,
+          // poll the server until it is, before showing the media attachment as uploaded
+
+          if (status === 200) {
+            dispatch(uploadComposeSuccess(data, f));
+          } else if (status === 202) {
+            const poll = () => {
+              api(getState).get(`/api/v1/media/${data.id}`).then(response => {
+                if (response.status === 200) {
+                  dispatch(uploadComposeSuccess(response.data, f));
+                } else if (response.status === 206) {
+                  setTimeout(() => poll(), 1000);
+                }
+              }).catch(error => dispatch(uploadComposeFail(error)));
+            };
+
+            poll();
+          }
+        });
       }).catch(error => dispatch(uploadComposeFail(error)));
     };
   };
diff --git a/app/javascript/flavours/glitch/actions/identity_proofs.js b/app/javascript/flavours/glitch/actions/identity_proofs.js
index a7241da20..18e679aec 100644
--- a/app/javascript/flavours/glitch/actions/identity_proofs.js
+++ b/app/javascript/flavours/glitch/actions/identity_proofs.js
@@ -27,4 +27,5 @@ export const fetchAccountIdentityProofsFail = (accountId, err) => ({
   type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
   accountId,
   err,
+  skipNotFound: true,
 });
diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js
index 2ef78025e..1bbdd6142 100644
--- a/app/javascript/flavours/glitch/actions/timelines.js
+++ b/app/javascript/flavours/glitch/actions/timelines.js
@@ -165,6 +165,7 @@ export function expandTimelineFail(timeline, error, isLoadingMore) {
     timeline,
     error,
     skipLoading: !isLoadingMore,
+    skipNotFound: timeline.startsWith('account:'),
   };
 };
 
diff --git a/app/javascript/flavours/glitch/components/domain.js b/app/javascript/flavours/glitch/components/domain.js
index 85729ca94..697065d87 100644
--- a/app/javascript/flavours/glitch/components/domain.js
+++ b/app/javascript/flavours/glitch/components/domain.js
@@ -5,7 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const messages = defineMessages({
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
 });
 
 export default @injectIntl
diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js
index 03b3700df..88f29892e 100644
--- a/app/javascript/flavours/glitch/components/intersection_observer_article.js
+++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js
@@ -45,7 +45,7 @@ export default class IntersectionObserverArticle extends React.Component {
     intersectionObserverWrapper.observe(
       id,
       this.node,
-      this.handleIntersection
+      this.handleIntersection,
     );
 
     this.componentMounted = true;
diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index 9754c73dc..9472e34bf 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -23,7 +23,7 @@ const messages = defineMessages({
     id: 'status.sensitive_toggle',
   },
   toggle_visible: {
-    defaultMessage: 'Toggle visibility',
+    defaultMessage: 'Hide media',
     id: 'media_gallery.toggle_visible',
   },
   warning: {
diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js
index 62965df94..46fd1e8c0 100644
--- a/app/javascript/flavours/glitch/components/poll.js
+++ b/app/javascript/flavours/glitch/components/poll.js
@@ -127,15 +127,7 @@ class Poll extends ImmutablePureComponent {
 
     return (
       <li key={option.get('title')}>
-        {showResults && (
-          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
-            {({ width }) =>
-              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
-            }
-          </Motion>
-        )}
-
-        <label className={classNames('poll__text', { selectable: !showResults })}>
+        <label className={classNames('poll__option', { selectable: !showResults })}>
           <input
             name='vote-options'
             type={poll.get('multiple') ? 'checkbox' : 'radio'}
@@ -157,12 +149,26 @@ class Poll extends ImmutablePureComponent {
             />
           )}
           {showResults && <span className='poll__number'>
-            {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
             {Math.round(percent)}%
           </span>}
 
-          <span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
+          <span
+            className='poll__option__text'
+            dangerouslySetInnerHTML={{ __html: titleEmojified }}
+          />
+
+          {!!voted && <span className='poll__voted'>
+            <Icon id='check' className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
+          </span>}
         </label>
+
+        {showResults && (
+          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
+            {({ width }) =>
+              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
+            }
+          </Motion>
+        )}
       </li>
     );
   }
diff --git a/app/javascript/flavours/glitch/containers/domain_container.js b/app/javascript/flavours/glitch/containers/domain_container.js
index 52d5c1613..e92e102ab 100644
--- a/app/javascript/flavours/glitch/containers/domain_container.js
+++ b/app/javascript/flavours/glitch/containers/domain_container.js
@@ -6,7 +6,7 @@ import Domain from '../components/domain';
 import { openModal } from '../actions/modal';
 
 const messages = defineMessages({
-  blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
+  blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
 });
 
 const makeMapStateToProps = () => {
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 6b4aff616..fb0f165ff 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -30,8 +30,8 @@ const messages = defineMessages({
   report: { id: 'account.report', defaultMessage: 'Report @{name}' },
   share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
   media: { id: 'account.media', defaultMessage: 'Media' },
-  blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
   hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
   showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
   pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
@@ -40,7 +40,7 @@ const messages = defineMessages({
   favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
   lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
   blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
-  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
+  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
   mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
   endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
   unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
@@ -136,7 +136,7 @@ class Header extends ImmutablePureComponent {
     if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
       info.push(<span className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
     } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
-      info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>);
+      info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
     }
 
     if (me !== account.get('id')) {
diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.js
index 033d92adf..49e91227f 100644
--- a/app/javascript/flavours/glitch/features/audio/index.js
+++ b/app/javascript/flavours/glitch/features/audio/index.js
@@ -199,8 +199,8 @@ class Audio extends React.PureComponent {
         <div className='video-player__controls active'>
           <div className='video-player__buttons-bar'>
             <div className='video-player__buttons left'>
-              <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
-              <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
 
               <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
                 &nbsp;
@@ -221,7 +221,7 @@ class Audio extends React.PureComponent {
             </div>
 
             <div className='video-player__buttons right'>
-              <button type='button' aria-label={intl.formatMessage(messages.download)}>
+              <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}>
                 <a className='video-player__download__icon' href={this.props.src} download>
                   <Icon id={'download'} fixedWidth />
                 </a>
diff --git a/app/javascript/flavours/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.js
index 9eb6fe02e..4992689ff 100644
--- a/app/javascript/flavours/glitch/features/blocks/index.js
+++ b/app/javascript/flavours/glitch/features/blocks/index.js
@@ -66,7 +66,7 @@ class Blocks extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} />
+            <AccountContainer key={id} id={id} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index daacbb73f..3d736e83f 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -81,18 +81,6 @@ class ComposeForm extends ImmutablePureComponent {
     this.props.onChange(e.target.value);
   }
 
-  handleKeyDown = ({ ctrlKey, keyCode, metaKey, altKey }) => {
-    //  We submit the status on control/meta + enter.
-    if (keyCode === 13 && (ctrlKey || metaKey)) {
-      this.handleSubmit();
-    }
-
-    // Submit the status with secondary visibility on alt + enter.
-    if (keyCode === 13 && altKey) {
-      this.handleSecondarySubmit();
-    }
-  }
-
   handleSubmit = (overriddenVisibility = null) => {
     const { textarea: { value }, uploadForm } = this;
     const {
@@ -123,7 +111,7 @@ class ComposeForm extends ImmutablePureComponent {
     // Submit unless there are media with missing descriptions
     if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) {
       const firstWithoutDescription = media.find(item => !item.get('description'));
-      onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'));
+      onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'), overriddenVisibility);
     } else if (onSubmit) {
       if (onChangeVisibility && overriddenVisibility) {
         onChangeVisibility(overriddenVisibility);
@@ -171,10 +159,20 @@ class ComposeForm extends ImmutablePureComponent {
   }
 
   //  When the escape key is released, we focus the UI.
-  handleKeyUp = ({ key }) => {
+  handleKeyUp = ({ key, ctrlKey, keyCode, metaKey, altKey }) => {
     if (key === 'Escape') {
       document.querySelector('.ui').parentElement.focus();
     }
+
+    //  We submit the status on control/meta + enter.
+    if (keyCode === 13 && (ctrlKey || metaKey)) {
+      this.handleSubmit();
+    }
+
+    // Submit the status with secondary visibility on alt + enter.
+    if (keyCode === 13 && altKey) {
+      this.handleSecondarySubmit();
+    }
   }
 
   //  Sets a reference to the textarea.
@@ -307,7 +305,6 @@ class ComposeForm extends ImmutablePureComponent {
             placeholder={intl.formatMessage(messages.spoiler_placeholder)}
             value={spoilerText}
             onChange={this.handleChangeSpoiler}
-            onKeyDown={this.handleKeyDown}
             onKeyUp={this.handleKeyUp}
             disabled={!spoiler}
             ref={this.handleRefSpoilerText}
@@ -328,9 +325,9 @@ class ComposeForm extends ImmutablePureComponent {
           disabled={isSubmitting}
           value={this.props.text}
           onChange={this.handleChange}
+          onKeyUp={this.handleKeyUp}
           suggestions={this.props.suggestions}
           onFocus={this.handleFocus}
-          onKeyDown={this.handleKeyDown}
           onSuggestionsFetchRequested={onFetchSuggestions}
           onSuggestionsClearRequested={onClearSuggestions}
           onSuggestionSelected={this.onSuggestionSelected}
diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js
index 92348b000..9e332aabd 100644
--- a/app/javascript/flavours/glitch/features/compose/components/options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/options.js
@@ -34,7 +34,7 @@ const messages = defineMessages({
     id: 'content-type.change',
   },
   direct_long: {
-    defaultMessage: 'Post to mentioned users only',
+    defaultMessage: 'Visible for mentioned users only',
     id: 'privacy.direct.long',
   },
   direct_short: {
@@ -66,7 +66,7 @@ const messages = defineMessages({
     id: 'compose.content-type.plain',
   },
   private_long: {
-    defaultMessage: 'Post to followers only',
+    defaultMessage: 'Visible for followers only',
     id: 'privacy.private.long',
   },
   private_short: {
@@ -74,7 +74,7 @@ const messages = defineMessages({
     id: 'privacy.private.short',
   },
   public_long: {
-    defaultMessage: 'Post to public timelines',
+    defaultMessage: 'Visible for all, shown in public timelines',
     id: 'privacy.public.long',
   },
   public_short: {
@@ -94,7 +94,7 @@ const messages = defineMessages({
     id: 'advanced_options.threaded_mode.short',
   },
   unlisted_long: {
-    defaultMessage: 'Do not show in public timelines',
+    defaultMessage: 'Visible for all, but not in public timelines',
     id: 'privacy.unlisted.long',
   },
   unlisted_short: {
diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.js b/app/javascript/flavours/glitch/features/compose/components/poll_form.js
index 3d818ea20..57fac10ac 100644
--- a/app/javascript/flavours/glitch/features/compose/components/poll_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/poll_form.js
@@ -62,7 +62,7 @@ class Option extends React.PureComponent {
 
     return (
       <li>
-        <label className='poll__text editable'>
+        <label className='poll__option editable'>
           <span className={classNames('poll__input', { checkbox: isPollMultiple })} />
 
           <AutosuggestInput
@@ -143,6 +143,7 @@ class PollForm extends ImmutablePureComponent {
             <option value='true'>{intl.formatMessage(messages.multiple_choices)}</option>
           </select>
 
+          {/* eslint-disable-next-line jsx-a11y/no-onchange */}
           <select value={expiresIn} onChange={this.handleSelectDuration}>
             <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
             <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
index 18e2b2f39..fcd2caf1b 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
@@ -114,11 +114,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(changeComposeVisibility(value));
   },
 
-  onMediaDescriptionConfirm(routerHistory, mediaId) {
+  onMediaDescriptionConfirm(routerHistory, mediaId, overriddenVisibility = null) {
     dispatch(openModal('CONFIRM', {
       message: intl.formatMessage(messages.missingDescriptionMessage),
       confirm: intl.formatMessage(messages.missingDescriptionConfirm),
-      onConfirm: () => dispatch(submitCompose(routerHistory)),
+      onConfirm: () => {
+        if (overriddenVisibility) {
+          dispatch(changeComposeVisibility(overriddenVisibility));
+        };
+        dispatch(submitCompose(routerHistory));
+      },
       secondary: intl.formatMessage(messages.missingDescriptionEdit),
       onSecondary: () => dispatch(openModal('FOCAL_POINT', { id: mediaId })),
       onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)),
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
index ba01f8d5c..47b92560d 100644
--- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
@@ -195,7 +195,7 @@ class Conversation extends ImmutablePureComponent {
     return (
       <HotKeys handlers={handlers}>
         <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
-          <div className='conversation__avatar'>
+          <div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
             <AvatarComposite accounts={accounts} size={48} />
           </div>
 
diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js
index cd105a49b..acce87d5a 100644
--- a/app/javascript/flavours/glitch/features/domain_blocks/index.js
+++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js
@@ -13,8 +13,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import ScrollableList from 'flavours/glitch/components/scrollable_list';
 
 const messages = defineMessages({
-  heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
 });
 
 const mapStateToProps = state => ({
@@ -54,7 +54,7 @@ class Blocks extends ImmutablePureComponent {
       );
     }
 
-    const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
+    const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no blocked domains yet.' />;
 
     return (
       <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
@@ -67,7 +67,7 @@ class Blocks extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {domains.map(domain =>
-            <DomainContainer key={domain} domain={domain} />
+            <DomainContainer key={domain} domain={domain} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js
index 953bf171f..bd6f782ce 100644
--- a/app/javascript/flavours/glitch/features/favourites/index.js
+++ b/app/javascript/flavours/glitch/features/favourites/index.js
@@ -88,7 +88,7 @@ class Favourites extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} withNote={false} />
+            <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js
index 36770aace..efbe1a23c 100644
--- a/app/javascript/flavours/glitch/features/follow_requests/index.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/index.js
@@ -11,6 +11,7 @@ import { fetchFollowRequests, expandFollowRequests } from 'flavours/glitch/actio
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import ScrollableList from 'flavours/glitch/components/scrollable_list';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
@@ -19,6 +20,8 @@ const messages = defineMessages({
 const mapStateToProps = state => ({
   accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
   hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
+  locked: !!state.getIn(['accounts', me, 'locked']),
+  domain: state.getIn(['meta', 'domain']),
 });
 
 export default @connect(mapStateToProps)
@@ -30,6 +33,8 @@ class FollowRequests extends ImmutablePureComponent {
     dispatch: PropTypes.func.isRequired,
     hasMore: PropTypes.bool,
     accountIds: ImmutablePropTypes.list,
+    locked: PropTypes.bool,
+    domain: PropTypes.string,
     intl: PropTypes.object.isRequired,
     multiColumn: PropTypes.bool,
   };
@@ -43,7 +48,7 @@ class FollowRequests extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, accountIds, hasMore, multiColumn } = this.props;
+    const { intl, accountIds, hasMore, multiColumn, locked, domain } = this.props;
 
     if (!accountIds) {
       return (
@@ -54,6 +59,15 @@ class FollowRequests extends ImmutablePureComponent {
     }
 
     const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
+    const unlockedPrependMessage = locked ? null : (
+      <div className='follow_requests-unlocked_explanation'>
+        <FormattedMessage
+          id='follow_requests.unlocked_explanation'
+          defaultMessage='Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.'
+          values={{ domain: domain }}
+        />
+      </div>
+    );
 
     return (
       <Column bindToDocument={!multiColumn} name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}>
@@ -65,9 +79,10 @@ class FollowRequests extends ImmutablePureComponent {
           hasMore={hasMore}
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
+          prepend={unlockedPrependMessage}
         >
           {accountIds.map(id =>
-            <AccountAuthorizeContainer key={id} id={id} />
+            <AccountAuthorizeContainer key={id} id={id} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index c78dcc8e4..2b86cc805 100644
--- a/app/javascript/flavours/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -105,7 +105,7 @@ class Followers extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} withNote={false} />
+            <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js
index df7c19c22..cf374e494 100644
--- a/app/javascript/flavours/glitch/features/following/index.js
+++ b/app/javascript/flavours/glitch/features/following/index.js
@@ -105,7 +105,7 @@ class Following extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} withNote={false} />
+            <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js
index e34c9009b..acaa78fe3 100644
--- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js
+++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js
@@ -95,6 +95,10 @@ class Content extends ImmutablePureComponent {
       } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
         link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
       } else {
+        let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url'));
+        if (status) {
+          link.addEventListener('click', this.onStatusClick.bind(this, status), false);
+        }
         link.setAttribute('title', link.href);
         link.classList.add('unhandled-link');
       }
@@ -120,6 +124,13 @@ class Content extends ImmutablePureComponent {
     }
   }
 
+  onStatusClick = (status, e) => {
+    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      this.context.router.history.push(`/statuses/${status.get('id')}`);
+    }
+  }
+
   handleEmojiMouseEnter = ({ target }) => {
     target.src = target.getAttribute('data-original');
   }
@@ -367,6 +378,14 @@ class Announcements extends ImmutablePureComponent {
     index: 0,
   };
 
+  static getDerivedStateFromProps(props, state) {
+    if (props.announcements.size > 0 && state.index >= props.announcements.size) {
+      return { index: props.announcements.size - 1 };
+    } else {
+      return null;
+    }
+  }
+
   componentDidMount () {
     this._markAnnouncementAsRead();
   }
diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js
index adde3dd5c..e384f301b 100644
--- a/app/javascript/flavours/glitch/features/lists/index.js
+++ b/app/javascript/flavours/glitch/features/lists/index.js
@@ -73,7 +73,7 @@ class Lists extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {lists.map(list =>
-            <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
+            <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js
index c27a530d5..62dccd971 100644
--- a/app/javascript/flavours/glitch/features/mutes/index.js
+++ b/app/javascript/flavours/glitch/features/mutes/index.js
@@ -66,7 +66,7 @@ class Mutes extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} />
+            <AccountContainer key={id} id={id} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js
index 258070358..d88916d19 100644
--- a/app/javascript/flavours/glitch/features/reblogs/index.js
+++ b/app/javascript/flavours/glitch/features/reblogs/index.js
@@ -89,7 +89,7 @@ class Reblogs extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} withNote={false} />
+            <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js
index 7352dc6b4..e3ee7dada 100644
--- a/app/javascript/flavours/glitch/features/status/components/card.js
+++ b/app/javascript/flavours/glitch/features/status/components/card.js
@@ -90,7 +90,7 @@ export default class Card extends React.PureComponent {
           },
         },
       ]),
-      0
+      0,
     );
   };
 
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index c4ac8f0a6..180b11a54 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -198,8 +198,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
       reblogIcon = 'lock';
     }
 
-    if (status.get('visibility') === 'private') {
-      reblogLink = <Icon id={reblogIcon} />;
+    if (!['unlisted', 'public'].includes(status.get('visibility'))) {
+      reblogLink = null;
     } else if (this.context.router) {
       reblogLink = (
         <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
@@ -265,7 +265,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
           <div className='detailed-status__meta'>
             <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
               <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
-            </a>{applicationLink} · {reblogLink} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} />
+            </a>{applicationLink} {!!reblogLink && ['·', reblogLink]} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} />
           </div>
         </div>
       </div>
diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index 1b5fbce9f..6a8952c8d 100644
--- a/app/javascript/flavours/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -488,8 +488,9 @@ class Video extends React.PureComponent {
 
           <div className='video-player__buttons-bar'>
             <div className='video-player__buttons left'>
-              <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
-              <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
+
               <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
                 &nbsp;
                 <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
@@ -512,16 +513,11 @@ class Video extends React.PureComponent {
             </div>
 
             <div className='video-player__buttons right'>
-              {(!onCloseVideo && !editable && !fullscreen) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
-              {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
-              {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
-              <button type='button' aria-label={intl.formatMessage(messages.download)}>
-                <a className='video-player__download__icon' href={this.props.src} download>
-                  <Icon id={'download'} fixedWidth />
-                </a>
-              </button>
-              <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
-
+              {(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
+              {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
+              {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
+              <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button>
+              <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
             </div>
           </div>
         </div>
diff --git a/app/javascript/flavours/glitch/middleware/errors.js b/app/javascript/flavours/glitch/middleware/errors.js
index 212c1f4ad..ade529a4e 100644
--- a/app/javascript/flavours/glitch/middleware/errors.js
+++ b/app/javascript/flavours/glitch/middleware/errors.js
@@ -8,7 +8,7 @@ export default function errorsMiddleware() {
       const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
 
       if (action.type.match(isFail)) {
-        dispatch(showAlertForError(action.error));
+        dispatch(showAlertForError(action.error, action.skipNotFound));
       }
     }
 
diff --git a/app/javascript/flavours/glitch/packs/common.js b/app/javascript/flavours/glitch/packs/common.js
index 94a4e6ee4..1fedc890a 100644
--- a/app/javascript/flavours/glitch/packs/common.js
+++ b/app/javascript/flavours/glitch/packs/common.js
@@ -1,4 +1,4 @@
-import { start } from 'rails-ujs';
+import { start } from '@rails/ujs';
 
 start();
 
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index d8a97704f..e5a567205 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -5,7 +5,7 @@ import loadKeyboardExtensions from 'flavours/glitch/util/load_keyboard_extension
 function main() {
   const IntlMessageFormat = require('intl-messageformat').default;
   const { timeAgoString } = require('flavours/glitch/components/relative_timestamp');
-  const { delegate } = require('rails-ujs');
+  const { delegate } = require('@rails/ujs');
   const emojify = require('flavours/glitch/util/emoji').default;
   const { getLocale } = require('locales');
   const { messages } = getLocale();
@@ -97,6 +97,28 @@ function main() {
 
     delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
+
+    delegate(document, '.status__content__spoiler-link', 'click', function() {
+      const contentEl = this.parentNode.parentNode.querySelector('.e-content');
+
+      if (contentEl.style.display === 'block') {
+        contentEl.style.display = 'none';
+        this.parentNode.style.marginBottom = 0;
+        this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
+      } else {
+        contentEl.style.display = 'block';
+        this.parentNode.style.marginBottom = null;
+        this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
+      }
+
+      return false;
+    });
+
+    [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
+      const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
+      const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
+    });
   });
 
   delegate(document, '.sidebar__toggle__icon', 'click', () => {
diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js
index edf1b82e0..8a9f23505 100644
--- a/app/javascript/flavours/glitch/packs/settings.js
+++ b/app/javascript/flavours/glitch/packs/settings.js
@@ -3,7 +3,7 @@ import ready from 'flavours/glitch/util/ready';
 import loadKeyboardExtensions from 'flavours/glitch/util/load_keyboard_extensions';
 
 function main() {
-  const { delegate } = require('rails-ujs');
+  const { delegate } = require('@rails/ujs');
 
   delegate(document, '.sidebar__toggle__icon', 'click', () => {
     const target = document.querySelector('.sidebar ul');
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 92a9859d9..f758d5c93 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -344,7 +344,6 @@ export default function compose(state = initialState, action) {
     });
   case COMPOSE_SPOILERNESS_CHANGE:
     return state.withMutations(map => {
-      map.set('spoiler_text', '');
       map.set('spoiler', !state.get('spoiler'));
       map.set('idempotencyKey', uuid());
 
diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js
index d0eb7bb5a..31d9611a3 100644
--- a/app/javascript/flavours/glitch/reducers/notifications.js
+++ b/app/javascript/flavours/glitch/reducers/notifications.js
@@ -91,11 +91,11 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
 
       mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
         const lastIndex = 1 + list.findLastIndex(
-          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
+          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')),
         );
 
         const firstIndex = 1 + list.take(lastIndex).findLastIndex(
-          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
+          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0,
         );
 
         return list.take(firstIndex).concat(items, list.skip(lastIndex));
diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js
index 1ea9ed645..be7b2441b 100644
--- a/app/javascript/flavours/glitch/reducers/timelines.js
+++ b/app/javascript/flavours/glitch/reducers/timelines.js
@@ -53,7 +53,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 
         return oldIds.take(firstIndex + 1).concat(
           isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds,
-          oldIds.skip(lastIndex)
+          oldIds.skip(lastIndex),
         );
       });
     }
@@ -171,7 +171,7 @@ export default function timelines(state = initialState, action) {
     return state.update(
       action.timeline,
       initialTimeline,
-      map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items)
+      map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items),
     );
   default:
     return state;
diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js
index ab7dac66a..4a3303c36 100644
--- a/app/javascript/flavours/glitch/selectors/index.js
+++ b/app/javascript/flavours/glitch/selectors/index.js
@@ -146,7 +146,7 @@ export const makeGetStatus = () => {
         map.set('account', accountBase);
         map.set('filtered', filtered);
       });
-    }
+    },
   );
 };
 
diff --git a/app/javascript/flavours/glitch/store/configureStore.js b/app/javascript/flavours/glitch/store/configureStore.js
index 7e7472841..e18af842f 100644
--- a/app/javascript/flavours/glitch/store/configureStore.js
+++ b/app/javascript/flavours/glitch/store/configureStore.js
@@ -10,6 +10,6 @@ export default function configureStore() {
     thunk,
     loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
     errorsMiddleware(),
-    soundsMiddleware()
+    soundsMiddleware(),
   ), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f));
 };
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 26a98c66f..0d24da4dd 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -418,6 +418,11 @@ body,
       }
     }
 
+    &--with-select strong {
+      display: block;
+      margin-bottom: 10px;
+    }
+
     a {
       display: inline-block;
       color: $darker-text-color;
@@ -567,19 +572,22 @@ body,
 }
 
 .log-entry {
-  margin-bottom: 20px;
   line-height: 20px;
+  padding: 15px 0;
+  background: $ui-base-color;
+  border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+  &:last-child {
+    border-bottom: 0;
+  }
 
   &__header {
     display: flex;
     justify-content: flex-start;
     align-items: center;
-    padding: 10px;
-    background: $ui-base-color;
     color: $darker-text-color;
-    border-radius: 4px 4px 0 0;
     font-size: 14px;
-    position: relative;
+    padding: 0 10px;
   }
 
   &__avatar {
@@ -606,44 +614,6 @@ body,
     color: $dark-text-color;
   }
 
-  &__extras {
-    background: lighten($ui-base-color, 6%);
-    border-radius: 0 0 4px 4px;
-    padding: 10px;
-    color: $darker-text-color;
-    font-family: $font-monospace, monospace;
-    font-size: 12px;
-    word-wrap: break-word;
-    min-height: 20px;
-  }
-
-  &__icon {
-    font-size: 28px;
-    margin-right: 10px;
-    color: $dark-text-color;
-  }
-
-  &__icon__overlay {
-    position: absolute;
-    top: 10px;
-    right: 10px;
-    width: 10px;
-    height: 10px;
-    border-radius: 50%;
-
-    &.positive {
-      background: $success-green;
-    }
-
-    &.negative {
-      background: lighten($error-red, 12%);
-    }
-
-    &.neutral {
-      background: $ui-highlight-color;
-    }
-  }
-
   a,
   .username,
   .target {
@@ -651,18 +621,6 @@ body,
     text-decoration: none;
     font-weight: 500;
   }
-
-  .diff-old {
-    color: lighten($error-red, 12%);
-  }
-
-  .diff-neutral {
-    color: $secondary-text-color;
-  }
-
-  .diff-new {
-    color: $success-green;
-  }
 }
 
 a.name-tag,
diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss
index 6305e2a4d..491ceb6ec 100644
--- a/app/javascript/flavours/glitch/styles/components/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/components/accounts.scss
@@ -51,7 +51,6 @@
     @include avatar-radius;
     overflow: hidden;
     position: relative;
-    cursor: default;
 
     & div {
       @include avatar-radius;
diff --git a/app/javascript/flavours/glitch/styles/components/announcements.scss b/app/javascript/flavours/glitch/styles/components/announcements.scss
index eab6c728b..52feefd3c 100644
--- a/app/javascript/flavours/glitch/styles/components/announcements.scss
+++ b/app/javascript/flavours/glitch/styles/components/announcements.scss
@@ -1,5 +1,6 @@
 .announcements__item__content {
   word-wrap: break-word;
+  overflow-y: auto;
 
   .emojione {
     width: 20px;
@@ -69,17 +70,21 @@
     box-sizing: border-box;
     width: 100%;
     padding: 15px;
-    padding-right: 15px + 18px;
     position: relative;
     font-size: 15px;
     line-height: 20px;
     word-wrap: break-word;
     font-weight: 400;
+    max-height: 50vh;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
 
     &__range {
       display: block;
       font-weight: 500;
       margin-bottom: 10px;
+      padding-right: 18px;
     }
 
     &__unread {
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index 525dcaf90..3269638eb 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -452,7 +452,8 @@
 }
 
 .empty-column-indicator,
-.error-column {
+.error-column,
+.follow_requests-unlocked_explanation {
   color: $dark-text-color;
   background: $ui-base-color;
   text-align: center;
@@ -482,6 +483,11 @@
   }
 }
 
+.follow_requests-unlocked_explanation {
+  background: darken($ui-base-color, 4%);
+  contain: initial;
+}
+
 .error-column {
   flex-direction: column;
 }
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index 943776010..460f75c1f 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -3,8 +3,8 @@
 
   .emoji-picker-dropdown {
     position: absolute;
-    right: 5px;
-    top: 5px;
+    top: 0;
+    right: 0;
 
     ::-webkit-scrollbar-track:hover,
     ::-webkit-scrollbar-track:active {
diff --git a/app/javascript/flavours/glitch/styles/components/emoji.scss b/app/javascript/flavours/glitch/styles/components/emoji.scss
index 160e9d811..9dfee346a 100644
--- a/app/javascript/flavours/glitch/styles/components/emoji.scss
+++ b/app/javascript/flavours/glitch/styles/components/emoji.scss
@@ -72,10 +72,7 @@
 
 .emoji-button {
   display: block;
-  font-size: 24px;
-  line-height: 24px;
-  margin-left: 2px;
-  width: 24px;
+  padding: 5px 5px 2px 2px;
   outline: 0;
   cursor: pointer;
 
@@ -91,7 +88,6 @@
     margin: 0;
     width: 22px;
     height: 22px;
-    margin-top: 2px;
   }
 
   &:hover,
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index d97ab436d..50cea8b26 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -1515,6 +1515,7 @@
     padding: 10px;
     padding-top: 12px;
     position: relative;
+    cursor: pointer;
   }
 
   &__unread {
diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss
index 39bfaae9a..3cb076191 100644
--- a/app/javascript/flavours/glitch/styles/components/media.scss
+++ b/app/javascript/flavours/glitch/styles/components/media.scss
@@ -62,12 +62,6 @@
 }
 
 .media-gallery__gifv {
-  &.autoplay {
-    .media-gallery__gifv__label {
-      display: none;
-    }
-  }
-
   &:hover {
     .media-gallery__gifv__label {
       opacity: 1;
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index fa26c4706..50b7f2a72 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -331,13 +331,11 @@
     }
 
     .display-name {
+      color: $light-text-color;
+
       strong {
         color: $inverted-text-color;
       }
-
-      span {
-        color: $lighter-text-color;
-      }
     }
 
     .status__content {
diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss
index 9c86cca58..44338338f 100644
--- a/app/javascript/flavours/glitch/styles/polls.scss
+++ b/app/javascript/flavours/glitch/styles/polls.scss
@@ -14,20 +14,18 @@
   }
 
   &__chart {
-    position: absolute;
-    top: 0;
-    left: 0;
-    height: 100%;
-    display: inline-block;
     border-radius: 4px;
-    background: darken($ui-primary-color, 14%);
+    display: block;
+    background: darken($ui-primary-color, 5%);
+    height: 5px;
+    min-width: 1%;
 
     &.leading {
       background: $ui-highlight-color;
     }
   }
 
-  &__text {
+  &__option {
     position: relative;
     display: flex;
     padding: 6px 0;
@@ -35,6 +33,13 @@
     cursor: default;
     overflow: hidden;
 
+    &__text {
+      display: inline-block;
+      word-wrap: break-word;
+      overflow-wrap: break-word;
+      max-width: calc(100% - 45px - 25px);
+    }
+
     input[type=radio],
     input[type=checkbox] {
       display: none;
@@ -102,8 +107,8 @@
     &:active,
     &:focus,
     &:hover {
+      border-color: lighten($valid-value-color, 15%);
       border-width: 4px;
-      background: none;
     }
 
     &::-moz-focus-inner {
@@ -119,19 +124,18 @@
 
   &__number {
     display: inline-block;
-    width: 52px;
+    width: 45px;
     font-weight: 700;
-    padding: 0 10px;
-    padding-left: 8px;
-    text-align: right;
-    margin-top: auto;
-    margin-bottom: auto;
-    flex: 0 0 52px;
+    flex: 0 0 45px;
   }
 
-  &__vote__mark {
-    float: left;
-    line-height: 18px;
+  &__voted {
+    padding: 0 5px;
+    display: inline-block;
+
+    &__mark {
+      font-size: 18px;
+    }
   }
 
   &__footer {
@@ -208,7 +212,7 @@
     display: flex;
     align-items: center;
 
-    .poll__text {
+    .poll__option {
       flex: 0 0 auto;
       width: calc(100% - (23px + 6px));
       margin-right: 6px;
diff --git a/app/javascript/flavours/glitch/util/log_out.js b/app/javascript/flavours/glitch/util/log_out.js
index 8e1659293..42dcee03e 100644
--- a/app/javascript/flavours/glitch/util/log_out.js
+++ b/app/javascript/flavours/glitch/util/log_out.js
@@ -1,4 +1,4 @@
-import Rails from 'rails-ujs';
+import Rails from '@rails/ujs';
 import { signOutLink } from 'flavours/glitch/util/backend_links';
 
 export const logOut = () => {
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index d4a824e2c..cb2c682a4 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -106,7 +106,7 @@ export function fetchAccount(id) {
       dispatch,
       getState,
       db.transaction('accounts', 'read').objectStore('accounts').index('id'),
-      id
+      id,
     ).then(() => db.close(), error => {
       db.close();
       throw error;
@@ -396,6 +396,7 @@ export function fetchFollowersFail(id, error) {
     type: FOLLOWERS_FETCH_FAIL,
     id,
     error,
+    skipNotFound: true,
   };
 };
 
@@ -482,6 +483,7 @@ export function fetchFollowingFail(id, error) {
     type: FOLLOWING_FETCH_FAIL,
     id,
     error,
+    skipNotFound: true,
   };
 };
 
@@ -571,6 +573,7 @@ export function fetchRelationshipsFail(error) {
     type: RELATIONSHIPS_FETCH_FAIL,
     error,
     skipLoading: true,
+    skipNotFound: true,
   };
 };
 
diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js
index cd36d8007..1670f9c10 100644
--- a/app/javascript/mastodon/actions/alerts.js
+++ b/app/javascript/mastodon/actions/alerts.js
@@ -34,11 +34,11 @@ export function showAlert(title = messages.unexpectedTitle, message = messages.u
   };
 };
 
-export function showAlertForError(error) {
+export function showAlertForError(error, skipNotFound = false) {
   if (error.response) {
     const { data, status, statusText, headers } = error.response;
 
-    if (status === 404 || status === 410) {
+    if (skipNotFound && (status === 404 || status === 410)) {
       // Skip these errors as they are reflected in the UI
       return { type: ALERT_NOOP };
     }
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index c3c6ff1a1..6b73fc90e 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -230,12 +230,31 @@ export function uploadCompose(files) {
         // Account for disparity in size of original image and resized data
         total += file.size - f.size;
 
-        return api(getState).post('/api/v1/media', data, {
+        return api(getState).post('/api/v2/media', data, {
           onUploadProgress: function({ loaded }){
             progress[i] = loaded;
             dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
           },
-        }).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
+        }).then(({ status, data }) => {
+          // If server-side processing of the media attachment has not completed yet,
+          // poll the server until it is, before showing the media attachment as uploaded
+
+          if (status === 200) {
+            dispatch(uploadComposeSuccess(data, f));
+          } else if (status === 202) {
+            const poll = () => {
+              api(getState).get(`/api/v1/media/${data.id}`).then(response => {
+                if (response.status === 200) {
+                  dispatch(uploadComposeSuccess(response.data, f));
+                } else if (response.status === 206) {
+                  setTimeout(() => poll(), 1000);
+                }
+              }).catch(error => dispatch(uploadComposeFail(error)));
+            };
+
+            poll();
+          }
+        });
       }).catch(error => dispatch(uploadComposeFail(error)));
     };
   };
diff --git a/app/javascript/mastodon/actions/identity_proofs.js b/app/javascript/mastodon/actions/identity_proofs.js
index 449debf61..103983956 100644
--- a/app/javascript/mastodon/actions/identity_proofs.js
+++ b/app/javascript/mastodon/actions/identity_proofs.js
@@ -27,4 +27,5 @@ export const fetchAccountIdentityProofsFail = (accountId, err) => ({
   type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
   accountId,
   err,
+  skipNotFound: true,
 });
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 054668655..cdd2111f8 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -149,6 +149,7 @@ export function expandTimelineFail(timeline, error, isLoadingMore) {
     timeline,
     error,
     skipLoading: !isLoadingMore,
+    skipNotFound: timeline.startsWith('account:'),
   };
 };
 
diff --git a/app/javascript/mastodon/common.js b/app/javascript/mastodon/common.js
index fba21316a..6818aa5d5 100644
--- a/app/javascript/mastodon/common.js
+++ b/app/javascript/mastodon/common.js
@@ -1,4 +1,4 @@
-import Rails from 'rails-ujs';
+import Rails from '@rails/ujs';
 
 export function start() {
   require('font-awesome/css/font-awesome.css');
diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js
index ea82f9ef9..1bb583583 100644
--- a/app/javascript/mastodon/components/column_header.js
+++ b/app/javascript/mastodon/components/column_header.js
@@ -76,8 +76,9 @@ class ColumnHeader extends React.PureComponent {
 
   handlePin = () => {
     if (!this.props.pinned) {
-      this.historyBack();
+      this.context.router.history.replace('/');
     }
+
     this.props.onPin();
   }
 
diff --git a/app/javascript/mastodon/components/domain.js b/app/javascript/mastodon/components/domain.js
index 85729ca94..697065d87 100644
--- a/app/javascript/mastodon/components/domain.js
+++ b/app/javascript/mastodon/components/domain.js
@@ -5,7 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const messages = defineMessages({
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
 });
 
 export default @injectIntl
diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js
index e453730ba..124b34b02 100644
--- a/app/javascript/mastodon/components/intersection_observer_article.js
+++ b/app/javascript/mastodon/components/intersection_observer_article.js
@@ -44,7 +44,7 @@ export default class IntersectionObserverArticle extends React.Component {
     intersectionObserverWrapper.observe(
       id,
       this.node,
-      this.handleIntersection
+      this.handleIntersection,
     );
 
     this.componentMounted = true;
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index cfe164a50..283d7e0a5 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -10,7 +10,7 @@ import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_s
 import { decode } from 'blurhash';
 
 const messages = defineMessages({
-  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
+  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide media' },
 });
 
 class Item extends React.PureComponent {
diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js
index 3a17e80e7..7525a1030 100644
--- a/app/javascript/mastodon/components/poll.js
+++ b/app/javascript/mastodon/components/poll.js
@@ -127,15 +127,7 @@ class Poll extends ImmutablePureComponent {
 
     return (
       <li key={option.get('title')}>
-        {showResults && (
-          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
-            {({ width }) =>
-              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
-            }
-          </Motion>
-        )}
-
-        <label className={classNames('poll__text', { selectable: !showResults })}>
+        <label className={classNames('poll__option', { selectable: !showResults })}>
           <input
             name='vote-options'
             type={poll.get('multiple') ? 'checkbox' : 'radio'}
@@ -157,12 +149,26 @@ class Poll extends ImmutablePureComponent {
             />
           )}
           {showResults && <span className='poll__number'>
-            {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
             {Math.round(percent)}%
           </span>}
 
-          <span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
+          <span
+            className='poll__option__text'
+            dangerouslySetInnerHTML={{ __html: titleEmojified }}
+          />
+
+          {!!voted && <span className='poll__voted'>
+            <Icon id='check' className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
+          </span>}
         </label>
+
+        {showResults && (
+          <Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
+            {({ width }) =>
+              <span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
+            }
+          </Motion>
+        )}
       </li>
     );
   }
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index 3a490e78e..65ca43911 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -82,15 +82,19 @@ export default class ScrollableList extends PureComponent {
   lastScrollWasSynthetic = false;
   scrollToTopOnMouseIdle = false;
 
+  _getScrollingElement = () => {
+    if (this.props.bindToDocument) {
+      return (document.scrollingElement || document.body);
+    } else {
+      return this.node;
+    }
+  }
+
   setScrollTop = newScrollTop => {
     if (this.getScrollTop() !== newScrollTop) {
       this.lastScrollWasSynthetic = true;
 
-      if (this.props.bindToDocument) {
-        document.scrollingElement.scrollTop = newScrollTop;
-      } else {
-        this.node.scrollTop = newScrollTop;
-      }
+      this._getScrollingElement().scrollTop = newScrollTop;
     }
   };
 
@@ -151,15 +155,15 @@ export default class ScrollableList extends PureComponent {
   }
 
   getScrollTop = () => {
-    return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop;
+    return this._getScrollingElement().scrollTop;
   }
 
   getScrollHeight = () => {
-    return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight;
+    return this._getScrollingElement().scrollHeight;
   }
 
   getClientHeight = () => {
-    return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight;
+    return this._getScrollingElement().clientHeight;
   }
 
   updateScrollBottom = (snapshot) => {
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 0dc00cb98..075ee1b87 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -432,16 +432,10 @@ class Status extends ImmutablePureComponent {
               </a>
             </div>
 
-            <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
+            <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
 
             {media}
 
-            {showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
-              <button className='status__content__read-more-button' onClick={this.handleClick}>
-                <FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
-              </button>
-            )}
-
             <StatusActionBar status={status} account={account} {...other} />
           </div>
         </div>
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index e2c8d43c9..bebbbcb5a 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -36,8 +36,8 @@ const messages = defineMessages({
   admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
   admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
   copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
-  blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
 });
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 5d921fd41..3200f2d82 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -20,6 +20,7 @@ export default class StatusContent extends React.PureComponent {
   static propTypes = {
     status: ImmutablePropTypes.map.isRequired,
     expanded: PropTypes.bool,
+    showThread: PropTypes.bool,
     onExpandedToggle: PropTypes.func,
     onClick: PropTypes.func,
     collapsable: PropTypes.bool,
@@ -181,6 +182,7 @@ export default class StatusContent extends React.PureComponent {
 
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
     const renderReadMore = this.props.onClick && status.get('collapsed');
+    const renderViewThread = this.props.showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
 
     const content = { __html: status.get('contentHtml') };
     const spoilerContent = { __html: status.get('spoilerHtml') };
@@ -195,6 +197,12 @@ export default class StatusContent extends React.PureComponent {
       directionStyle.direction = 'rtl';
     }
 
+    const showThreadButton = (
+      <button className='status__content__read-more-button' onClick={this.props.onClick}>
+        <FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
+      </button>
+    );
+
     const readMoreButton = (
       <button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
         <FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth />
@@ -229,6 +237,8 @@ export default class StatusContent extends React.PureComponent {
           <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
 
           {!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
+
+          {renderViewThread && showThreadButton}
         </div>
       );
     } else if (this.props.onClick) {
@@ -237,6 +247,8 @@ export default class StatusContent extends React.PureComponent {
           <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
 
           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
+
+          {renderViewThread && showThreadButton}
         </div>,
       ];
 
@@ -251,6 +263,8 @@ export default class StatusContent extends React.PureComponent {
           <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
 
           {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
+
+          {renderViewThread && showThreadButton}
         </div>
       );
     }
diff --git a/app/javascript/mastodon/containers/domain_container.js b/app/javascript/mastodon/containers/domain_container.js
index 813178bbf..8a8ba1df1 100644
--- a/app/javascript/mastodon/containers/domain_container.js
+++ b/app/javascript/mastodon/containers/domain_container.js
@@ -6,7 +6,7 @@ import Domain from '../components/domain';
 import { openModal } from '../actions/modal';
 
 const messages = defineMessages({
-  blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
+  blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
 });
 
 const makeMapStateToProps = () => {
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 8bd7f2db5..92780a70b 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -29,8 +29,8 @@ const messages = defineMessages({
   report: { id: 'account.report', defaultMessage: 'Report @{name}' },
   share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
   media: { id: 'account.media', defaultMessage: 'Media' },
-  blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
   hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
   showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
   pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
@@ -39,7 +39,7 @@ const messages = defineMessages({
   favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
   lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
   blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
-  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
+  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
   mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
   endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
   unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
@@ -142,7 +142,7 @@ class Header extends ImmutablePureComponent {
     if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
       info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
     } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
-      info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>);
+      info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
     }
 
     if (me !== account.get('id')) {
diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js
index fda5a074f..95c9c7751 100644
--- a/app/javascript/mastodon/features/audio/index.js
+++ b/app/javascript/mastodon/features/audio/index.js
@@ -214,8 +214,8 @@ class Audio extends React.PureComponent {
         <div className='video-player__controls active'>
           <div className='video-player__buttons-bar'>
             <div className='video-player__buttons left'>
-              <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
-              <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
 
               <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
                 &nbsp;
@@ -236,7 +236,7 @@ class Audio extends React.PureComponent {
             </div>
 
             <div className='video-player__buttons right'>
-              <button type='button' aria-label={intl.formatMessage(messages.download)}>
+              <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}>
                 <a className='video-player__download__icon' href={this.props.src} download>
                   <Icon id={'download'} fixedWidth />
                 </a>
diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js
index 051431ed2..870c0de09 100644
--- a/app/javascript/mastodon/features/blocks/index.js
+++ b/app/javascript/mastodon/features/blocks/index.js
@@ -68,7 +68,7 @@ class Blocks extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} />
+            <AccountContainer key={id} id={id} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/compose/components/action_bar.js b/app/javascript/mastodon/features/compose/components/action_bar.js
index dd2632796..07d92bb7e 100644
--- a/app/javascript/mastodon/features/compose/components/action_bar.js
+++ b/app/javascript/mastodon/features/compose/components/action_bar.js
@@ -16,6 +16,7 @@ const messages = defineMessages({
   mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
   filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
   logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
+  bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
 });
 
 export default @injectIntl
@@ -42,6 +43,7 @@ class ActionBar extends React.PureComponent {
     menu.push(null);
     menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
     menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
+    menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
     menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
     menu.push(null);
     menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js
index cac3776bb..271019dfe 100644
--- a/app/javascript/mastodon/features/compose/components/poll_form.js
+++ b/app/javascript/mastodon/features/compose/components/poll_form.js
@@ -75,7 +75,7 @@ class Option extends React.PureComponent {
 
     return (
       <li>
-        <label className='poll__text editable'>
+        <label className='poll__option editable'>
           <span
             className={classNames('poll__input', { checkbox: isPollMultiple })}
             onClick={this.handleToggleMultiple}
@@ -155,6 +155,7 @@ class PollForm extends ImmutablePureComponent {
         <div className='poll__footer'>
           <button disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
 
+          {/* eslint-disable-next-line jsx-a11y/no-onchange */}
           <select value={expiresIn} onChange={this.handleSelectDuration}>
             <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
             <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index 7cbfe463a..de030b7a2 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -11,13 +11,13 @@ import Icon from 'mastodon/components/icon';
 
 const messages = defineMessages({
   public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
-  public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
+  public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' },
   unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
-  unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
+  unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' },
   private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
-  private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
+  private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
   direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
-  direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
+  direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
   change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
 });
 
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
index 235cb7ad8..f9e45067f 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
@@ -160,7 +160,7 @@ class Conversation extends ImmutablePureComponent {
     return (
       <HotKeys handlers={handlers}>
         <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
-          <div className='conversation__avatar'>
+          <div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
             <AvatarComposite accounts={accounts} size={48} />
           </div>
 
diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js
index 482245c86..a6d988912 100644
--- a/app/javascript/mastodon/features/domain_blocks/index.js
+++ b/app/javascript/mastodon/features/domain_blocks/index.js
@@ -13,8 +13,8 @@ import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_bloc
 import ScrollableList from '../../components/scrollable_list';
 
 const messages = defineMessages({
-  heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
 });
 
 const mapStateToProps = state => ({
@@ -55,7 +55,7 @@ class Blocks extends ImmutablePureComponent {
       );
     }
 
-    const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
+    const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no blocked domains yet.' />;
 
     return (
       <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
@@ -69,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {domains.map(domain =>
-            <DomainContainer key={domain} domain={domain} />
+            <DomainContainer key={domain} domain={domain} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js
index 249e6a044..75cb00c0e 100644
--- a/app/javascript/mastodon/features/favourites/index.js
+++ b/app/javascript/mastodon/features/favourites/index.js
@@ -79,7 +79,7 @@ class Favourites extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} withNote={false} />
+            <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js
index 57ef44145..7078e4e6c 100644
--- a/app/javascript/mastodon/features/follow_requests/index.js
+++ b/app/javascript/mastodon/features/follow_requests/index.js
@@ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 import AccountAuthorizeContainer from './containers/account_authorize_container';
 import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
 import ScrollableList from '../../components/scrollable_list';
+import { me } from '../../initial_state';
 
 const messages = defineMessages({
   heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
@@ -19,6 +20,8 @@ const messages = defineMessages({
 const mapStateToProps = state => ({
   accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
   hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
+  locked: !!state.getIn(['accounts', me, 'locked']),
+  domain: state.getIn(['meta', 'domain']),
 });
 
 export default @connect(mapStateToProps)
@@ -31,6 +34,8 @@ class FollowRequests extends ImmutablePureComponent {
     shouldUpdateScroll: PropTypes.func,
     hasMore: PropTypes.bool,
     accountIds: ImmutablePropTypes.list,
+    locked: PropTypes.bool,
+    domain: PropTypes.string,
     intl: PropTypes.object.isRequired,
     multiColumn: PropTypes.bool,
   };
@@ -44,7 +49,7 @@ class FollowRequests extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn } = this.props;
+    const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn, locked, domain } = this.props;
 
     if (!accountIds) {
       return (
@@ -55,6 +60,15 @@ class FollowRequests extends ImmutablePureComponent {
     }
 
     const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
+    const unlockedPrependMessage = locked ? null : (
+      <div className='follow_requests-unlocked_explanation'>
+        <FormattedMessage
+          id='follow_requests.unlocked_explanation'
+          defaultMessage='Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.'
+          values={{ domain: domain }}
+        />
+      </div>
+    );
 
     return (
       <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}>
@@ -66,9 +80,10 @@ class FollowRequests extends ImmutablePureComponent {
           shouldUpdateScroll={shouldUpdateScroll}
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
+          prepend={unlockedPrependMessage}
         >
           {accountIds.map(id =>
-            <AccountAuthorizeContainer key={id} id={id} />
+            <AccountAuthorizeContainer key={id} id={id} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index 9e635d250..f8723e055 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -93,7 +93,7 @@ class Followers extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {blockedBy ? [] : accountIds.map(id =>
-            <AccountContainer key={id} id={id} withNote={false} />
+            <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js
index 284ae2c11..5112bfa9d 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -93,7 +93,7 @@ class Following extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {blockedBy ? [] : accountIds.map(id =>
-            <AccountContainer key={id} id={id} withNote={false} />
+            <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js
index 91cf6215e..1896994da 100644
--- a/app/javascript/mastodon/features/getting_started/components/announcements.js
+++ b/app/javascript/mastodon/features/getting_started/components/announcements.js
@@ -95,6 +95,10 @@ class Content extends ImmutablePureComponent {
       } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
         link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
       } else {
+        let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url'));
+        if (status) {
+          link.addEventListener('click', this.onStatusClick.bind(this, status), false);
+        }
         link.setAttribute('title', link.href);
         link.classList.add('unhandled-link');
       }
@@ -120,6 +124,13 @@ class Content extends ImmutablePureComponent {
     }
   }
 
+  onStatusClick = (status, e) => {
+    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      e.preventDefault();
+      this.context.router.history.push(`/statuses/${status.get('id')}`);
+    }
+  }
+
   handleEmojiMouseEnter = ({ target }) => {
     target.src = target.getAttribute('data-original');
   }
@@ -367,6 +378,14 @@ class Announcements extends ImmutablePureComponent {
     index: 0,
   };
 
+  static getDerivedStateFromProps(props, state) {
+    if (props.announcements.size > 0 && state.index >= props.announcements.size) {
+      return { index: props.announcements.size - 1 };
+    } else {
+      return null;
+    }
+  }
+
   componentDidMount () {
     this._markAnnouncementAsRead();
   }
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index adbc147d1..d9838e1c7 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -106,20 +106,20 @@ class GettingStarted extends ImmutablePureComponent {
 
       if (profile_directory) {
         navItems.push(
-          <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />
+          <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
         );
 
         height += 48;
       }
 
       navItems.push(
-        <ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />
+        <ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />,
       );
 
       height += 34;
     } else if (profile_directory) {
       navItems.push(
-        <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />
+        <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
       );
 
       height += 48;
@@ -129,7 +129,7 @@ class GettingStarted extends ImmutablePureComponent {
       <ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
       <ColumnLink key={i++} icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
       <ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
-      <ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />
+      <ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
     );
 
     height += 48*4;
diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js
index 7f7f5009c..ca1fa1f5e 100644
--- a/app/javascript/mastodon/features/lists/index.js
+++ b/app/javascript/mastodon/features/lists/index.js
@@ -74,7 +74,7 @@ class Lists extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {lists.map(list =>
-            <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
+            <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js
index 91dd268c1..3f58a62d2 100644
--- a/app/javascript/mastodon/features/mutes/index.js
+++ b/app/javascript/mastodon/features/mutes/index.js
@@ -68,7 +68,7 @@ class Mutes extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} />
+            <AccountContainer key={id} id={id} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js
index 9179e51db..4becb5fb7 100644
--- a/app/javascript/mastodon/features/reblogs/index.js
+++ b/app/javascript/mastodon/features/reblogs/index.js
@@ -79,7 +79,7 @@ class Reblogs extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} withNote={false} />
+            <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index 959774da4..ba62d7b10 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -32,8 +32,8 @@ const messages = defineMessages({
   admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
   admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
   copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
-  blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
-  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+  blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
+  unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
 });
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index 2993fe29a..b8344a667 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -98,7 +98,7 @@ export default class Card extends React.PureComponent {
           },
         },
       ]),
-      0
+      0,
     );
   };
 
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 2fec247c1..7a82fa13a 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -166,7 +166,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
       reblogIcon = 'lock';
     }
 
-    if (status.get('visibility') === 'private') {
+    if (['private', 'direct'].includes(status.get('visibility'))) {
       reblogLink = <Icon id={reblogIcon} />;
     } else if (this.context.router) {
       reblogLink = (
diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
index 11cc1b6e8..89cb2458d 100644
--- a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
+++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
@@ -19,7 +19,7 @@ describe('<Column />', () => {
       const wrapper = mount(
         <Column heading='notifications'>
           <div className='scrollable' />
-        </Column>
+        </Column>,
       );
       wrapper.find(ColumnHeader).find('button').simulate('click');
       expect(global.requestAnimationFrame.mock.calls.length).toEqual(1);
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 8ac9c8db7..42ded9d21 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -492,8 +492,8 @@ class Video extends React.PureComponent {
 
           <div className='video-player__buttons-bar'>
             <div className='video-player__buttons left'>
-              <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
-              <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
+              <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
 
               <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
                 &nbsp;
@@ -517,11 +517,11 @@ class Video extends React.PureComponent {
             </div>
 
             <div className='video-player__buttons right'>
-              {(!onCloseVideo && !editable && !fullscreen) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
-              {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
-              {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
-              <button type='button' aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button>
-              <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
+              {(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
+              {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
+              {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
+              <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button>
+              <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
             </div>
           </div>
         </div>
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index 8006a1306..38653e0fe 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -123,7 +123,7 @@
   "directory.federated": "Dende'l fediversu",
   "directory.local": "Dende {domain} namái",
   "directory.new_arrivals": "Cuentes nueves",
-  "directory.recently_active": "Recently active",
+  "directory.recently_active": "Actividá recién",
   "embed.instructions": "Empotra esti estáu nun sitiu web copiando'l códigu d'embaxo.",
   "embed.preview": "Asina ye cómo va vese:",
   "emoji_button.activity": "Actividaes",
@@ -154,7 +154,7 @@
   "empty_column.home": "¡Tienes la llinia temporal balera! Visita {public} o usa la gueta pa entamar y conocer a otros usuarios.",
   "empty_column.home.public_timeline": "la llinia temporal pública",
   "empty_column.list": "Entá nun hai nada nesta llista. Cuando los miembros d'esta llista espublicen estaos nuevos, van apaecer equí.",
-  "empty_column.lists": "Entá nun tienes nunenguna llista. Cuando crees una, va amosase equí.",
+  "empty_column.lists": "Entá nun tienes nenguna llista. Cuando crees una, va amosase equí.",
   "empty_column.mutes": "Entá nun silenciesti a nunengún usuariu.",
   "empty_column.notifications": "Entá nun tienes nunengún avisu. Interactúa con otros p'aniciar la conversación.",
   "empty_column.public": "¡Equí nun hai nada! Escribi daqué público o sigui a usuarios d'otros sirvidores pa rellenar esto",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 5429f358f..c1a7bb533 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Зареждане...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json
index 96092457f..19a091fa0 100644
--- a/app/javascript/mastodon/locales/br.json
+++ b/app/javascript/mastodon/locales/br.json
@@ -1,64 +1,64 @@
 {
-  "account.add_or_remove_from_list": "Ouzhpenn pe lemel ag ar listennadoù",
+  "account.add_or_remove_from_list": "Ouzhpenn pe dilemel eus al listennadoù",
   "account.badges.bot": "Robot",
   "account.badges.group": "Strollad",
-  "account.block": "Stankañ @{name}",
-  "account.block_domain": "Kuzh kement tra a {domain}",
+  "account.block": "Berzañ @{name}",
+  "account.block_domain": "Berzañ pep tra eus {domain}",
   "account.blocked": "Stanket",
-  "account.cancel_follow_request": "Nullañ ar pedad heuliañ",
-  "account.direct": "Kas ur c'hemennad da @{name}",
-  "account.domain_blocked": "Domani kuzhet",
+  "account.cancel_follow_request": "Nullañ ar bedadenn heuliañ",
+  "account.direct": "Kas ur gemennadenn da @{name}",
+  "account.domain_blocked": "Domani berzet",
   "account.edit_profile": "Aozañ ar profil",
   "account.endorse": "Lakaat war-wel war ar profil",
   "account.follow": "Heuliañ",
-  "account.followers": "Heilour·ezed·ion",
-  "account.followers.empty": "Den na heul an implijour-mañ c'hoazh.",
+  "account.followers": "Heulier·ezed·ien",
+  "account.followers.empty": "Den na heul an implijer-mañ c'hoazh.",
   "account.follows": "Koumanantoù",
-  "account.follows.empty": "An implijer-mañ na heul ket den ebet.",
+  "account.follows.empty": "An implijer·ez-mañ na heul den ebet.",
   "account.follows_you": "Ho heul",
-  "account.hide_reblogs": "Kuzh toudoù skignet gant @{name}",
+  "account.hide_reblogs": "Kuzh toudoù rannet gant @{name}",
   "account.last_status": "Oberiantiz zivezhañ",
-  "account.link_verified_on": "Ownership of this link was checked on {date}",
-  "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
+  "account.link_verified_on": "Gwiriet eo bet perc'hennidigezh al liamm d'an deiziad-mañ : {date}",
+  "account.locked_info": "Prennet eo ar gon-mañ. Dibab a ra ar perc'henn ar re a c'hall heuliañ anezhi pe anezhañ.",
   "account.media": "Media",
   "account.mention": "Menegiñ @{name}",
-  "account.moved_to": "Dilojet en·he deus {name} da:",
+  "account.moved_to": "Dilojet en·he deus {name} da :",
   "account.mute": "Kuzhat @{name}",
-  "account.mute_notifications": "Kuzh kemennoù a @{name}",
+  "account.mute_notifications": "Kuzh kemennoù eus @{name}",
   "account.muted": "Kuzhet",
   "account.never_active": "Birviken",
-  "account.posts": "Toudoù",
+  "account.posts": "a doudoù",
   "account.posts_with_replies": "Toudoù ha respontoù",
   "account.report": "Disklêriañ @{name}",
-  "account.requested": "É c'hortoz bout aprouet. Clikit da nullañ ar pedad heuliañ",
+  "account.requested": "O c'hortoz an asant. Klikit evit nullañ ar goulenn heuliañ",
   "account.share": "Skignañ profil @{name}",
-  "account.show_reblogs": "Diskouez toudoù a @{name}",
-  "account.unblock": "Distankañ @{name}",
-  "account.unblock_domain": "Diguzh {domain}",
+  "account.show_reblogs": "Diskouez skignadennoù @{name}",
+  "account.unblock": "Diverzañ @{name}",
+  "account.unblock_domain": "Diverzañ an domani {domain}",
   "account.unendorse": "Paouez da lakaat war-wel war ar profil",
   "account.unfollow": "Diheuliañ",
   "account.unmute": "Diguzhat @{name}",
   "account.unmute_notifications": "Diguzhat kemennoù a @{name}",
   "alert.rate_limited.message": "Klaskit en-dro a-benn {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.title": "Feur bevennet",
   "alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.",
-  "alert.unexpected.title": "C'hem !",
+  "alert.unexpected.title": "Hopala!",
   "announcement.announcement": "Kemenn",
   "autosuggest_hashtag.per_week": "{count} bep sizhun",
   "boost_modal.combo": "Ar wezh kentañ e c'halliot gwaskañ war {combo} evit tremen hebiou",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Klask endro",
+  "bundle_column_error.body": "Degouezhet ez eus bet ur fazi en ur gargañ an elfenn-mañ.",
+  "bundle_column_error.retry": "Klask en-dro",
   "bundle_column_error.title": "Fazi rouedad",
   "bundle_modal_error.close": "Serriñ",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Klask endro",
-  "column.blocks": "Implijour·ezed·ion stanket",
+  "bundle_modal_error.message": "Degouezhet ez eus bet ur fazi en ur gargañ an elfenn-mañ.",
+  "bundle_modal_error.retry": "Klask en-dro",
+  "column.blocks": "Implijer·ezed·ien berzet",
   "column.bookmarks": "Sinedoù",
   "column.community": "Red-amzer lec'hel",
   "column.direct": "Kemennadoù prevez",
   "column.directory": "Mont a-dreuz ar profiloù",
-  "column.domain_blocks": "Domani kuzhet",
-  "column.favourites": "Ar re vuiañ-karet",
+  "column.domain_blocks": "Domani berzet",
+  "column.favourites": "Muiañ-karet",
   "column.follow_requests": "Pedadoù heuliañ",
   "column.home": "Degemer",
   "column.lists": "Listennoù",
@@ -68,150 +68,150 @@
   "column.public": "Red-amzer kevreet",
   "column_back_button.label": "Distro",
   "column_header.hide_settings": "Kuzhat an arventennoù",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.moveLeft_settings": "Dilec'hiañ ar bannad a-gleiz",
+  "column_header.moveRight_settings": "Dilec'hiañ ar bannad a-zehou",
   "column_header.pin": "Spilhennañ",
   "column_header.show_settings": "Diskouez an arventennoù",
   "column_header.unpin": "Dispilhennañ",
   "column_subheading.settings": "Arventennoù",
   "community.column_settings.media_only": "Nemet Mediaoù",
-  "compose_form.direct_message_warning": "An toud-mañ a vo kaset nemet d'an implijer·ion·ezed meneget.",
-  "compose_form.direct_message_warning_learn_more": "Gouiet hiroc'h",
-  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
-  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+  "compose_form.direct_message_warning": "An toud-mañ a vo kaset nemet d'an implijer·ezed·ien meneget.",
+  "compose_form.direct_message_warning_learn_more": "Gouzout hiroc'h",
+  "compose_form.hashtag_warning": "Ne vo ket lakaet an toud-mañ er rolloù gerioù-klik dre mard eo anlistennet. N'eus nemet an toudoù foran a c'hall bezañ klasket dre c'her-klik.",
+  "compose_form.lock_disclaimer": "N'eo ket {locked} ho kont. An holl a c'hal heuliañ ac'hanoc'h evit gwelout ho toudoù prevez.",
   "compose_form.lock_disclaimer.lock": "prennet",
   "compose_form.placeholder": "Petra eh oc'h é soñjal a-barzh ?",
   "compose_form.poll.add_option": "Ouzhpenniñ un dibab",
-  "compose_form.poll.duration": "Poll duration",
+  "compose_form.poll.duration": "Pad ar sontadeg",
   "compose_form.poll.option_placeholder": "Dibab {number}",
   "compose_form.poll.remove_option": "Lemel an dibab-mañ",
-  "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
-  "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
+  "compose_form.poll.switch_to_multiple": "Kemmañ ar sontadeg evit aotren meur a zibab",
+  "compose_form.poll.switch_to_single": "Kemmañ ar sontadeg evit aotren un dibab hepken",
   "compose_form.publish": "Toudañ",
   "compose_form.publish_loud": "{publish} !",
-  "compose_form.sensitive.hide": "Mark media as sensitive",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
+  "compose_form.sensitive.hide": "Merkañ ar media evel kizidik",
+  "compose_form.sensitive.marked": "Merket eo ar media evel kizidik",
+  "compose_form.sensitive.unmarked": "N'eo ket merket ar media evel kizidik",
+  "compose_form.spoiler.marked": "Kuzhet eo an destenn a-dreñv ur c'hemenn",
   "compose_form.spoiler.unmarked": "N'eo ket kuzhet an destenn",
-  "compose_form.spoiler_placeholder": "Write your warning here",
+  "compose_form.spoiler_placeholder": "Skrivit ho kemenn amañ",
   "confirmation_modal.cancel": "Nullañ",
-  "confirmations.block.block_and_report": "Block & Report",
-  "confirmations.block.confirm": "Block",
-  "confirmations.block.message": "Are you sure you want to block {name}?",
+  "confirmations.block.block_and_report": "Berzañ ha Disklêriañ",
+  "confirmations.block.confirm": "Stankañ",
+  "confirmations.block.message": "Ha sur oc'h e fell deoc'h stankañ {name} ?",
   "confirmations.delete.confirm": "Dilemel",
-  "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete.message": "Ha sur oc'h e fell deoc'h dilemel an toud-mañ ?",
   "confirmations.delete_list.confirm": "Dilemel",
   "confirmations.delete_list.message": "Ha sur eo hoc'h eus c'hoant da zilemel ar roll-mañ da vat ?",
-  "confirmations.domain_block.confirm": "Kuzhat an domani a-bezh",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
+  "confirmations.domain_block.confirm": "Berzañ an domani a-bezh",
+  "confirmations.domain_block.message": "Ha sur oc'h e fell deoc'h berzañ an {domain} a-bezh? Peurvuiañ eo trawalc'h berzañ pe mudañ un nebeud implijer·ezed·ien. Ne welot danvez ebet o tont eus an domani-mañ. Dilamet e vo ar c'houmanantoù war an domani-mañ.",
   "confirmations.logout.confirm": "Digevreañ",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?",
   "confirmations.mute.confirm": "Kuzhat",
-  "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
+  "confirmations.mute.explanation": "Kuzhat a raio an toudoù skrivet gantañ·i hag ar re a veneg anezhañ·i, met aotren a raio anezhañ·i da welet ho todoù ha a heuliañ ac'hanoc'h.",
   "confirmations.mute.message": "Ha sur oc'h e fell deoc'h kuzhaat {name} ?",
   "confirmations.redraft.confirm": "Diverkañ ha skrivañ en-dro",
-  "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
+  "confirmations.redraft.message": "Ha sur oc'h e fell deoc'h dilemel ar statud-mañ hag adlakaat anezhañ er bouilhoñs? Kollet e vo ar merkoù muiañ-karet hag ar skignadennoù hag emzivat e vo ar respontoù d'an toud orin.",
   "confirmations.reply.confirm": "Respont",
-  "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
+  "confirmations.reply.message": "Respont bremañ a zilamo ar gemennadenn emaoc'h o skrivañ. Sur e oc'h e fell deoc'h kenderc'hel ganti?",
   "confirmations.unfollow.confirm": "Diheuliañ",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "conversation.delete": "Delete conversation",
-  "conversation.mark_as_read": "Mark as read",
-  "conversation.open": "View conversation",
-  "conversation.with": "With {names}",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
-  "emoji_button.activity": "Activity",
-  "emoji_button.custom": "Custom",
-  "emoji_button.flags": "Flags",
-  "emoji_button.food": "Food & Drink",
-  "emoji_button.label": "Insert emoji",
-  "emoji_button.nature": "Nature",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
-  "emoji_button.objects": "Objects",
-  "emoji_button.people": "People",
-  "emoji_button.recent": "Frequently used",
-  "emoji_button.search": "Search...",
-  "emoji_button.search_results": "Search results",
-  "emoji_button.symbols": "Symbols",
-  "emoji_button.travel": "Travel & Places",
-  "empty_column.account_timeline": "No toots here!",
-  "empty_column.account_unavailable": "Profile unavailable",
-  "empty_column.blocks": "You haven't blocked any users yet.",
-  "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
-  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
-  "empty_column.domain_blocks": "There are no hidden domains yet.",
-  "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.",
-  "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.",
-  "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
-  "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.public_timeline": "the public timeline",
-  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
-  "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
-  "empty_column.mutes": "You haven't muted any users yet.",
-  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
-  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
-  "error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
-  "error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
-  "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
-  "errors.unexpected_crash.report_issue": "Report issue",
-  "follow_request.authorize": "Authorize",
-  "follow_request.reject": "Reject",
-  "getting_started.developers": "Developers",
-  "getting_started.directory": "Profile directory",
-  "getting_started.documentation": "Documentation",
-  "getting_started.heading": "Getting started",
-  "getting_started.invite": "Invite people",
+  "confirmations.unfollow.message": "Ha sur oc'h e fell deoc'h paouez da heuliañ {name}?",
+  "conversation.delete": "Dilemel ar gaozeadenn",
+  "conversation.mark_as_read": "Merkañ evel lennet",
+  "conversation.open": "Gwelout ar gaozeadenn",
+  "conversation.with": "Gant {names}",
+  "directory.federated": "Eus ar c'hevrebed anavezet",
+  "directory.local": "Eus {domain} hepken",
+  "directory.new_arrivals": "Degouezhet a-nevez",
+  "directory.recently_active": "Oberiant nevez zo",
+  "embed.instructions": "Enkorfit ar statud war ho lec'hienn en ur eilañ ar c'hod dindan.",
+  "embed.preview": "Setu penaos e vo diskouezet:",
+  "emoji_button.activity": "Obererezh",
+  "emoji_button.custom": "Kempennet",
+  "emoji_button.flags": "Bannieloù",
+  "emoji_button.food": "Boued hag Evaj",
+  "emoji_button.label": "Enlakaat un emoji",
+  "emoji_button.nature": "Natur",
+  "emoji_button.not_found": "Emoji ebet !! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Traoù",
+  "emoji_button.people": "Tud",
+  "emoji_button.recent": "Implijet alies",
+  "emoji_button.search": "O klask...",
+  "emoji_button.search_results": "Disoc'hoù an enklask",
+  "emoji_button.symbols": "Arouezioù",
+  "emoji_button.travel": "Lec'hioù ha Beajoù",
+  "empty_column.account_timeline": "Toud ebet amañ!",
+  "empty_column.account_unavailable": "Profil dihegerz",
+  "empty_column.blocks": "N'eus ket bet berzet implijer·ez ganeoc'h c'hoazh.",
+  "empty_column.bookmarked_statuses": "N'ho peus toud ebet enrollet en ho sinedoù c'hoazh. Pa vo ouzhpennet unan ganeoc'h e teuio war wel amañ.",
+  "empty_column.community": "Goulo eo ar red-amzer lec'hel. Skrivit'ta un dra evit lakaat tan dezhi !",
+  "empty_column.direct": "N'ho peus kemennad prevez ebet c'hoazh. Pa vo resevet pe kaset unan ganeoc'h e teuio war wel amañ.",
+  "empty_column.domain_blocks": "N'eus domani kuzh ebet c'hoazh.",
+  "empty_column.favourited_statuses": "N'ho peus toud muiañ-karet ebet c'hoazh. Pa vo lakaet unan ganeoc'h e vo diskouezet amañ.",
+  "empty_column.favourites": "Den ebet n'eus lakaet an toud-mañ en e reoù muiañ-karet. Pa vo graet gant unan bennak e vo diskouezet amañ.",
+  "empty_column.follow_requests": "N'ho peus goulenn heuliañ ebet c'hoazh. Pa resevot reoù e vo diskouezet amañ.",
+  "empty_column.hashtag": "N'eus netra er ger-klik-mañ c'hoazh.",
+  "empty_column.home": "Goullo eo ho red-amzer degemer! Kit da weladenniñ {public} pe implijit ar c'hlask evit kregiñ ganti ha kejañ gant implijer·ien·ezed all.",
+  "empty_column.home.public_timeline": "ar red-amzer publik",
+  "empty_column.list": "Goullo eo ar roll-mañ evit ar poent. Pa vo toudet gant e izili e vo diskouezet amañ.",
+  "empty_column.lists": "N'ho peus roll ebet c'hoazh. Pa vo krouet unan ganeoc'h e vo diskouezet amañ.",
+  "empty_column.mutes": "N'ho peus kuzhet implijer ebet c'hoazh.",
+  "empty_column.notifications": "N'ho peus kemenn ebet c'hoazh. Grit gant implijer·ezed·ien all evit loc'hañ ar gomz.",
+  "empty_column.public": "N'eus netra amañ! Skrivit un dra bennak foran pe heuilhit implijer·ien·ezed eus dafariadoù all evit leuniañ",
+  "error.unexpected_crash.explanation": "Abalamour d'ur beug en hor c'hod pe d'ur gudenn geverlec'hded n'hallomp ket skrammañ ar bajenn-mañ en un doare dereat.",
+  "error.unexpected_crash.next_steps": "Klaskit azbevaat ar bajenn. Ma n'a ket en-dro e c'hallit klask ober gant Mastodon dre ur merdeer disheñvel pe dre an arload genidik.",
+  "errors.unexpected_crash.copy_stacktrace": "Eilañ ar roudoù diveugañ er golver",
+  "errors.unexpected_crash.report_issue": "Danevellañ ur fazi",
+  "follow_request.authorize": "Aotren",
+  "follow_request.reject": "Nac'hañ",
+  "getting_started.developers": "Diorroerien",
+  "getting_started.directory": "Roll ar profiloù",
+  "getting_started.documentation": "Teuliadur",
+  "getting_started.heading": "Loc'hañ",
+  "getting_started.invite": "Pediñ tud",
   "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
-  "getting_started.security": "Security",
-  "getting_started.terms": "Terms of service",
-  "hashtag.column_header.tag_mode.all": "and {additional}",
-  "hashtag.column_header.tag_mode.any": "or {additional}",
-  "hashtag.column_header.tag_mode.none": "without {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
-  "hashtag.column_settings.tag_mode.all": "All of these",
-  "hashtag.column_settings.tag_mode.any": "Any of these",
-  "hashtag.column_settings.tag_mode.none": "None of these",
-  "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
-  "home.column_settings.basic": "Basic",
-  "home.column_settings.show_reblogs": "Show boosts",
-  "home.column_settings.show_replies": "Show replies",
+  "getting_started.security": "Arventennoù ar gont",
+  "getting_started.terms": "Divizoù gwerzhañ hollek",
+  "hashtag.column_header.tag_mode.all": "ha {additional}",
+  "hashtag.column_header.tag_mode.any": "pe {additional}",
+  "hashtag.column_header.tag_mode.none": "hep {additional}",
+  "hashtag.column_settings.select.no_options_message": "N'eus bet kavet ali ebet",
+  "hashtag.column_settings.select.placeholder": "Ouzhpennañ gerioù-klik…",
+  "hashtag.column_settings.tag_mode.all": "An holl elfennoù-mañ",
+  "hashtag.column_settings.tag_mode.any": "Unan e mesk anezho",
+  "hashtag.column_settings.tag_mode.none": "Hini ebet anezho",
+  "hashtag.column_settings.tag_toggle": "Endelc'her gerioù-alc'hwez ouzhpenn evit ar bannad-mañ",
+  "home.column_settings.basic": "Diazez",
+  "home.column_settings.show_reblogs": "Diskouez ar skignadennoù",
+  "home.column_settings.show_replies": "Diskouez ar respontoù",
   "home.hide_announcements": "Hide announcements",
   "home.show_announcements": "Show announcements",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
-  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
-  "introduction.federation.action": "Next",
-  "introduction.federation.federated.headline": "Federated",
+  "intervals.full.days": "{number, plural, one {# devezh} other{# a zevezhioù}}",
+  "intervals.full.hours": "{number, plural, one {# eurvezh} other{# eurvezh}}",
+  "intervals.full.minutes": "{number, plural, one {# munut} other{# a vunutoù}}",
+  "introduction.federation.action": "Da-heul",
+  "introduction.federation.federated.headline": "Kevreet",
   "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
-  "introduction.federation.home.headline": "Home",
+  "introduction.federation.home.headline": "Degemer",
   "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
-  "introduction.federation.local.headline": "Local",
+  "introduction.federation.local.headline": "Lec'hel",
   "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
   "introduction.interactions.action": "Finish toot-orial!",
-  "introduction.interactions.favourite.headline": "Favourite",
+  "introduction.interactions.favourite.headline": "Muiañ-karet",
   "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
-  "introduction.interactions.reblog.headline": "Boost",
+  "introduction.interactions.reblog.headline": "Skignañ",
   "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
-  "introduction.interactions.reply.headline": "Reply",
+  "introduction.interactions.reply.headline": "Respont",
   "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
-  "introduction.welcome.action": "Let's go!",
-  "introduction.welcome.headline": "First steps",
+  "introduction.welcome.action": "Bec'h dezhi!",
+  "introduction.welcome.headline": "Pazennoù kentañ",
   "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
   "keyboard_shortcuts.back": "to navigate back",
   "keyboard_shortcuts.blocked": "to open blocked users list",
-  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.boost": "da skignañ",
   "keyboard_shortcuts.column": "to focus a status in one of the columns",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.description": "Deskrivadur",
   "keyboard_shortcuts.direct": "to open direct messages column",
   "keyboard_shortcuts.down": "to move down in the list",
   "keyboard_shortcuts.enter": "to open status",
@@ -230,7 +230,7 @@
   "keyboard_shortcuts.open_media": "to open media",
   "keyboard_shortcuts.pinned": "to open pinned toots list",
   "keyboard_shortcuts.profile": "to open author's profile",
-  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.reply": "da respont",
   "keyboard_shortcuts.requests": "to open follow requests list",
   "keyboard_shortcuts.search": "to focus search",
   "keyboard_shortcuts.start": "to open \"get started\" column",
@@ -239,48 +239,48 @@
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
-  "lightbox.close": "Close",
+  "lightbox.close": "Serriñ",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
   "lightbox.view_context": "View context",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
-  "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.edit.submit": "Change title",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
+  "lists.account.add": "Ouzhpennañ d'al listenn",
+  "lists.account.remove": "Lemel kuit eus al listenn",
+  "lists.delete": "Dilemel al listenn",
+  "lists.edit": "Aozañ al listenn",
+  "lists.edit.submit": "Cheñch an titl",
+  "lists.new.create": "Ouzhpennañ ul listenn",
+  "lists.new.title_placeholder": "Titl nevez al listenn",
   "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.subheading": "Ho listennoù",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
-  "loading_indicator.label": "Loading...",
+  "loading_indicator.label": "O kargañ...",
   "media_gallery.toggle_visible": "Toggle visibility",
-  "missing_indicator.label": "Not found",
+  "missing_indicator.label": "Digavet",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
-  "navigation_bar.apps": "Mobile apps",
-  "navigation_bar.blocks": "Blocked users",
-  "navigation_bar.bookmarks": "Bookmarks",
-  "navigation_bar.community_timeline": "Local timeline",
-  "navigation_bar.compose": "Compose new toot",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.discover": "Discover",
-  "navigation_bar.domain_blocks": "Hidden domains",
-  "navigation_bar.edit_profile": "Edit profile",
-  "navigation_bar.favourites": "Favourites",
-  "navigation_bar.filters": "Muted words",
-  "navigation_bar.follow_requests": "Follow requests",
+  "navigation_bar.apps": "Arloadoù pellgomz",
+  "navigation_bar.blocks": "Implijer·ezed·ien berzet",
+  "navigation_bar.bookmarks": "Sinedoù",
+  "navigation_bar.community_timeline": "Red-amzer lec'hel",
+  "navigation_bar.compose": "Skrivañ un toud nevez",
+  "navigation_bar.direct": "Kemennadoù prevez",
+  "navigation_bar.discover": "Dizoleiñ",
+  "navigation_bar.domain_blocks": "Domanioù kuzhet",
+  "navigation_bar.edit_profile": "Aozañ ar profil",
+  "navigation_bar.favourites": "Ar re vuiañ-karet",
+  "navigation_bar.filters": "Gerioù kuzhet",
+  "navigation_bar.follow_requests": "Pedadoù heuliañ",
   "navigation_bar.follows_and_followers": "Follows and followers",
-  "navigation_bar.info": "About this server",
-  "navigation_bar.keyboard_shortcuts": "Hotkeys",
-  "navigation_bar.lists": "Lists",
-  "navigation_bar.logout": "Logout",
-  "navigation_bar.mutes": "Muted users",
-  "navigation_bar.personal": "Personal",
-  "navigation_bar.pins": "Pinned toots",
-  "navigation_bar.preferences": "Preferences",
-  "navigation_bar.public_timeline": "Federated timeline",
-  "navigation_bar.security": "Security",
+  "navigation_bar.info": "Diwar-benn an dafariad-mañ",
+  "navigation_bar.keyboard_shortcuts": "Berradurioù",
+  "navigation_bar.lists": "Listennoù",
+  "navigation_bar.logout": "Digennaskañ",
+  "navigation_bar.mutes": "Implijer·ion·ezed kuzhet",
+  "navigation_bar.personal": "Personel",
+  "navigation_bar.pins": "Toudoù spilhennet",
+  "navigation_bar.preferences": "Gwellvezioù",
+  "navigation_bar.public_timeline": "Red-amzer kevreet",
+  "navigation_bar.security": "Diogelroez",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} followed you",
   "notification.follow_request": "{name} has requested to follow you",
@@ -291,135 +291,135 @@
   "notifications.clear": "Clear notifications",
   "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
   "notifications.column_settings.alert": "Desktop notifications",
-  "notifications.column_settings.favourite": "Favourites:",
+  "notifications.column_settings.favourite": "Ar re vuiañ-karet:",
   "notifications.column_settings.filter_bar.advanced": "Display all categories",
-  "notifications.column_settings.filter_bar.category": "Quick filter bar",
-  "notifications.column_settings.filter_bar.show": "Show",
+  "notifications.column_settings.filter_bar.category": "Barrenn siloù prim",
+  "notifications.column_settings.filter_bar.show": "Diskouez",
   "notifications.column_settings.follow": "New followers:",
   "notifications.column_settings.follow_request": "New follow requests:",
-  "notifications.column_settings.mention": "Mentions:",
-  "notifications.column_settings.poll": "Poll results:",
+  "notifications.column_settings.mention": "Menegoù:",
+  "notifications.column_settings.poll": "Disoc'hoù ar sontadeg:",
   "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.reblog": "Boosts:",
-  "notifications.column_settings.show": "Show in column",
-  "notifications.column_settings.sound": "Play sound",
-  "notifications.filter.all": "All",
-  "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
+  "notifications.column_settings.reblog": "Skignadennoù:",
+  "notifications.column_settings.show": "Diskouez er bann",
+  "notifications.column_settings.sound": "Seniñ",
+  "notifications.filter.all": "Pep tra",
+  "notifications.filter.boosts": "Skignadennoù",
+  "notifications.filter.favourites": "Muiañ-karet",
   "notifications.filter.follows": "Follows",
-  "notifications.filter.mentions": "Mentions",
-  "notifications.filter.polls": "Poll results",
-  "notifications.group": "{count} notifications",
-  "poll.closed": "Closed",
-  "poll.refresh": "Refresh",
+  "notifications.filter.mentions": "Menegoù",
+  "notifications.filter.polls": "Disoc'hoù ar sontadegoù",
+  "notifications.group": "{count} a gemennoù",
+  "poll.closed": "Serret",
+  "poll.refresh": "Azbevaat",
   "poll.total_people": "{count, plural, one {# person} other {# people}}",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
-  "poll.vote": "Vote",
-  "poll.voted": "You voted for this answer",
-  "poll_button.add_poll": "Add a poll",
-  "poll_button.remove_poll": "Remove poll",
-  "privacy.change": "Adjust status privacy",
-  "privacy.direct.long": "Post to mentioned users only",
-  "privacy.direct.short": "Direct",
-  "privacy.private.long": "Post to followers only",
-  "privacy.private.short": "Followers-only",
-  "privacy.public.long": "Post to public timelines",
-  "privacy.public.short": "Public",
-  "privacy.unlisted.long": "Do not show in public timelines",
-  "privacy.unlisted.short": "Unlisted",
-  "refresh": "Refresh",
-  "regeneration_indicator.label": "Loading…",
-  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "poll.vote": "Mouezhiañ",
+  "poll.voted": "Mouezhiet ho peus evit ar respont-mañ",
+  "poll_button.add_poll": "Ouzhpennañ ur sontadeg",
+  "poll_button.remove_poll": "Dilemel ar sontadeg",
+  "privacy.change": "Kemmañ gwelidigezh ar statud",
+  "privacy.direct.long": "Embann evit an implijer·ezed·ien meneget hepken",
+  "privacy.direct.short": "War-eeun",
+  "privacy.private.long": "Embann evit ar re a heuilh ac'hanon hepken",
+  "privacy.private.short": "Ar re a heuilh ac'hanon hepken",
+  "privacy.public.long": "Embann war ar redoù-amzer foran",
+  "privacy.public.short": "Publik",
+  "privacy.unlisted.long": "Na embann war ar redoù-amzer foran",
+  "privacy.unlisted.short": "Anlistennet",
+  "refresh": "Freskaat",
+  "regeneration_indicator.label": "O kargañ…",
+  "regeneration_indicator.sublabel": "War brientiñ emañ ho red degemer!",
   "relative_time.days": "{number}d",
-  "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.hours": "{number}e",
+  "relative_time.just_now": "bremañ",
   "relative_time.minutes": "{number}m",
-  "relative_time.seconds": "{number}s",
-  "relative_time.today": "today",
-  "reply_indicator.cancel": "Cancel",
-  "report.forward": "Forward to {target}",
+  "relative_time.seconds": "{number}eil",
+  "relative_time.today": "hiziv",
+  "reply_indicator.cancel": "Nullañ",
+  "report.forward": "Treuzkas da: {target}",
   "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
   "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Report {target}",
-  "search.placeholder": "Search",
+  "search.placeholder": "Klask",
   "search_popout.search_format": "Advanced search format",
   "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
-  "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "status",
+  "search_popout.tips.hashtag": "ger-klik",
+  "search_popout.tips.status": "statud",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_popout.tips.user": "implijer·ez",
+  "search_results.accounts": "Tud",
+  "search_results.hashtags": "Gerioù-klik",
+  "search_results.statuses": "a doudoù",
   "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "status.admin_account": "Open moderation interface for @{name}",
   "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
-  "status.bookmark": "Bookmark",
+  "status.bookmark": "Ouzhpennañ d'ar sinedoù",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
   "status.copy": "Copy link to status",
-  "status.delete": "Delete",
+  "status.delete": "Dilemel",
   "status.detailed_status": "Detailed conversation view",
-  "status.direct": "Direct message @{name}",
-  "status.embed": "Embed",
-  "status.favourite": "Favourite",
+  "status.direct": "Kas ur c'hemennad da @{name}",
+  "status.embed": "Enframmañ",
+  "status.favourite": "Muiañ-karet",
   "status.filtered": "Filtered",
-  "status.load_more": "Load more",
-  "status.media_hidden": "Media hidden",
-  "status.mention": "Mention @{name}",
-  "status.more": "More",
-  "status.mute": "Mute @{name}",
-  "status.mute_conversation": "Mute conversation",
+  "status.load_more": "Kargañ muioc'h",
+  "status.media_hidden": "Media kuzhet",
+  "status.mention": "Menegiñ @{name}",
+  "status.more": "Muioc'h",
+  "status.mute": "Kuzhat @{name}",
+  "status.mute_conversation": "Kuzhat ar gaozeadenn",
   "status.open": "Expand this status",
-  "status.pin": "Pin on profile",
-  "status.pinned": "Pinned toot",
-  "status.read_more": "Read more",
-  "status.reblog": "Boost",
+  "status.pin": "Spilhennañ d'ar profil",
+  "status.pinned": "Toud spilhennet",
+  "status.read_more": "Lenn muioc'h",
+  "status.reblog": "Skignañ",
   "status.reblog_private": "Boost to original audience",
   "status.reblogged_by": "{name} boosted",
   "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
   "status.redraft": "Delete & re-draft",
   "status.remove_bookmark": "Remove bookmark",
-  "status.reply": "Reply",
+  "status.reply": "Respont",
   "status.replyAll": "Reply to thread",
-  "status.report": "Report @{name}",
+  "status.report": "Disklêriañ @{name}",
   "status.sensitive_warning": "Sensitive content",
-  "status.share": "Share",
+  "status.share": "Rannañ",
   "status.show_less": "Show less",
   "status.show_less_all": "Show less for all",
   "status.show_more": "Show more",
   "status.show_more_all": "Show more for all",
   "status.show_thread": "Show thread",
   "status.uncached_media_warning": "Not available",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
+  "status.unmute_conversation": "Diguzhat ar gaozeadenn",
+  "status.unpin": "Dispilhennañ eus ar profil",
   "suggestions.dismiss": "Dismiss suggestion",
   "suggestions.header": "You might be interested in…",
   "tabs_bar.federated_timeline": "Federated",
-  "tabs_bar.home": "Home",
-  "tabs_bar.local_timeline": "Local",
-  "tabs_bar.notifications": "Notifications",
-  "tabs_bar.search": "Search",
+  "tabs_bar.home": "Degemer",
+  "tabs_bar.local_timeline": "Lec'hel",
+  "tabs_bar.notifications": "Kemennoù",
+  "tabs_bar.search": "Klask",
   "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
   "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
   "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
-  "trends.trending_now": "Trending now",
+  "trends.trending_now": "Luskad ar mare",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
-  "upload_button.label": "Add media ({formats})",
+  "upload_button.label": "Ouzhpennañ ur media ({formats})",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.audio_description": "Describe for people with hearing loss",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.edit": "Edit",
-  "upload_form.undo": "Delete",
+  "upload_form.edit": "Aozañ",
+  "upload_form.undo": "Dilemel",
   "upload_form.video_description": "Describe for people with hearing loss or visual impairment",
   "upload_modal.analyzing_picture": "Analyzing picture…",
   "upload_modal.apply": "Apply",
@@ -428,9 +428,9 @@
   "upload_modal.edit_media": "Edit media",
   "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
   "upload_modal.preview_label": "Preview ({ratio})",
-  "upload_progress.label": "Uploading…",
+  "upload_progress.label": "O pellgargañ...",
   "video.close": "Close video",
-  "video.download": "Download file",
+  "video.download": "Pellgargañ ar restr",
   "video.exit_fullscreen": "Exit full screen",
   "video.expand": "Expand video",
   "video.fullscreen": "Full screen",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 7b427c3f0..9df1e0c65 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -184,8 +184,8 @@
   "home.column_settings.basic": "Základní",
   "home.column_settings.show_reblogs": "Zobrazit boosty",
   "home.column_settings.show_replies": "Zobrazit odpovědi",
-  "home.hide_announcements": "Hide announcements",
-  "home.show_announcements": "Show announcements",
+  "home.hide_announcements": "Skrýt oznámení",
+  "home.show_announcements": "Zobrazit oznámení",
   "intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dní} other {# dní}}",
   "intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodin} other {# hodin}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}",
@@ -335,7 +335,7 @@
   "relative_time.just_now": "teď",
   "relative_time.minutes": "{number} m",
   "relative_time.seconds": "{number} s",
-  "relative_time.today": "today",
+  "relative_time.today": "dnes",
   "reply_indicator.cancel": "Zrušit",
   "report.forward": "Přeposlat na {target}",
   "report.forward_hint": "Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii?",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index cd4ba01a5..cf28408c4 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -3,7 +3,7 @@
   "account.badges.bot": "Bot",
   "account.badges.group": "Gruppe",
   "account.block": "@{name} blockieren",
-  "account.block_domain": "Alles von {domain} verstecken",
+  "account.block_domain": "Alles von {domain} blockieren",
   "account.blocked": "Blockiert",
   "account.cancel_follow_request": "Folgeanfrage abbrechen",
   "account.direct": "Direktnachricht an @{name}",
@@ -34,7 +34,7 @@
   "account.share": "Profil von @{name} teilen",
   "account.show_reblogs": "Von @{name} geteilte Beiträge anzeigen",
   "account.unblock": "@{name} entblocken",
-  "account.unblock_domain": "{domain} wieder anzeigen",
+  "account.unblock_domain": "Blockieren von {domain} beenden",
   "account.unendorse": "Nicht auf Profil hervorheben",
   "account.unfollow": "Entfolgen",
   "account.unmute": "@{name} nicht mehr stummschalten",
@@ -86,7 +86,7 @@
   "compose_form.poll.option_placeholder": "Wahl {number}",
   "compose_form.poll.remove_option": "Wahl entfernen",
   "compose_form.poll.switch_to_multiple": "Umfrage ändern, um mehrere Optionen zu erlauben",
-  "compose_form.poll.switch_to_single": "Umfrage ändern, um eine einzige Wahl zu ermöglichen",
+  "compose_form.poll.switch_to_single": "Umfrage ändern, um eine einzige Wahl zu erlauben",
   "compose_form.publish": "Tröt",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.hide": "Medien als heikel markieren",
@@ -143,7 +143,7 @@
   "empty_column.account_timeline": "Keine Beiträge!",
   "empty_column.account_unavailable": "Konto nicht verfügbar",
   "empty_column.blocks": "Du hast keine Profile blockiert.",
-  "empty_column.bookmarked_statuses": "Du hast bis jetzt keine Beiträge als Toots gespeichert. Wenn du einen Beitrag als Toot speicherst, wird er hier erscheinen.",
+  "empty_column.bookmarked_statuses": "Du hast bis jetzt keine Beiträge als Lesezeichen gespeichert. Wenn du einen Beitrag als Lesezeichen speicherst wird er hier erscheinen.",
   "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe einen öffentlichen Beitrag, um den Ball ins Rollen zu bringen!",
   "empty_column.direct": "Du hast noch keine Direktnachrichten erhalten. Wenn du eine sendest oder empfängst, wird sie hier zu sehen sein.",
   "empty_column.domain_blocks": "Es ist noch keine versteckten Domains.",
@@ -158,7 +158,7 @@
   "empty_column.mutes": "Du hast keine Profile stummgeschaltet.",
   "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
   "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Servern, um die Zeitleiste aufzufüllen",
-  "error.unexpected_crash.explanation": "Aufgrund eines Fehlers in unserem Code oder einer Browsereinkompatibilität konnte diese Seite nicht korrekt angezeigt werden.",
+  "error.unexpected_crash.explanation": "Aufgrund eines Fehlers in unserem Code oder einer Browser-Inkompatibilität konnte diese Seite nicht korrekt angezeigt werden.",
   "error.unexpected_crash.next_steps": "Versuche die Seite zu aktualisieren. Wenn das nicht hilft, kannst du Mastodon über einen anderen Browser oder eine native App verwenden.",
   "errors.unexpected_crash.copy_stacktrace": "Fehlerlog in die Zwischenablage kopieren",
   "errors.unexpected_crash.report_issue": "Problem melden",
@@ -296,7 +296,7 @@
   "notifications.column_settings.filter_bar.category": "Schnellfilterleiste",
   "notifications.column_settings.filter_bar.show": "Anzeigen",
   "notifications.column_settings.follow": "Neue Folgende:",
-  "notifications.column_settings.follow_request": "Neue Folge-Anfragen:",
+  "notifications.column_settings.follow_request": "Neue Folgeanfragen:",
   "notifications.column_settings.mention": "Erwähnungen:",
   "notifications.column_settings.poll": "Ergebnisse von Umfragen:",
   "notifications.column_settings.push": "Push-Benachrichtigungen",
@@ -321,7 +321,7 @@
   "privacy.change": "Sichtbarkeit des Beitrags anpassen",
   "privacy.direct.long": "Wird an erwähnte Profile gesendet",
   "privacy.direct.short": "Direktnachricht",
-  "privacy.private.long": "Wird nur für deine Folgende sichtbar sein",
+  "privacy.private.long": "Beitrag nur an Folgende",
   "privacy.private.short": "Nur für Folgende",
   "privacy.public.long": "Wird in öffentlichen Zeitleisten erscheinen",
   "privacy.public.short": "Öffentlich",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 18692bc44..993347273 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -142,7 +142,7 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Unhide {domain}",
+        "defaultMessage": "Unblock domain {domain}",
         "id": "account.unblock_domain"
       }
     ],
@@ -217,7 +217,7 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Toggle visibility",
+        "defaultMessage": "Hide media",
         "id": "media_gallery.toggle_visible"
       },
       {
@@ -451,11 +451,11 @@
         "id": "status.copy"
       },
       {
-        "defaultMessage": "Hide everything from {domain}",
+        "defaultMessage": "Block domain {domain}",
         "id": "account.block_domain"
       },
       {
-        "defaultMessage": "Unhide {domain}",
+        "defaultMessage": "Unblock domain {domain}",
         "id": "account.unblock_domain"
       },
       {
@@ -523,7 +523,7 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Hide entire domain",
+        "defaultMessage": "Block entire domain",
         "id": "confirmations.domain_block.confirm"
       },
       {
@@ -697,11 +697,11 @@
         "id": "account.media"
       },
       {
-        "defaultMessage": "Hide everything from {domain}",
+        "defaultMessage": "Block domain {domain}",
         "id": "account.block_domain"
       },
       {
-        "defaultMessage": "Unhide {domain}",
+        "defaultMessage": "Unblock domain {domain}",
         "id": "account.unblock_domain"
       },
       {
@@ -737,7 +737,7 @@
         "id": "navigation_bar.blocks"
       },
       {
-        "defaultMessage": "Hidden domains",
+        "defaultMessage": "Blocked domains",
         "id": "navigation_bar.domain_blocks"
       },
       {
@@ -773,7 +773,7 @@
         "id": "account.muted"
       },
       {
-        "defaultMessage": "Domain hidden",
+        "defaultMessage": "Domain blocked",
         "id": "account.domain_blocked"
       },
       {
@@ -917,6 +917,10 @@
       {
         "defaultMessage": "Logout",
         "id": "navigation_bar.logout"
+      },
+      {
+        "defaultMessage": "Bookmarks",
+        "id": "navigation_bar.bookmarks"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/components/action_bar.json"
@@ -1073,7 +1077,7 @@
         "id": "privacy.public.short"
       },
       {
-        "defaultMessage": "Post to public timelines",
+        "defaultMessage": "Visible for all, shown in public timelines",
         "id": "privacy.public.long"
       },
       {
@@ -1081,7 +1085,7 @@
         "id": "privacy.unlisted.short"
       },
       {
-        "defaultMessage": "Do not show in public timelines",
+        "defaultMessage": "Visible for all, but not in public timelines",
         "id": "privacy.unlisted.long"
       },
       {
@@ -1089,7 +1093,7 @@
         "id": "privacy.private.short"
       },
       {
-        "defaultMessage": "Post to followers only",
+        "defaultMessage": "Visible for followers only",
         "id": "privacy.private.long"
       },
       {
@@ -1097,7 +1101,7 @@
         "id": "privacy.direct.short"
       },
       {
-        "defaultMessage": "Post to mentioned users only",
+        "defaultMessage": "Visible for mentioned users only",
         "id": "privacy.direct.long"
       },
       {
@@ -1466,15 +1470,15 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Hidden domains",
+        "defaultMessage": "Blocked domains",
         "id": "column.domain_blocks"
       },
       {
-        "defaultMessage": "Unhide {domain}",
+        "defaultMessage": "Unblock domain {domain}",
         "id": "account.unblock_domain"
       },
       {
-        "defaultMessage": "There are no hidden domains yet.",
+        "defaultMessage": "There are no blocked domains yet.",
         "id": "empty_column.domain_blocks"
       }
     ],
@@ -1528,6 +1532,10 @@
       {
         "defaultMessage": "You don't have any follow requests yet. When you receive one, it will show up here.",
         "id": "empty_column.follow_requests"
+      },
+      {
+        "defaultMessage": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
+        "id": "follow_requests.unlocked_explanation"
       }
     ],
     "path": "app/javascript/mastodon/features/follow_requests/index.json"
@@ -2384,11 +2392,11 @@
         "id": "status.copy"
       },
       {
-        "defaultMessage": "Hide everything from {domain}",
+        "defaultMessage": "Block domain {domain}",
         "id": "account.block_domain"
       },
       {
-        "defaultMessage": "Unhide {domain}",
+        "defaultMessage": "Unblock domain {domain}",
         "id": "account.unblock_domain"
       },
       {
@@ -2957,4 +2965,4 @@
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
   }
-]
\ No newline at end of file
+]
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index 5b0067acb..d92a0e35a 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -150,7 +150,7 @@
   "empty_column.favourited_statuses": "Δεν έχεις κανένα αγαπημένο τουτ ακόμα. Μόλις αγαπήσεις κάποιο, θα εμφανιστεί εδώ.",
   "empty_column.favourites": "Κανείς δεν έχει αγαπήσει αυτό το τουτ ακόμα. Μόλις το κάνει κάποια, θα εμφανιστούν εδώ.",
   "empty_column.follow_requests": "Δεν έχεις κανένα αίτημα παρακολούθησης ακόμα. Μόλις λάβεις κάποιο, θα εμφανιστεί εδώ.",
-  "empty_column.hashtag": "Δεν υπάρχει ακόμα κάτι για αυτή την ταμπέλα.",
+  "empty_column.hashtag": "Δεν υπάρχει ακόμα κάτι για αυτή την ετικέτα.",
   "empty_column.home": "Η τοπική σου ροή είναι κενή! Πήγαινε στο {public} ή κάνε αναζήτηση για να ξεκινήσεις και να γνωρίσεις άλλους χρήστες.",
   "empty_column.home.public_timeline": "η δημόσια ροή",
   "empty_column.list": "Δεν υπάρχει τίποτα σε αυτή τη λίστα ακόμα. Όταν τα μέλη της δημοσιεύσουν νέες καταστάσεις, θα εμφανιστούν εδώ.",
@@ -176,7 +176,7 @@
   "hashtag.column_header.tag_mode.any": "ή {additional}",
   "hashtag.column_header.tag_mode.none": "χωρίς {additional}",
   "hashtag.column_settings.select.no_options_message": "Δεν βρέθηκαν προτάσεις",
-  "hashtag.column_settings.select.placeholder": "Γράψε μερικές ταμπέλες…",
+  "hashtag.column_settings.select.placeholder": "Γράψε μερικές ετικέτες…",
   "hashtag.column_settings.tag_mode.all": "Όλα αυτά",
   "hashtag.column_settings.tag_mode.any": "Οποιοδήποτε από αυτά",
   "hashtag.column_settings.tag_mode.none": "Κανένα από αυτά",
@@ -345,13 +345,13 @@
   "report.target": "Καταγγελία {target}",
   "search.placeholder": "Αναζήτηση",
   "search_popout.search_format": "Προχωρημένη αναζήτηση",
-  "search_popout.tips.full_text": "Απλό κείμενο που επιστρέφει καταστάσεις που έχεις γράψει, σημειώσει ως αγαπημένες, προωθήσει ή έχεις αναφερθεί σε αυτές, καθώς και όσα ονόματα χρηστών και ταμπέλες ταιριάζουν.",
-  "search_popout.tips.hashtag": "ταμπέλα",
+  "search_popout.tips.full_text": "Απλό κείμενο που επιστρέφει καταστάσεις που έχεις γράψει, έχεις σημειώσει ως αγαπημένες, έχεις προωθήσει ή έχεις αναφερθεί σε αυτές, καθώς και όσα ονόματα χρηστών και ετικέτες ταιριάζουν.",
+  "search_popout.tips.hashtag": "ετικέτα",
   "search_popout.tips.status": "κατάσταση",
-  "search_popout.tips.text": "Απλό κείμενο που επιστρέφει ονόματα και ταμπέλες που ταιριάζουν",
+  "search_popout.tips.text": "Απλό κείμενο που επιστρέφει ονόματα και ετικέτες που ταιριάζουν",
   "search_popout.tips.user": "χρήστης",
   "search_results.accounts": "Άνθρωποι",
-  "search_results.hashtags": "Ταμπέλες",
+  "search_results.hashtags": "Ετικέτες",
   "search_results.statuses": "Τουτ",
   "search_results.statuses_fts_disabled": "Η αναζήτηση τουτ βάσει του περιεχόμενού τους δεν είναι ενεργοποιημένη σε αυτό τον κόμβο.",
   "search_results.total": "{count, number} {count, plural, zero {αποτελέσματα} one {αποτέλεσμα} other {αποτελέσματα}}",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index e25199905..c7153b7b1 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -3,11 +3,11 @@
   "account.badges.bot": "Bot",
   "account.badges.group": "Group",
   "account.block": "Block @{name}",
-  "account.block_domain": "Hide everything from {domain}",
+  "account.block_domain": "Block domain {domain}",
   "account.blocked": "Blocked",
   "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct message @{name}",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "Domain blocked",
   "account.edit_profile": "Edit profile",
   "account.endorse": "Feature on profile",
   "account.follow": "Follow",
@@ -34,7 +34,7 @@
   "account.share": "Share @{name}'s profile",
   "account.show_reblogs": "Show boosts from @{name}",
   "account.unblock": "Unblock @{name}",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "Unblock domain {domain}",
   "account.unendorse": "Don't feature on profile",
   "account.unfollow": "Unfollow",
   "account.unmute": "Unmute @{name}",
@@ -57,7 +57,7 @@
   "column.community": "Local timeline",
   "column.direct": "Direct messages",
   "column.directory": "Browse profiles",
-  "column.domain_blocks": "Hidden domains",
+  "column.domain_blocks": "Blocked domains",
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Home",
@@ -107,7 +107,7 @@
   "confirmations.delete.message": "Are you sure you want to delete this status?",
   "confirmations.delete_list.confirm": "Delete",
   "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
+  "confirmations.domain_block.confirm": "Block entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
   "confirmations.logout.confirm": "Log out",
   "confirmations.logout.message": "Are you sure you want to log out?",
@@ -150,7 +150,7 @@
   "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
-  "empty_column.domain_blocks": "There are no hidden domains yet.",
+  "empty_column.domain_blocks": "There are no blocked domains yet.",
   "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.",
   "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.",
   "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
@@ -168,6 +168,7 @@
   "errors.unexpected_crash.report_issue": "Report issue",
   "follow_request.authorize": "Authorize",
   "follow_request.reject": "Reject",
+  "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
   "getting_started.developers": "Developers",
   "getting_started.directory": "Profile directory",
   "getting_started.documentation": "Documentation",
@@ -258,7 +259,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
@@ -269,7 +270,7 @@
   "navigation_bar.compose": "Compose new toot",
   "navigation_bar.direct": "Direct messages",
   "navigation_bar.discover": "Discover",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.domain_blocks": "Blocked domains",
   "navigation_bar.edit_profile": "Edit profile",
   "navigation_bar.favourites": "Favourites",
   "navigation_bar.filters": "Muted words",
@@ -324,13 +325,13 @@
   "poll_button.add_poll": "Add a poll",
   "poll_button.remove_poll": "Remove poll",
   "privacy.change": "Adjust status privacy",
-  "privacy.direct.long": "Post to mentioned users only",
+  "privacy.direct.long": "Visible for mentioned users only",
   "privacy.direct.short": "Direct",
-  "privacy.private.long": "Post to followers only",
+  "privacy.private.long": "Visible for followers only",
   "privacy.private.short": "Followers-only",
-  "privacy.public.long": "Post to public timelines",
+  "privacy.public.long": "Visible for all, shown in public timelines",
   "privacy.public.short": "Public",
-  "privacy.unlisted.long": "Do not post to public timelines",
+  "privacy.unlisted.long": "Visible for all, but not in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "refresh": "Refresh",
   "regeneration_indicator.label": "Loading…",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index a315c9226..5138b85a0 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -374,7 +374,7 @@
   "status.more": "Pli",
   "status.mute": "Silentigi @{name}",
   "status.mute_conversation": "Silentigi konversacion",
-  "status.open": "Grandigi",
+  "status.open": "Grandigi ĉi tiun mesaĝon",
   "status.pin": "Alpingli profile",
   "status.pinned": "Alpinglita mesaĝo",
   "status.read_more": "Legi pli",
@@ -392,7 +392,7 @@
   "status.show_less": "Malgrandigi",
   "status.show_less_all": "Malgrandigi ĉiujn",
   "status.show_more": "Grandigi",
-  "status.show_more_all": "Grandigi ĉiujn",
+  "status.show_more_all": "Malfoldi ĉiun",
   "status.show_thread": "Montri la fadenon",
   "status.uncached_media_warning": "Nedisponebla",
   "status.unmute_conversation": "Malsilentigi la konversacion",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index 8e1f84ab8..4f7a9e59f 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -375,7 +375,7 @@
   "status.mute": "Silenciar a @{name}",
   "status.mute_conversation": "Silenciar conversación",
   "status.open": "Expandir este estado",
-  "status.pin": "Pin en el perfil",
+  "status.pin": "Fijar en el perfil",
   "status.pinned": "Toot fijado",
   "status.read_more": "Leer más",
   "status.reblog": "Retootear",
@@ -396,7 +396,7 @@
   "status.show_thread": "Mostrar hilo",
   "status.uncached_media_warning": "No disponible",
   "status.unmute_conversation": "Dejar de silenciar conversación",
-  "status.unpin": "Desmarcar del perfil",
+  "status.unpin": "Dejar de fijar",
   "suggestions.dismiss": "Descartar sugerencia",
   "suggestions.header": "Es posible que te interese…",
   "tabs_bar.federated_timeline": "Federado",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 5dc4e60c2..3a44dceb5 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -27,8 +27,8 @@
   "account.mute_notifications": "Mututu @{name}(r)en jakinarazpenak",
   "account.muted": "Mutututa",
   "account.never_active": "Inoiz ez",
-  "account.posts": "Tootak",
-  "account.posts_with_replies": "Toot-ak eta erantzunak",
+  "account.posts": "Toot",
+  "account.posts_with_replies": "Tootak eta erantzunak",
   "account.report": "Salatu @{name}",
   "account.requested": "Onarpenaren zain. Klikatu jarraitzeko eskaera ezeztatzeko",
   "account.share": "@{name}(e)ren profila elkarbanatu",
@@ -64,7 +64,7 @@
   "column.lists": "Zerrendak",
   "column.mutes": "Mutututako erabiltzaileak",
   "column.notifications": "Jakinarazpenak",
-  "column.pins": "Finkatutako toot-ak",
+  "column.pins": "Finkatutako tootak",
   "column.public": "Federatutako denbora-lerroa",
   "column_back_button.label": "Atzera",
   "column_header.hide_settings": "Ezkutatu ezarpenak",
@@ -140,7 +140,7 @@
   "emoji_button.search_results": "Bilaketaren emaitzak",
   "emoji_button.symbols": "Sinboloak",
   "emoji_button.travel": "Bidaiak eta tokiak",
-  "empty_column.account_timeline": "Ez dago toot-ik hemen!",
+  "empty_column.account_timeline": "Ez dago tootik hemen!",
   "empty_column.account_unavailable": "Profila ez dago eskuragarri",
   "empty_column.blocks": "Ez duzu erabiltzailerik blokeatu oraindik.",
   "empty_column.bookmarked_statuses": "Oraindik ez dituzu toot laster-markatutarik. Bat laster-markatzerakoan, hemen agertuko da.",
@@ -228,7 +228,7 @@
   "keyboard_shortcuts.my_profile": "zure profila irekitzeko",
   "keyboard_shortcuts.notifications": "jakinarazpenen zutabea irekitzeko",
   "keyboard_shortcuts.open_media": "media zabaltzeko",
-  "keyboard_shortcuts.pinned": "finkatutako toot-en zerrenda irekitzeko",
+  "keyboard_shortcuts.pinned": "finkatutako tooten zerrenda irekitzeko",
   "keyboard_shortcuts.profile": "egilearen profila irekitzeko",
   "keyboard_shortcuts.reply": "erantzutea",
   "keyboard_shortcuts.requests": "jarraitzeko eskarien zerrenda irekitzeko",
@@ -277,7 +277,7 @@
   "navigation_bar.logout": "Amaitu saioa",
   "navigation_bar.mutes": "Mutututako erabiltzaileak",
   "navigation_bar.personal": "Pertsonala",
-  "navigation_bar.pins": "Finkatutako toot-ak",
+  "navigation_bar.pins": "Finkatutako tootak",
   "navigation_bar.preferences": "Hobespenak",
   "navigation_bar.public_timeline": "Federatutako denbora-lerroa",
   "navigation_bar.security": "Segurtasuna",
@@ -352,8 +352,8 @@
   "search_popout.tips.user": "erabiltzailea",
   "search_results.accounts": "Jendea",
   "search_results.hashtags": "Traolak",
-  "search_results.statuses": "Toot-ak",
-  "search_results.statuses_fts_disabled": "Mastodon zerbitzari honek ez du Toot-en edukiaren bilaketa gaitu.",
+  "search_results.statuses": "Tootak",
+  "search_results.statuses_fts_disabled": "Mastodon zerbitzari honek ez du tooten edukiaren bilaketa gaitu.",
   "search_results.total": "{count, number} {count, plural, one {emaitza} other {emaitza}}",
   "status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea",
   "status.admin_status": "Ireki mezu hau moderazio interfazean",
@@ -376,7 +376,7 @@
   "status.mute_conversation": "Mututu elkarrizketa",
   "status.open": "Hedatu mezu hau",
   "status.pin": "Finkatu profilean",
-  "status.pinned": "Finkatutako toot-a",
+  "status.pinned": "Finkatutako toota",
   "status.read_more": "Irakurri gehiago",
   "status.reblog": "Bultzada",
   "status.reblog_private": "Bultzada jatorrizko hartzaileei",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 0fc39b16d..f2e7f300a 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -254,7 +254,7 @@
   "lists.subheading": "فهرست‌های شما",
   "load_pending": "{count, plural, one {# مورد تازه} other {# مورد تازه}}",
   "loading_indicator.label": "بارگیری...",
-  "media_gallery.toggle_visible": "تغییر پیدایی",
+  "media_gallery.toggle_visible": "تغییر وضعیت نمایانی",
   "missing_indicator.label": "پیدا نشد",
   "missing_indicator.sublabel": "این منبع پیدا نشد",
   "mute_modal.hide_notifications": "اعلان‌های این کاربر پنهان شود؟",
@@ -319,13 +319,13 @@
   "poll_button.add_poll": "افزودن نظرسنجی",
   "poll_button.remove_poll": "حذف نظرسنجی",
   "privacy.change": "تنظیم محرمانگی نوشته",
-  "privacy.direct.long": "تنها به کاربران نام‌برده‌شده نشان بده",
+  "privacy.direct.long": "ارسال فقط به کاربران اشاره‌شده",
   "privacy.direct.short": "خصوصی",
-  "privacy.private.long": "تنها به پیگیران نشان بده",
+  "privacy.private.long": "ارسال فقط به پی‌گیران",
   "privacy.private.short": "خصوصی",
-  "privacy.public.long": "نمایش در فهرست عمومی",
+  "privacy.public.long": "ارسال به خط‌زمانی عمومی",
   "privacy.public.short": "عمومی",
-  "privacy.unlisted.long": "عمومی، ولی فهرست نکن",
+  "privacy.unlisted.long": "ارسال نکردن به خط‌زمانی عمومی",
   "privacy.unlisted.short": "فهرست‌نشده",
   "refresh": "به‌روزرسانی",
   "regeneration_indicator.label": "در حال باز شدن…",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index b46083477..32bc8de49 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -1,7 +1,7 @@
 {
   "account.add_or_remove_from_list": "Lisää tai poista listoilta",
   "account.badges.bot": "Botti",
-  "account.badges.group": "Group",
+  "account.badges.group": "Ryhmä",
   "account.block": "Estä @{name}",
   "account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}",
   "account.blocked": "Estetty",
@@ -34,7 +34,7 @@
   "account.share": "Jaa käyttäjän @{name} profiili",
   "account.show_reblogs": "Näytä buustaukset käyttäjältä @{name}",
   "account.unblock": "Salli @{name}",
-  "account.unblock_domain": "Näytä {domain}",
+  "account.unblock_domain": "Salli {domain}",
   "account.unendorse": "Poista suosittelu profiilistasi",
   "account.unfollow": "Lakkaa seuraamasta",
   "account.unmute": "Poista käyttäjän @{name} mykistys",
@@ -43,7 +43,7 @@
   "alert.rate_limited.title": "Määrää rajoitettu",
   "alert.unexpected.message": "Tapahtui odottamaton virhe.",
   "alert.unexpected.title": "Hups!",
-  "announcement.announcement": "Announcement",
+  "announcement.announcement": "Ilmoitus",
   "autosuggest_hashtag.per_week": "{count} viikossa",
   "boost_modal.combo": "Ensi kerralla voit ohittaa tämän painamalla {combo}",
   "bundle_column_error.body": "Jokin meni vikaan komponenttia ladattaessa.",
@@ -85,8 +85,8 @@
   "compose_form.poll.duration": "Äänestyksen kesto",
   "compose_form.poll.option_placeholder": "Valinta numero",
   "compose_form.poll.remove_option": "Poista tämä valinta",
-  "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
-  "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
+  "compose_form.poll.switch_to_multiple": "Muuta kysely monivalinnaksi",
+  "compose_form.poll.switch_to_single": "Muuta kysely sallimaan vain yksi valinta",
   "compose_form.publish": "Tuuttaa",
   "compose_form.publish_loud": "Julkista!",
   "compose_form.sensitive.hide": "Valitse tämä arkaluontoisena",
@@ -143,7 +143,7 @@
   "empty_column.account_timeline": "Ei ole 'toots' täällä!",
   "empty_column.account_unavailable": "Profiilia ei löydy",
   "empty_column.blocks": "Et ole vielä estänyt yhtään käyttäjää.",
-  "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
+  "empty_column.bookmarked_statuses": "Et ole vielä lisännyt tuuttauksia kirjanmerkkeihisi. Kun teet niin, tuuttaus näkyy tässä.",
   "empty_column.community": "Paikallinen aikajana on tyhjä. Homma lähtee käyntiin, kun kirjoitat jotain julkista!",
   "empty_column.direct": "Sinulla ei ole vielä yhtään viestiä yksittäiselle käyttäjälle. Kun lähetät tai vastaanotat sellaisen, se näkyy täällä.",
   "empty_column.domain_blocks": "Yhtään verkko-osoitetta ei ole vielä piilotettu.",
@@ -160,7 +160,7 @@
   "empty_column.public": "Täällä ei ole mitään! Saat sisältöä, kun kirjoitat jotain julkisesti tai käyt seuraamassa muiden instanssien käyttäjiä",
   "error.unexpected_crash.explanation": "Sivua ei voi näyttää oikein, johtuen bugista tai ongelmasta selaimen yhteensopivuudessa.",
   "error.unexpected_crash.next_steps": "Kokeile päivittää sivu. Jos tämä ei auta, saatat yhä pystyä käyttämään Mastodonia toisen selaimen tai sovelluksen kautta.",
-  "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
+  "errors.unexpected_crash.copy_stacktrace": "Kopioi stacktrace leikepöydälle",
   "errors.unexpected_crash.report_issue": "Ilmoita ongelmasta",
   "follow_request.authorize": "Valtuuta",
   "follow_request.reject": "Hylkää",
@@ -184,8 +184,8 @@
   "home.column_settings.basic": "Perusasetukset",
   "home.column_settings.show_reblogs": "Näytä buustaukset",
   "home.column_settings.show_replies": "Näytä vastaukset",
-  "home.hide_announcements": "Hide announcements",
-  "home.show_announcements": "Show announcements",
+  "home.hide_announcements": "Piilota ilmoitukset",
+  "home.show_announcements": "Näytä ilmoitukset",
   "intervals.full.days": "Päivä päiviä",
   "intervals.full.hours": "Tunti tunteja",
   "intervals.full.minutes": "Minuuti minuuteja",
@@ -215,7 +215,7 @@
   "keyboard_shortcuts.direct": "avaa pikaviestisarake",
   "keyboard_shortcuts.down": "siirry listassa alaspäin",
   "keyboard_shortcuts.enter": "avaa tilapäivitys",
-  "keyboard_shortcuts.favourite": "tykkää",
+  "keyboard_shortcuts.favourite": "lisää suosikkeihin",
   "keyboard_shortcuts.favourites": "avaa lista suosikeista",
   "keyboard_shortcuts.federated": "avaa yleinen aikajana",
   "keyboard_shortcuts.heading": "Näppäinkomennot",
@@ -227,7 +227,7 @@
   "keyboard_shortcuts.muted": "avaa lista mykistetyistä käyttäjistä",
   "keyboard_shortcuts.my_profile": "avaa profiilisi",
   "keyboard_shortcuts.notifications": "avaa ilmoitukset-sarake",
-  "keyboard_shortcuts.open_media": "to open media",
+  "keyboard_shortcuts.open_media": "median avaus",
   "keyboard_shortcuts.pinned": "avaa lista kiinnitetyistä tuuttauksista",
   "keyboard_shortcuts.profile": "avaa kirjoittajan profiili",
   "keyboard_shortcuts.reply": "vastaa",
@@ -260,7 +260,7 @@
   "mute_modal.hide_notifications": "Piilota tältä käyttäjältä tulevat ilmoitukset?",
   "navigation_bar.apps": "Mobiilisovellukset",
   "navigation_bar.blocks": "Estetyt käyttäjät",
-  "navigation_bar.bookmarks": "Bookmarks",
+  "navigation_bar.bookmarks": "Kirjanmerkit",
   "navigation_bar.community_timeline": "Paikallinen aikajana",
   "navigation_bar.compose": "Kirjoita uusi tuuttaus",
   "navigation_bar.direct": "Viestit",
@@ -283,9 +283,9 @@
   "navigation_bar.security": "Tunnukset",
   "notification.favourite": "{name} tykkäsi tilastasi",
   "notification.follow": "{name} seurasi sinua",
-  "notification.follow_request": "{name} has requested to follow you",
+  "notification.follow_request": "{name} haluaa seurata sinua",
   "notification.mention": "{name} mainitsi sinut",
-  "notification.own_poll": "Your poll has ended",
+  "notification.own_poll": "Kyselysi on päättynyt",
   "notification.poll": "Kysely, johon osallistuit, on päättynyt",
   "notification.reblog": "{name} buustasi tilaasi",
   "notifications.clear": "Tyhjennä ilmoitukset",
@@ -296,7 +296,7 @@
   "notifications.column_settings.filter_bar.category": "Pikasuodatuspalkki",
   "notifications.column_settings.filter_bar.show": "Näytä",
   "notifications.column_settings.follow": "Uudet seuraajat:",
-  "notifications.column_settings.follow_request": "New follow requests:",
+  "notifications.column_settings.follow_request": "Uudet seuraamispyynnöt:",
   "notifications.column_settings.mention": "Maininnat:",
   "notifications.column_settings.poll": "Kyselyn tulokset:",
   "notifications.column_settings.push": "Push-ilmoitukset",
@@ -335,7 +335,7 @@
   "relative_time.just_now": "nyt",
   "relative_time.minutes": "{number} m",
   "relative_time.seconds": "{number} s",
-  "relative_time.today": "today",
+  "relative_time.today": "tänään",
   "reply_indicator.cancel": "Peruuta",
   "report.forward": "Välitä kohteeseen {target}",
   "report.forward_hint": "Tämä tili on toisella palvelimella. Haluatko lähettää nimettömän raportin myös sinne?",
@@ -358,7 +358,7 @@
   "status.admin_account": "Avaa moderaattorinäkymä tilistä @{name}",
   "status.admin_status": "Avaa tilapäivitys moderaattorinäkymässä",
   "status.block": "Estä @{name}",
-  "status.bookmark": "Bookmark",
+  "status.bookmark": "Tallenna kirjanmerkki",
   "status.cancel_reblog_private": "Peru buustaus",
   "status.cannot_reblog": "Tätä julkaisua ei voi buustata",
   "status.copy": "Kopioi linkki tilapäivitykseen",
@@ -383,7 +383,7 @@
   "status.reblogged_by": "{name} buustasi",
   "status.reblogs.empty": "Kukaan ei ole vielä buustannut tätä tuuttausta. Kun joku tekee niin, näkyy kyseinen henkilö tässä.",
   "status.redraft": "Poista & palauta muokattavaksi",
-  "status.remove_bookmark": "Remove bookmark",
+  "status.remove_bookmark": "Poista kirjanmerkki",
   "status.reply": "Vastaa",
   "status.replyAll": "Vastaa ketjuun",
   "status.report": "Raportoi @{name}",
@@ -416,11 +416,11 @@
   "upload_button.label": "Lisää mediaa",
   "upload_error.limit": "Tiedostolatauksien raja ylitetty.",
   "upload_error.poll": "Tiedon lataaminen ei ole sallittua kyselyissä.",
-  "upload_form.audio_description": "Describe for people with hearing loss",
+  "upload_form.audio_description": "Kuvaile kuulovammaisille",
   "upload_form.description": "Anna kuvaus näkörajoitteisia varten",
   "upload_form.edit": "Muokkaa",
   "upload_form.undo": "Peru",
-  "upload_form.video_description": "Describe for people with hearing loss or visual impairment",
+  "upload_form.video_description": "Kuvaile kuulo- tai näkövammaisille",
   "upload_modal.analyzing_picture": "Analysoidaan kuvaa…",
   "upload_modal.apply": "Käytä",
   "upload_modal.description_placeholder": "Eräänä jäätävänä ja pimeänä yönä gorilla ratkaisi sudokun kahdessa minuutissa",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index f61c15d57..cf07cd3db 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -3,29 +3,29 @@
   "account.badges.bot": "Robot",
   "account.badges.group": "Groupe",
   "account.block": "Bloquer @{name}",
-  "account.block_domain": "Tout masquer venant de {domain}",
+  "account.block_domain": "Bloquer le domaine {domain}",
   "account.blocked": "Bloqué·e",
   "account.cancel_follow_request": "Annuler la demande de suivi",
   "account.direct": "Envoyer un message direct à @{name}",
-  "account.domain_blocked": "Domaine caché",
+  "account.domain_blocked": "Domaine bloqué",
   "account.edit_profile": "Modifier le profil",
   "account.endorse": "Recommander sur le profil",
   "account.follow": "Suivre",
-  "account.followers": "Abonné⋅e⋅s",
-  "account.followers.empty": "Personne ne suit cet utilisateur·rice pour l’instant.",
+  "account.followers": "Abonné·e·s",
+  "account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour l’instant.",
   "account.follows": "Abonnements",
   "account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.",
   "account.follows_you": "Vous suit",
   "account.hide_reblogs": "Masquer les partages de @{name}",
   "account.last_status": "Dernière activité",
   "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}",
-  "account.locked_info": "Ce compte est verrouillé. Son propriétaire approuve manuellement qui peut le ou la suivre.",
-  "account.media": "Média",
+  "account.locked_info": "Ce compte est verrouillé. Son ou sa propriétaire approuve manuellement qui peut le suivre.",
+  "account.media": "Médias",
   "account.mention": "Mentionner @{name}",
   "account.moved_to": "{name} a déménagé vers :",
   "account.mute": "Masquer @{name}",
   "account.mute_notifications": "Ignorer les notifications de @{name}",
-  "account.muted": "Silencé·e",
+  "account.muted": "Masqué·e",
   "account.never_active": "Jamais",
   "account.posts": "Pouets",
   "account.posts_with_replies": "Pouets et réponses",
@@ -38,14 +38,14 @@
   "account.unendorse": "Ne plus recommander sur le profil",
   "account.unfollow": "Ne plus suivre",
   "account.unmute": "Ne plus masquer @{name}",
-  "account.unmute_notifications": "Réactiver les notifications de @{name}",
+  "account.unmute_notifications": "Ne plus masquer les notifications de @{name}",
   "alert.rate_limited.message": "Veuillez réessayer après {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Débit limité",
+  "alert.rate_limited.title": "Taux limité",
   "alert.unexpected.message": "Une erreur inattendue s’est produite.",
   "alert.unexpected.title": "Oups !",
   "announcement.announcement": "Annonce",
   "autosuggest_hashtag.per_week": "{count} par semaine",
-  "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci, la prochaine fois",
+  "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois",
   "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
   "bundle_column_error.retry": "Réessayer",
   "bundle_column_error.title": "Erreur réseau",
@@ -55,7 +55,7 @@
   "column.blocks": "Comptes bloqués",
   "column.bookmarks": "Marque-pages",
   "column.community": "Fil public local",
-  "column.direct": "Messages privés",
+  "column.direct": "Messages directs",
   "column.directory": "Parcourir les profils",
   "column.domain_blocks": "Domaines cachés",
   "column.favourites": "Favoris",
@@ -67,7 +67,7 @@
   "column.pins": "Pouets épinglés",
   "column.public": "Fil public global",
   "column_back_button.label": "Retour",
-  "column_header.hide_settings": "Masquer les paramètres",
+  "column_header.hide_settings": "Cacher les paramètres",
   "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
   "column_header.moveRight_settings": "Déplacer la colonne vers la droite",
   "column_header.pin": "Épingler",
@@ -94,39 +94,39 @@
   "compose_form.sensitive.unmarked": "Le média n’est pas marqué comme sensible",
   "compose_form.spoiler.marked": "Le texte est caché derrière un avertissement",
   "compose_form.spoiler.unmarked": "Le texte n’est pas caché",
-  "compose_form.spoiler_placeholder": "Écrivez ici votre avertissement",
+  "compose_form.spoiler_placeholder": "Écrivez votre avertissement ici",
   "confirmation_modal.cancel": "Annuler",
   "confirmations.block.block_and_report": "Bloquer et signaler",
   "confirmations.block.confirm": "Bloquer",
-  "confirmations.block.message": "Confirmez-vous le blocage de {name} ?",
+  "confirmations.block.message": "Voulez-vous vraiment bloquer {name} ?",
   "confirmations.delete.confirm": "Supprimer",
-  "confirmations.delete.message": "Confirmez-vous la suppression de ce pouet ?",
+  "confirmations.delete.message": "Voulez-vous vraiment supprimer ce pouet ?",
   "confirmations.delete_list.confirm": "Supprimer",
-  "confirmations.delete_list.message": "Êtes-vous sûr·e de vouloir supprimer définitivement cette liste ?",
+  "confirmations.delete_list.message": "Voulez-vous vraiment supprimer définitivement cette liste ?",
   "confirmations.domain_block.confirm": "Masquer le domaine entier",
-  "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables. Vous ne verrez plus de contenu provenant de ce domaine, ni dans fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s.",
-  "confirmations.logout.confirm": "Déconnexion",
-  "confirmations.logout.message": "Êtes-vous sûr·e de vouloir vous déconnecter ?",
+  "confirmations.domain_block.message": "Voulez-vous vraiment, vraiment bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables. Vous ne verrez plus de contenu provenant de ce domaine, ni dans fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s.",
+  "confirmations.logout.confirm": "Se déconnecter",
+  "confirmations.logout.message": "Voulez-vous vraiment vous déconnecter ?",
   "confirmations.mute.confirm": "Masquer",
   "confirmations.mute.explanation": "Cela masquera ses messages et les messages le ou la mentionnant, mais cela lui permettra quand même de voir vos messages et de vous suivre.",
-  "confirmations.mute.message": "Êtes-vous sûr·e de vouloir masquer {name} ?",
-  "confirmations.redraft.confirm": "Effacer et ré-écrire",
-  "confirmations.redraft.message": "Êtes-vous sûr·e de vouloir effacer ce statut pour le ré-écrire ? Ses partages ainsi que ses mises en favori seront perdu·e·s et ses réponses seront orphelines.",
+  "confirmations.mute.message": "Voulez-vous vraiment masquer {name} ?",
+  "confirmations.redraft.confirm": "Supprimer et ré-écrire",
+  "confirmations.redraft.message": "Voulez-vous vraiment supprimer ce pouet pour le ré-écrire ? Ses partages ainsi que ses mises en favori seront perdu·e·s et ses réponses seront orphelines.",
   "confirmations.reply.confirm": "Répondre",
-  "confirmations.reply.message": "Répondre maintenant écrasera le message que vous composez actuellement. Êtes-vous sûr·e de vouloir continuer ?",
+  "confirmations.reply.message": "Répondre maintenant écrasera le message que vous rédigez actuellement. Voulez-vous vraiment continuer ?",
   "confirmations.unfollow.confirm": "Ne plus suivre",
-  "confirmations.unfollow.message": "Êtes-vous sûr·e de vouloir arrêter de suivre {name} ?",
+  "confirmations.unfollow.message": "Voulez-vous vraiment arrêter de suivre {name} ?",
   "conversation.delete": "Supprimer la conversation",
   "conversation.mark_as_read": "Marquer comme lu",
   "conversation.open": "Afficher la conversation",
   "conversation.with": "Avec {names}",
   "directory.federated": "Du fédiverse connu",
   "directory.local": "De {domain} seulement",
-  "directory.new_arrivals": "Nouveaux·elles arrivant·e·s",
-  "directory.recently_active": "Récemment actif·ve·s",
+  "directory.new_arrivals": "Inscrit·e·s récemment",
+  "directory.recently_active": "Actif·ve·s récemment",
   "embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.",
   "embed.preview": "Il apparaîtra comme cela :",
-  "emoji_button.activity": "Activités",
+  "emoji_button.activity": "Activité",
   "emoji_button.custom": "Personnalisés",
   "emoji_button.flags": "Drapeaux",
   "emoji_button.food": "Nourriture & Boisson",
@@ -136,7 +136,7 @@
   "emoji_button.objects": "Objets",
   "emoji_button.people": "Personnes",
   "emoji_button.recent": "Fréquemment utilisés",
-  "emoji_button.search": "Recherche…",
+  "emoji_button.search": "Recherche...",
   "emoji_button.search_results": "Résultats de la recherche",
   "emoji_button.symbols": "Symboles",
   "emoji_button.travel": "Lieux & Voyages",
@@ -155,7 +155,7 @@
   "empty_column.home.public_timeline": "le fil public",
   "empty_column.list": "Il n’y a rien dans cette liste pour l’instant. Dès que des personnes de cette liste publieront de nouveaux statuts, ils apparaîtront ici.",
   "empty_column.lists": "Vous n’avez pas encore de liste. Lorsque vous en créerez une, elle apparaîtra ici.",
-  "empty_column.mutes": "Vous n’avez pas encore silencié d’utilisateur·rice·s.",
+  "empty_column.mutes": "Vous n’avez masqué aucun·e utilisateur·rice pour le moment.",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres personnes pour débuter la conversation.",
   "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des personnes d’autres serveurs pour remplir le fil public",
   "error.unexpected_crash.explanation": "En raison d’un bug dans notre code ou d’un problème de compatibilité avec votre navigateur, cette page n’a pas pu être affichée correctement.",
@@ -176,12 +176,12 @@
   "hashtag.column_header.tag_mode.any": "ou {additional}",
   "hashtag.column_header.tag_mode.none": "sans {additional}",
   "hashtag.column_settings.select.no_options_message": "Aucune suggestion trouvée",
-  "hashtag.column_settings.select.placeholder": "Ajouter des hashtags…",
+  "hashtag.column_settings.select.placeholder": "Entrer des hashtags…",
   "hashtag.column_settings.tag_mode.all": "Tous ces éléments",
   "hashtag.column_settings.tag_mode.any": "Au moins un de ces éléments",
   "hashtag.column_settings.tag_mode.none": "Aucun de ces éléments",
-  "hashtag.column_settings.tag_toggle": "Inclure des mots-clés additionnels dans cette colonne",
-  "home.column_settings.basic": "Base",
+  "hashtag.column_settings.tag_toggle": "Inclure des hashtags additionnels pour cette colonne",
+  "home.column_settings.basic": "Basique",
   "home.column_settings.show_reblogs": "Afficher les partages",
   "home.column_settings.show_replies": "Afficher les réponses",
   "home.hide_announcements": "Masquer les annonces",
@@ -199,46 +199,46 @@
   "introduction.interactions.action": "Finir le tutoriel !",
   "introduction.interactions.favourite.headline": "Favoris",
   "introduction.interactions.favourite.text": "Vous pouvez garder un pouet pour plus tard et faire savoir à son auteur·ice que vous l’avez aimé, en l'ajoutant aux favoris.",
-  "introduction.interactions.reblog.headline": "Repartager",
+  "introduction.interactions.reblog.headline": "Partager",
   "introduction.interactions.reblog.text": "Vous pouvez partager les pouets d'autres personnes avec vos abonné·e·s en les repartageant.",
   "introduction.interactions.reply.headline": "Répondre",
   "introduction.interactions.reply.text": "Vous pouvez répondre aux pouets d'autres personnes et à vos propres pouets, ce qui les enchaînera dans une conversation.",
   "introduction.welcome.action": "Allons-y !",
   "introduction.welcome.headline": "Premiers pas",
   "introduction.welcome.text": "Bienvenue dans le fédiverse ! Dans quelques instants, vous pourrez diffuser des messages et parler à vos ami·e·s sur une grande variété de serveurs. Mais ce serveur, {domain}, est spécial - il héberge votre profil, alors souvenez-vous de son nom.",
-  "keyboard_shortcuts.back": "pour revenir en arrière",
-  "keyboard_shortcuts.blocked": "pour ouvrir la liste des comptes bloqués",
-  "keyboard_shortcuts.boost": "pour partager",
-  "keyboard_shortcuts.column": "pour focaliser un statut dans l’une des colonnes",
-  "keyboard_shortcuts.compose": "pour focaliser la zone de rédaction",
+  "keyboard_shortcuts.back": "revenir en arrière",
+  "keyboard_shortcuts.blocked": "ouvrir la liste des comptes bloqués",
+  "keyboard_shortcuts.boost": "partager",
+  "keyboard_shortcuts.column": "cibler un pouet d’une des colonnes",
+  "keyboard_shortcuts.compose": "cibler la zone de rédaction",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.direct": "pour ouvrir la colonne des messages directs",
-  "keyboard_shortcuts.down": "pour descendre dans la liste",
-  "keyboard_shortcuts.enter": "pour ouvrir le statut",
-  "keyboard_shortcuts.favourite": "pour ajouter aux favoris",
-  "keyboard_shortcuts.favourites": "pour ouvrir la liste des pouets favoris",
-  "keyboard_shortcuts.federated": "pour ouvrir le fil public global",
+  "keyboard_shortcuts.direct": "ouvrir la colonne des messages directs",
+  "keyboard_shortcuts.down": "descendre dans la liste",
+  "keyboard_shortcuts.enter": "ouvrir le pouet",
+  "keyboard_shortcuts.favourite": "ajouter aux favoris",
+  "keyboard_shortcuts.favourites": "ouvrir la liste des favoris",
+  "keyboard_shortcuts.federated": "ouvrir le fil public global",
   "keyboard_shortcuts.heading": "Raccourcis clavier",
-  "keyboard_shortcuts.home": "pour ouvrir l’accueil",
-  "keyboard_shortcuts.hotkey": "Raccourci",
-  "keyboard_shortcuts.legend": "pour afficher cette légende",
-  "keyboard_shortcuts.local": "pour ouvrir le fil public local",
-  "keyboard_shortcuts.mention": "pour mentionner l’auteur·rice",
-  "keyboard_shortcuts.muted": "pour ouvrir la liste des utilisateur·rice·s muté·e·s",
-  "keyboard_shortcuts.my_profile": "pour ouvrir votre profil",
-  "keyboard_shortcuts.notifications": "pour ouvrir votre colonne de notifications",
-  "keyboard_shortcuts.open_media": "pour ouvrir le média",
-  "keyboard_shortcuts.pinned": "pour ouvrir une liste des pouets épinglés",
-  "keyboard_shortcuts.profile": "pour ouvrir le profil de l’auteur·rice",
-  "keyboard_shortcuts.reply": "pour répondre",
-  "keyboard_shortcuts.requests": "pour ouvrir la liste de demandes de suivi",
-  "keyboard_shortcuts.search": "pour cibler la recherche",
-  "keyboard_shortcuts.start": "pour ouvrir la colonne « pour commencer »",
-  "keyboard_shortcuts.toggle_hidden": "pour afficher/cacher un texte derrière CW",
-  "keyboard_shortcuts.toggle_sensitivity": "pour afficher/cacher les médias",
-  "keyboard_shortcuts.toot": "pour démarrer un tout nouveau pouet",
-  "keyboard_shortcuts.unfocus": "pour quitter la zone de composition/recherche",
-  "keyboard_shortcuts.up": "pour remonter dans la liste",
+  "keyboard_shortcuts.home": "le fil d’accueil",
+  "keyboard_shortcuts.hotkey": "Raccourci clavier",
+  "keyboard_shortcuts.legend": "afficher cet aide-mémoire",
+  "keyboard_shortcuts.local": "ouvrir le fil public local",
+  "keyboard_shortcuts.mention": "mentionner l’auteur·rice",
+  "keyboard_shortcuts.muted": "ouvrir la liste des comptes masqués",
+  "keyboard_shortcuts.my_profile": "ouvrir votre profil",
+  "keyboard_shortcuts.notifications": "ouvrir la colonne de notifications",
+  "keyboard_shortcuts.open_media": "ouvrir le média",
+  "keyboard_shortcuts.pinned": "ouvrir la liste des pouets épinglés",
+  "keyboard_shortcuts.profile": "ouvrir le profil de l’auteur·rice",
+  "keyboard_shortcuts.reply": "répondre",
+  "keyboard_shortcuts.requests": "ouvrir la liste de demandes d’abonnement",
+  "keyboard_shortcuts.search": "cibler la zone de recherche",
+  "keyboard_shortcuts.start": "ouvrir la colonne « Pour commencer »",
+  "keyboard_shortcuts.toggle_hidden": "déplier/replier le texte derrière un CW",
+  "keyboard_shortcuts.toggle_sensitivity": "afficher/cacher les médias",
+  "keyboard_shortcuts.toot": "démarrer un tout nouveau pouet",
+  "keyboard_shortcuts.unfocus": "quitter la zone de rédaction/recherche",
+  "keyboard_shortcuts.up": "remonter dans la liste",
   "lightbox.close": "Fermer",
   "lightbox.next": "Suivant",
   "lightbox.previous": "Précédent",
@@ -254,7 +254,7 @@
   "lists.subheading": "Vos listes",
   "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}",
   "loading_indicator.label": "Chargement…",
-  "media_gallery.toggle_visible": "Modifier la visibilité",
+  "media_gallery.toggle_visible": "Intervertir la visibilité",
   "missing_indicator.label": "Non trouvé",
   "missing_indicator.sublabel": "Ressource introuvable",
   "mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
@@ -268,7 +268,7 @@
   "navigation_bar.domain_blocks": "Domaines cachés",
   "navigation_bar.edit_profile": "Modifier le profil",
   "navigation_bar.favourites": "Favoris",
-  "navigation_bar.filters": "Mots silenciés",
+  "navigation_bar.filters": "Mots masqués",
   "navigation_bar.follow_requests": "Demandes de suivi",
   "navigation_bar.follows_and_followers": "Abonnements et abonné⋅e·s",
   "navigation_bar.info": "À propos de ce serveur",
@@ -287,15 +287,15 @@
   "notification.mention": "{name} vous a mentionné·e :",
   "notification.own_poll": "Votre sondage est terminé",
   "notification.poll": "Un sondage auquel vous avez participé vient de se terminer",
-  "notification.reblog": "{name} a partagé votre statut :",
-  "notifications.clear": "Nettoyer les notifications",
-  "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
-  "notifications.column_settings.alert": "Notifications locales",
+  "notification.reblog": "{name} a partagé votre statut",
+  "notifications.clear": "Effacer les notifications",
+  "notifications.clear_confirmation": "Voulez-vous vraiment effacer toutes vos notifications ?",
+  "notifications.column_settings.alert": "Notifications du navigateur",
   "notifications.column_settings.favourite": "Favoris :",
   "notifications.column_settings.filter_bar.advanced": "Afficher toutes les catégories",
   "notifications.column_settings.filter_bar.category": "Barre de filtrage rapide",
   "notifications.column_settings.filter_bar.show": "Afficher",
-  "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e·s :",
+  "notifications.column_settings.follow": "Nouveaux·elles abonné·e·s :",
   "notifications.column_settings.follow_request": "Nouvelles demandes d’abonnement :",
   "notifications.column_settings.mention": "Mentions :",
   "notifications.column_settings.poll": "Résultats des sondage :",
@@ -306,7 +306,7 @@
   "notifications.filter.all": "Tout",
   "notifications.filter.boosts": "Partages",
   "notifications.filter.favourites": "Favoris",
-  "notifications.filter.follows": "Abonné·e·s",
+  "notifications.filter.follows": "Abonnés",
   "notifications.filter.mentions": "Mentions",
   "notifications.filter.polls": "Résultats des sondages",
   "notifications.group": "{count} notifications",
@@ -321,8 +321,8 @@
   "privacy.change": "Ajuster la confidentialité du message",
   "privacy.direct.long": "N’envoyer qu’aux personnes mentionnées",
   "privacy.direct.short": "Direct",
-  "privacy.private.long": "Seul⋅e⋅s vos abonné⋅e⋅s verront vos statuts",
-  "privacy.private.short": "Abonné⋅e⋅s uniquement",
+  "privacy.private.long": "Seul·e·s vos abonné·e·s verront vos statuts",
+  "privacy.private.short": "Abonné·e·s uniquement",
   "privacy.public.long": "Afficher dans les fils publics",
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Ne pas afficher dans les fils publics",
@@ -347,22 +347,22 @@
   "search_popout.search_format": "Recherche avancée",
   "search_popout.tips.full_text": "Un texte normal retourne les pouets que vous avez écris, mis en favori, partagés, ou vous mentionnant, ainsi que les identifiants, les noms affichés, et les hashtags des personnes et messages correspondant.",
   "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "statuts",
+  "search_popout.tips.status": "pouet",
   "search_popout.tips.text": "Un texte simple renvoie les noms affichés, les identifiants et les hashtags correspondants",
-  "search_popout.tips.user": "utilisateur⋅ice",
+  "search_popout.tips.user": "utilisateur·ice",
   "search_results.accounts": "Comptes",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Pouets",
   "search_results.statuses_fts_disabled": "La recherche de pouets par leur contenu n'est pas activée sur ce serveur Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
   "status.admin_account": "Ouvrir l’interface de modération pour @{name}",
-  "status.admin_status": "Ouvrir ce statut dans l’interface de modération",
+  "status.admin_status": "Ouvrir ce pouet dans l’interface de modération",
   "status.block": "Bloquer @{name}",
   "status.bookmark": "Ajouter aux marque-pages",
   "status.cancel_reblog_private": "Annuler le partage",
   "status.cannot_reblog": "Ce pouet ne peut pas être partagé",
   "status.copy": "Copier le lien vers le pouet",
-  "status.delete": "Effacer",
+  "status.delete": "Supprimer",
   "status.detailed_status": "Vue détaillée de la conversation",
   "status.direct": "Envoyer un message direct à @{name}",
   "status.embed": "Intégrer",
@@ -374,15 +374,15 @@
   "status.more": "Plus",
   "status.mute": "Masquer @{name}",
   "status.mute_conversation": "Masquer la conversation",
-  "status.open": "Déplier ce statut",
+  "status.open": "Voir les détails du pouet",
   "status.pin": "Épingler sur le profil",
   "status.pinned": "Pouet épinglé",
   "status.read_more": "En savoir plus",
   "status.reblog": "Partager",
   "status.reblog_private": "Partager à l’audience originale",
-  "status.reblogged_by": "{name} a partagé :",
+  "status.reblogged_by": "{name} a partagé",
   "status.reblogs.empty": "Personne n’a encore partagé ce pouet. Lorsque quelqu’un le fera, il apparaîtra ici.",
-  "status.redraft": "Effacer et ré-écrire",
+  "status.redraft": "Supprimer et ré-écrire",
   "status.remove_bookmark": "Retirer des marque-pages",
   "status.reply": "Répondre",
   "status.replyAll": "Répondre au fil",
@@ -393,7 +393,7 @@
   "status.show_less_all": "Tout replier",
   "status.show_more": "Déplier",
   "status.show_more_all": "Tout déplier",
-  "status.show_thread": "Lire le fil",
+  "status.show_thread": "Montrer le fil",
   "status.uncached_media_warning": "Indisponible",
   "status.unmute_conversation": "Ne plus masquer la conversation",
   "status.unpin": "Retirer du profil",
@@ -409,7 +409,7 @@
   "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} restantes",
   "time_remaining.moments": "Encore quelques instants",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} restantes",
-  "trends.count_by_accounts": "{count} {rawCount, plural, one {personne} other {personnes}} discutent",
+  "trends.count_by_accounts": "{count} {rawCount, plural, one {personne discute} other {personnes discutent}}",
   "trends.trending_now": "Tendance en ce moment",
   "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
   "upload_area.title": "Glissez et déposez pour envoyer",
diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json
index 0e9c9d6d1..b69ec2b95 100644
--- a/app/javascript/mastodon/locales/ga.json
+++ b/app/javascript/mastodon/locales/ga.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 76657cdde..f26b39c21 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -128,7 +128,7 @@
   "embed.preview": "Así será mostrado:",
   "emoji_button.activity": "Actividade",
   "emoji_button.custom": "Personalizado",
-  "emoji_button.flags": "Bandeiras",
+  "emoji_button.flags": "Marcas",
   "emoji_button.food": "Comida e Bebida",
   "emoji_button.label": "Inserir emoticona",
   "emoji_button.nature": "Natureza",
@@ -150,33 +150,33 @@
   "empty_column.favourited_statuses": "Aínda non tes toots favoritos. Cando che goste algún, aparecerá aquí.",
   "empty_column.favourites": "A ninguén lle gostou este toot polo momento. Cando a alguén lle goste, aparecerá aquí.",
   "empty_column.follow_requests": "Non tes peticións de seguimento. Cando recibas unha, amosarase aquí.",
-  "empty_column.hashtag": "Aínda non hai nada con este cancelo.",
+  "empty_column.hashtag": "Aínda non hai nada con esta etiqueta.",
   "empty_column.home": "A túa cronoloxía inicial está baleira! Visita {public} ou emprega a procura para atopar outras usuarias.",
   "empty_column.home.public_timeline": "a cronoloxía pública",
   "empty_column.list": "Aínda non hai nada en esta lista. Cando as usuarias incluídas na lista publiquen mensaxes, aparecerán aquí.",
   "empty_column.lists": "Aínda non tes listaxes. Cando crees unha, amosarase aquí.",
   "empty_column.mutes": "Aínda non silenciaches a ningúnha usuaria.",
-  "empty_column.notifications": "Aínda non tes notificacións. Interactúa con outros para comezar unha conversa.",
+  "empty_column.notifications": "Aínda non tes notificacións. Interactúa con outras para comezar unha conversa.",
   "empty_column.public": "Nada por aquí! Escribe algo de xeito público, ou segue de xeito manual usuarias doutros servidores para ir enchéndoo",
   "error.unexpected_crash.explanation": "Debido a un erro no noso código ou a unha compatilidade co teu navegador, esta páxina non pode ser amosada correctamente.",
-  "error.unexpected_crash.next_steps": "Tenta actualizar a páxina. Se esto non axuda podes tamén empregar o Mastodon noutro navegador ou aplicación nativa.",
+  "error.unexpected_crash.next_steps": "Tenta actualizar a páxina. Se esto non axuda podes tamén empregar Mastodon noutro navegador ou aplicación nativa.",
   "errors.unexpected_crash.copy_stacktrace": "Copiar trazas (stacktrace) ó portapapeis",
-  "errors.unexpected_crash.report_issue": "Denunciar un problema",
+  "errors.unexpected_crash.report_issue": "Informar sobre un problema",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rexeitar",
-  "getting_started.developers": "Desenvolvedores",
+  "getting_started.developers": "Desenvolvedoras",
   "getting_started.directory": "Directorio local",
   "getting_started.documentation": "Documentación",
   "getting_started.heading": "Primeiros pasos",
   "getting_started.invite": "Convidar persoas",
-  "getting_started.open_source_notice": "O Mastodon é software de código aberto. Podes contribuír ou informar de fallos no GitHub en {github}.",
+  "getting_started.open_source_notice": "Mastodon é software de código aberto. Podes contribuír ou informar de fallos en GitHub en {github}.",
   "getting_started.security": "Seguranza",
   "getting_started.terms": "Termos do servizo",
   "hashtag.column_header.tag_mode.all": "e {additional}",
   "hashtag.column_header.tag_mode.any": "ou {additional}",
   "hashtag.column_header.tag_mode.none": "sen {additional}",
   "hashtag.column_settings.select.no_options_message": "Non se atoparon suxestións",
-  "hashtag.column_settings.select.placeholder": "Inserir cancelos…",
+  "hashtag.column_settings.select.placeholder": "Inserir etiquetas…",
   "hashtag.column_settings.tag_mode.all": "Todos estes",
   "hashtag.column_settings.tag_mode.any": "Calquera destes",
   "hashtag.column_settings.tag_mode.none": "Ningún destes",
@@ -192,7 +192,7 @@
   "introduction.federation.action": "Seguinte",
   "introduction.federation.federated.headline": "Federado",
   "introduction.federation.federated.text": "Publicacións públicas doutros servidores do fediverso aparecerán na cronoloxía federada.",
-  "introduction.federation.home.headline": "Páxina inicial",
+  "introduction.federation.home.headline": "Inicio",
   "introduction.federation.home.text": "Publicacións de persoas que ti segues aparecerán na cronoloxía do inicio. Podes seguir calquera persoa en calquera servidor!",
   "introduction.federation.local.headline": "Local",
   "introduction.federation.local.text": "Publicacións públicas de persoas no teu mesmo servidor aparecerán na cronoloxía local.",
@@ -205,7 +205,7 @@
   "introduction.interactions.reply.text": "Podes responder ós toots doutras persoas e ós teus propios, así ficarán encadeados nunha conversa.",
   "introduction.welcome.action": "Imos!",
   "introduction.welcome.headline": "Primeiros pasos",
-  "introduction.welcome.text": "Benvido ó fediverso! Nun intre poderás difundir mensaxes e falar coas túas amizades nun grande número de servidores. Mais este servidor, {domain}, é especial—hospeda o teu perfil, por iso lémbrate do seu nome.",
+  "introduction.welcome.text": "Benvida ó fediverso! Nun intre poderás difundir mensaxes e falar coas túas amizades nun grande número de servidores. Mais este servidor, {domain}, é especial—hospeda o teu perfil, por iso lémbra o seu nome.",
   "keyboard_shortcuts.back": "para voltar atrás",
   "keyboard_shortcuts.blocked": "abrir lista de usuarias bloqueadas",
   "keyboard_shortcuts.boost": "promover",
@@ -270,7 +270,7 @@
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.filters": "Palabras silenciadas",
   "navigation_bar.follow_requests": "Peticións de seguimento",
-  "navigation_bar.follows_and_followers": "Seguindo e seguidores",
+  "navigation_bar.follows_and_followers": "Seguindo e seguidoras",
   "navigation_bar.info": "Sobre este servidor",
   "navigation_bar.keyboard_shortcuts": "Atallos do teclado",
   "navigation_bar.lists": "Listaxes",
@@ -295,7 +295,7 @@
   "notifications.column_settings.filter_bar.advanced": "Amosar todas as categorías",
   "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
   "notifications.column_settings.filter_bar.show": "Amosar",
-  "notifications.column_settings.follow": "Novos seguidores:",
+  "notifications.column_settings.follow": "Novas seguidoras:",
   "notifications.column_settings.follow_request": "Novas peticións de seguimento:",
   "notifications.column_settings.mention": "Mencións:",
   "notifications.column_settings.poll": "Resultados da enquisa:",
@@ -312,7 +312,7 @@
   "notifications.group": "{count} notificacións",
   "poll.closed": "Pechado",
   "poll.refresh": "Actualizar",
-  "poll.total_people": "{count, plural,one {# persoa}other {# persoas}}",
+  "poll.total_people": "{count, plural,one {# persoa} other {# persoas}}",
   "poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
   "poll.vote": "Votar",
   "poll.voted": "Votaches por esta opción",
@@ -321,14 +321,14 @@
   "privacy.change": "Axustar privacidade",
   "privacy.direct.long": "Só para as usuarias mencionadas",
   "privacy.direct.short": "Directo",
-  "privacy.private.long": "Só para os seguidores",
-  "privacy.private.short": "Só seguidores",
+  "privacy.private.long": "Só para os seguidoras",
+  "privacy.private.short": "Só para seguidoras",
   "privacy.public.long": "Publicar nas cronoloxías públicas",
   "privacy.public.short": "Público",
   "privacy.unlisted.long": "Non publicar nas cronoloxías públicas",
   "privacy.unlisted.short": "Non listado",
   "refresh": "Actualizar",
-  "regeneration_indicator.label": "Estase a cargar…",
+  "regeneration_indicator.label": "Cargando…",
   "regeneration_indicator.sublabel": "Estase a preparar a túa cronoloxía de inicio!",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
@@ -365,7 +365,7 @@
   "status.delete": "Eliminar",
   "status.detailed_status": "Vista detallada da conversa",
   "status.direct": "Mensaxe directa a @{name}",
-  "status.embed": "Embeber nunha web",
+  "status.embed": "Incrustar",
   "status.favourite": "Favorito",
   "status.filtered": "Filtrado",
   "status.load_more": "Cargar máis",
@@ -411,7 +411,7 @@
   "time_remaining.seconds": "{number, plural, one {# segundo} other {# segundos}} restantes",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persoa} other {persoas}} falando",
   "trends.trending_now": "Tendencias actuais",
-  "ui.beforeunload": "O borrador perderase se saes do Mastodon.",
+  "ui.beforeunload": "O borrador perderase se saes de Mastodon.",
   "upload_area.title": "Arrastra e solta para subir",
   "upload_button.label": "Engadir multimedia ({formats})",
   "upload_error.limit": "Límite máximo do ficheiro a subir excedido.",
diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json
index d4659bc2e..ee9d701b0 100644
--- a/app/javascript/mastodon/locales/hi.json
+++ b/app/javascript/mastodon/locales/hi.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "लोड हो रहा है...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "नहीं मिला",
   "missing_indicator.sublabel": "यह संसाधन नहीं मिल सका।",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index b25369326..d2d851436 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -130,7 +130,7 @@
   "emoji_button.custom": "Egyéni",
   "emoji_button.flags": "Zászlók",
   "emoji_button.food": "Étel és Ital",
-  "emoji_button.label": "Emoji beszúrása",
+  "emoji_button.label": "Emodzsi beszúrása",
   "emoji_button.nature": "Természet",
   "emoji_button.not_found": "Nincsenek emodzsik!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Tárgyak",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 6a0e825b0..b3f1df94c 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -40,10 +40,10 @@
   "account.unmute": "Ապալռեցնել @{name}֊ին",
   "account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից",
   "alert.rate_limited.message": "Փորձէք  որոշ ժամանակ անց՝ {retry_time, time, medium}։",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.title": "Գործողությունների հաճախությունը գերազանցում է թույլատրելին",
   "alert.unexpected.message": "Անսպասելի սխալ տեղի ունեցաւ։",
   "alert.unexpected.title": "Վա՜յ",
-  "announcement.announcement": "Announcement",
+  "announcement.announcement": "Հայտարարություններ",
   "autosuggest_hashtag.per_week": "շաբաթը՝ {count}",
   "boost_modal.combo": "Կարող ես սեղմել {combo}՝ սա հաջորդ անգամ բաց թողնելու համար",
   "bundle_column_error.body": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանվեց։",
@@ -85,8 +85,8 @@
   "compose_form.poll.duration": "Հարցման տեւողութիւնը",
   "compose_form.poll.option_placeholder": "Տարբերակ {number}",
   "compose_form.poll.remove_option": "Հեռացնել այս տարբերակը",
-  "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
-  "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
+  "compose_form.poll.switch_to_multiple": "Հարցումը դարձնել բազմակի ընտրությամբ",
+  "compose_form.poll.switch_to_single": "Հարցումը դարձնել եզակի ընտրությամբ",
   "compose_form.publish": "Թթել",
   "compose_form.publish_loud": "Թթե՜լ",
   "compose_form.sensitive.hide": "Նշել մեդիան որպէս դիւրազգաց",
@@ -143,29 +143,29 @@
   "empty_column.account_timeline": "Այստեղ թթեր չկա՛ն։",
   "empty_column.account_unavailable": "Անձնական էջը հասանելի չի",
   "empty_column.blocks": "Դու դեռ ոչ մէկի չես արգելափակել։",
-  "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
+  "empty_column.bookmarked_statuses": "Դու դեռ չունես որեւէ էջանշւած թութ։ Երբ էջանշես, դրանք կերեւան այստեղ։",
   "empty_column.community": "Տեղական հոսքը դատա՛րկ է։ Հրապարակային մի բան գրիր շարժիչը խոդ տալու համար։",
   "empty_column.direct": "Դու դեռ չունես ոչ մի հասցէագրուած հաղորդագրութիւն։ Երբ ուղարկես կամ ստանաս որեւէ անձնական նամակ, այն այստեղ կերեւայ։",
   "empty_column.domain_blocks": "Թաքցուած տիրոյթներ դեռ չկան։",
   "empty_column.favourited_statuses": "Դու դեռ չունես որեւէ հաւանած թութ։ Երբ հաւանես, դրանք կերեւան այստեղ։",
   "empty_column.favourites": "Այս թութը ոչ մէկ դեռ չի հաւանել։ Հաւանողները կերեւան այստեղ, երբ նշեն թութը հաւանած։",
-  "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
+  "empty_column.follow_requests": "Դու դեռ չունես որեւէ հետևելու հայտ։ Բոլոր նման հայտերը կհայտնվեն այստեղ։",
   "empty_column.hashtag": "Այս պիտակով դեռ ոչինչ չկա։",
   "empty_column.home": "Քո հիմնական հոսքը դատա՛րկ է։ Այցելի՛ր {public}ը կամ օգտվիր որոնումից՝ այլ մարդկանց հանդիպելու համար։",
   "empty_column.home.public_timeline": "հրապարակային հոսք",
   "empty_column.list": "Այս ցանկում դեռ ոչինչ չկա։ Երբ ցանկի անդամներից որեւէ մեկը նոր թութ գրի, այն կհայտնվի այստեղ։",
-  "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
-  "empty_column.mutes": "Առայժմ ոչ ոքի չեք արգելափակել։",
+  "empty_column.lists": "Դուք դեռ չունեք ստեղծած ցանկ։ Ցանկ ստեղծելուն պես այն կհայտնվի այստեղ։",
+  "empty_column.mutes": "Առայժմ ոչ ոքի չեք լռեցրել։",
   "empty_column.notifications": "Ոչ մի ծանուցում դեռ չունես։ Բզիր մյուսներին՝ խոսակցությունը սկսելու համար։",
   "empty_column.public": "Այստեղ բան չկա՛։ Հրապարակային մի բան գրիր կամ հետեւիր այլ հանգույցներից էակների՝ այն լցնելու համար։",
-  "error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
-  "error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
-  "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
+  "error.unexpected_crash.explanation": "Մեր ծրագրակազմում վրիպակի կամ դիտարկչի անհամատեղելիության պատճառով այս էջը չի կարող լիարժեք պատկերվել։",
+  "error.unexpected_crash.next_steps": "Փորձիր թարմացնել էջը։ Եթե դա չօգնի ապա կարող ես օգտվել Մաստադոնից ուրիշ դիտարկիչով կամ հավելվածով։",
+  "errors.unexpected_crash.copy_stacktrace": "Պատճենել սթաքթրեյսը սեղմատախտակին",
   "errors.unexpected_crash.report_issue": "Զեկուցել խնդրի մասին",
   "follow_request.authorize": "Վավերացնել",
   "follow_request.reject": "Մերժել",
   "getting_started.developers": "Մշակողներ",
-  "getting_started.directory": "Պրոֆիլի տեղադրավայրը",
+  "getting_started.directory": "Օգտատերերի շտեմարան",
   "getting_started.documentation": "Փաստաթղթեր",
   "getting_started.heading": "Ինչպես սկսել",
   "getting_started.invite": "Հրավիրել մարդկանց",
@@ -175,8 +175,8 @@
   "hashtag.column_header.tag_mode.all": "և {additional}",
   "hashtag.column_header.tag_mode.any": "կամ {additional}",
   "hashtag.column_header.tag_mode.none": "առանց {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Առաջարկներ չկան",
+  "hashtag.column_settings.select.placeholder": "Ավելացրու հեշթեգեր…",
   "hashtag.column_settings.tag_mode.all": "Բոլորը",
   "hashtag.column_settings.tag_mode.any": "Ցանկացածը",
   "hashtag.column_settings.tag_mode.none": "Ոչ մեկը",
@@ -184,50 +184,50 @@
   "home.column_settings.basic": "Հիմնական",
   "home.column_settings.show_reblogs": "Ցուցադրել տարածածները",
   "home.column_settings.show_replies": "Ցուցադրել պատասխանները",
-  "home.hide_announcements": "Hide announcements",
-  "home.show_announcements": "Show announcements",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
-  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
+  "home.hide_announcements": "Թաքցնել հայտարարությունները",
+  "home.show_announcements": "Ցուցադրել հայտարարությունները",
+  "intervals.full.days": "{number, plural, one {# օր} other {# օր}}",
+  "intervals.full.hours": "{number, plural, one {# ժամ} other {# ժամ}}",
+  "intervals.full.minutes": "{number, plural, one {# րոպե} other {# րոպե}}",
   "introduction.federation.action": "Հաջորդ",
-  "introduction.federation.federated.headline": "Federated",
-  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
+  "introduction.federation.federated.headline": "Դաշնային",
+  "introduction.federation.federated.text": "Դաշտնեզերքի հարևան հանգույցների հանրային գրառումները կհայտնվեն դաշնային հոսքում։",
   "introduction.federation.home.headline": "Հիմնական",
-  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
+  "introduction.federation.home.text": "Այն անձանց թթերը ում հետևում ես, կհայտնվի հիմնական հոսքում։ Դու կարող ես հետևել ցանկացած անձի ցանկացած հանգույցից։",
   "introduction.federation.local.headline": "Տեղային",
-  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
+  "introduction.federation.local.text": "Տեղական հոսքում կարող ես տեսնել քո հանգույցի բոլոր հանրային գրառումները։",
   "introduction.interactions.action": "Finish toot-orial!",
   "introduction.interactions.favourite.headline": "Նախընտրելի",
-  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
+  "introduction.interactions.favourite.text": "Փոխանցիր հեղինակին որ քեզ դուր է եկել իր թութը հավանելով այն։",
   "introduction.interactions.reblog.headline": "Տարածել",
-  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
+  "introduction.interactions.reblog.text": "Կիսիր այլ օգտատերերի թութերը քո հետևորդների հետ տարածելով դրանք քո անձնական էջում։",
   "introduction.interactions.reply.headline": "Պատասխանել",
-  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
+  "introduction.interactions.reply.text": "Արձագանքիր ուրիշների և քո թթերին, դրանք կդարսվեն մեկ ընհանուր քննարկման շղթայով։",
   "introduction.welcome.action": "Գնացի՜նք։",
   "introduction.welcome.headline": "Առաջին քայլեր",
-  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
+  "introduction.welcome.text": "Դաշնեզերքը ողջունում է ձեզ։ Շուտով կկարողանաս ուղարկել նամակներ ու շփվել տարբեր հանգույցների ընկերներիդ հետ։ Բայց մտապահիր {domain} հանգույցը, այն յուրահատուկ է, այստեղ է պահվում քո հաշիվը։",
   "keyboard_shortcuts.back": "ետ նավարկելու համար",
   "keyboard_shortcuts.blocked": "արգելափակված օգտատերերի ցանկը բացելու համար",
   "keyboard_shortcuts.boost": "տարածելու համար",
   "keyboard_shortcuts.column": "սյուներից մեկի վրա սեւեռվելու համար",
   "keyboard_shortcuts.compose": "շարադրման տիրույթին սեւեռվելու համար",
   "keyboard_shortcuts.description": "Նկարագրություն",
-  "keyboard_shortcuts.direct": "to open direct messages column",
+  "keyboard_shortcuts.direct": "հասցեագրված գրվածքների հոսքը բացելու համար",
   "keyboard_shortcuts.down": "ցանկով ներքեւ շարժվելու համար",
   "keyboard_shortcuts.enter": "թութը բացելու համար",
   "keyboard_shortcuts.favourite": "հավանելու համար",
-  "keyboard_shortcuts.favourites": "to open favourites list",
-  "keyboard_shortcuts.federated": "to open federated timeline",
+  "keyboard_shortcuts.favourites": "էջանիշերի ցուցակը բացելու համար",
+  "keyboard_shortcuts.federated": "դաշնային հոսքին անցնելու համար",
   "keyboard_shortcuts.heading": "Ստեղնաշարի կարճատներ",
-  "keyboard_shortcuts.home": "to open home timeline",
+  "keyboard_shortcuts.home": "անձնական հոսքին անցնելու համար",
   "keyboard_shortcuts.hotkey": "Հատուկ ստեղն",
   "keyboard_shortcuts.legend": "այս ձեռնարկը ցուցադրելու համար",
-  "keyboard_shortcuts.local": "to open local timeline",
+  "keyboard_shortcuts.local": "տեղական հոսքին անցնելու համար",
   "keyboard_shortcuts.mention": "հեղինակին նշելու համար",
-  "keyboard_shortcuts.muted": "to open muted users list",
-  "keyboard_shortcuts.my_profile": "to open your profile",
-  "keyboard_shortcuts.notifications": "to open notifications column",
-  "keyboard_shortcuts.open_media": "to open media",
+  "keyboard_shortcuts.muted": "լռեցված օգտատերերի ցանկը բացելու համար",
+  "keyboard_shortcuts.my_profile": "սեփական էջին անցնելու համար",
+  "keyboard_shortcuts.notifications": "ծանուցումեների սյունակը բացելու համար",
+  "keyboard_shortcuts.open_media": "ցուցադրել մեդիան",
   "keyboard_shortcuts.pinned": "ամրացուած թթերի ցանկը բացելու համար",
   "keyboard_shortcuts.profile": "հեղինակի անձնական էջը բացելու համար",
   "keyboard_shortcuts.reply": "պատասխանելու համար",
@@ -252,7 +252,7 @@
   "lists.new.title_placeholder": "Նոր ցանկի վերնագիր",
   "lists.search": "Փնտրել քո հետեւած մարդկանց մեջ",
   "lists.subheading": "Քո ցանկերը",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# նոր նյութ} other {# նոր նյութ}}",
   "loading_indicator.label": "Բեռնվում է…",
   "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել",
   "missing_indicator.label": "Չգտնվեց",
@@ -283,7 +283,7 @@
   "navigation_bar.security": "Անվտանգություն",
   "notification.favourite": "{name} հավանեց թութդ",
   "notification.follow": "{name} սկսեց հետեւել քեզ",
-  "notification.follow_request": "{name} has requested to follow you",
+  "notification.follow_request": "{name} քեզ հետևելու հայց է ուղարկել",
   "notification.mention": "{name} նշեց քեզ",
   "notification.own_poll": "Հարցումդ աւարտուեց",
   "notification.poll": "Հարցումը, ուր դու քուէարկել ես, աւարտուեց։",
@@ -296,7 +296,7 @@
   "notifications.column_settings.filter_bar.category": "Արագ զտման վահանակ",
   "notifications.column_settings.filter_bar.show": "Ցուցադրել",
   "notifications.column_settings.follow": "Նոր հետեւողներ՝",
-  "notifications.column_settings.follow_request": "New follow requests:",
+  "notifications.column_settings.follow_request": "Նոր հետեւելու հայցեր:",
   "notifications.column_settings.mention": "Նշումներ՝",
   "notifications.column_settings.poll": "Հարցման արդիւնքները՝",
   "notifications.column_settings.push": "Հրելու ծանուցումներ",
@@ -312,8 +312,8 @@
   "notifications.group": "{count} ծանուցում",
   "poll.closed": "Փակ",
   "poll.refresh": "Թարմացնել",
-  "poll.total_people": "{count, plural, one {# person} other {# people}}",
-  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.total_people": "{count, plural, one {# հոգի} other {# հոգի}}",
+  "poll.total_votes": "{count, plural, one {# ձայն} other {# ձայն}}",
   "poll.vote": "Քուէարկել",
   "poll.voted": "Դու քուէարկել ես այս տարբերակի համար",
   "poll_button.add_poll": "Աւելացնել հարցում",
@@ -335,7 +335,7 @@
   "relative_time.just_now": "նոր",
   "relative_time.minutes": "{number}ր",
   "relative_time.seconds": "{number}վ",
-  "relative_time.today": "today",
+  "relative_time.today": "Այսօր",
   "reply_indicator.cancel": "Չեղարկել",
   "report.forward": "Փոխանցել {target}֊ին",
   "report.forward_hint": "Այս հաշիւ այլ հանգոյցից է։ Ուղարկե՞մ այնտեղ էլ այս բողոքի անոնիմ պատճէնը։",
@@ -361,10 +361,10 @@
   "status.bookmark": "Էջանիշ",
   "status.cancel_reblog_private": "Ապատարածել",
   "status.cannot_reblog": "Այս թութը չի կարող տարածվել",
-  "status.copy": "Copy link to status",
+  "status.copy": "Պատճենել գրառման հղումը",
   "status.delete": "Ջնջել",
-  "status.detailed_status": "Detailed conversation view",
-  "status.direct": "Direct message @{name}",
+  "status.detailed_status": "Շղթայի ընդլայնված դիտում",
+  "status.direct": "Նամակ գրել {name} -ին",
   "status.embed": "Ներդնել",
   "status.favourite": "Հավանել",
   "status.filtered": "Զտված",
@@ -376,57 +376,57 @@
   "status.mute_conversation": "Լռեցնել խոսակցությունը",
   "status.open": "Ընդարձակել այս թութը",
   "status.pin": "Ամրացնել անձնական էջում",
-  "status.pinned": "Pinned toot",
+  "status.pinned": "Ամրացված թութ",
   "status.read_more": "Կարդալ ավելին",
   "status.reblog": "Տարածել",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Տարածել սեփական լսարանին",
   "status.reblogged_by": "{name} տարածել է",
-  "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
-  "status.redraft": "Delete & re-draft",
-  "status.remove_bookmark": "Remove bookmark",
+  "status.reblogs.empty": "Այս թութը ոչ մէկ դեռ չի տարածել։ Տարածողները կերեւան այստեղ, երբ որևէ մեկը տարածի։",
+  "status.redraft": "Ջնջել եւ վերակազմել",
+  "status.remove_bookmark": "Հեռացնել էջանիշերից",
   "status.reply": "Պատասխանել",
   "status.replyAll": "Պատասխանել թելին",
   "status.report": "Բողոքել @{name}֊ից",
   "status.sensitive_warning": "Կասկածելի բովանդակություն",
   "status.share": "Կիսվել",
   "status.show_less": "Պակաս",
-  "status.show_less_all": "Show less for all",
+  "status.show_less_all": "Թաքցնել բոլոր նախազգուշացնումները",
   "status.show_more": "Ավելին",
-  "status.show_more_all": "Show more for all",
-  "status.show_thread": "Show thread",
+  "status.show_more_all": "Ցուցադրել բոլոր նախազգուշացնումները",
+  "status.show_thread": "Բացել շղթան",
   "status.uncached_media_warning": "Անհասանելի",
   "status.unmute_conversation": "Ապալռեցնել խոսակցությունը",
   "status.unpin": "Հանել անձնական էջից",
-  "suggestions.dismiss": "Dismiss suggestion",
-  "suggestions.header": "You might be interested in…",
+  "suggestions.dismiss": "Անտեսել առաջարկը",
+  "suggestions.header": "Միգուցե քեզ հետաքրքրի…",
   "tabs_bar.federated_timeline": "Դաշնային",
   "tabs_bar.home": "Հիմնական",
   "tabs_bar.local_timeline": "Տեղական",
   "tabs_bar.notifications": "Ծանուցումներ",
   "tabs_bar.search": "Փնտրել",
-  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
+  "time_remaining.days": "{number, plural, one {մնաց # օր} other {մնաց # օր}}",
   "time_remaining.hours": "{number, plural, one {# ժամ} other {# ժամ}} անց",
   "time_remaining.minutes": "{number, plural, one {# րոպե} other {# րոպե}} անց",
-  "time_remaining.moments": "Moments remaining",
+  "time_remaining.moments": "Մնացել է մի քանի վարկյան",
   "time_remaining.seconds": "{number, plural, one {# վայրկյան} other {# վայրկյան}} անց",
-  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.count_by_accounts": "{count} {rawCount, plural, one {հոգի} other {հոգի}} խոսում է սրա մասին",
   "trends.trending_now": "Այժմ արդիական",
   "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։",
   "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար",
   "upload_button.label": "Ավելացնել մեդիա",
   "upload_error.limit": "Ֆայլի վերբեռնման սահմանաչափը գերազանցված է։",
-  "upload_error.poll": "File upload not allowed with polls.",
-  "upload_form.audio_description": "Describe for people with hearing loss",
+  "upload_error.poll": "Հարցումների հետ ֆայլ կցել հնարավոր չէ։",
+  "upload_form.audio_description": "Նկարագրիր ձայնագրության բովանդակությունը լսողական խնդիրներով անձանց համար",
   "upload_form.description": "Նկարագրություն ավելացրու տեսողական խնդիրներ ունեցողների համար",
   "upload_form.edit": "Խմբագրել",
   "upload_form.undo": "Հետարկել",
-  "upload_form.video_description": "Describe for people with hearing loss or visual impairment",
+  "upload_form.video_description": "Նկարագրիր տեսանյութը լսողական կամ տեսողական խնդիրներով անձանց համար",
   "upload_modal.analyzing_picture": "Լուսանկարի վերլուծում…",
   "upload_modal.apply": "Կիրառել",
   "upload_modal.description_placeholder": "Ճկուն շագանակագույն աղվեսը ցատկում է ծույլ շան վրայով",
   "upload_modal.detect_text": "Հայտնբերել տեքստը նկարից",
   "upload_modal.edit_media": "Խմբագրել մեդիան",
-  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.hint": "Սեղմեք և տեղաշարժեք նախատեսքի վրայի շրջանակը ընտրելու այն կետը որը միշտ տեսանելի կլինի մանրապատկերներում։",
   "upload_modal.preview_label": "Նախադիտում ({ratio})",
   "upload_progress.label": "Վերբեռնվում է…",
   "video.close": "Փակել  տեսագրությունը",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 94f81116c..c1b47f860 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -5,7 +5,7 @@
   "account.block": "Blocca @{name}",
   "account.block_domain": "Nascondi tutto da {domain}",
   "account.blocked": "Bloccato",
-  "account.cancel_follow_request": "Annulla richiesta di seguito",
+  "account.cancel_follow_request": "Annulla richiesta di seguire",
   "account.direct": "Invia messaggio privato a @{name}",
   "account.domain_blocked": "Dominio nascosto",
   "account.edit_profile": "Modifica profilo",
@@ -36,7 +36,7 @@
   "account.unblock": "Sblocca @{name}",
   "account.unblock_domain": "Non nascondere {domain}",
   "account.unendorse": "Non mettere in evidenza sul profilo",
-  "account.unfollow": "Non seguire",
+  "account.unfollow": "Smetti di seguire",
   "account.unmute": "Non silenziare @{name}",
   "account.unmute_notifications": "Non silenziare più le notifiche da @{name}",
   "alert.rate_limited.message": "Riprova dopo {retry_time, time, medium}.",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index b1a9ad9ce..83c7db6c1 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -3,11 +3,11 @@
   "account.badges.bot": "Bot",
   "account.badges.group": "Group",
   "account.block": "@{name}さんをブロック",
-  "account.block_domain": "{domain}全体を非表示",
+  "account.block_domain": "{domain}全体をブロック",
   "account.blocked": "ブロック済み",
   "account.cancel_follow_request": "フォローリクエストを取り消す",
   "account.direct": "@{name}さんにダイレクトメッセージ",
-  "account.domain_blocked": "ドメイン非表示中",
+  "account.domain_blocked": "ドメインブロック中",
   "account.edit_profile": "プロフィール編集",
   "account.endorse": "プロフィールで紹介する",
   "account.follow": "フォロー",
@@ -34,7 +34,7 @@
   "account.share": "@{name}さんのプロフィールを共有する",
   "account.show_reblogs": "@{name}さんからのブーストを表示",
   "account.unblock": "@{name}さんのブロックを解除",
-  "account.unblock_domain": "{domain}の非表示を解除",
+  "account.unblock_domain": "{domain}のブロックを解除",
   "account.unendorse": "プロフィールから外す",
   "account.unfollow": "フォロー解除",
   "account.unmute": "@{name}さんのミュートを解除",
@@ -57,7 +57,7 @@
   "column.community": "ローカルタイムライン",
   "column.direct": "ダイレクトメッセージ",
   "column.directory": "ディレクトリ",
-  "column.domain_blocks": "非表示にしたドメイン",
+  "column.domain_blocks": "ブロックしたドメイン",
   "column.favourites": "お気に入り",
   "column.follow_requests": "フォローリクエスト",
   "column.home": "ホーム",
@@ -107,7 +107,7 @@
   "confirmations.delete.message": "本当に削除しますか?",
   "confirmations.delete_list.confirm": "削除",
   "confirmations.delete_list.message": "本当にこのリストを完全に削除しますか?",
-  "confirmations.domain_block.confirm": "ドメイン全体を非表示",
+  "confirmations.domain_block.confirm": "ドメイン全体をブロック",
   "confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。公開タイムラインにそのドメインのコンテンツが表示されなくなり、通知も届かなくなります。そのドメインのフォロワーはアンフォローされます。",
   "confirmations.logout.confirm": "ログアウト",
   "confirmations.logout.message": "本当にログアウトしますか?",
@@ -150,7 +150,7 @@
   "empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
   "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
   "empty_column.direct": "ダイレクトメッセージはまだありません。ダイレクトメッセージをやりとりすると、ここに表示されます。",
-  "empty_column.domain_blocks": "非表示にしているドメインはありません。",
+  "empty_column.domain_blocks": "ブロックしているドメインはありません。",
   "empty_column.favourited_statuses": "まだ何もお気に入り登録していません。お気に入り登録するとここに表示されます。",
   "empty_column.favourites": "まだ誰もお気に入り登録していません。お気に入り登録されるとここに表示されます。",
   "empty_column.follow_requests": "まだフォローリクエストを受けていません。フォローリクエストを受けるとここに表示されます。",
@@ -258,7 +258,7 @@
   "lists.subheading": "あなたのリスト",
   "load_pending": "{count} 件の新着",
   "loading_indicator.label": "読み込み中...",
-  "media_gallery.toggle_visible": "表示切り替え",
+  "media_gallery.toggle_visible": "メディアを隠す",
   "missing_indicator.label": "見つかりません",
   "missing_indicator.sublabel": "見つかりませんでした",
   "mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?",
@@ -269,7 +269,7 @@
   "navigation_bar.compose": "トゥートの新規作成",
   "navigation_bar.direct": "ダイレクトメッセージ",
   "navigation_bar.discover": "見つける",
-  "navigation_bar.domain_blocks": "非表示にしたドメイン",
+  "navigation_bar.domain_blocks": "ブロックしたドメイン",
   "navigation_bar.edit_profile": "プロフィールを編集",
   "navigation_bar.favourites": "お気に入り",
   "navigation_bar.filters": "フィルター設定",
@@ -324,13 +324,13 @@
   "poll_button.add_poll": "アンケートを追加",
   "poll_button.remove_poll": "アンケートを削除",
   "privacy.change": "公開範囲を変更",
-  "privacy.direct.long": "メンションしたユーザーだけに公開",
+  "privacy.direct.long": "送信した相手のみ閲覧可",
   "privacy.direct.short": "ダイレクト",
-  "privacy.private.long": "フォロワーだけに公開",
+  "privacy.private.long": "フォロワーのみ閲覧可",
   "privacy.private.short": "フォロワー限定",
-  "privacy.public.long": "公開TLに投稿する",
+  "privacy.public.long": "誰でも閲覧可、公開TLに表示",
   "privacy.public.short": "公開",
-  "privacy.unlisted.long": "公開TLで表示しない",
+  "privacy.unlisted.long": "誰でも閲覧可、公開TLに非表示",
   "privacy.unlisted.short": "未収載",
   "refresh": "更新",
   "regeneration_indicator.label": "読み込み中…",
diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json
index e78b4cc4f..8ce4c2774 100644
--- a/app/javascript/mastodon/locales/kab.json
+++ b/app/javascript/mastodon/locales/kab.json
@@ -1,51 +1,51 @@
 {
-  "account.add_or_remove_from_list": "Rnu neγ kkes seg tebdarin",
+  "account.add_or_remove_from_list": "Rnu neɣ kkes seg tebdarin",
   "account.badges.bot": "Aṛubut",
   "account.badges.group": "Agraw",
   "account.block": "Seḥbes @{name}",
   "account.block_domain": "Ffer kra i d-yekkan seg {domain}",
   "account.blocked": "Yettuseḥbes",
-  "account.cancel_follow_request": "Sefsex asuter n weḍfaṛ",
+  "account.cancel_follow_request": "Sefsex asuter n uḍfaṛ",
   "account.direct": "Izen usrid i @{name}",
-  "account.domain_blocked": "Taγult yeffren",
-  "account.edit_profile": "Ẓreg amaγnu",
-  "account.endorse": "Welleh fell-as deg umaγnu-inek",
+  "account.domain_blocked": "Taɣult yeffren",
+  "account.edit_profile": "Ẓreg amaɣnu",
+  "account.endorse": "Welleh fell-as deg umaɣnu-inek",
   "account.follow": "Ḍfeṛ",
   "account.followers": "Imeḍfaṛen",
   "account.followers.empty": "Ar tura, ulac yiwen i yeṭṭafaṛen amseqdac-agi.",
-  "account.follows": "Ig ṭafaṛ",
+  "account.follows": "I yeṭṭafaṛ",
   "account.follows.empty": "Ar tura, amseqdac-agi ur yeṭṭafaṛ yiwen.",
   "account.follows_you": "Yeṭṭafaṛ-ik",
   "account.hide_reblogs": "Ffer ayen i ibeṭṭu @{name}",
   "account.last_status": "Armud aneggaru",
   "account.link_verified_on": "Taγara n useγwen-a tettwasenqed ass n {date}",
   "account.locked_info": "Amiḍan-agi uslig isekweṛ. D bab-is kan i izemren ad yeǧǧ, s ufus-is, win ara t-iḍefṛen.",
-  "account.media": "Allal n teywalt",
+  "account.media": "Amidya",
   "account.mention": "Bder-d @{name}",
-  "account.moved_to": "{name} ibeddel γer:",
+  "account.moved_to": "{name} ibeddel ɣer:",
   "account.mute": "Sgugem @{name}",
-  "account.mute_notifications": "Susem ilγa sγur @{name}",
+  "account.mute_notifications": "Sgugem tilɣa sγur @{name}",
   "account.muted": "Yettwasgugem",
   "account.never_active": "Werǧin",
   "account.posts": "Tijewwaqin",
   "account.posts_with_replies": "Tijewwaqin akked tririyin",
-  "account.report": "Sewɛed @{name}",
-  "account.requested": "Di laɛḍil ad yettwaqbel. Ssit iwakken ad yefsex usuter n weḍfar",
-  "account.share": "Bḍu amaγnu n @{name}",
+  "account.report": "Cetki ɣef @{name}",
+  "account.requested": "Di laɛḍil ad yettwaqbel. Ssit i wakken ad yefsex usuter n uḍfar",
+  "account.share": "Bḍu amaɣnu n @{name}",
   "account.show_reblogs": "Sken-d inebḍa n @{name}",
   "account.unblock": "Serreḥ i @{name}",
-  "account.unblock_domain": "Kkes tuffra i {domain}",
-  "account.unendorse": "Ur ttwellih ara fell-as deg umaγnu-inek",
+  "account.unblock_domain": "Sken-d {domain}",
+  "account.unendorse": "Ur ttwellih ara fell-as deg umaɣnu-inek",
   "account.unfollow": "Ur ṭṭafaṛ ara",
-  "account.unmute": "Kkes asgugem γef @{name}",
-  "account.unmute_notifications": "Serreḥ ilγa sγur @{name}",
-  "alert.rate_limited.message": "Ma ulac aγilif ɛreḍ tikelt-nniḍen mbeɛd {retry_time, time, medium}.",
+  "account.unmute": "Kkes asgugem ɣef @{name}",
+  "account.unmute_notifications": "Serreḥ ilɣa sɣur @{name}",
+  "alert.rate_limited.message": "Ma ulac aɣilif ɛreḍ tikelt-nniḍen akka {retry_time, time, medium}.",
   "alert.rate_limited.title": "Aktum s talast",
-  "alert.unexpected.message": "Tella-d tuccḍa i γef ur nedmi ara.",
+  "alert.unexpected.message": "Yeḍra-d unezri ur netturaǧu ara.",
   "alert.unexpected.title": "Ayhuh!",
-  "announcement.announcement": "Ulγu",
+  "announcement.announcement": "Ulɣu",
   "autosuggest_hashtag.per_week": "{count} i yimalas",
-  "boost_modal.combo": "Tzemreḍ ad tetekkiḍ γef {combo} akken ad tessurfeḍ aya tikelt-nniḍen",
+  "boost_modal.combo": "Tzemreḍ ad tetekkiḍ ɣef {combo} akken ad tessurfeḍ aya tikelt-nniḍen",
   "bundle_column_error.body": "Tella-d kra n tuccḍa mi d-yettali ugbur-agi.",
   "bundle_column_error.retry": "Ɛreḍ tikelt-nniḍen",
   "bundle_column_error.title": "Tuccḍa deg uẓeṭṭa",
@@ -56,22 +56,22 @@
   "column.bookmarks": "Ticraḍ",
   "column.community": "Tasuddemt tadigant",
   "column.direct": "Iznan usriden",
-  "column.directory": "Qelleb deg imaγnuten",
-  "column.domain_blocks": "Tiγula yettwaffren",
+  "column.directory": "Inig deg imaɣnuten",
+  "column.domain_blocks": "Taɣulin yeffren",
   "column.favourites": "Ismenyifen",
   "column.follow_requests": "Isuturen n teḍfeṛt",
   "column.home": "Agejdan",
   "column.lists": "Tibdarin",
   "column.mutes": "Imiḍanen yettwasgugmen",
-  "column.notifications": "Tilγa",
+  "column.notifications": "Tilɣa",
   "column.pins": "Tijewwaqin yettwasenṭḍen",
   "column.public": "Tasuddemt tamatut",
-  "column_back_button.label": "Tuγalin",
-  "column_header.hide_settings": "Ffer iγewwaṛen",
-  "column_header.moveLeft_settings": "Err ajgu γer tama tazelmaḍt",
-  "column_header.moveRight_settings": "Err ajgu γer tama tayfust",
+  "column_back_button.label": "Tuɣalin",
+  "column_header.hide_settings": "Ffer iɣewwaṛen",
+  "column_header.moveLeft_settings": "Err ajgu ɣer tama tazelmaḍt",
+  "column_header.moveRight_settings": "Err ajgu ɣer tama tayfust",
   "column_header.pin": "Senteḍ",
-  "column_header.show_settings": "Sken iγewwaṛen",
+  "column_header.show_settings": "Sken iɣewwaṛen",
   "column_header.unpin": "Kkes asenteḍ",
   "column_subheading.settings": "Iγewwaṛen",
   "community.column_settings.media_only": "Allal n teywalt kan",
@@ -102,7 +102,7 @@
   "confirmations.delete.confirm": "Kkes",
   "confirmations.delete.message": "Tebγiḍ s tidet ad tekkseḍ tasuffeγt-agi?",
   "confirmations.delete_list.confirm": "Kkes",
-  "confirmations.delete_list.message": "Tebγiḍ s tidet ad tekkseḍ tabdert-agi i lebda?",
+  "confirmations.delete_list.message": "Tebγiḍ s tidet ad tekkseḍ umuγ-agi i lebda?",
   "confirmations.domain_block.confirm": "Ffer taγult meṛṛa",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
   "confirmations.logout.confirm": "Ffeγ",
@@ -137,7 +137,7 @@
   "emoji_button.people": "Medden",
   "emoji_button.recent": "Wid yettuseqdacen s waṭas",
   "emoji_button.search": "Nadi…",
-  "emoji_button.search_results": "Igmaḍ u unadi",
+  "emoji_button.search_results": "Igemmaḍ n unadi",
   "emoji_button.symbols": "Izamulen",
   "emoji_button.travel": "Imeḍqan d Yinigen",
   "empty_column.account_timeline": "Ulac tijewwaqin dagi!",
@@ -153,8 +153,8 @@
   "empty_column.hashtag": "Ar tura ulac kra n ugbur yesɛan assaγ γer uhacṭag-agi.",
   "empty_column.home": "Tasuddemt tagejdant n yisallen d tilemt! Ẓer {public} neγ nadi ad tafeḍ imseqdacen-nniḍen ad ten-ḍefṛeḍ.",
   "empty_column.home.public_timeline": "tasuddemt tazayezt n yisallen",
-  "empty_column.list": "Ar tura ur yelli kra deg tebdert-a. Ad d-yettwasken da ticki iɛeggalen n tebdert-a suffγen-d kra.",
-  "empty_column.lists": "Ulac γur-k kra n tebdert yakan. Ad d-tettwasken da ticki tesluleḍ-d yiwet.",
+  "empty_column.list": "Ar tura ur yelli kra deg umuγ-a. Ad d-yettwasken da ticki iɛeggalen n wumuγ-a suffγen-d kra.",
+  "empty_column.lists": "Ulac γur-k kra n wumuγ yakan. Ad d-tettwasken da ticki tesluleḍ-d yiwet.",
   "empty_column.mutes": "Ulac γur-k imseqdacen i yettwasgugmen.",
   "empty_column.notifications": "Ulac γur-k tilγa. Sedmer akked yemdanen-nniḍen akken ad tebduḍ adiwenni.",
   "empty_column.public": "Ulac kra da! Aru kra, neγ ḍfeṛ imdanen i yellan deg yiqeddacen-nniḍen akken ad d-teččar tsuddemt tazayezt",
@@ -202,58 +202,58 @@
   "introduction.interactions.reblog.headline": "Bḍu tikelt-nniḍen",
   "introduction.interactions.reblog.text": "Tzemreḍ ad tebḍuḍ tijewwaqin n medden akk d yimeḍfaṛen-ik s beṭṭu-nsent tikelt-nniḍen.",
   "introduction.interactions.reply.headline": "Err",
-  "introduction.interactions.reply.text": "Tzemreḍ ad terreḍ γef tjewwakin-ik d tid n medden-nniḍen, d acu ara tent-id-iɛeqden ta deffir ta deg yiwen udiwenni.",
+  "introduction.interactions.reply.text": "Tzemreḍ ad terreḍ γef tjewwaqin-ik·im akked tid n medden-nniḍen, aya atent-id-icudd ta deffir ta deg yiwen udiwenni.",
   "introduction.welcome.action": "Bdu!",
   "introduction.welcome.headline": "Isurifen imenza",
   "introduction.welcome.text": "Anṣuf γer fediverse! Deg kra n yimiren, ad tizmireḍ ad tzzuzreḍ iznan neγ ad tmeslayeḍ i yemddukkal deg waṭas n yiqeddacen. Maca aqeddac-agi, {domain}, mačči am wiyaḍ - deg-s i yella umaγnu-ik, ihi cfu γef yisem-is.",
   "keyboard_shortcuts.back": "uγal ar deffir",
-  "keyboard_shortcuts.blocked": "akken ad teldiḍ tabdert n yimseqdacen yettwasḥebsen",
+  "keyboard_shortcuts.blocked": "akken ad teldiḍ umuγ n yimseqdacen yettwasḥebsen",
   "keyboard_shortcuts.boost": "i beṭṭu tikelt-nniḍen",
   "keyboard_shortcuts.column": "to focus a status in one of the columns",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Aglam",
   "keyboard_shortcuts.direct": "akken ad teldiḍ ajgu n yiznan usriden",
-  "keyboard_shortcuts.down": "i kennu γer wadda n tebdert",
+  "keyboard_shortcuts.down": "i kennu γer wadda n wumuγ",
   "keyboard_shortcuts.enter": "i tildin n tsuffeγt",
   "keyboard_shortcuts.favourite": "akken ad ternuḍ γer yismenyifen",
-  "keyboard_shortcuts.favourites": "i tildin n tebdert n yismenyifen",
+  "keyboard_shortcuts.favourites": "i tildin umuγ n yismenyifen",
   "keyboard_shortcuts.federated": "i tildin n tsuddemt tamatut n yisallen",
   "keyboard_shortcuts.heading": "Inegzumen n unasiw",
   "keyboard_shortcuts.home": "i tildin n tsuddemt tagejdant n yisallen",
   "keyboard_shortcuts.hotkey": "Inegzumen",
-  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.legend": "akken ad tsekneḍ taneffust-agi",
   "keyboard_shortcuts.local": "i tildin n tsuddemt tadigant n yisallen",
   "keyboard_shortcuts.mention": "akken ad d-bedreḍ ameskar",
-  "keyboard_shortcuts.muted": "akken ad teldiḍ tabdert n yimseqdacen yettwasgugmen",
+  "keyboard_shortcuts.muted": "akken ad teldiḍ umuγ n yimseqdacen yettwasgugmen",
   "keyboard_shortcuts.my_profile": "akken ad d-teldiḍ amaγnu-ik",
   "keyboard_shortcuts.notifications": "akken ad d-teldiḍ ajgu n tilγa",
-  "keyboard_shortcuts.open_media": "to open media",
-  "keyboard_shortcuts.pinned": "i tildin n tebdert n tjewwaqin yettwasentḍen",
+  "keyboard_shortcuts.open_media": "i taɣwalin yeldin ",
+  "keyboard_shortcuts.pinned": "akken ad teldiḍ umuγ n tjewwiqin yettwasentḍen",
   "keyboard_shortcuts.profile": "akken ad d-teldiḍ amaγnu n umeskar",
   "keyboard_shortcuts.reply": "i tririt",
-  "keyboard_shortcuts.requests": "akken ad d-teldiḍ tabdert n yisuturen n teḍfeṛt",
+  "keyboard_shortcuts.requests": "akken ad d-teldiḍ umuγ n yisuturen n teḍfeṛt",
   "keyboard_shortcuts.search": "to focus search",
   "keyboard_shortcuts.start": "akken ad d-teldiḍ ajgu n \"bdu\"",
   "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
   "keyboard_shortcuts.toggle_sensitivity": "i teskent/tuffra n yimidyaten",
   "keyboard_shortcuts.toot": "i wakken attebdud tajewwaqt tamaynut",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "i tulin γer ufella n tebdert",
+  "keyboard_shortcuts.up": "i tulin γer d asawen n wumuγ",
   "lightbox.close": "Mdel",
   "lightbox.next": "Γer zdat",
   "lightbox.previous": "Γer deffir",
   "lightbox.view_context": "Ẓer amnaḍ",
-  "lists.account.add": "Rnu γer tabdart",
-  "lists.account.remove": "Kkes seg tebdart",
-  "lists.delete": "Kkes tabdert",
-  "lists.edit": "Ẓreg tabdert",
+  "lists.account.add": "Rnu γer wumuγ",
+  "lists.account.remove": "Kkes seg umuγ",
+  "lists.delete": "Kkes umuγ",
+  "lists.edit": "Ẓreg umuγ",
   "lists.edit.submit": "Beddel azwel",
-  "lists.new.create": "Rnu tabdart",
-  "lists.new.title_placeholder": "Azwel n tebdert tamaynut",
+  "lists.new.create": "Rnu umuγ",
+  "lists.new.title_placeholder": "Azwel amaynut n wumuγ",
   "lists.search": "Nadi gar yemdanen i teṭṭafaṛeḍ",
-  "lists.subheading": "Tibdarin-ik·im",
+  "lists.subheading": "Umuγen-ik·im",
   "load_pending": "{count, plural, one {# n uferdis amaynut} other {# n yiferdisen imaynuten}}",
-  "loading_indicator.label": "Yessalay-ed…",
+  "loading_indicator.label": "Yessalay-d…",
   "media_gallery.toggle_visible": "Sken / Ffer",
   "missing_indicator.label": "Ulac-it",
   "missing_indicator.sublabel": "Ur nufi ara aγbalu-a",
@@ -271,9 +271,9 @@
   "navigation_bar.filters": "Awalen i yettwasgugmen",
   "navigation_bar.follow_requests": "Isuturen n teḍfeṛt",
   "navigation_bar.follows_and_followers": "Imeḍfaṛen akked wid i teṭṭafaṛeḍ",
-  "navigation_bar.info": "Γef uqeddac-a",
+  "navigation_bar.info": "Ɣef uqeddac-agi",
   "navigation_bar.keyboard_shortcuts": "Inegzumen n unasiw",
-  "navigation_bar.lists": "Tibdarin",
+  "navigation_bar.lists": "Umuγen",
   "navigation_bar.logout": "Ffeγ",
   "navigation_bar.mutes": "Iseqdacen yettwasusmen",
   "navigation_bar.personal": "Udmawan",
@@ -326,15 +326,15 @@
   "privacy.public.long": "Bḍu deg tsuddemt tazayezt",
   "privacy.public.short": "Azayez",
   "privacy.unlisted.long": "Ur beṭṭu ara deg tsuddemt tazayezt",
-  "privacy.unlisted.short": "War tabdert",
+  "privacy.unlisted.short": "War umuγ",
   "refresh": "Smiren",
-  "regeneration_indicator.label": "Yessalay-ed…",
+  "regeneration_indicator.label": "Yessalay-d…",
   "regeneration_indicator.sublabel": "Tasuddemt tagejdant ara d-tettwaheggay!",
   "relative_time.days": "{number}u",
-  "relative_time.hours": "{number}a",
+  "relative_time.hours": "{number}isr",
   "relative_time.just_now": "tura",
-  "relative_time.minutes": "{number}t",
-  "relative_time.seconds": "{number}t",
+  "relative_time.minutes": "{number}tis",
+  "relative_time.seconds": "{number}tas",
   "relative_time.today": "assa",
   "reply_indicator.cancel": "Sefsex",
   "report.forward": "Bren-it γeṛ {target}",
@@ -391,13 +391,13 @@
   "status.share": "Bḍu",
   "status.show_less": "Sken-d drus",
   "status.show_less_all": "Semẓi akk tisuffγin",
-  "status.show_more": "Sken-ed ugar",
+  "status.show_more": "Sken-d ugar",
   "status.show_more_all": "Ẓerr ugar lebda",
-  "status.show_thread": "Sken-ed lxiḍ",
+  "status.show_thread": "Sken-d lxiḍ",
   "status.uncached_media_warning": "Ulac-it",
   "status.unmute_conversation": "Kkes asgugem n udiwenni",
   "status.unpin": "Kkes asenteḍ seg umaγnu",
-  "suggestions.dismiss": "Dismiss suggestion",
+  "suggestions.dismiss": "Sefsex asumer",
   "suggestions.header": "Ahat ad tcelgeḍ deg…",
   "tabs_bar.federated_timeline": "Amatu",
   "tabs_bar.home": "Agejdan",
@@ -408,10 +408,10 @@
   "time_remaining.hours": "Mazal {number, plural, one {# n usrag} other {# n yesragen}}",
   "time_remaining.minutes": "Mazal {number, plural, one {# n tesdat} other {# n tesdatin}}",
   "time_remaining.moments": "Moments remaining",
-  "time_remaining.seconds": "Mazal {number, plural, one {# n tasint} other {# n tsinin}}",
+  "time_remaining.seconds": "Mazal {number, plural, one {# n tasint} other {# n tsinin}} id yugran",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {n umdan} other {n yemdanen}} i yettmeslayen",
   "trends.trending_now": "Trending now",
-  "ui.beforeunload": "Arewway-ik·im ad iruḥ ma yella tefeγ-ed deg Maṣṭudun.",
+  "ui.beforeunload": "Arewway-ik·im ad iruḥ ma yella tefeɣ-d deg Maṣṭudun.",
   "upload_area.title": "Zuḥeb rnu sers i tasalyt",
   "upload_button.label": "Rnu Taγwalt ({formats})",
   "upload_error.limit": "File upload limit exceeded.",
diff --git a/app/javascript/mastodon/locales/kn.json b/app/javascript/mastodon/locales/kn.json
index 5385de456..eafb7ede7 100644
--- a/app/javascript/mastodon/locales/kn.json
+++ b/app/javascript/mastodon/locales/kn.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 6d6bf2ea2..cdcce62ff 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -53,7 +53,7 @@
   "bundle_modal_error.message": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
   "bundle_modal_error.retry": "다시 시도",
   "column.blocks": "차단 중인 사용자",
-  "column.bookmarks": "갈무리",
+  "column.bookmarks": "보관함",
   "column.community": "로컬 타임라인",
   "column.direct": "다이렉트 메시지",
   "column.directory": "프로필 둘러보기",
@@ -143,7 +143,7 @@
   "empty_column.account_timeline": "여긴 툿이 없어요!",
   "empty_column.account_unavailable": "프로필 사용 불가",
   "empty_column.blocks": "아직 아무도 차단하지 않았습니다.",
-  "empty_column.bookmarked_statuses": "아직 갈무리한 툿이 없습니다. 툿을 갈무리하면 여기에 나타납니다.",
+  "empty_column.bookmarked_statuses": "아직 보관한 툿이 없습니다. 툿을 보관하면 여기에 나타납니다.",
   "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
   "empty_column.direct": "아직 다이렉트 메시지가 없습니다. 다이렉트 메시지를 보내거나 받은 경우, 여기에 표시 됩니다.",
   "empty_column.domain_blocks": "아직 숨겨진 도메인이 없습니다.",
@@ -260,7 +260,7 @@
   "mute_modal.hide_notifications": "이 사용자로부터의 알림을 숨기시겠습니까?",
   "navigation_bar.apps": "모바일 앱",
   "navigation_bar.blocks": "차단한 사용자",
-  "navigation_bar.bookmarks": "갈무리",
+  "navigation_bar.bookmarks": "보관함",
   "navigation_bar.community_timeline": "로컬 타임라인",
   "navigation_bar.compose": "새 툿 작성",
   "navigation_bar.direct": "다이렉트 메시지",
@@ -358,7 +358,7 @@
   "status.admin_account": "@{name}에 대한 중재 화면 열기",
   "status.admin_status": "중재 화면에서 이 게시물 열기",
   "status.block": "@{name} 차단",
-  "status.bookmark": "갈무리",
+  "status.bookmark": "보관",
   "status.cancel_reblog_private": "부스트 취소",
   "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
   "status.copy": "게시물 링크 복사",
@@ -383,7 +383,7 @@
   "status.reblogged_by": "{name}님이 부스트 했습니다",
   "status.reblogs.empty": "아직 아무도 이 툿을 부스트하지 않았습니다. 부스트 한 사람들이 여기에 표시 됩니다.",
   "status.redraft": "지우고 다시 쓰기",
-  "status.remove_bookmark": "갈무리 삭제",
+  "status.remove_bookmark": "보관한 툿 삭제",
   "status.reply": "답장",
   "status.replyAll": "전원에게 답장",
   "status.report": "신고",
@@ -398,7 +398,7 @@
   "status.unmute_conversation": "이 대화의 뮤트 해제하기",
   "status.unpin": "고정 해제",
   "suggestions.dismiss": "추천 지우기",
-  "suggestions.header": "이것에 관심이 있을 것 같습니다…",
+  "suggestions.header": "여기에 관심이 있을 것 같습니다…",
   "tabs_bar.federated_timeline": "연합",
   "tabs_bar.home": "홈",
   "tabs_bar.local_timeline": "로컬",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 5385de456..eafb7ede7 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 1b5b7cf5e..82ec3b8e8 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/mk.json b/app/javascript/mastodon/locales/mk.json
index 4a1f736cf..ee8b13ace 100644
--- a/app/javascript/mastodon/locales/mk.json
+++ b/app/javascript/mastodon/locales/mk.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json
index 58bb9b723..b3a06d6ed 100644
--- a/app/javascript/mastodon/locales/ml.json
+++ b/app/javascript/mastodon/locales/ml.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json
index 8c0fed9b0..be5d9a396 100644
--- a/app/javascript/mastodon/locales/mr.json
+++ b/app/javascript/mastodon/locales/mr.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index 0d23e0d2c..19f3ca257 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 5048a3a49..6589c2b1d 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -2,11 +2,11 @@
   "account.add_or_remove_from_list": "Toevoegen of verwijderen vanuit lijsten",
   "account.badges.bot": "Bot",
   "account.badges.group": "Groep",
-  "account.block": "Blokkeer @{name}",
-  "account.block_domain": "Verberg alles van {domain}",
+  "account.block": "@{name} blokkeren",
+  "account.block_domain": "Alles van {domain} verbergen",
   "account.blocked": "Geblokkeerd",
   "account.cancel_follow_request": "Volgverzoek annuleren",
-  "account.direct": "Direct bericht @{name}",
+  "account.direct": "@{name} een direct bericht sturen",
   "account.domain_blocked": "Domein verborgen",
   "account.edit_profile": "Profiel bewerken",
   "account.endorse": "Op profiel weergeven",
@@ -21,19 +21,19 @@
   "account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}",
   "account.locked_info": "De privacystatus van dit account is op besloten gezet. De eigenaar bepaalt handmatig wie hen kan volgen.",
   "account.media": "Media",
-  "account.mention": "Vermeld @{name}",
+  "account.mention": "@{name} vermelden",
   "account.moved_to": "{name} is verhuisd naar:",
-  "account.mute": "Negeer @{name}",
-  "account.mute_notifications": "Negeer meldingen van @{name}",
+  "account.mute": "@{name} negeren",
+  "account.mute_notifications": "Meldingen van @{name} negeren",
   "account.muted": "Genegeerd",
   "account.never_active": "Nooit",
   "account.posts": "Toots",
   "account.posts_with_replies": "Toots en reacties",
-  "account.report": "Rapporteer @{name}",
+  "account.report": "@{name} rapporteren",
   "account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren",
   "account.share": "Profiel van @{name} delen",
   "account.show_reblogs": "Toon boosts van @{name}",
-  "account.unblock": "Deblokkeer @{name}",
+  "account.unblock": "@{name} deblokkeren",
   "account.unblock_domain": "{domain} niet langer verbergen",
   "account.unendorse": "Niet op profiel weergeven",
   "account.unfollow": "Ontvolgen",
@@ -254,7 +254,7 @@
   "lists.subheading": "Jouw lijsten",
   "load_pending": "{count, plural, one {# nieuw item} other {# nieuwe items}}",
   "loading_indicator.label": "Laden…",
-  "media_gallery.toggle_visible": "Media wel/niet tonen",
+  "media_gallery.toggle_visible": "Media verbergen",
   "missing_indicator.label": "Niet gevonden",
   "missing_indicator.sublabel": "Deze hulpbron kan niet gevonden worden",
   "mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
@@ -323,9 +323,9 @@
   "privacy.direct.short": "Direct",
   "privacy.private.long": "Alleen aan volgers tonen",
   "privacy.private.short": "Alleen volgers",
-  "privacy.public.long": "Op openbare tijdlijnen tonen",
+  "privacy.public.long": "Voor iedereen zichtbaar en op openbare tijdlijnen tonen",
   "privacy.public.short": "Openbaar",
-  "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen",
+  "privacy.unlisted.long": "Voor iedereen zichtbaar, maar niet op openbare tijdlijnen tonen",
   "privacy.unlisted.short": "Minder openbaar",
   "refresh": "Vernieuwen",
   "regeneration_indicator.label": "Aan het laden…",
@@ -357,22 +357,22 @@
   "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
   "status.admin_account": "Moderatie-omgeving van @{name} openen",
   "status.admin_status": "Deze toot in de moderatie-omgeving openen",
-  "status.block": "Blokkeer @{name}",
+  "status.block": "@{name} blokkeren",
   "status.bookmark": "Bladwijzer toevoegen",
   "status.cancel_reblog_private": "Niet langer boosten",
   "status.cannot_reblog": "Deze toot kan niet geboost worden",
   "status.copy": "Link naar toot kopiëren",
   "status.delete": "Verwijderen",
   "status.detailed_status": "Uitgebreide gespreksweergave",
-  "status.direct": "Direct bericht @{name}",
+  "status.direct": "@{name} een direct bericht sturen",
   "status.embed": "Insluiten",
   "status.favourite": "Favoriet",
   "status.filtered": "Gefilterd",
   "status.load_more": "Meer laden",
   "status.media_hidden": "Media verborgen",
-  "status.mention": "Vermeld @{name}",
+  "status.mention": "@{name} vermelden",
   "status.more": "Meer",
-  "status.mute": "Negeer @{name}",
+  "status.mute": "@{name} negeren",
   "status.mute_conversation": "Negeer gesprek",
   "status.open": "Uitgebreide toot tonen",
   "status.pin": "Aan profielpagina vastmaken",
@@ -386,7 +386,7 @@
   "status.remove_bookmark": "Bladwijzer verwijderen",
   "status.reply": "Reageren",
   "status.replyAll": "Reageer op iedereen",
-  "status.report": "Rapporteer @{name}",
+  "status.report": "@{name} rapporteren",
   "status.sensitive_warning": "Gevoelige inhoud",
   "status.share": "Delen",
   "status.show_less": "Minder tonen",
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index 9dd48767d..39fe5158e 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -43,7 +43,7 @@
   "alert.rate_limited.title": "Begrensa rate",
   "alert.unexpected.message": "Eit uventa problem oppstod.",
   "alert.unexpected.title": "Oi sann!",
-  "announcement.announcement": "Kunngjøring",
+  "announcement.announcement": "Kunngjering",
   "autosuggest_hashtag.per_week": "{count} per veke",
   "boost_modal.combo": "Du kan trykkja {combo} for å hoppa over dette neste gong",
   "bundle_column_error.body": "Noko gjekk gale mens denne komponenten vart lasta ned.",
@@ -85,8 +85,8 @@
   "compose_form.poll.duration": "Varigskap for røysting",
   "compose_form.poll.option_placeholder": "Val {number}",
   "compose_form.poll.remove_option": "Ta vekk dette valet",
-  "compose_form.poll.switch_to_multiple": "Endre avstemning til å tillate flere valg",
-  "compose_form.poll.switch_to_single": "Endre avstemning til å tillate ett valg",
+  "compose_form.poll.switch_to_multiple": "Endre avstemninga til å tillate fleirval",
+  "compose_form.poll.switch_to_single": "Endra avstemninga til tillate berre eitt val",
   "compose_form.publish": "Tut",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.hide": "Merk medium som sensitivt",
@@ -184,8 +184,8 @@
   "home.column_settings.basic": "Enkelt",
   "home.column_settings.show_reblogs": "Vis framhevingar",
   "home.column_settings.show_replies": "Vis svar",
-  "home.hide_announcements": "Skjul kunngjøring",
-  "home.show_announcements": "Vis kunngjøring",
+  "home.hide_announcements": "Skjul kunngjeringar",
+  "home.show_announcements": "Vis kunngjeringar",
   "intervals.full.days": "{number, plural, one {# dag} other {# dagar}}",
   "intervals.full.hours": "{number, plural, one {# time} other {# timar}}",
   "intervals.full.minutes": "{number, plural, one {# minutt} other {# minutt}}",
@@ -237,7 +237,7 @@
   "keyboard_shortcuts.toggle_hidden": "for å visa/gøyma tekst bak innhaldsvarsel",
   "keyboard_shortcuts.toggle_sensitivity": "for å visa/gøyma media",
   "keyboard_shortcuts.toot": "for å laga ein heilt ny tut",
-  "keyboard_shortcuts.unfocus": "å ufokusere komponerings-/søkefeltet",
+  "keyboard_shortcuts.unfocus": "for å fokusere vekk skrive-/søkefeltet",
   "keyboard_shortcuts.up": "for å flytta seg opp på lista",
   "lightbox.close": "Lukk att",
   "lightbox.next": "Neste",
@@ -416,11 +416,11 @@
   "upload_button.label": "Legg til medium ({formats})",
   "upload_error.limit": "Du har gått over opplastingsgrensa.",
   "upload_error.poll": "Filopplasting ikkje tillate med meiningsmålingar.",
-  "upload_form.audio_description": "Beskriv det for folk med hørselstap",
+  "upload_form.audio_description": "Grei ut for folk med nedsett høyrsel",
   "upload_form.description": "Skildr for synshemja",
   "upload_form.edit": "Rediger",
   "upload_form.undo": "Slett",
-  "upload_form.video_description": "Beskriv det for folk med hørselstap eller synshemminger",
+  "upload_form.video_description": "Greit ut for folk med nedsett høyrsel eller syn",
   "upload_modal.analyzing_picture": "Analyserer bilete…",
   "upload_modal.apply": "Bruk",
   "upload_modal.description_placeholder": "Ein rask brun rev hoppar over den late hunden",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 024bbb9df..a019f39ee 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -65,7 +65,7 @@
   "column.mutes": "Usuários silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Toots fixados",
-  "column.public": "Linha global",
+  "column.public": "Global",
   "column_back_button.label": "Voltar",
   "column_header.hide_settings": "Ocultar configurações",
   "column_header.moveLeft_settings": "Mover coluna para a esquerda",
@@ -92,7 +92,7 @@
   "compose_form.sensitive.hide": "Marcar mídia como sensível",
   "compose_form.sensitive.marked": "Mídia está marcada como sensível",
   "compose_form.sensitive.unmarked": "Mídia não está marcada como sensível",
-  "compose_form.spoiler.marked": "O texto está oculto por um aviso de conteúdo",
+  "compose_form.spoiler.marked": "Com Aviso de Conteúdo",
   "compose_form.spoiler.unmarked": "Sem Aviso de Conteúdo",
   "compose_form.spoiler_placeholder": "Aviso de Conteúdo aqui",
   "confirmation_modal.cancel": "Cancelar",
@@ -100,7 +100,7 @@
   "confirmations.block.confirm": "Bloquear",
   "confirmations.block.message": "Você tem certeza de que deseja bloquear {name}?",
   "confirmations.delete.confirm": "Excluir",
-  "confirmations.delete.message": "Excluir este toot?",
+  "confirmations.delete.message": "Você tem certeza de que deseja excluir este toot?",
   "confirmations.delete_list.confirm": "Excluir",
   "confirmations.delete_list.message": "Você tem certeza de que deseja excluir esta lista?",
   "confirmations.domain_block.confirm": "Bloquear domínio",
@@ -113,9 +113,9 @@
   "confirmations.redraft.confirm": "Excluir e rascunhar",
   "confirmations.redraft.message": "Você tem certeza de que deseja apagar o toot e usá-lo como rascunho? Boosts e favoritos serão perdidos e as respostas ao toot original ficarão desconectadas.",
   "confirmations.reply.confirm": "Responder",
-  "confirmations.reply.message": "Responder agora vai sobrescrever o toot que você está compondo. Deseja continuar?",
+  "confirmations.reply.message": "Responder agora sobrescreverá o toot que você está compondo. Deseja continuar?",
   "confirmations.unfollow.confirm": "Deixar de seguir",
-  "confirmations.unfollow.message": "Deixar de seguir {name}?",
+  "confirmations.unfollow.message": "Você tem certeza de que deseja deixar de seguir {name}?",
   "conversation.delete": "Excluir conversa",
   "conversation.mark_as_read": "Marcar como lida",
   "conversation.open": "Ver conversa",
@@ -143,7 +143,7 @@
   "empty_column.account_timeline": "Nada aqui!",
   "empty_column.account_unavailable": "Perfil indisponível",
   "empty_column.blocks": "Nada aqui.",
-  "empty_column.bookmarked_statuses": "Sem toots salvos. Quando você salvar alguns, eles aparecerão aqui.",
+  "empty_column.bookmarked_statuses": "Nada aqui. Quando você salvar um toot, ele aparecerá aqui.",
   "empty_column.community": "A linha do tempo local está vazia. Poste algo publicamente para começar!",
   "empty_column.direct": "Nada aqui. Quando você enviar ou receber toots diretos, eles aparecerão aqui.",
   "empty_column.domain_blocks": "Nada aqui.",
@@ -185,7 +185,7 @@
   "home.column_settings.show_reblogs": "Mostrar boosts",
   "home.column_settings.show_replies": "Mostrar respostas",
   "home.hide_announcements": "Ocultar anúncios",
-  "home.show_announcements": "Exibir anúncios",
+  "home.show_announcements": "Mostrar anúncios",
   "intervals.full.days": "{number, plural, one {# dia} other {# dias}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
@@ -198,7 +198,7 @@
   "introduction.federation.local.text": "Toots públicos de pessoas na mesma instância que você aparecerão na linha local.",
   "introduction.interactions.action": "Terminar o tutorial!",
   "introduction.interactions.favourite.headline": "Favoritos",
-  "introduction.interactions.favourite.text": "Ao favoritar, você salva o toot para mais tarde ou sinaliza ao autor que você gostou do toot.",
+  "introduction.interactions.favourite.text": "Ao favoritar, você sinaliza ao autor que você gostou do toot.",
   "introduction.interactions.reblog.headline": "Boost",
   "introduction.interactions.reblog.text": "Ao dar boost, você compartilha toots de outras pessoas para seus seguidores.",
   "introduction.interactions.reply.headline": "Responder",
@@ -289,14 +289,14 @@
   "notification.poll": "Uma enquete que você votou terminou",
   "notification.reblog": "{name} deu boost no seu toot",
   "notifications.clear": "Limpar notificações",
-  "notifications.clear_confirmation": "Você tem certeza de que quer limpar todas as suas notificações?",
+  "notifications.clear_confirmation": "Você tem certeza de que deseja limpar todas as suas notificações?",
   "notifications.column_settings.alert": "Notificações no computador",
   "notifications.column_settings.favourite": "Favoritos:",
   "notifications.column_settings.filter_bar.advanced": "Mostrar todas as categorias",
   "notifications.column_settings.filter_bar.category": "Barra de filtro rápido",
   "notifications.column_settings.filter_bar.show": "Mostrar",
   "notifications.column_settings.follow": "Seguidores:",
-  "notifications.column_settings.follow_request": "Novos seguidores pendentes:",
+  "notifications.column_settings.follow_request": "Seguidores pendentes:",
   "notifications.column_settings.mention": "Menções:",
   "notifications.column_settings.poll": "Enquetes:",
   "notifications.column_settings.push": "Enviar notificações",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 34acbfc9a..b8902d65e 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -17,9 +17,9 @@
   "account.follows.empty": "Este utilizador ainda não segue alguém.",
   "account.follows_you": "É teu seguidor",
   "account.hide_reblogs": "Esconder partilhas de @{name}",
-  "account.last_status": "Última actividade",
+  "account.last_status": "Última atividade",
   "account.link_verified_on": "A posse deste link foi verificada em {date}",
-  "account.locked_info": "O estatuto de privacidade desta conta é fechado. O dono revê manualmente que a pode seguir.",
+  "account.locked_info": "O estatuto de privacidade desta conta é fechado. O dono revê manualmente quem a pode seguir.",
   "account.media": "Média",
   "account.mention": "Mencionar @{name}",
   "account.moved_to": "{name} mudou a sua conta para:",
@@ -53,7 +53,7 @@
   "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.",
   "bundle_modal_error.retry": "Tente de novo",
   "column.blocks": "Utilizadores Bloqueados",
-  "column.bookmarks": "Favoritos",
+  "column.bookmarks": "Marcadores",
   "column.community": "Cronologia local",
   "column.direct": "Mensagens directas",
   "column.directory": "Procurar perfis",
@@ -74,7 +74,7 @@
   "column_header.show_settings": "Mostrar configurações",
   "column_header.unpin": "Desafixar",
   "column_subheading.settings": "Configurações",
-  "community.column_settings.media_only": "Somente multimédia",
+  "community.column_settings.media_only": "Somente média",
   "compose_form.direct_message_warning": "Esta publicação será enviada apenas para os utilizadores mencionados.",
   "compose_form.direct_message_warning_learn_more": "Conhecer mais",
   "compose_form.hashtag_warning": "Este toot não será listado em nenhuma hashtag por ser não listado. Apenas toots públics podem ser pesquisados por hashtag.",
@@ -89,7 +89,7 @@
   "compose_form.poll.switch_to_single": "Alterar a votação para permitir uma única escolha",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.hide": "Marcar multimédia como sensível",
+  "compose_form.sensitive.hide": "Marcar média como sensível",
   "compose_form.sensitive.marked": "Média marcada como sensível",
   "compose_form.sensitive.unmarked": "Média não está marcada como sensível",
   "compose_form.spoiler.marked": "Texto escondido atrás de aviso",
@@ -124,7 +124,7 @@
   "directory.local": "Apenas de {domain}",
   "directory.new_arrivals": "Recém chegados",
   "directory.recently_active": "Com actividade recente",
-  "embed.instructions": "Publica esta publicação no teu site copiando o código abaixo.",
+  "embed.instructions": "Incorpora esta publicação no teu site copiando o código abaixo.",
   "embed.preview": "Podes ver aqui como irá ficar:",
   "emoji_button.activity": "Actividade",
   "emoji_button.custom": "Personalizar",
@@ -143,7 +143,7 @@
   "empty_column.account_timeline": "Sem toots por aqui!",
   "empty_column.account_unavailable": "Perfil indisponível",
   "empty_column.blocks": "Ainda não bloqueaste qualquer utilizador.",
-  "empty_column.bookmarked_statuses": "Ainda não assinalou toots como favoritos. Quando o fizer, eles aparecerão aqui.",
+  "empty_column.bookmarked_statuses": "Ainda não adicionou nenhum toot aos Itens salvos. Quando adicionar, eles serão exibidos aqui.",
   "empty_column.community": "A timeline local está vazia. Escreve algo publicamente para começar!",
   "empty_column.direct": "Ainda não tens qualquer mensagem directa. Quando enviares ou receberes alguma, ela irá aparecer aqui.",
   "empty_column.domain_blocks": "Ainda não há qualquer domínio escondido.",
@@ -214,7 +214,7 @@
   "keyboard_shortcuts.description": "Descrição",
   "keyboard_shortcuts.direct": "para abrir a coluna das mensagens directas",
   "keyboard_shortcuts.down": "para mover para baixo na lista",
-  "keyboard_shortcuts.enter": "para expandir um estado",
+  "keyboard_shortcuts.enter": "para expandir uma publicação",
   "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
   "keyboard_shortcuts.favourites": "para abrir a lista dos favoritos",
   "keyboard_shortcuts.federated": "para abrir a cronologia federada",
@@ -254,13 +254,13 @@
   "lists.subheading": "As tuas listas",
   "load_pending": "{count, plural, one {# novo item} other {# novos itens}}",
   "loading_indicator.label": "A carregar...",
-  "media_gallery.toggle_visible": "Mostrar/ocultar",
+  "media_gallery.toggle_visible": "Alternar visibilidade",
   "missing_indicator.label": "Não encontrado",
   "missing_indicator.sublabel": "Este recurso não foi encontrado",
   "mute_modal.hide_notifications": "Esconder notificações deste utilizador?",
   "navigation_bar.apps": "Aplicações móveis",
   "navigation_bar.blocks": "Utilizadores bloqueados",
-  "navigation_bar.bookmarks": "Favoritos",
+  "navigation_bar.bookmarks": "Marcadores",
   "navigation_bar.community_timeline": "Cronologia local",
   "navigation_bar.compose": "Escrever novo toot",
   "navigation_bar.direct": "Mensagens directas",
@@ -281,13 +281,13 @@
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.public_timeline": "Cronologia federada",
   "navigation_bar.security": "Segurança",
-  "notification.favourite": "{name} adicionou o teu estado aos favoritos",
+  "notification.favourite": "{name} adicionou a tua publicação aos favoritos",
   "notification.follow": "{name} começou a seguir-te",
   "notification.follow_request": "{name} pediu para segui-lo",
   "notification.mention": "{name} mencionou-te",
   "notification.own_poll": "A sua votação terminou",
   "notification.poll": "Uma votação em participaste chegou ao fim",
-  "notification.reblog": "{name} fez boost ao teu o teu estado",
+  "notification.reblog": "{name} partilhou a tua publicação",
   "notifications.clear": "Limpar notificações",
   "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
   "notifications.column_settings.alert": "Notificações no computador",
@@ -318,7 +318,7 @@
   "poll.voted": "Você votou nesta resposta",
   "poll_button.add_poll": "Adicionar votação",
   "poll_button.remove_poll": "Remover votação",
-  "privacy.change": "Ajustar a privacidade da mensagem",
+  "privacy.change": "Ajustar a privacidade da publicação",
   "privacy.direct.long": "Apenas para utilizadores mencionados",
   "privacy.direct.short": "Directo",
   "privacy.private.long": "Apenas para os seguidores",
@@ -347,7 +347,7 @@
   "search_popout.search_format": "Formato avançado de pesquisa",
   "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, marcaste como favorita, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.",
   "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "estado",
+  "search_popout.tips.status": "publicação",
   "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags",
   "search_popout.tips.user": "utilizador",
   "search_results.accounts": "Pessoas",
@@ -358,7 +358,7 @@
   "status.admin_account": "Abrir a interface de moderação para @{name}",
   "status.admin_status": "Abrir esta publicação na interface de moderação",
   "status.block": "Bloquear @{name}",
-  "status.bookmark": "Favorito",
+  "status.bookmark": "Salvar",
   "status.cancel_reblog_private": "Remover boost",
   "status.cannot_reblog": "Não é possível fazer boost a esta publicação",
   "status.copy": "Copiar o link para a publicação",
@@ -383,7 +383,7 @@
   "status.reblogged_by": "{name} fez boost",
   "status.reblogs.empty": "Ainda ninguém fez boost a este toot. Quando alguém o fizer, ele irá aparecer aqui.",
   "status.redraft": "Apagar & reescrever",
-  "status.remove_bookmark": "Remover favorito",
+  "status.remove_bookmark": "Remover dos itens salvos",
   "status.reply": "Responder",
   "status.replyAll": "Responder à conversa",
   "status.report": "Denunciar @{name}",
@@ -394,7 +394,7 @@
   "status.show_more": "Mostrar mais",
   "status.show_more_all": "Mostrar mais para todas",
   "status.show_thread": "Mostrar conversa",
-  "status.uncached_media_warning": "Não diponível",
+  "status.uncached_media_warning": "Não disponível",
   "status.unmute_conversation": "Deixar de silenciar esta conversa",
   "status.unpin": "Não fixar no perfil",
   "suggestions.dismiss": "Dispensar a sugestão",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index f805215c1..ac88661bb 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -41,7 +41,7 @@
   "account.unmute_notifications": "Показывать уведомления от @{name}",
   "alert.rate_limited.message": "Пожалуйста, повторите после {retry_time, time, medium}.",
   "alert.rate_limited.title": "Вы выполняете действие слишком часто",
-  "alert.unexpected.message": "Что-то пошло не так.",
+  "alert.unexpected.message": "Произошла непредвиденная ошибка.",
   "alert.unexpected.title": "Ой!",
   "announcement.announcement": "Объявление",
   "autosuggest_hashtag.per_week": "{count} / неделю",
@@ -155,12 +155,12 @@
   "empty_column.home.public_timeline": "публичные ленты",
   "empty_column.list": "В этом списке пока ничего нет.",
   "empty_column.lists": "У вас ещё нет списков. Созданные вами списки будут показаны здесь.",
-  "empty_column.mutes": "Вы никого не игнорируете и всех внимательно выслушиваете.",
+  "empty_column.mutes": "Вы ещё никого не добавляли в список игнорируемых.",
   "empty_column.notifications": "У вас пока нет уведомлений. Взаимодействуйте с другими, чтобы завести разговор.",
   "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту",
   "error.unexpected_crash.explanation": "Из-за несовместимого браузера или ошибки в нашем коде, эта страница не может быть корректно отображена.",
   "error.unexpected_crash.next_steps": "Попробуйте обновить страницу. Если проблема не исчезает, используйте Mastodon из-под другого браузера или приложения.",
-  "errors.unexpected_crash.copy_stacktrace": "Копировать стектрейс в буфер обмена",
+  "errors.unexpected_crash.copy_stacktrace": "Скопировать диагностическую информацию",
   "errors.unexpected_crash.report_issue": "Сообщить о проблеме",
   "follow_request.authorize": "Авторизовать",
   "follow_request.reject": "Отказать",
@@ -197,12 +197,12 @@
   "introduction.federation.local.headline": "Локальная лента",
   "introduction.federation.local.text": "Публичные посты от людей с того же сервера, что и вы, будут отображены в локальной ленте.",
   "introduction.interactions.action": "Завершить обучение",
-  "introduction.interactions.favourite.headline": "Отметки «нравится»",
+  "introduction.interactions.favourite.headline": "Помечайте избранное",
   "introduction.interactions.favourite.text": "Дайте автору знать, что пост вам понравился и вернитесь к нему позже, добавив его в избранное.",
-  "introduction.interactions.reblog.headline": "Продвижения",
-  "introduction.interactions.reblog.text": "Вы можете делиться постами других людей, продвигая их в своей учётной записи.",
-  "introduction.interactions.reply.headline": "Ответы",
-  "introduction.interactions.reply.text": "Вы можете отвечать свои и чужие посты, образуя цепочки сообщений (обсуждения).",
+  "introduction.interactions.reblog.headline": "Продвигайте",
+  "introduction.interactions.reblog.text": "Делитесь постами других людей со своими подписчиками, продвигая их в своём профиле.",
+  "introduction.interactions.reply.headline": "Отвечайте",
+  "introduction.interactions.reply.text": "Отвечайте на свои или чужие посты, образуя цепочки сообщений — обсуждения.",
   "introduction.welcome.action": "Поехали!",
   "introduction.welcome.headline": "Первые шаги",
   "introduction.welcome.text": "Добро пожаловать в Федиверс! Уже через мгновение вы сможете отправлять сообщения и общаться со своими друзьями с любых узлов. Но этот узел — {domain} — особенный: на нём располагается ваш профиль, так что не забудьте его название.",
@@ -366,7 +366,7 @@
   "status.detailed_status": "Подробный просмотр обсуждения",
   "status.direct": "Написать @{name}",
   "status.embed": "Встроить на свой сайт",
-  "status.favourite": "Нравится",
+  "status.favourite": "В избранное",
   "status.filtered": "Отфильтровано",
   "status.load_more": "Загрузить остальное",
   "status.media_hidden": "Файл скрыт",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index ad4407e9d..819496911 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -10,12 +10,12 @@
   "account.domain_blocked": "Doména ukrytá",
   "account.edit_profile": "Uprav profil",
   "account.endorse": "Zobrazuj na profile",
-  "account.follow": "Následuj",
+  "account.follow": "Nasleduj",
   "account.followers": "Sledujúci",
-  "account.followers.empty": "Tohto užívateľa ešte nikto nenásleduje.",
-  "account.follows": "Následuje",
-  "account.follows.empty": "Tento užívateľ ešte nikoho nenásleduje.",
-  "account.follows_you": "Následuje ťa",
+  "account.followers.empty": "Tohto používateľa ešte nikto nenásleduje.",
+  "account.follows": "Nasleduje",
+  "account.follows.empty": "Tento používateľ ešte nikoho nenasleduje.",
+  "account.follows_you": "Nasleduje ťa",
   "account.hide_reblogs": "Skry vyzdvihnutia od @{name}",
   "account.last_status": "Naposledy aktívny",
   "account.link_verified_on": "Vlastníctvo tohto odkazu bolo skontrolované {date}",
@@ -193,7 +193,7 @@
   "introduction.federation.federated.headline": "Federovaná",
   "introduction.federation.federated.text": "Verejné príspevky z ostatných serverov vo fediverse budú zobrazené vo federovanej časovej osi.",
   "introduction.federation.home.headline": "Domovská",
-  "introduction.federation.home.text": "Príspevky od ľudí ktorých následuješ sa zobrazia na tvojej domovskej nástenke. Môžeš následovať hocikoho na ktoromkoľvek serveri!",
+  "introduction.federation.home.text": "Príspevky od ľudí ktorých nasleduješ sa zobrazia na tvojej domovskej nástenke. Môžeš nasledovať hocikoho na ktoromkoľvek serveri!",
   "introduction.federation.local.headline": "Miestna",
   "introduction.federation.local.text": "Verejné príspevky od ľudí v rámci toho istého serveru na akom si aj ty, budú zobrazované na miestnej časovej osi.",
   "introduction.interactions.action": "Ukonči návod!",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index fbe48bf2e..b50cc611c 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -1,11 +1,11 @@
 {
-  "account.add_or_remove_from_list": "Add or Remove from lists",
+  "account.add_or_remove_from_list": "Додај или Одстрани са листа",
   "account.badges.bot": "Бот",
-  "account.badges.group": "Group",
+  "account.badges.group": "Група",
   "account.block": "Блокирај @{name}",
   "account.block_domain": "Сакриј све са домена {domain}",
   "account.blocked": "Блокиран",
-  "account.cancel_follow_request": "Cancel follow request",
+  "account.cancel_follow_request": "Поништи захтеве за праћење",
   "account.direct": "Директна порука @{name}",
   "account.domain_blocked": "Домен сакривен",
   "account.edit_profile": "Измени профил",
@@ -17,16 +17,16 @@
   "account.follows.empty": "Корисник тренутно не прати никога.",
   "account.follows_you": "Прати Вас",
   "account.hide_reblogs": "Сакриј подршке које даје корисника @{name}",
-  "account.last_status": "Last active",
-  "account.link_verified_on": "Ownership of this link was checked on {date}",
-  "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
+  "account.last_status": "Последњи пут активан/на",
+  "account.link_verified_on": "Власништво над овом везом је проверено {date}",
+  "account.locked_info": "Статус приватности овог налога је подешен на закључано. Власник ручно прегледа ко га може пратити.",
   "account.media": "Медији",
   "account.mention": "Помени корисника @{name}",
   "account.moved_to": "{name} се померио на:",
   "account.mute": "Ућуткај корисника @{name}",
   "account.mute_notifications": "Искључи обавештења од корисника @{name}",
   "account.muted": "Ућуткан",
-  "account.never_active": "Never",
+  "account.never_active": "Никада",
   "account.posts": "Трубе",
   "account.posts_with_replies": "Трубе и одговори",
   "account.report": "Пријави @{name}",
@@ -39,12 +39,12 @@
   "account.unfollow": "Отпрати",
   "account.unmute": "Уклони ућуткавање кориснику @{name}",
   "account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
-  "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
-  "alert.rate_limited.title": "Rate limited",
+  "alert.rate_limited.message": "Молимо покушајте поново после {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Ограничена брзина",
   "alert.unexpected.message": "Појавила се неочекивана грешка.",
   "alert.unexpected.title": "Упс!",
-  "announcement.announcement": "Announcement",
-  "autosuggest_hashtag.per_week": "{count} per week",
+  "announcement.announcement": "Најава",
+  "autosuggest_hashtag.per_week": "{count} недељно",
   "boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
   "bundle_column_error.body": "Нешто је пошло по злу приликом учитавања ове компоненте.",
   "bundle_column_error.retry": "Покушајте поново",
@@ -53,10 +53,10 @@
   "bundle_modal_error.message": "Нешто није било у реду при учитавању ове компоненте.",
   "bundle_modal_error.retry": "Покушајте поново",
   "column.blocks": "Блокирани корисници",
-  "column.bookmarks": "Bookmarks",
+  "column.bookmarks": "Обележивачи",
   "column.community": "Локална временска линија",
   "column.direct": "Директне поруке",
-  "column.directory": "Browse profiles",
+  "column.directory": "Претражиј профиле",
   "column.domain_blocks": "Скривени домени",
   "column.favourites": "Омиљене",
   "column.follow_requests": "Захтеви за праћење",
@@ -81,22 +81,22 @@
   "compose_form.lock_disclaimer": "Ваш налог није {locked}. Свако може да Вас запрати и да види објаве намењене само Вашим пратиоцима.",
   "compose_form.lock_disclaimer.lock": "закључан",
   "compose_form.placeholder": "Шта Вам је на уму?",
-  "compose_form.poll.add_option": "Add a choice",
-  "compose_form.poll.duration": "Poll duration",
-  "compose_form.poll.option_placeholder": "Choice {number}",
-  "compose_form.poll.remove_option": "Remove this choice",
-  "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
-  "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
+  "compose_form.poll.add_option": "Додајте избор",
+  "compose_form.poll.duration": "Трајање анкете",
+  "compose_form.poll.option_placeholder": "Избор {number}",
+  "compose_form.poll.remove_option": "Одстрани овај избор",
+  "compose_form.poll.switch_to_multiple": "Промените анкету да бисте омогућили више избора",
+  "compose_form.poll.switch_to_single": "Промените анкету да бисте омогућили један избор",
   "compose_form.publish": "Труби",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.hide": "Mark media as sensitive",
+  "compose_form.sensitive.hide": "Означи мултимедију као осетљиву",
   "compose_form.sensitive.marked": "Медији су означени као осетљиви",
   "compose_form.sensitive.unmarked": "Медији су означени као не-осетљиви",
   "compose_form.spoiler.marked": "Текст је сакривен иза упозорења",
   "compose_form.spoiler.unmarked": "Текст није сакривен",
   "compose_form.spoiler_placeholder": "Овде упишите упозорење",
   "confirmation_modal.cancel": "Поништи",
-  "confirmations.block.block_and_report": "Block & Report",
+  "confirmations.block.block_and_report": "Блокирај и Пријави",
   "confirmations.block.confirm": "Блокирај",
   "confirmations.block.message": "Да ли сте сигурни да желите да блокирате корисника {name}?",
   "confirmations.delete.confirm": "Обриши",
@@ -105,25 +105,25 @@
   "confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?",
   "confirmations.domain_block.confirm": "Сакриј цео домен",
   "confirmations.domain_block.message": "Да ли сте заиста сигурни да желите да блокирате цео домен {domain}? У већини случајева, неколико добро промишљених блокирања или ућуткавања су довољна и препоручљива.",
-  "confirmations.logout.confirm": "Log out",
-  "confirmations.logout.message": "Are you sure you want to log out?",
+  "confirmations.logout.confirm": "Одјави се",
+  "confirmations.logout.message": "Да ли се сигурни да желите да се одјавите?",
   "confirmations.mute.confirm": "Ућуткај",
-  "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
+  "confirmations.mute.explanation": "Ово ће сакрити објаве од њих и објаве које их помињу, али ће им и даље дозволити да виде ваше постове и да вас запрате.",
   "confirmations.mute.message": "Да ли стварно желите да ућуткате корисника {name}?",
   "confirmations.redraft.confirm": "Избриши и преправи",
   "confirmations.redraft.message": "Да ли сте сигурни да желите да избришете овај статус и да га преправите? Сва стављања у омиљене трубе, као и подршке ће бити изгубљене, а одговори на оригинални пост ће бити поништени.",
-  "confirmations.reply.confirm": "Reply",
-  "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
+  "confirmations.reply.confirm": "Одговори",
+  "confirmations.reply.message": "Одговарањем ћете обрисати поруку коју састављате. Јесте ли сигурни да желите да наставите?",
   "confirmations.unfollow.confirm": "Отпрати",
   "confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?",
-  "conversation.delete": "Delete conversation",
-  "conversation.mark_as_read": "Mark as read",
-  "conversation.open": "View conversation",
-  "conversation.with": "With {names}",
-  "directory.federated": "From known fediverse",
-  "directory.local": "From {domain} only",
-  "directory.new_arrivals": "New arrivals",
-  "directory.recently_active": "Recently active",
+  "conversation.delete": "Обриши преписку",
+  "conversation.mark_as_read": "Означи као прочитано",
+  "conversation.open": "Прикажи преписку",
+  "conversation.with": "Са {names}",
+  "directory.federated": "Са знаних здружених инстанци",
+  "directory.local": "Само са {domain}",
+  "directory.new_arrivals": "Новопридошли",
+  "directory.recently_active": "Недавно активни",
   "embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.",
   "embed.preview": "Овако ће да изгледа:",
   "emoji_button.activity": "Активност",
@@ -140,10 +140,10 @@
   "emoji_button.search_results": "Резултати претраге",
   "emoji_button.symbols": "Симболи",
   "emoji_button.travel": "Путовања и места",
-  "empty_column.account_timeline": "No toots here!",
-  "empty_column.account_unavailable": "Profile unavailable",
+  "empty_column.account_timeline": "Овде нема труба!",
+  "empty_column.account_unavailable": "Профил недоступан",
   "empty_column.blocks": "Још увек немате блокираних корисника.",
-  "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
+  "empty_column.bookmarked_statuses": "Још увек немате обележене трубе. Када их обележите, појавиће се овде.",
   "empty_column.community": "Локална временска линија је празна. Напишите нешто јавно да започнете!",
   "empty_column.direct": "Још увек немате директних порука. Када пошаљете или примите једну, појавиће се овде.",
   "empty_column.domain_blocks": "Још увек нема сакривених домена.",
@@ -158,41 +158,41 @@
   "empty_column.mutes": "Још увек немате ућутканих корисника.",
   "empty_column.notifications": "Тренутно немате обавештења. Дружите се мало да започнете разговор.",
   "empty_column.public": "Овде нема ничега! Напишите нешто јавно, или нађите кориснике са других инстанци које ћете запратити да попуните ову празнину",
-  "error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
-  "error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
-  "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
-  "errors.unexpected_crash.report_issue": "Report issue",
+  "error.unexpected_crash.explanation": "Због грешке у нашем коду или проблема са компатибилношћу прегледача, ова страница се није могла правилно приказати.",
+  "error.unexpected_crash.next_steps": "Покушајте да освежите страницу. Ако то не помогне, можда ћете и даље моћи да користите Мастодон путем другог прегледача или матичне апликације.",
+  "errors.unexpected_crash.copy_stacktrace": "Копирај \"stacktrace\" у клипборд",
+  "errors.unexpected_crash.report_issue": "Пријави проблем",
   "follow_request.authorize": "Одобри",
   "follow_request.reject": "Одбиј",
   "getting_started.developers": "Програмери",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Профил фасцикле",
   "getting_started.documentation": "Документација",
   "getting_started.heading": "Да почнете",
   "getting_started.invite": "Позовите људе",
   "getting_started.open_source_notice": "Мастoдон је софтвер отвореног кода. Можете му допринети или пријавити проблеме преко ГитХаба на {github}.",
   "getting_started.security": "Безбедност",
   "getting_started.terms": "Услови коришћења",
-  "hashtag.column_header.tag_mode.all": "and {additional}",
-  "hashtag.column_header.tag_mode.any": "or {additional}",
-  "hashtag.column_header.tag_mode.none": "without {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
-  "hashtag.column_settings.tag_mode.all": "All of these",
-  "hashtag.column_settings.tag_mode.any": "Any of these",
-  "hashtag.column_settings.tag_mode.none": "None of these",
+  "hashtag.column_header.tag_mode.all": "и {additional}",
+  "hashtag.column_header.tag_mode.any": "или {additional}",
+  "hashtag.column_header.tag_mode.none": "без {additional}",
+  "hashtag.column_settings.select.no_options_message": "Нису пронађени предлози",
+  "hashtag.column_settings.select.placeholder": "Унеси хештег…",
+  "hashtag.column_settings.tag_mode.all": "Све оve",
+  "hashtag.column_settings.tag_mode.any": "Било које од ових",
+  "hashtag.column_settings.tag_mode.none": "Ништа од ових",
   "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
   "home.column_settings.basic": "Основно",
   "home.column_settings.show_reblogs": "Прикажи и подржавања",
   "home.column_settings.show_replies": "Прикажи одговоре",
-  "home.hide_announcements": "Hide announcements",
-  "home.show_announcements": "Show announcements",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
-  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
-  "introduction.federation.action": "Next",
-  "introduction.federation.federated.headline": "Federated",
-  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
-  "introduction.federation.home.headline": "Home",
+  "home.hide_announcements": "Сакриј најаве",
+  "home.show_announcements": "Пријажи најаве",
+  "intervals.full.days": "{number, plural, one {# дан} other {# дана}}",
+  "intervals.full.hours": "{number, plural, one {# сат} other {# сати}}",
+  "intervals.full.minutes": "{number, plural, one {# минут} other {# минута}}",
+  "introduction.federation.action": "Даље",
+  "introduction.federation.federated.headline": "Федерисано",
+  "introduction.federation.federated.text": "Јавне објаве са осталих сервера из здружених инстанци ће се појавити у федерисаној временској линији.",
+  "introduction.federation.home.headline": "Почетна",
   "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
   "introduction.federation.local.headline": "Local",
   "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
@@ -355,13 +355,13 @@
   "search_results.statuses": "Трубе",
   "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
   "search_results.total": "{count, number} {count, plural, one {резултат} few {резултата} other {резултата}}",
-  "status.admin_account": "Open moderation interface for @{name}",
-  "status.admin_status": "Open this status in the moderation interface",
+  "status.admin_account": "Отвори модераторски интерфејс за @{name}",
+  "status.admin_status": "Отвори овај статус у модераторском интерфејсу",
   "status.block": "Блокирај @{name}",
-  "status.bookmark": "Bookmark",
+  "status.bookmark": "Обележи",
   "status.cancel_reblog_private": "Уклони подршку",
   "status.cannot_reblog": "Овај статус не може да се подржи",
-  "status.copy": "Copy link to status",
+  "status.copy": "Копирај везу на статус",
   "status.delete": "Обриши",
   "status.detailed_status": "Детаљни преглед разговора",
   "status.direct": "Директна порука @{name}",
@@ -377,13 +377,13 @@
   "status.open": "Прошири овај статус",
   "status.pin": "Закачи на профил",
   "status.pinned": "Закачена труба",
-  "status.read_more": "Read more",
+  "status.read_more": "Прочитајте више",
   "status.reblog": "Подржи",
   "status.reblog_private": "Подржи да види првобитна публика",
   "status.reblogged_by": "{name} подржао/ла",
   "status.reblogs.empty": "Још увек нико није подржао ову трубу. Када буде подржана, појавиће се овде.",
   "status.redraft": "Избриши и преправи",
-  "status.remove_bookmark": "Remove bookmark",
+  "status.remove_bookmark": "Уклони обележивач",
   "status.reply": "Одговори",
   "status.replyAll": "Одговори на дискусију",
   "status.report": "Пријави корисника @{name}",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 29b8b023e..763dae5ef 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -184,8 +184,8 @@
   "home.column_settings.basic": "Grundläggande",
   "home.column_settings.show_reblogs": "Visa knuffar",
   "home.column_settings.show_replies": "Visa svar",
-  "home.hide_announcements": "Hide announcements",
-  "home.show_announcements": "Show announcements",
+  "home.hide_announcements": "Dölj notiser",
+  "home.show_announcements": "Visa notiser",
   "intervals.full.days": "{number, plural, one {# dag} other {# dagar}}",
   "intervals.full.hours": "{number, plural, one {# timme} other {# timmar}}",
   "intervals.full.minutes": "{number, plural, one {# minut} other {# minuter}}",
@@ -335,7 +335,7 @@
   "relative_time.just_now": "nu",
   "relative_time.minutes": "{number}min",
   "relative_time.seconds": "{number}sek",
-  "relative_time.today": "today",
+  "relative_time.today": "idag",
   "reply_indicator.cancel": "Ångra",
   "report.forward": "Vidarebefordra till {target}",
   "report.forward_hint": "Kontot är från en annan server. Skicka även en anonymiserad kopia av anmälan dit?",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 71de4beaf..479d0ab31 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -3,11 +3,11 @@
   "account.badges.bot": "บอต",
   "account.badges.group": "กลุ่ม",
   "account.block": "ปิดกั้น @{name}",
-  "account.block_domain": "ซ่อนทุกอย่างจาก {domain}",
+  "account.block_domain": "ปิดกั้นโดเมน {domain}",
   "account.blocked": "ปิดกั้นอยู่",
   "account.cancel_follow_request": "ยกเลิกคำขอติดตาม",
   "account.direct": "ส่งข้อความโดยตรงถึง @{name}",
-  "account.domain_blocked": "ซ่อนโดเมนอยู่",
+  "account.domain_blocked": "ปิดกั้นโดเมนอยู่",
   "account.edit_profile": "แก้ไขโปรไฟล์",
   "account.endorse": "แสดงให้เห็นในโปรไฟล์",
   "account.follow": "ติดตาม",
@@ -34,7 +34,7 @@
   "account.share": "แบ่งปันโปรไฟล์ของ @{name}",
   "account.show_reblogs": "แสดงการดันจาก @{name}",
   "account.unblock": "เลิกปิดกั้น @{name}",
-  "account.unblock_domain": "เลิกซ่อน {domain}",
+  "account.unblock_domain": "เลิกปิดกั้นโดเมน {domain}",
   "account.unendorse": "ไม่แสดงให้เห็นในโปรไฟล์",
   "account.unfollow": "เลิกติดตาม",
   "account.unmute": "เลิกปิดเสียง @{name}",
@@ -54,10 +54,10 @@
   "bundle_modal_error.retry": "ลองอีกครั้ง",
   "column.blocks": "ผู้ใช้ที่ปิดกั้นอยู่",
   "column.bookmarks": "ที่คั่นหน้า",
-  "column.community": "เส้นเวลาในเว็บ",
+  "column.community": "เส้นเวลาในเซิร์ฟเวอร์",
   "column.direct": "ข้อความโดยตรง",
   "column.directory": "เรียกดูโปรไฟล์",
-  "column.domain_blocks": "โดเมนที่ซ่อนอยู่",
+  "column.domain_blocks": "โดเมนที่ปิดกั้นอยู่",
   "column.favourites": "รายการโปรด",
   "column.follow_requests": "คำขอติดตาม",
   "column.home": "หน้าแรก",
@@ -103,7 +103,7 @@
   "confirmations.delete.message": "คุณแน่ใจหรือไม่ว่าต้องการลบสถานะนี้?",
   "confirmations.delete_list.confirm": "ลบ",
   "confirmations.delete_list.message": "คุณแน่ใจหรือไม่ว่าต้องการลบรายการนี้อย่างถาวร?",
-  "confirmations.domain_block.confirm": "ซ่อนทั้งโดเมน",
+  "confirmations.domain_block.confirm": "ปิดกั้นทั้งโดเมน",
   "confirmations.domain_block.message": "คุณแน่ใจจริง ๆ หรือไม่ว่าต้องการปิดกั้นทั้ง {domain}? ในกรณีส่วนใหญ่ การปิดกั้นหรือการปิดเสียงแบบกำหนดเป้าหมายไม่กี่รายการนั้นเพียงพอและเป็นที่นิยม คุณจะไม่เห็นเนื้อหาจากโดเมนนั้นในเส้นเวลาสาธารณะใด ๆ หรือการแจ้งเตือนของคุณ จะเอาผู้ติดตามของคุณจากโดเมนนั้นออก",
   "confirmations.logout.confirm": "ออกจากระบบ",
   "confirmations.logout.message": "คุณแน่ใจหรือไม่ว่าต้องการออกจากระบบ?",
@@ -144,9 +144,9 @@
   "empty_column.account_unavailable": "ไม่มีโปรไฟล์",
   "empty_column.blocks": "คุณยังไม่ได้ปิดกั้นผู้ใช้ใด ๆ",
   "empty_column.bookmarked_statuses": "คุณยังไม่มีโพสต์ที่เพิ่มที่คั่นหน้าไว้ใด ๆ เมื่อคุณเพิ่มที่คั่นหน้าโพสต์ โพสต์จะปรากฏที่นี่",
-  "empty_column.community": "เส้นเวลาในเว็บว่างเปล่า เขียนบางอย่างเป็นสาธารณะเพื่อเริ่มต้น!",
+  "empty_column.community": "เส้นเวลาในเซิร์ฟเวอร์ว่างเปล่า เขียนบางอย่างเป็นสาธารณะเพื่อเริ่มต้น!",
   "empty_column.direct": "คุณยังไม่มีข้อความโดยตรงใด ๆ เมื่อคุณส่งหรือรับข้อความ ข้อความจะปรากฏที่นี่",
-  "empty_column.domain_blocks": "ยังไม่มีโดเมนที่ซ่อนอยู่",
+  "empty_column.domain_blocks": "ยังไม่มีโดเมนที่ปิดกั้นอยู่",
   "empty_column.favourited_statuses": "คุณยังไม่มีโพสต์ที่ชื่นชอบใด ๆ เมื่อคุณชื่นชอบโพสต์ โพสต์จะปรากฏที่นี่",
   "empty_column.favourites": "ยังไม่มีใครชื่นชอบโพสต์นี้ เมื่อใครสักคนชื่นชอบ เขาจะปรากฏที่นี่",
   "empty_column.follow_requests": "คุณยังไม่มีคำขอติดตามใด ๆ เมื่อคุณได้รับคำขอ คำขอจะปรากฏที่นี่",
@@ -194,8 +194,8 @@
   "introduction.federation.federated.text": "โพสต์สาธารณะจากเซิร์ฟเวอร์อื่น ๆ ของเฟดิเวิร์สจะปรากฏในเส้นเวลาที่ติดต่อกับภายนอก",
   "introduction.federation.home.headline": "หน้าแรก",
   "introduction.federation.home.text": "โพสต์จากผู้คนที่คุณติดตามจะปรากฏในฟีดหน้าแรกของคุณ คุณสามารถติดตามใครก็ตามในเซิร์ฟเวอร์ใดก็ตาม!",
-  "introduction.federation.local.headline": "ในเว็บ",
-  "introduction.federation.local.text": "โพสต์สาธารณะจากผู้คนในเซิร์ฟเวอร์เดียวกันกับคุณจะปรากฏในเส้นเวลาในเว็บ",
+  "introduction.federation.local.headline": "ในเซิร์ฟเวอร์",
+  "introduction.federation.local.text": "โพสต์สาธารณะจากผู้คนในเซิร์ฟเวอร์เดียวกันกับคุณจะปรากฏในเส้นเวลาในเซิร์ฟเวอร์",
   "introduction.interactions.action": "เสร็จสิ้นบทช่วยสอน!",
   "introduction.interactions.favourite.headline": "ชื่นชอบ",
   "introduction.interactions.favourite.text": "คุณสามารถบันทึกโพสต์ไว้ในภายหลังและแจ้งให้ผู้สร้างทราบว่าคุณชอบโพสต์โดยการชื่นชอบโพสต์",
@@ -222,7 +222,7 @@
   "keyboard_shortcuts.home": "เพื่อเปิดเส้นเวลาหน้าแรก",
   "keyboard_shortcuts.hotkey": "ปุ่มลัด",
   "keyboard_shortcuts.legend": "เพื่อแสดงคำอธิบายนี้",
-  "keyboard_shortcuts.local": "เพื่อเปิดเส้นเวลาในเว็บ",
+  "keyboard_shortcuts.local": "เพื่อเปิดเส้นเวลาในเซิร์ฟเวอร์",
   "keyboard_shortcuts.mention": "เพื่อกล่าวถึงผู้สร้าง",
   "keyboard_shortcuts.muted": "เพื่อเปิดรายการผู้ใช้ที่ปิดเสียงอยู่",
   "keyboard_shortcuts.my_profile": "เพื่อเปิดโปรไฟล์ของคุณ",
@@ -254,18 +254,18 @@
   "lists.subheading": "รายการของคุณ",
   "load_pending": "{count, plural, other {# รายการใหม่}}",
   "loading_indicator.label": "กำลังโหลด...",
-  "media_gallery.toggle_visible": "เปิด/ปิดการมองเห็น",
+  "media_gallery.toggle_visible": "ซ่อนสื่อ",
   "missing_indicator.label": "ไม่พบ",
   "missing_indicator.sublabel": "ไม่พบทรัพยากรนี้",
   "mute_modal.hide_notifications": "ซ่อนการแจ้งเตือนจากผู้ใช้นี้?",
   "navigation_bar.apps": "แอปมือถือ",
   "navigation_bar.blocks": "ผู้ใช้ที่ปิดกั้นอยู่",
   "navigation_bar.bookmarks": "ที่คั่นหน้า",
-  "navigation_bar.community_timeline": "เส้นเวลาในเว็บ",
+  "navigation_bar.community_timeline": "เส้นเวลาในเซิร์ฟเวอร์",
   "navigation_bar.compose": "เขียนโพสต์ใหม่",
   "navigation_bar.direct": "ข้อความโดยตรง",
   "navigation_bar.discover": "ค้นพบ",
-  "navigation_bar.domain_blocks": "โดเมนที่ซ่อนอยู่",
+  "navigation_bar.domain_blocks": "โดเมนที่ปิดกั้นอยู่",
   "navigation_bar.edit_profile": "แก้ไขโปรไฟล์",
   "navigation_bar.favourites": "รายการโปรด",
   "navigation_bar.filters": "คำที่ปิดเสียงอยู่",
@@ -319,13 +319,13 @@
   "poll_button.add_poll": "เพิ่มการสำรวจความคิดเห็น",
   "poll_button.remove_poll": "เอาการสำรวจความคิดเห็นออก",
   "privacy.change": "ปรับเปลี่ยนความเป็นส่วนตัวของสถานะ",
-  "privacy.direct.long": "โพสต์ไปยังผู้ใช้ที่กล่าวถึงเท่านั้น",
+  "privacy.direct.long": "ปรากฏแก่ผู้ใช้ที่กล่าวถึงเท่านั้น",
   "privacy.direct.short": "โดยตรง",
-  "privacy.private.long": "โพสต์ไปยังผู้ติดตามเท่านั้น",
+  "privacy.private.long": "ปรากฏแก่ผู้ติดตามเท่านั้น",
   "privacy.private.short": "ผู้ติดตามเท่านั้น",
-  "privacy.public.long": "โพสต์ไปยังเส้นเวลาสาธารณะ",
+  "privacy.public.long": "ปรากฏแก่ทุกคน แสดงในเส้นเวลาสาธารณะ",
   "privacy.public.short": "สาธารณะ",
-  "privacy.unlisted.long": "ไม่โพสต์ไปยังเส้นเวลาสาธารณะ",
+  "privacy.unlisted.long": "ปรากฏแก่ทุกคน แต่ไม่อยู่ในเส้นเวลาสาธารณะ",
   "privacy.unlisted.short": "ไม่อยู่ในรายการ",
   "refresh": "รีเฟรช",
   "regeneration_indicator.label": "กำลังโหลด…",
@@ -401,7 +401,7 @@
   "suggestions.header": "คุณอาจสนใจ…",
   "tabs_bar.federated_timeline": "ที่ติดต่อกับภายนอก",
   "tabs_bar.home": "หน้าแรก",
-  "tabs_bar.local_timeline": "ในเว็บ",
+  "tabs_bar.local_timeline": "ในเซิร์ฟเวอร์",
   "tabs_bar.notifications": "การแจ้งเตือน",
   "tabs_bar.search": "ค้นหา",
   "time_remaining.days": "เหลืออีก {number, plural, other {# วัน}}",
@@ -409,7 +409,7 @@
   "time_remaining.minutes": "เหลืออีก {number, plural, other {# นาที}}",
   "time_remaining.moments": "ช่วงเวลาที่เหลือ",
   "time_remaining.seconds": "เหลืออีก {number, plural, other {# วินาที}}",
-  "trends.count_by_accounts": "{count} {rawCount, plural, other {คน}}กำลังคุย",
+  "trends.count_by_accounts": "{count} {rawCount, plural, other {คน}}กำลังพูดคุย",
   "trends.trending_now": "กำลังนิยม",
   "ui.beforeunload": "แบบร่างของคุณจะหายไปหากคุณออกจาก Mastodon",
   "upload_area.title": "ลากแล้วปล่อยเพื่ออัปโหลด",
diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json
index 664eebfbf..d625a88bf 100644
--- a/app/javascript/mastodon/locales/ur.json
+++ b/app/javascript/mastodon/locales/ur.json
@@ -254,7 +254,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading...",
-  "media_gallery.toggle_visible": "Toggle visibility",
+  "media_gallery.toggle_visible": "Hide media",
   "missing_indicator.label": "Not found",
   "missing_indicator.sublabel": "This resource could not be found",
   "mute_modal.hide_notifications": "Hide notifications from this user?",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 41ecfe1f2..57658706a 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -158,7 +158,7 @@
   "empty_column.mutes": "你没有隐藏任何用户。",
   "empty_column.notifications": "你还没有收到过任何通知,快和其他用户互动吧。",
   "empty_column.public": "这里什么都没有!写一些公开的嘟文,或者关注其他服务器的用户后,这里就会有嘟文出现了",
-  "error.unexpected_crash.explanation": "此页面无法正确现实,这可能是因为我们的代码中有错误,也可能是因为浏览器兼容问题。",
+  "error.unexpected_crash.explanation": "此页面无法正确显示,这可能是因为我们的代码中有错误,也可能是因为浏览器兼容问题。",
   "error.unexpected_crash.next_steps": "刷新一下页面试试。如果没用,您可以换个浏览器或者用本地应用。",
   "errors.unexpected_crash.copy_stacktrace": "把堆栈跟踪信息复制到剪贴板",
   "errors.unexpected_crash.report_issue": "报告问题",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 12f154a2c..a321e5a66 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -1,7 +1,7 @@
 {
   "account.add_or_remove_from_list": "從名單中新增或移除",
   "account.badges.bot": "機械人",
-  "account.badges.group": "Group",
+  "account.badges.group": "群組",
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的一切文章",
   "account.blocked": "封鎖",
@@ -43,7 +43,7 @@
   "alert.rate_limited.title": "已限速",
   "alert.unexpected.message": "發生不可預期的錯誤。",
   "alert.unexpected.title": "噢!",
-  "announcement.announcement": "Announcement",
+  "announcement.announcement": "公告",
   "autosuggest_hashtag.per_week": "{count} / 週",
   "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
   "bundle_column_error.body": "加載本組件出錯。",
@@ -85,8 +85,8 @@
   "compose_form.poll.duration": "投票期限",
   "compose_form.poll.option_placeholder": "第 {number} 個選擇",
   "compose_form.poll.remove_option": "移除此選擇",
-  "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
-  "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
+  "compose_form.poll.switch_to_multiple": "變更投票為允許多個選項",
+  "compose_form.poll.switch_to_single": "變更投票為允許單一選項",
   "compose_form.publish": "發文",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.hide": "標記媒體為敏感內容",
@@ -184,8 +184,8 @@
   "home.column_settings.basic": "基本",
   "home.column_settings.show_reblogs": "顯示被轉推的文章",
   "home.column_settings.show_replies": "顯示回應文章",
-  "home.hide_announcements": "Hide announcements",
-  "home.show_announcements": "Show announcements",
+  "home.hide_announcements": "隱藏公告",
+  "home.show_announcements": "顯示公告",
   "intervals.full.days": "{number, plural, one {# 天} other {# 天}}",
   "intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}",
   "intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}",
@@ -335,7 +335,7 @@
   "relative_time.just_now": "剛剛",
   "relative_time.minutes": "{number}分鐘",
   "relative_time.seconds": "{number}秒",
-  "relative_time.today": "today",
+  "relative_time.today": "今天",
   "reply_indicator.cancel": "取消",
   "report.forward": "轉寄到 {target}",
   "report.forward_hint": "這個帳戶屬於其他服務站。要向該服務站發送匿名的舉報訊息嗎?",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 56c4de066..112d5cb2f 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -1,7 +1,7 @@
 {
   "account.add_or_remove_from_list": "從名單中新增或移除",
   "account.badges.bot": "機器人",
-  "account.badges.group": "Group",
+  "account.badges.group": "群組",
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的所有內容",
   "account.blocked": "已封鎖",
@@ -43,7 +43,7 @@
   "alert.rate_limited.title": "已限速",
   "alert.unexpected.message": "發生了非預期的錯誤。",
   "alert.unexpected.title": "哎呀!",
-  "announcement.announcement": "Announcement",
+  "announcement.announcement": "公告",
   "autosuggest_hashtag.per_week": "{count} / 週",
   "boost_modal.combo": "下次您可以按 {combo} 跳過",
   "bundle_column_error.body": "載入此元件時發生錯誤。",
@@ -85,8 +85,8 @@
   "compose_form.poll.duration": "投票期限",
   "compose_form.poll.option_placeholder": "第 {number} 個選擇",
   "compose_form.poll.remove_option": "移除此選擇",
-  "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
-  "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
+  "compose_form.poll.switch_to_multiple": "變更投票為允許多個選項",
+  "compose_form.poll.switch_to_single": "變更投票為允許單一選項",
   "compose_form.publish": "嘟出去",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.hide": "標記媒體為敏感內容",
@@ -184,8 +184,8 @@
   "home.column_settings.basic": "基本",
   "home.column_settings.show_reblogs": "顯示轉嘟",
   "home.column_settings.show_replies": "顯示回覆",
-  "home.hide_announcements": "Hide announcements",
-  "home.show_announcements": "Show announcements",
+  "home.hide_announcements": "隱藏公告",
+  "home.show_announcements": "顯示公告",
   "intervals.full.days": "{number, plural, one {# 天} other {# 天}}",
   "intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}",
   "intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}",
@@ -335,7 +335,7 @@
   "relative_time.just_now": "剛剛",
   "relative_time.minutes": "{number} 分",
   "relative_time.seconds": "{number} 秒",
-  "relative_time.today": "today",
+  "relative_time.today": "今天",
   "reply_indicator.cancel": "取消",
   "report.forward": "轉寄到 {target}",
   "report.forward_hint": "這個帳戶屬於其他站點。要像該站點發送匿名的檢舉訊息嗎?",
diff --git a/app/javascript/mastodon/middleware/errors.js b/app/javascript/mastodon/middleware/errors.js
index 3cebb42e0..0a65fd321 100644
--- a/app/javascript/mastodon/middleware/errors.js
+++ b/app/javascript/mastodon/middleware/errors.js
@@ -8,7 +8,7 @@ export default function errorsMiddleware() {
       const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');
 
       if (action.type.match(isFail)) {
-        dispatch(showAlertForError(action.error));
+        dispatch(showAlertForError(action.error, action.skipNotFound));
       }
     }
 
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index c6653fe4c..e6e6d2ae1 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -261,7 +261,6 @@ export default function compose(state = initialState, action) {
     });
   case COMPOSE_SPOILERNESS_CHANGE:
     return state.withMutations(map => {
-      map.set('spoiler_text', '');
       map.set('spoiler', !state.get('spoiler'));
       map.set('idempotencyKey', uuid());
 
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index 60e901e39..ed1ba0272 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -72,11 +72,11 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
 
       mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
         const lastIndex = 1 + list.findLastIndex(
-          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
+          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')),
         );
 
         const firstIndex = 1 + list.take(lastIndex).findLastIndex(
-          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
+          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0,
         );
 
         return list.take(firstIndex).concat(items, list.skip(lastIndex));
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 0d7222e10..63b76773d 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -54,7 +54,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
 
         return oldIds.take(firstIndex + 1).concat(
           isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds,
-          oldIds.skip(lastIndex)
+          oldIds.skip(lastIndex),
         );
       });
     }
@@ -166,7 +166,7 @@ export default function timelines(state = initialState, action) {
     return state.update(
       action.timeline,
       initialTimeline,
-      map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items)
+      map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items),
     );
   default:
     return state;
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 6a48f3b3f..673268c5a 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -117,7 +117,7 @@ export const makeGetStatus = () => {
         map.set('account', accountBase);
         map.set('filtered', filtered);
       });
-    }
+    },
   );
 };
 
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index 1ab0dc0fa..958e5fc12 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -117,7 +117,7 @@ const handlePush = (event) => {
         badge: '/badge.png',
         data: { access_token, preferred_locale, url: '/web/notifications' },
       });
-    })
+    }),
   );
 };
 
diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js
index 7e7472841..e18af842f 100644
--- a/app/javascript/mastodon/store/configureStore.js
+++ b/app/javascript/mastodon/store/configureStore.js
@@ -10,6 +10,6 @@ export default function configureStore() {
     thunk,
     loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
     errorsMiddleware(),
-    soundsMiddleware()
+    soundsMiddleware(),
   ), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f));
 };
diff --git a/app/javascript/mastodon/utils/log_out.js b/app/javascript/mastodon/utils/log_out.js
index b43417f4b..3a4cc8ecb 100644
--- a/app/javascript/mastodon/utils/log_out.js
+++ b/app/javascript/mastodon/utils/log_out.js
@@ -1,4 +1,4 @@
-import Rails from 'rails-ujs';
+import Rails from '@rails/ujs';
 
 export const logOut = () => {
   const form = document.createElement('form');
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 13cb5d548..5b699e767 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -8,7 +8,7 @@ start();
 function main() {
   const IntlMessageFormat = require('intl-messageformat').default;
   const { timeAgoString } = require('../mastodon/components/relative_timestamp');
-  const { delegate } = require('rails-ujs');
+  const { delegate } = require('@rails/ujs');
   const emojify = require('../mastodon/features/emoji/emoji').default;
   const { getLocale } = require('../mastodon/locales');
   const { messages } = getLocale();
@@ -101,6 +101,28 @@ function main() {
 
     delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
+
+    delegate(document, '.status__content__spoiler-link', 'click', function() {
+      const contentEl = this.parentNode.parentNode.querySelector('.e-content');
+
+      if (contentEl.style.display === 'block') {
+        contentEl.style.display = 'none';
+        this.parentNode.style.marginBottom = 0;
+        this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
+      } else {
+        contentEl.style.display = 'block';
+        this.parentNode.style.marginBottom = null;
+        this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
+      }
+
+      return false;
+    });
+
+    [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
+      const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
+      const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
+      spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
+    });
   });
 
   delegate(document, '.sidebar__toggle__icon', 'click', () => {
diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index 05e52966b..7a846bcc6 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -142,7 +142,7 @@ html {
 }
 
 .compose-form__autosuggest-wrapper,
-.poll__text input[type="text"],
+.poll__option input[type="text"],
 .compose-form .spoiler-input__input,
 .compose-form__poll-wrapper select,
 .search__input,
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index fb136d1a3..7bff2daa1 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -418,6 +418,11 @@ body,
       }
     }
 
+    &--with-select strong {
+      display: block;
+      margin-bottom: 10px;
+    }
+
     a {
       display: inline-block;
       color: $darker-text-color;
@@ -583,19 +588,22 @@ body,
 }
 
 .log-entry {
-  margin-bottom: 20px;
   line-height: 20px;
+  padding: 15px 0;
+  background: $ui-base-color;
+  border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+  &:last-child {
+    border-bottom: 0;
+  }
 
   &__header {
     display: flex;
     justify-content: flex-start;
     align-items: center;
-    padding: 10px;
-    background: $ui-base-color;
     color: $darker-text-color;
-    border-radius: 4px 4px 0 0;
     font-size: 14px;
-    position: relative;
+    padding: 0 10px;
   }
 
   &__avatar {
@@ -622,44 +630,6 @@ body,
     color: $dark-text-color;
   }
 
-  &__extras {
-    background: lighten($ui-base-color, 6%);
-    border-radius: 0 0 4px 4px;
-    padding: 10px;
-    color: $darker-text-color;
-    font-family: $font-monospace, monospace;
-    font-size: 12px;
-    word-wrap: break-word;
-    min-height: 20px;
-  }
-
-  &__icon {
-    font-size: 28px;
-    margin-right: 10px;
-    color: $dark-text-color;
-  }
-
-  &__icon__overlay {
-    position: absolute;
-    top: 10px;
-    right: 10px;
-    width: 10px;
-    height: 10px;
-    border-radius: 50%;
-
-    &.positive {
-      background: $success-green;
-    }
-
-    &.negative {
-      background: lighten($error-red, 12%);
-    }
-
-    &.neutral {
-      background: $ui-highlight-color;
-    }
-  }
-
   a,
   .username,
   .target {
@@ -667,18 +637,6 @@ body,
     text-decoration: none;
     font-weight: 500;
   }
-
-  .diff-old {
-    color: lighten($error-red, 12%);
-  }
-
-  .diff-neutral {
-    color: $secondary-text-color;
-  }
-
-  .diff-new {
-    color: $success-green;
-  }
 }
 
 a.name-tag,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index aa885e241..dd82b0824 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -388,8 +388,8 @@
 
   .emoji-picker-dropdown {
     position: absolute;
-    top: 5px;
-    right: 5px;
+    top: 0;
+    right: 0;
   }
 
   .compose-form__autosuggest-wrapper {
@@ -870,6 +870,7 @@
 
 .announcements__item__content {
   word-wrap: break-word;
+  overflow-y: auto;
 
   .emojione {
     width: 20px;
@@ -1027,13 +1028,11 @@
     }
 
     .display-name {
+      color: $light-text-color;
+
       strong {
         color: $inverted-text-color;
       }
-
-      span {
-        color: $light-text-color;
-      }
     }
 
     .status__content {
@@ -1332,7 +1331,6 @@
     border-radius: 50%;
     overflow: hidden;
     position: relative;
-    cursor: default;
 
     & > div {
       float: left;
@@ -3802,7 +3800,8 @@ a.status-card.compact:hover {
 }
 
 .empty-column-indicator,
-.error-column {
+.error-column,
+.follow_requests-unlocked_explanation {
   color: $dark-text-color;
   background: $ui-base-color;
   text-align: center;
@@ -3833,6 +3832,11 @@ a.status-card.compact:hover {
   }
 }
 
+.follow_requests-unlocked_explanation {
+  background: darken($ui-base-color, 4%);
+  contain: initial;
+}
+
 .error-column {
   flex-direction: column;
 }
@@ -4060,10 +4064,7 @@ a.status-card.compact:hover {
 
 .emoji-button {
   display: block;
-  font-size: 24px;
-  line-height: 24px;
-  margin-left: 2px;
-  width: 24px;
+  padding: 5px 5px 2px 2px;
   outline: 0;
   cursor: pointer;
 
@@ -4079,7 +4080,6 @@ a.status-card.compact:hover {
     margin: 0;
     width: 22px;
     height: 22px;
-    margin-top: 2px;
   }
 
   &:hover,
@@ -5058,12 +5058,6 @@ a.status-card.compact:hover {
 }
 
 .media-gallery__gifv {
-  &.autoplay {
-    .media-gallery__gifv__label {
-      display: none;
-    }
-  }
-
   &:hover {
     .media-gallery__gifv__label {
       opacity: 1;
@@ -6579,6 +6573,7 @@ noscript {
     padding: 10px;
     padding-top: 12px;
     position: relative;
+    cursor: pointer;
   }
 
   &__unread {
@@ -6682,17 +6677,21 @@ noscript {
     box-sizing: border-box;
     width: 100%;
     padding: 15px;
-    padding-right: 15px + 18px;
     position: relative;
     font-size: 15px;
     line-height: 20px;
     word-wrap: break-word;
     font-weight: 400;
+    max-height: 50vh;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
 
     &__range {
       display: block;
       font-weight: 500;
       margin-bottom: 10px;
+      padding-right: 18px;
     }
 
     &__unread {
diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss
index d7d850a1e..1ecc8434d 100644
--- a/app/javascript/styles/mastodon/polls.scss
+++ b/app/javascript/styles/mastodon/polls.scss
@@ -8,20 +8,18 @@
   }
 
   &__chart {
-    position: absolute;
-    top: 0;
-    left: 0;
-    height: 100%;
-    display: inline-block;
     border-radius: 4px;
-    background: darken($ui-primary-color, 14%);
+    display: block;
+    background: darken($ui-primary-color, 5%);
+    height: 5px;
+    min-width: 1%;
 
     &.leading {
       background: $ui-highlight-color;
     }
   }
 
-  &__text {
+  &__option {
     position: relative;
     display: flex;
     padding: 6px 0;
@@ -29,6 +27,13 @@
     cursor: default;
     overflow: hidden;
 
+    &__text {
+      display: inline-block;
+      word-wrap: break-word;
+      overflow-wrap: break-word;
+      max-width: calc(100% - 45px - 25px);
+    }
+
     input[type=radio],
     input[type=checkbox] {
       display: none;
@@ -95,8 +100,8 @@
     &:active,
     &:focus,
     &:hover {
+      border-color: lighten($valid-value-color, 15%);
       border-width: 4px;
-      background: none;
     }
 
     &::-moz-focus-inner {
@@ -112,19 +117,18 @@
 
   &__number {
     display: inline-block;
-    width: 52px;
+    width: 45px;
     font-weight: 700;
-    padding: 0 10px;
-    padding-left: 8px;
-    text-align: right;
-    margin-top: auto;
-    margin-bottom: auto;
-    flex: 0 0 52px;
+    flex: 0 0 45px;
   }
 
-  &__vote__mark {
-    float: left;
-    line-height: 18px;
+  &__voted {
+    padding: 0 5px;
+    display: inline-block;
+
+    &__mark {
+      font-size: 18px;
+    }
   }
 
   &__footer {
@@ -199,7 +203,7 @@
     display: flex;
     align-items: center;
 
-    .poll__text {
+    .poll__option {
       flex: 0 0 auto;
       width: calc(100% - (23px + 6px));
       margin-right: 6px;
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index ed680d762..1523f86d4 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -15,6 +15,8 @@ class ActivityPub::TagManager
   def url_for(target)
     return target.url if target.respond_to?(:local?) && !target.local?
 
+    return unless target.respond_to?(:object_type)
+
     case target.object_type
     when :person
       target.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(target)
diff --git a/app/lib/entity_cache.rb b/app/lib/entity_cache.rb
index 35a3773d2..afdbd70f2 100644
--- a/app/lib/entity_cache.rb
+++ b/app/lib/entity_cache.rb
@@ -7,6 +7,10 @@ class EntityCache
 
   MAX_EXPIRATION = 7.days.freeze
 
+  def status(url)
+    Rails.cache.fetch(to_key(:status, url), expires_in: MAX_EXPIRATION) { FetchRemoteStatusService.new.call(url) }
+  end
+
   def mention(username, domain)
     Rails.cache.fetch(to_key(:mention, username, domain), expires_in: MAX_EXPIRATION) { Account.select(:id, :username, :domain, :url).find_remote(username, domain) }
   end
diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb
index 01346bfe5..3362576b0 100644
--- a/app/lib/exceptions.rb
+++ b/app/lib/exceptions.rb
@@ -8,6 +8,7 @@ module Mastodon
   class LengthValidationError < ValidationError; end
   class DimensionsValidationError < ValidationError; end
   class RaceConditionError < Error; end
+  class RateLimitExceededError < Error; end
 
   class UnexpectedResponseError < Error
     def initialize(response = nil)
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index fcc99d009..051f27408 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -59,7 +59,7 @@ class Formatter
     html = "RT @#{prepend_reblog} #{html}" if prepend_reblog
     html = format_markdown(html) if status.content_type == 'text/markdown'
     html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type))
-    html = reformat(html) if %w(text/markdown text/html).include?(status.content_type)
+    html = reformat(html, true) if %w(text/markdown text/html).include?(status.content_type)
     html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify]
 
     unless %w(text/markdown text/html).include?(status.content_type)
@@ -75,8 +75,8 @@ class Formatter
     html.delete("\r").delete("\n")
   end
 
-  def reformat(html)
-    sanitize(html, Sanitize::Config::MASTODON_STRICT)
+  def reformat(html, outgoing = false)
+    sanitize(html, Sanitize::Config::MASTODON_STRICT.merge(outgoing: outgoing))
   rescue ArgumentError
     ''
   end
@@ -131,7 +131,7 @@ class Formatter
   end
 
   def link_url(url)
-    "<a href=\"#{encode(url)}\" target=\"blank\" rel=\"nofollow noopener\">#{link_html(url)}</a>"
+    "<a href=\"#{encode(url)}\" target=\"blank\" rel=\"nofollow noopener noreferrer\">#{link_html(url)}</a>"
   end
 
   private
diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb
index 302072bcc..05a06726d 100644
--- a/app/lib/language_detector.rb
+++ b/app/lib/language_detector.rb
@@ -52,8 +52,10 @@ class LanguageDetector
 
   def detect_language_code(text)
     return if unreliable_input?(text)
+
     result = @identifier.find_language(text)
-    iso6391(result.language.to_s).to_sym if result.reliable?
+
+    iso6391(result.language.to_s).to_sym if result&.reliable?
   end
 
   def iso6391(bcp47)
diff --git a/app/lib/rate_limiter.rb b/app/lib/rate_limiter.rb
new file mode 100644
index 000000000..0e2c9a894
--- /dev/null
+++ b/app/lib/rate_limiter.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+class RateLimiter
+  include Redisable
+
+  FAMILIES = {
+    follows: {
+      limit: 400,
+      period: 24.hours.freeze,
+    }.freeze,
+
+    statuses: {
+      limit: 300,
+      period: 3.hours.freeze,
+    }.freeze,
+
+    reports: {
+      limit: 400,
+      period: 24.hours.freeze,
+    }.freeze,
+  }.freeze
+
+  def initialize(by, options = {})
+    @by     = by
+    @family = options[:family]
+    @limit  = FAMILIES[@family][:limit]
+    @period = FAMILIES[@family][:period].to_i
+  end
+
+  def record!
+    count = redis.get(key)
+
+    if count.nil?
+      redis.set(key, 0)
+      redis.expire(key, (@period - (last_epoch_time % @period) + 1).to_i)
+    end
+
+    raise Mastodon::RateLimitExceededError if count.present? && count.to_i >= @limit
+
+    redis.incr(key)
+  end
+
+  def rollback!
+    redis.decr(key)
+  end
+
+  def to_headers(now = Time.now.utc)
+    {
+      'X-RateLimit-Limit' => @limit.to_s,
+      'X-RateLimit-Remaining' => (@limit - (redis.get(key) || 0).to_i).to_s,
+      'X-RateLimit-Reset' => (now + (@period - now.to_i % @period)).iso8601(6),
+    }
+  end
+
+  private
+
+  def key
+    @key ||= "rate_limit:#{@by.id}:#{@family}:#{(last_epoch_time / @period).to_i}"
+  end
+
+  def last_epoch_time
+    @last_epoch_time ||= Time.now.to_i
+  end
+end
diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb
index e3fc94ba6..34793ed93 100644
--- a/app/lib/sanitize_config.rb
+++ b/app/lib/sanitize_config.rb
@@ -54,6 +54,18 @@ class Sanitize
       end
     end
 
+    LINK_REL_TRANSFORMER = lambda do |env|
+      return unless env[:node_name] == 'a'
+
+      node = env[:node]
+
+      rel = (node['rel'] || '').split(' ') & ['tag']
+      unless env[:config][:outgoing] && TagManager.instance.local_url?(node['href'])
+        rel += ['nofollow', 'noopener', 'noreferrer']
+      end
+      node['rel'] = rel.join(' ')
+    end
+
     UNSUPPORTED_HREF_TRANSFORMER = lambda do |env|
       return unless env[:node_name] == 'a'
 
@@ -82,7 +94,6 @@ class Sanitize
 
       add_attributes: {
         'a' => {
-          'rel' => 'nofollow noopener tag noreferrer',
           'target' => '_blank',
         },
       },
@@ -96,6 +107,7 @@ class Sanitize
         CLASS_WHITELIST_TRANSFORMER,
         IMG_TAG_TRANSFORMER,
         UNSUPPORTED_HREF_TRANSFORMER,
+        LINK_REL_TRANSFORMER,
       ]
     )
 
diff --git a/app/models/account.rb b/app/models/account.rb
index 0fcf897c9..82d4d10de 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -46,6 +46,7 @@
 #  silenced_at             :datetime
 #  suspended_at            :datetime
 #  trust_level             :integer
+#  hide_collections        :boolean
 #
 
 class Account < ApplicationRecord
@@ -106,6 +107,7 @@ class Account < ApplicationRecord
   scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
   scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
   scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
+  scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
   scope :popular, -> { order('account_stats.followers_count desc') }
   scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
   scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
@@ -324,6 +326,14 @@ class Account < ApplicationRecord
     save!
   end
 
+  def hides_followers?
+    hide_collections? || user_hides_network?
+  end
+
+  def hides_following?
+    hide_collections? || user_hides_network?
+  end
+
   def object_type
     :person
   end
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index c7bf07787..7b6012e0f 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -14,6 +14,7 @@ class AccountFilter
     email
     ip
     staff
+    order
   ).freeze
 
   attr_reader :params
@@ -24,7 +25,7 @@ class AccountFilter
   end
 
   def results
-    scope = Account.recent.includes(:user)
+    scope = Account.includes(:user).reorder(nil)
 
     params.each do |key, value|
       scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
@@ -38,6 +39,7 @@ class AccountFilter
   def set_defaults!
     params['local']  = '1' if params['remote'].blank?
     params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank?
+    params['order']  = 'recent' if params['order'].blank?
   end
 
   def scope_for(key, value)
@@ -51,9 +53,9 @@ class AccountFilter
     when 'active'
       Account.without_suspended
     when 'pending'
-      accounts_with_users.merge User.pending
+      accounts_with_users.merge(User.pending)
     when 'disabled'
-      accounts_with_users.merge User.disabled
+      accounts_with_users.merge(User.disabled)
     when 'silenced'
       Account.silenced
     when 'suspended'
@@ -63,16 +65,31 @@ class AccountFilter
     when 'display_name'
       Account.matches_display_name(value)
     when 'email'
-      accounts_with_users.merge User.matches_email(value)
+      accounts_with_users.merge(User.matches_email(value))
     when 'ip'
       valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
     when 'staff'
-      accounts_with_users.merge User.staff
+      accounts_with_users.merge(User.staff)
+    when 'order'
+      order_scope(value)
     else
       raise "Unknown filter: #{key}"
     end
   end
 
+  def order_scope(value)
+    case value
+    when 'active'
+      params['remote'] ? Account.joins(:account_stat).by_recent_status : Account.joins(:user).by_recent_sign_in
+    when 'recent'
+      Account.recent
+    when 'alphabetic'
+      Account.alphabetic
+    else
+      raise "Unknown order: #{value}"
+    end
+  end
+
   def accounts_with_users
     Account.joins(:user)
   end
diff --git a/app/models/account_warning_preset.rb b/app/models/account_warning_preset.rb
index ba8ceabb3..c20f683cf 100644
--- a/app/models/account_warning_preset.rb
+++ b/app/models/account_warning_preset.rb
@@ -8,8 +8,11 @@
 #  text       :text             default(""), not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
+#  title      :string           default(""), not null
 #
 
 class AccountWarningPreset < ApplicationRecord
   validates :text, presence: true
+
+  scope :alphabetic, -> { order(title: :asc, text: :asc) }
 end
diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb
index e9da003a3..b30a82369 100644
--- a/app/models/admin/account_action.rb
+++ b/app/models/admin/account_action.rb
@@ -62,8 +62,6 @@ class Admin::AccountAction
 
   def process_action!
     case type
-    when 'none'
-      handle_resolve!
     when 'disable'
       handle_disable!
     when 'silence'
@@ -105,16 +103,6 @@ class Admin::AccountAction
     end
   end
 
-  def handle_resolve!
-    if with_report? && report.account_id == -99 && target_account.trust_level == Account::TRUST_LEVELS[:untrusted]
-      # This is an automated report and it is being dismissed, so it's
-      # a false positive, in which case update the account's trust level
-      # to prevent further spam checks
-
-      target_account.update(trust_level: Account::TRUST_LEVELS[:trusted])
-    end
-  end
-
   def handle_disable!
     authorize(target_account.user, :disable?)
     log_action(:disable, target_account.user)
diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb
new file mode 100644
index 000000000..0ba7e1609
--- /dev/null
+++ b/app/models/admin/action_log_filter.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+class Admin::ActionLogFilter
+  KEYS = %i(
+    action_type
+    account_id
+    target_account_id
+  ).freeze
+
+  ACTION_TYPE_MAP = {
+    assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
+    change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
+    confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
+    create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze,
+    create_announcement: { target_type: 'Announcement', action: 'create' }.freeze,
+    create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze,
+    create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
+    create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
+    create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
+    demote_user: { target_type: 'User', action: 'demote' }.freeze,
+    destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
+    destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
+    destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
+    destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
+    destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
+    destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
+    disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
+    disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
+    disable_user: { target_type: 'User', action: 'disable' }.freeze,
+    enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze,
+    enable_user: { target_type: 'User', action: 'enable' }.freeze,
+    memorialize_account: { target_type: 'Account', action: 'memorialize' }.freeze,
+    promote_user: { target_type: 'User', action: 'promote' }.freeze,
+    remove_avatar_user: { target_type: 'User', action: 'remove_avatar' }.freeze,
+    reopen_report: { target_type: 'Report', action: 'reopen' }.freeze,
+    reset_password_user: { target_type: 'User', action: 'reset_password' }.freeze,
+    resolve_report: { target_type: 'Report', action: 'resolve' }.freeze,
+    silence_account: { target_type: 'Account', action: 'silence' }.freeze,
+    suspend_account: { target_type: 'Account', action: 'suspend' }.freeze,
+    unassigned_report: { target_type: 'Report', action: 'unassigned' }.freeze,
+    unsilence_account: { target_type: 'Account', action: 'unsilence' }.freeze,
+    unsuspend_account: { target_type: 'Account', action: 'unsuspend' }.freeze,
+    update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
+    update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze,
+    update_status: { target_type: 'Status', action: 'update' }.freeze,
+  }.freeze
+
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def results
+    scope = Admin::ActionLog.includes(:target)
+
+    params.each do |key, value|
+      next if key.to_s == 'page'
+
+      scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present?
+    end
+
+    scope
+  end
+
+  private
+
+  def scope_for(key, value)
+    case key
+    when 'action_type'
+      Admin::ActionLog.where(ACTION_TYPE_MAP[value.to_sym])
+    when 'account_id'
+      Admin::ActionLog.where(account_id: value)
+    when 'target_account_id'
+      account = Account.find(value)
+      Admin::ActionLog.where(target: [account, account.user].compact)
+    else
+      raise "Unknown filter: #{key}"
+    end
+  end
+end
diff --git a/app/models/announcement.rb b/app/models/announcement.rb
index d99502f44..a4e427b49 100644
--- a/app/models/announcement.rb
+++ b/app/models/announcement.rb
@@ -14,6 +14,7 @@
 #  created_at   :datetime         not null
 #  updated_at   :datetime         not null
 #  published_at :datetime
+#  status_ids   :bigint           is an Array
 #
 
 class Announcement < ApplicationRecord
@@ -48,6 +49,16 @@ class Announcement < ApplicationRecord
     @mentions ||= Account.from_text(text)
   end
 
+  def statuses
+    @statuses ||= begin
+      if status_ids.nil?
+        []
+      else
+        Status.where(id: status_ids, visibility: [:public, :unlisted])
+      end
+    end
+  end
+
   def tags
     @tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text))
   end
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 14bcf7bb1..32fcb5397 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -87,10 +87,10 @@ module AccountInteractions
     has_many :announcement_mutes, dependent: :destroy
   end
 
-  def follow!(other_account, reblogs: nil, uri: nil)
+  def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
     reblogs = true if reblogs.nil?
 
-    rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri)
+    rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
                               .find_or_create_by!(target_account: other_account)
 
     rel.update!(show_reblogs: reblogs)
@@ -99,6 +99,18 @@ module AccountInteractions
     rel
   end
 
+  def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
+    reblogs = true if reblogs.nil?
+
+    rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
+                         .find_or_create_by!(target_account: other_account)
+
+    rel.update!(show_reblogs: reblogs)
+    remove_potential_friendship(other_account)
+
+    rel
+  end
+
   def block!(other_account, uri: nil)
     remove_potential_friendship(other_account)
     block_relationships.create_with(uri: uri)
diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb
index 43ff8ac12..18b872c1e 100644
--- a/app/models/concerns/attachmentable.rb
+++ b/app/models/concerns/attachmentable.rb
@@ -74,7 +74,7 @@ module Attachmentable
     self.class.attachment_definitions.each_key do |attachment_name|
       attachment = send(attachment_name)
 
-      next if attachment.blank? || attachment.queued_for_write[:original].blank?
+      next if attachment.blank? || attachment.queued_for_write[:original].blank? || attachment.options[:preserve_files]
 
       attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
     end
diff --git a/app/models/concerns/rate_limitable.rb b/app/models/concerns/rate_limitable.rb
new file mode 100644
index 000000000..ad1b5e44e
--- /dev/null
+++ b/app/models/concerns/rate_limitable.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module RateLimitable
+  extend ActiveSupport::Concern
+
+  def rate_limit=(value)
+    @rate_limit = value
+  end
+
+  def rate_limit?
+    @rate_limit
+  end
+
+  def rate_limiter(by, options = {})
+    return @rate_limiter if defined?(@rate_limiter)
+
+    @rate_limiter = RateLimiter.new(by, options)
+  end
+
+  class_methods do
+    def rate_limit(options = {})
+      after_create do
+        by = public_send(options[:by])
+
+        if rate_limit? && by&.local?
+          rate_limiter(by, options).record!
+          @rate_limit_recorded = true
+        end
+      end
+
+      after_rollback do
+        rate_limiter(public_send(options[:by]), options).rollback! if @rate_limit_recorded
+      end
+    end
+  end
+end
diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb
index bc70dea25..f50fa46ba 100644
--- a/app/models/email_domain_block.rb
+++ b/app/models/email_domain_block.rb
@@ -7,13 +7,27 @@
 #  domain     :string           default(""), not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
+#  parent_id  :bigint(8)
 #
 
 class EmailDomainBlock < ApplicationRecord
   include DomainNormalizable
 
+  belongs_to :parent, class_name: 'EmailDomainBlock', optional: true
+  has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
+
   validates :domain, presence: true, uniqueness: true, domain: true
 
+  def with_dns_records=(val)
+    @with_dns_records = ActiveModel::Type::Boolean.new.cast(val)
+  end
+
+  def with_dns_records?
+    @with_dns_records
+  end
+
+  alias with_dns_records with_dns_records?
+
   def self.block?(email)
     _, domain = email.split('@', 2)
 
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 87fa11425..f3e48a2ed 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -15,6 +15,9 @@
 class Follow < ApplicationRecord
   include Paginable
   include RelationshipCacheable
+  include RateLimitable
+
+  rate_limit by: :account, family: :follows
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 96ac7eaa5..3325e264c 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -15,6 +15,9 @@
 class FollowRequest < ApplicationRecord
   include Paginable
   include RelationshipCacheable
+  include RateLimitable
+
+  rate_limit by: :account, family: :follows
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 6a0b892f6..40624c73c 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -19,12 +19,14 @@
 #  description         :text
 #  scheduled_status_id :bigint(8)
 #  blurhash            :string
+#  processing          :integer
 #
 
 class MediaAttachment < ApplicationRecord
   self.inheritance_column = nil
 
   enum type: [:image, :gifv, :video, :unknown, :audio]
+  enum processing: [:queued, :in_progress, :complete, :failed], _prefix: true
 
   MAX_DESCRIPTION_LENGTH = 1_500
 
@@ -55,6 +57,43 @@ class MediaAttachment < ApplicationRecord
     },
   }.freeze
 
+  VIDEO_FORMAT = {
+    format: 'mp4',
+    content_type: 'video/mp4',
+    convert_options: {
+      output: {
+        'loglevel' => 'fatal',
+        'movflags' => 'faststart',
+        'pix_fmt' => 'yuv420p',
+        'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
+        'vsync' => 'cfr',
+        'c:v' => 'h264',
+        'maxrate' => '1300K',
+        'bufsize' => '1300K',
+        'frames:v' => 60 * 60 * 3,
+        'crf' => 18,
+        'map_metadata' => '-1',
+      },
+    },
+  }.freeze
+
+  VIDEO_PASSTHROUGH_OPTIONS = {
+    video_codecs: ['h264'],
+    audio_codecs: ['aac', nil],
+    colorspaces: ['yuv420p'],
+    options: {
+      format: 'mp4',
+      convert_options: {
+        output: {
+          'loglevel' => 'fatal',
+          'map_metadata' => '-1',
+          'c:v' => 'copy',
+          'c:a' => 'copy',
+        },
+      },
+    },
+  }.freeze
+
   VIDEO_STYLES = {
     small: {
       convert_options: {
@@ -69,17 +108,7 @@ class MediaAttachment < ApplicationRecord
       blurhash: BLURHASH_OPTIONS,
     },
 
-    original: {
-      keep_same_format: true,
-      convert_options: {
-        output: {
-          'loglevel' => 'fatal',
-          'map_metadata' => '-1',
-          'c:v' => 'copy',
-          'c:a' => 'copy',
-        },
-      },
-    },
+    original: VIDEO_FORMAT.merge(passthrough_options: VIDEO_PASSTHROUGH_OPTIONS),
   }.freeze
 
   AUDIO_STYLES = {
@@ -96,26 +125,6 @@ class MediaAttachment < ApplicationRecord
     },
   }.freeze
 
-  VIDEO_FORMAT = {
-    format: 'mp4',
-    content_type: 'video/mp4',
-    convert_options: {
-      output: {
-        'loglevel' => 'fatal',
-        'movflags' => 'faststart',
-        'pix_fmt' => 'yuv420p',
-        'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
-        'vsync' => 'cfr',
-        'c:v' => 'h264',
-        'maxrate' => '1300K',
-        'bufsize' => '1300K',
-        'frames:v' => 60 * 60 * 3,
-        'crf' => 18,
-        'map_metadata' => '-1',
-      },
-    },
-  }.freeze
-
   VIDEO_CONVERTED_STYLES = {
     small: VIDEO_STYLES[:small],
     original: VIDEO_FORMAT,
@@ -124,6 +133,9 @@ class MediaAttachment < ApplicationRecord
   IMAGE_LIMIT = (ENV['MAX_IMAGE_SIZE'] || 10.megabytes).to_i
   VIDEO_LIMIT = (ENV['MAX_VIDEO_SIZE'] || 40.megabytes).to_i
 
+  MAX_VIDEO_MATRIX_LIMIT = 2_304_000 # 1920x1200px
+  MAX_VIDEO_FRAME_RATE   = 60
+
   belongs_to :account,          inverse_of: :media_attachments, optional: true
   belongs_to :status,           inverse_of: :media_attachments, optional: true
   belongs_to :scheduled_status, inverse_of: :media_attachments, optional: true
@@ -156,6 +168,10 @@ class MediaAttachment < ApplicationRecord
     remote_url.blank?
   end
 
+  def not_processed?
+    processing.present? && !processing_complete?
+  end
+
   def needs_redownload?
     file.blank? && remote_url.present?
   end
@@ -168,18 +184,6 @@ class MediaAttachment < ApplicationRecord
     audio? || video?
   end
 
-  def variant?(other_file_name)
-    return true if file_file_name == other_file_name
-
-    formats = file.styles.values.map(&:format).compact
-
-    return false if formats.empty?
-
-    extension = File.extname(other_file_name)
-
-    formats.include?(extension.delete('.')) && File.basename(other_file_name, extension) == File.basename(file_file_name, File.extname(file_file_name))
-  end
-
   def to_param
     shortcode
   end
@@ -202,12 +206,21 @@ class MediaAttachment < ApplicationRecord
     "#{x},#{y}"
   end
 
+  attr_writer :delay_processing
+
+  def delay_processing?
+    @delay_processing
+  end
+
+  after_commit :enqueue_processing, on: :create
   after_commit :reset_parent_cache, on: :update
 
   before_create :prepare_description, unless: :local?
   before_create :set_shortcode
+  before_create :set_processing
 
   before_post_process :set_type_and_extension
+  before_post_process :check_video_dimensions
 
   before_save :set_meta
 
@@ -276,6 +289,21 @@ class MediaAttachment < ApplicationRecord
     end
   end
 
+  def set_processing
+    self.processing = delay_processing? ? :queued : :complete
+  end
+
+  def check_video_dimensions
+    return unless (video? || gifv?) && file.queued_for_write[:original].present?
+
+    movie = FFMPEG::Movie.new(file.queued_for_write[:original].path)
+
+    return unless movie.valid?
+
+    raise Mastodon::DimensionsValidationError, "#{movie.width}x#{movie.height} videos are not supported" if movie.width * movie.height > MAX_VIDEO_MATRIX_LIMIT
+    raise Mastodon::DimensionsValidationError, "#{movie.frame_rate.to_i}fps videos are not supported" if movie.frame_rate > MAX_VIDEO_FRAME_RATE
+  end
+
   def set_meta
     meta = populate_meta
 
@@ -321,9 +349,11 @@ class MediaAttachment < ApplicationRecord
     }.compact
   end
 
-  def reset_parent_cache
-    return if status_id.nil?
+  def enqueue_processing
+    PostProcessMediaWorker.perform_async(id) if delay_processing?
+  end
 
-    Rails.cache.delete("statuses/#{status_id}")
+  def reset_parent_cache
+    Rails.cache.delete("statuses/#{status_id}") if status_id.present?
   end
 end
diff --git a/app/models/report.rb b/app/models/report.rb
index fb2e040ee..f31bcfd2e 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -18,6 +18,9 @@
 
 class Report < ApplicationRecord
   include Paginable
+  include RateLimitable
+
+  rate_limit by: :account, family: :reports
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
@@ -59,6 +62,14 @@ class Report < ApplicationRecord
   end
 
   def resolve!(acting_account)
+    if account_id == -99 && target_account.trust_level == Account::TRUST_LEVELS[:untrusted]
+      # This is an automated report and it is being dismissed, so it's
+      # a false positive, in which case update the account's trust level
+      # to prevent further spam checks
+
+      target_account.update(trust_level: Account::TRUST_LEVELS[:trusted])
+    end
+
     RemovalWorker.push_bulk(Status.with_discarded.discarded.where(id: status_ids).pluck(:id)) { |status_id| [status_id, { immediate: true }] }
     update!(action_taken: true, action_taken_by_account_id: acting_account.id)
   end
diff --git a/app/models/status.rb b/app/models/status.rb
index f4284f771..31e77770d 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -35,6 +35,9 @@ class Status < ApplicationRecord
   include Paginable
   include Cacheable
   include StatusThreadingConcern
+  include RateLimitable
+
+  rate_limit by: :account, family: :statuses
 
   self.discard_column = :deleted_at
 
@@ -145,10 +148,12 @@ class Status < ApplicationRecord
       ids += mentions.where(account: Account.local).pluck(:account_id)
       ids += favourites.where(account: Account.local).pluck(:account_id)
       ids += reblogs.where(account: Account.local).pluck(:account_id)
+      ids += bookmarks.where(account: Account.local).pluck(:account_id)
     else
       ids += preloaded.mentions[id] || []
       ids += preloaded.favourites[id] || []
       ids += preloaded.reblogs[id] || []
+      ids += preloaded.bookmarks[id] || []
     end
 
     ids.uniq
@@ -416,6 +421,21 @@ class Status < ApplicationRecord
       end
     end
 
+    def from_text(text)
+      return [] if text.blank?
+
+      text.scan(FetchLinkCardService::URL_PATTERN).map(&:first).uniq.map do |url|
+        status = begin
+          if TagManager.instance.local_url?(url)
+            ActivityPub::TagManager.instance.uri_to_resource(url, Status)
+          else
+            EntityCache.instance.status(url)
+          end
+        end
+        status&.distributable? ? status : nil
+      end.compact
+    end
+
     private
 
     def timeline_scope(local_only = false)
diff --git a/app/policies/settings_policy.rb b/app/policies/settings_policy.rb
index 2dcb79f51..874f97bab 100644
--- a/app/policies/settings_policy.rb
+++ b/app/policies/settings_policy.rb
@@ -8,4 +8,8 @@ class SettingsPolicy < ApplicationPolicy
   def show?
     admin?
   end
+
+  def destroy?
+    admin?
+  end
 end
diff --git a/app/serializers/rest/announcement_serializer.rb b/app/serializers/rest/announcement_serializer.rb
index f27feb669..9343b97d2 100644
--- a/app/serializers/rest/announcement_serializer.rb
+++ b/app/serializers/rest/announcement_serializer.rb
@@ -7,6 +7,7 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer
   attribute :read, if: :current_user?
 
   has_many :mentions
+  has_many :statuses
   has_many :tags, serializer: REST::StatusSerializer::TagSerializer
   has_many :emojis, serializer: REST::CustomEmojiSerializer
   has_many :reactions, serializer: REST::ReactionSerializer
@@ -46,4 +47,16 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer
       object.pretty_acct
     end
   end
+
+  class StatusSerializer < ActiveModel::Serializer
+    attributes :id, :url
+
+    def id
+      object.id.to_s
+    end
+
+    def url
+      ActivityPub::TagManager.instance.url_for(object)
+    end
+  end
 end
diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb
index 1b3498ea4..cc10e3001 100644
--- a/app/serializers/rest/media_attachment_serializer.rb
+++ b/app/serializers/rest/media_attachment_serializer.rb
@@ -12,7 +12,9 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
   end
 
   def url
-    if object.needs_redownload?
+    if object.not_processed?
+      nil
+    elsif object.needs_redownload?
       media_proxy_url(object.id, :original)
     else
       full_asset_url(object.file.url(:original))
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index d217dabb3..493813aab 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -171,7 +171,7 @@ class AccountSearchService < BaseService
   end
 
   def username_complete?
-    query.include?('@') && "@#{query}" =~ Account::MENTION_RE
+    query.include?('@') && "@#{query}" =~ /\A#{Account::MENTION_RE}\Z/
   end
 
   def likely_acct?
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index d5ede0388..7b4c53d50 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -94,6 +94,7 @@ class ActivityPub::ProcessAccountService < BaseService
     @account.statuses_count    = outbox_total_items    if outbox_total_items.present?
     @account.following_count   = following_total_items if following_total_items.present?
     @account.followers_count   = followers_total_items if followers_total_items.present?
+    @account.hide_collections  = following_private? || followers_private?
     @account.moved_to_account  = @json['movedTo'].present? ? moved_account : nil
   end
 
@@ -166,26 +167,36 @@ class ActivityPub::ProcessAccountService < BaseService
   end
 
   def outbox_total_items
-    collection_total_items('outbox')
+    collection_info('outbox').first
   end
 
   def following_total_items
-    collection_total_items('following')
+    collection_info('following').first
   end
 
   def followers_total_items
-    collection_total_items('followers')
+    collection_info('followers').first
   end
 
-  def collection_total_items(type)
-    return if @json[type].blank?
+  def following_private?
+    !collection_info('following').last
+  end
+
+  def followers_private?
+    !collection_info('followers').last
+  end
+
+  def collection_info(type)
+    return [nil, nil] if @json[type].blank?
     return @collections[type] if @collections.key?(type)
 
     collection = fetch_resource_without_id_validation(@json[type])
 
-    @collections[type] = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
+    total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
+    has_first_page = collection.is_a?(Hash) && collection['first'].present?
+    @collections[type] = [total_items, has_first_page]
   rescue HTTP::Error, OpenSSL::SSL::SSLError
-    @collections[type] = nil
+    @collections[type] = [nil, nil]
   end
 
   def moved_account
diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb
index abe7766d4..880cdde92 100644
--- a/app/services/fetch_resource_service.rb
+++ b/app/services/fetch_resource_service.rb
@@ -5,6 +5,8 @@ class FetchResourceService < BaseService
 
   ACCEPT_HEADER = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", text/html;q=0.1'
 
+  attr_reader :response_code
+
   def call(url)
     return if url.blank?
 
@@ -27,6 +29,7 @@ class FetchResourceService < BaseService
   end
 
   def process_response(response, terminal = false)
+    @response_code = response.code
     return nil if response.code != 200
 
     if ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 4d19002c4..311ae7fa6 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -7,54 +7,68 @@ class FollowService < BaseService
   # Follow a remote user, notify remote user about the follow
   # @param [Account] source_account From which to follow
   # @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
-  # @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
-  def call(source_account, target_account, reblogs: nil, bypass_locked: false)
-    reblogs = true if reblogs.nil?
-    target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
-
-    raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
-    raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? || (!target_account.local? && target_account.ostatus?) || source_account.domain_blocking?(target_account.domain)
-
-    if source_account.following?(target_account)
-      # We're already following this account, but we'll call follow! again to
-      # make sure the reblogs status is set correctly.
-      return source_account.follow!(target_account, reblogs: reblogs)
-    elsif source_account.requested?(target_account)
-      # This isn't managed by a method in AccountInteractions, so we modify it
-      # ourselves if necessary.
-      req = source_account.follow_requests.find_by(target_account: target_account)
-      req.update!(show_reblogs: reblogs)
-      return req
+  # @param [Hash] options
+  # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
+  # @option [Boolean] :bypass_locked
+  # @option [Boolean] :with_rate_limit
+  def call(source_account, target_account, options = {})
+    @source_account = source_account
+    @target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
+    @options        = { reblogs: true, bypass_locked: false, with_rate_limit: false }.merge(options)
+
+    raise ActiveRecord::RecordNotFound if following_not_possible?
+    raise Mastodon::NotPermittedError  if following_not_allowed?
+
+    if @source_account.following?(@target_account)
+      return change_follow_options!
+    elsif @source_account.requested?(@target_account)
+      return change_follow_request_options!
     end
 
     ActivityTracker.increment('activity:interactions')
 
-    if (target_account.locked? && !bypass_locked) || source_account.silenced? || target_account.activitypub?
-      request_follow(source_account, target_account, reblogs: reblogs)
-    elsif target_account.local?
-      direct_follow(source_account, target_account, reblogs: reblogs)
+    if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub?
+      request_follow!
+    elsif @target_account.local?
+      direct_follow!
     end
   end
 
   private
 
-  def request_follow(source_account, target_account, reblogs: true)
-    follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
+  def following_not_possible?
+    @target_account.nil? || @target_account.id == @source_account.id || @target_account.suspended?
+  end
+
+  def following_not_allowed?
+    @target_account.blocking?(@source_account) || @source_account.blocking?(@target_account) || @target_account.moved? || (!@target_account.local? && @target_account.ostatus?) || @source_account.domain_blocking?(@target_account.domain)
+  end
+
+  def change_follow_options!
+    @source_account.follow!(@target_account, reblogs: @options[:reblogs])
+  end
+
+  def change_follow_request_options!
+    @source_account.request_follow!(@target_account, reblogs: @options[:reblogs])
+  end
+
+  def request_follow!
+    follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
 
-    if target_account.local?
-      LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name)
-    elsif target_account.activitypub?
-      ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url)
+    if @target_account.local?
+      LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name)
+    elsif @target_account.activitypub?
+      ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
     end
 
     follow_request
   end
 
-  def direct_follow(source_account, target_account, reblogs: true)
-    follow = source_account.follow!(target_account, reblogs: reblogs)
+  def direct_follow!
+    follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
 
-    LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
-    MergeWorker.perform_async(target_account.id, source_account.id)
+    LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name)
+    MergeWorker.perform_async(@target_account.id, @source_account.id)
 
     follow
   end
diff --git a/app/services/import_service.rb b/app/services/import_service.rb
index 4ee431ea3..c0d741d57 100644
--- a/app/services/import_service.rb
+++ b/app/services/import_service.rb
@@ -64,7 +64,8 @@ class ImportService < BaseService
   end
 
   def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
-    items = @data.take(limit).map { |row| [row['Account address']&.strip, Hash[extra_fields.map { |key, header| [key, row[header]&.strip] }]] }.reject { |(id, _)| id.blank? }
+    local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
+    items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, header| [key, row[header]&.strip] }]] }.reject { |(id, _)| id.blank? }
 
     if @import.overwrite?
       presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 936e6ac55..250d0e8ed 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -19,6 +19,7 @@ class PostStatusService < BaseService
   # @option [Enumerable] :media_ids Optional array of media IDs to attach
   # @option [Doorkeeper::Application] :application
   # @option [String] :idempotency Optional idempotency key
+  # @option [Boolean] :with_rate_limit
   # @return [Status]
   def call(account, options = {})
     @account     = account
@@ -49,16 +50,17 @@ class PostStatusService < BaseService
   def preprocess_attributes!
     if @text.blank? && @options[:spoiler_text].present?
      @text = '.'
-     if @media.find(&:video?) || @media.find(&:gifv?)
+     if @media&.find(&:video?) || @media&.find(&:gifv?)
        @text = '📹'
-     elsif @media.find(&:audio?)
+     elsif @media&.find(&:audio?)
        @text = '🎵'
-     elsif @media.find(&:image?)
+     elsif @media&.find(&:image?)
        @text = '🖼'
      end
     end
+    @sensitive    = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?
     @visibility   = @options[:visibility] || @account.user&.setting_default_privacy
-    @visibility   = :unlisted if @visibility == :public && @account.silenced?
+    @visibility   = :unlisted if @visibility&.to_sym == :public && @account.silenced?
     @scheduled_at = @options[:scheduled_at]&.to_datetime
     @scheduled_at = nil if scheduled_in_the_past?
   rescue ArgumentError
@@ -109,6 +111,7 @@ class PostStatusService < BaseService
     @media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i))
 
     raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:audio_or_video?)
+    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.not_ready') if @media.any?(&:not_processed?)
   end
 
   def language_from_option(str)
@@ -164,12 +167,13 @@ class PostStatusService < BaseService
       media_attachments: @media || [],
       thread: @in_reply_to,
       poll_attributes: poll_attributes,
-      sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?,
+      sensitive: @sensitive,
       spoiler_text: @options[:spoiler_text] || '',
       visibility: @visibility,
       language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account),
       application: @options[:application],
       content_type: @options[:content_type] || @account.user&.setting_default_content_type,
+      rate_limit: @options[:with_rate_limit],
     }.compact
   end
 
@@ -189,10 +193,11 @@ class PostStatusService < BaseService
 
   def scheduled_options
     @options.tap do |options_hash|
-      options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id
-      options_hash[:application_id] = options_hash.delete(:application)&.id
-      options_hash[:scheduled_at]   = nil
-      options_hash[:idempotency]    = nil
+      options_hash[:in_reply_to_id]  = options_hash.delete(:thread)&.id
+      options_hash[:application_id]  = options_hash.delete(:application)&.id
+      options_hash[:scheduled_at]    = nil
+      options_hash[:idempotency]     = nil
+      options_hash[:with_rate_limit] = false
     end
   end
 end
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 0b12f143c..0a46509f8 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -8,6 +8,8 @@ class ReblogService < BaseService
   # @param [Account] account Account to reblog from
   # @param [Status] reblogged_status Status to be reblogged
   # @param [Hash] options
+  # @option [String]  :visibility
+  # @option [Boolean] :with_rate_limit
   # @return [Status]
   def call(account, reblogged_status, options = {})
     reblogged_status = reblogged_status.reblog if reblogged_status.reblog?
@@ -18,9 +20,15 @@ class ReblogService < BaseService
 
     return reblog unless reblog.nil?
 
-    visibility = options[:visibility] || account.user&.setting_default_privacy
-    visibility = reblogged_status.visibility if reblogged_status.hidden?
-    reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
+    visibility = begin
+      if reblogged_status.hidden?
+        reblogged_status.visibility
+      else
+        options[:visibility] || account.user&.setting_default_privacy
+      end
+    end
+
+    reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit])
 
     DistributionWorker.perform_async(reblog.id)
     ActivityPub::DistributionWorker.perform_async(reblog.id) unless reblogged_status.local_only?
@@ -45,7 +53,9 @@ class ReblogService < BaseService
 
   def bump_potential_friendship(account, reblog)
     ActivityTracker.increment('activity:interactions')
+
     return if account.following?(reblog.reblog.account_id)
+
     PotentialFriendshipTracker.record(account.id, reblog.reblog.account_id, :reblog)
   end
 
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
index 1a2b0d60c..78080d878 100644
--- a/app/services/resolve_url_service.rb
+++ b/app/services/resolve_url_service.rb
@@ -12,7 +12,7 @@ class ResolveURLService < BaseService
       process_local_url
     elsif !fetched_resource.nil?
       process_url
-    elsif @on_behalf_of.present?
+    else
       process_url_from_db
     end
   end
@@ -30,6 +30,8 @@ class ResolveURLService < BaseService
   end
 
   def process_url_from_db
+    return unless @on_behalf_of.present? && [401, 403, 404].include?(fetch_resource_service.response_code)
+
     # It may happen that the resource is a private toot, and thus not fetchable,
     # but we can return the toot if we already know about it.
     status = Status.find_by(uri: @url) || Status.find_by(url: @url)
@@ -40,7 +42,11 @@ class ResolveURLService < BaseService
   end
 
   def fetched_resource
-    @fetched_resource ||= FetchResourceService.new.call(@url)
+    @fetched_resource ||= fetch_resource_service.call(@url)
+  end
+
+  def fetch_resource_service
+    @_fetch_resource_service ||= FetchResourceService.new
   end
 
   def resource_url
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 090fd409b..830de4de3 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -10,10 +10,10 @@ class SearchService < BaseService
     @resolve = options[:resolve] || false
 
     default_results.tap do |results|
-      next if @query.blank?
+      next if @query.blank? || @limit.zero?
 
       if url_query?
-        results.merge!(url_resource_results) unless url_resource.nil? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
+        results.merge!(url_resource_results) unless url_resource.nil? || @offset.positive? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
       elsif @query.present?
         results[:accounts] = perform_accounts_search! if account_searchable?
         results[:statuses] = perform_statuses_search! if full_text_searchable?
diff --git a/app/views/admin/account_actions/new.html.haml b/app/views/admin/account_actions/new.html.haml
index 20fbeef33..aa88b1448 100644
--- a/app/views/admin/account_actions/new.html.haml
+++ b/app/views/admin/account_actions/new.html.haml
@@ -21,7 +21,7 @@
 
     - unless @warning_presets.empty?
       .fields-group
-        = f.input :warning_preset_id, collection: @warning_presets, label_method: :text, wrapper: :with_block_label
+        = f.input :warning_preset_id, collection: @warning_presets, label_method: ->(warning_preset) { [warning_preset.title.presence, truncate(warning_preset.text)].compact.join(' - ') }, wrapper: :with_block_label
 
     .fields-group
       = f.input :text, as: :text, wrapper: :with_block_label, hint: t('simple_form.hints.admin_account_action.text_html', path: admin_warning_presets_path)
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index b057d3e42..44b10af6e 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -11,6 +11,8 @@
   %td
     - if account.user_current_sign_in_at
       %time.time-ago{ datetime: account.user_current_sign_in_at.iso8601, title: l(account.user_current_sign_in_at) }= l account.user_current_sign_in_at
+    - elsif account.last_status_at.present?
+      %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
     - else
       \-
   %td
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 3a85324c9..7592161c9 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -19,6 +19,12 @@
     %ul
       %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil
       %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1'
+  .filter-subset
+    %strong= t 'generic.order_by'
+    %ul
+      %li= filter_link_to t('relationships.most_recent'), order: nil
+      %li= filter_link_to t('admin.accounts.username'), order: 'alphabetic'
+      %li= filter_link_to t('relationships.last_active'), order: 'active'
 
 = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
   .fields-group
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index a30b78db2..965fd6fb6 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -53,7 +53,7 @@
       .dashboard__counters__num= number_with_delimiter @account.targeted_reports.count
       .dashboard__counters__label= t '.targeted_reports'
   %div
-    %div
+    = link_to admin_action_logs_path(target_account_id: @account.id) do
       .dashboard__counters__text
         - if @account.local? && @account.user.nil?
           %span.neutral= t('admin.accounts.deleted')
@@ -96,10 +96,17 @@
               = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
 
           %tr
-            %th= t('admin.accounts.email')
-            %td= @account.user_email
+            %th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email')
+            %td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= @account.user_email
             %td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user)
 
+          %tr
+            %td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{@account.user_email.split('@').last}")
+
+          - if can?(:create, :email_domain_block)
+            %tr
+              %td= table_link_to 'ban', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: @account.user_email.split('@').last)
+
           - if @account.user_unconfirmed_email.present?
             %tr
               %th= t('admin.accounts.unconfirmed_email')
@@ -204,7 +211,7 @@
         = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
 
       - unless @account.local?
-        - if DomainBlock.where(domain: @account.domain).exists?
+        - if DomainBlock.rule_for(@account.domain)
           = link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button'
         - else
           = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
diff --git a/app/views/admin/action_logs/_action_log.html.haml b/app/views/admin/action_logs/_action_log.html.haml
index a545e189e..59905f341 100644
--- a/app/views/admin/action_logs/_action_log.html.haml
+++ b/app/views/admin/action_logs/_action_log.html.haml
@@ -7,9 +7,3 @@
         = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe
       .log-entry__timestamp
         %time.formatted{ datetime: action_log.created_at.iso8601 }
-    .spacer
-    .log-entry__icon
-      = fa_icon icon_for_log(action_log)
-      .log-entry__icon__overlay{ class: class_for_log_icon(action_log) }
-  .log-entry__extras
-    = log_extra_attributes relevant_log_changes(action_log)
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
index a4d3871a9..937664c4b 100644
--- a/app/views/admin/action_logs/index.html.haml
+++ b/app/views/admin/action_logs/index.html.haml
@@ -1,6 +1,25 @@
 - content_for :page_title do
   = t('admin.action_logs.title')
 
-= render @action_logs
+= form_tag admin_action_logs_url, method: 'GET', class: 'simple_form' do
+  = hidden_field_tag :target_account_id, params[:target_account_id] if params[:target_account_id].present?
+
+  .filters
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.action_logs.filter_by_user')
+      .input.select.optional
+        = select_tag :account_id, options_from_collection_for_select(Account.joins(:user).merge(User.staff), :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
+
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.action_logs.filter_by_action')
+      .input.select.optional
+        = select_tag :action_type, options_for_select(Admin::ActionLogFilter::ACTION_TYPE_MAP.keys.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key]}, params[:action_type]), prompt: I18n.t('admin.accounts.moderation.all')
+
+- if @action_logs.empty?
+  %div.muted-hint.center-text
+    = t 'admin.action_logs.empty'
+- else
+  .announcements-list
+    = render @action_logs
 
 = paginate @action_logs
diff --git a/app/views/admin/email_domain_blocks/_email_domain_block.html.haml b/app/views/admin/email_domain_blocks/_email_domain_block.html.haml
index bf66c9001..41ab8c171 100644
--- a/app/views/admin/email_domain_blocks/_email_domain_block.html.haml
+++ b/app/views/admin/email_domain_blocks/_email_domain_block.html.haml
@@ -3,3 +3,13 @@
     %samp= email_domain_block.domain
   %td
     = table_link_to 'trash', t('admin.email_domain_blocks.delete'), admin_email_domain_block_path(email_domain_block), method: :delete
+
+- email_domain_block.children.each do |child_email_domain_block|
+  %tr
+    %td
+      %samp= child_email_domain_block.domain
+      %span.muted-hint
+        = surround '(', ')' do
+          = t('admin.email_domain_blocks.from_html', domain: content_tag(:samp, email_domain_block.domain))
+    %td
+      = table_link_to 'trash', t('admin.email_domain_blocks.delete'), admin_email_domain_block_path(child_email_domain_block), method: :delete
diff --git a/app/views/admin/email_domain_blocks/new.html.haml b/app/views/admin/email_domain_blocks/new.html.haml
index f372fa512..4a346f240 100644
--- a/app/views/admin/email_domain_blocks/new.html.haml
+++ b/app/views/admin/email_domain_blocks/new.html.haml
@@ -5,7 +5,10 @@
   = render 'shared/error_messages', object: @email_domain_block
 
   .fields-group
-    = f.input :domain, wrapper: :with_label, label: t('admin.email_domain_blocks.domain')
+    = f.input :domain, wrapper: :with_block_label, label: t('admin.email_domain_blocks.domain')
+
+  .fields-group
+    = f.input :with_dns_records, as: :boolean, wrapper: :with_label
 
   .actions
     = f.button :button, t('.create'), type: :submit
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 63b352361..bff706389 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -1,7 +1,10 @@
 - content_for :page_title do
   = t('admin.settings.title')
 
-= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f|
+  - content_for :heading_actions do
+    = button_tag t('generic.save_changes'), class: 'button', form: 'edit_admin'
+
+= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch, id: 'edit_admin' } do |f|
   = render 'shared/error_messages', object: @admin_settings
 
   .fields-group
@@ -27,13 +30,13 @@
 
   .fields-row
     .fields-row__column.fields-row__column-6.fields-group
-      = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html')
+      = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: site_upload_delete_hint(t('admin.settings.thumbnail.desc_html'), :thumbnail)
     .fields-row__column.fields-row__column-6.fields-group
-      = f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: t('admin.settings.hero.desc_html')
+      = f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: site_upload_delete_hint(t('admin.settings.hero.desc_html'), :hero)
 
   .fields-row
     .fields-row__column.fields-row__column-6.fields-group
-      = f.input :mascot, as: :file, wrapper: :with_block_label, label: t('admin.settings.mascot.title'), hint: t('admin.settings.mascot.desc_html')
+      = f.input :mascot, as: :file, wrapper: :with_block_label, label: t('admin.settings.mascot.title'), hint: site_upload_delete_hint(t('admin.settings.mascot.desc_html'), :mascot)
 
   %hr.spacer/
 
diff --git a/app/views/admin/warning_presets/_warning_preset.html.haml b/app/views/admin/warning_presets/_warning_preset.html.haml
new file mode 100644
index 000000000..a58199c80
--- /dev/null
+++ b/app/views/admin/warning_presets/_warning_preset.html.haml
@@ -0,0 +1,10 @@
+.announcements-list__item
+  = link_to edit_admin_warning_preset_path(warning_preset), class: 'announcements-list__item__title' do
+    = warning_preset.title.presence || truncate(warning_preset.text)
+
+  .announcements-list__item__action-bar
+    .announcements-list__item__meta
+      = truncate(warning_preset.text)
+
+    %div
+      = table_link_to 'trash', t('admin.warning_presets.delete'), admin_warning_preset_path(warning_preset), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, warning_preset)
diff --git a/app/views/admin/warning_presets/edit.html.haml b/app/views/admin/warning_presets/edit.html.haml
index 9522746cd..b5c5107ef 100644
--- a/app/views/admin/warning_presets/edit.html.haml
+++ b/app/views/admin/warning_presets/edit.html.haml
@@ -5,6 +5,9 @@
   = render 'shared/error_messages', object: @warning_preset
 
   .fields-group
+    = f.input :title, wrapper: :with_block_label
+
+  .fields-group
     = f.input :text, wrapper: :with_block_label
 
   .actions
diff --git a/app/views/admin/warning_presets/index.html.haml b/app/views/admin/warning_presets/index.html.haml
index 45913ef73..dbc23fa30 100644
--- a/app/views/admin/warning_presets/index.html.haml
+++ b/app/views/admin/warning_presets/index.html.haml
@@ -6,6 +6,9 @@
     = render 'shared/error_messages', object: @warning_preset
 
     .fields-group
+      = f.input :title, wrapper: :with_block_label
+
+    .fields-group
       = f.input :text, wrapper: :with_block_label
 
     .actions
@@ -13,18 +16,9 @@
 
   %hr.spacer/
 
-- unless @warning_presets.empty?
-  .table-wrapper
-    %table.table
-      %thead
-        %tr
-          %th= t('simple_form.labels.account_warning_preset.text')
-          %th
-      %tbody
-        - @warning_presets.each do |preset|
-          %tr
-            %td
-              = Formatter.instance.linkify(preset.text)
-            %td
-              = table_link_to 'pencil', t('admin.warning_presets.edit'), edit_admin_warning_preset_path(preset)
-              = table_link_to 'trash', t('admin.warning_presets.delete'), admin_warning_preset_path(preset), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
+- if @warning_presets.empty?
+  %div.muted-hint.center-text
+    = t 'admin.warning_presets.empty'
+- else
+  .announcements-list
+    = render partial: 'warning_preset', collection: @warning_presets
diff --git a/app/views/errors/429.html.haml b/app/views/errors/429.html.haml
new file mode 100644
index 000000000..2df4f4175
--- /dev/null
+++ b/app/views/errors/429.html.haml
@@ -0,0 +1,5 @@
+- content_for :page_title do
+  = t('errors.429')
+
+- content_for :content do
+  = t('errors.429')
diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml
index f460cebba..5453177fd 100644
--- a/app/views/settings/preferences/appearance/show.html.haml
+++ b/app/views/settings/preferences/appearance/show.html.haml
@@ -1,7 +1,10 @@
 - content_for :page_title do
   = t('settings.appearance')
 
-= simple_form_for current_user, url: settings_preferences_appearance_path, html: { method: :put } do |f|
+- content_for :heading_actions do
+  = button_tag t('generic.save_changes'), class: 'button', form: 'edit_user'
+
+= simple_form_for current_user, url: settings_preferences_appearance_path, html: { method: :put, id: 'edit_user' } do |f|
   .fields-group
     = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false
 
diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml
index a496be21b..d7cc1ed5d 100644
--- a/app/views/settings/preferences/notifications/show.html.haml
+++ b/app/views/settings/preferences/notifications/show.html.haml
@@ -1,7 +1,10 @@
 - content_for :page_title do
   = t('settings.notifications')
 
-= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put } do |f|
+- content_for :heading_actions do
+  = button_tag t('generic.save_changes'), class: 'button', form: 'edit_notification'
+
+= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put, id: 'edit_notification' } do |f|
   = render 'shared/error_messages', object: current_user
 
   %h4= t 'notifications.email_events'
@@ -32,6 +35,3 @@
       = ff.input :must_be_follower, as: :boolean, wrapper: :with_label
       = ff.input :must_be_following, as: :boolean, wrapper: :with_label
       = ff.input :must_be_following_dm, as: :boolean, wrapper: :with_label
-
-  .actions
-    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml
index 9bdcb368d..3b5c7016d 100644
--- a/app/views/settings/preferences/other/show.html.haml
+++ b/app/views/settings/preferences/other/show.html.haml
@@ -1,7 +1,10 @@
 - content_for :page_title do
   = t('settings.preferences')
 
-= simple_form_for current_user, url: settings_preferences_other_path, html: { method: :put } do |f|
+- content_for :heading_actions do
+  = button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences'
+
+= simple_form_for current_user, url: settings_preferences_other_path, html: { method: :put, id: 'edit_preferences' } do |f|
   = render 'shared/error_messages', object: current_user
 
   .fields-group
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index f5d928233..7413be1db 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -1,7 +1,10 @@
 - content_for :page_title do
   = t('settings.edit_profile')
 
-= simple_form_for @account, url: settings_profile_path, html: { method: :put } do |f|
+- content_for :heading_actions do
+  = button_tag t('generic.save_changes'), class: 'button', form: 'edit_profile'
+
+= simple_form_for @account, url: settings_profile_path, html: { method: :put, id: 'edit_profile' } do |f|
   = render 'shared/error_messages', object: @account
 
   .fields-row
diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml
index d1aba6ef9..c17476657 100644
--- a/app/views/statuses/_poll.html.haml
+++ b/app/views/statuses/_poll.html.haml
@@ -8,16 +8,16 @@
       %li
         - if show_results
           - percent = total_votes_count > 0 ? 100 * option.votes_count / total_votes_count : 0
-          %span.poll__chart{ style: "width: #{percent}%" }
-
-          %label.poll__text><
+          %label.poll__option><
             %span.poll__number><
               - if own_votes.include?(index)
-                %i.poll__vote__mark.fa.fa-check
+                %i.poll__voted__mark.fa.fa-check
               = percent.round
             = Formatter.instance.format_poll_option(status, option, autoplay: autoplay)
+
+            %span.poll__chart{ style: "width: #{percent}%" }
         - else
-          %label.poll__text><
+          %label.poll__option><
             %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}><
             = Formatter.instance.format_poll_option(status, option, autoplay: autoplay)
   .poll__footer
diff --git a/app/workers/activitypub/distribute_poll_update_worker.rb b/app/workers/activitypub/distribute_poll_update_worker.rb
index 37a13db2b..601075ea6 100644
--- a/app/workers/activitypub/distribute_poll_update_worker.rb
+++ b/app/workers/activitypub/distribute_poll_update_worker.rb
@@ -4,7 +4,7 @@ class ActivityPub::DistributePollUpdateWorker
   include Sidekiq::Worker
   include Payloadable
 
-  sidekiq_options queue: 'push', unique: :until_executed, retry: 0
+  sidekiq_options queue: 'push', lock: :until_executed, retry: 0
 
   def perform(status_id)
     @status  = Status.find(status_id)
diff --git a/app/workers/activitypub/synchronize_featured_collection_worker.rb b/app/workers/activitypub/synchronize_featured_collection_worker.rb
index 7b16d3426..7a0898e89 100644
--- a/app/workers/activitypub/synchronize_featured_collection_worker.rb
+++ b/app/workers/activitypub/synchronize_featured_collection_worker.rb
@@ -3,7 +3,7 @@
 class ActivityPub::SynchronizeFeaturedCollectionWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'pull', unique: :until_executed
+  sidekiq_options queue: 'pull', lock: :until_executed
 
   def perform(account_id)
     ActivityPub::FetchFeaturedCollectionService.new.call(Account.find(account_id))
diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb
deleted file mode 100644
index ce9c65834..000000000
--- a/app/workers/after_remote_follow_request_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class AfterRemoteFollowRequestWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'pull', retry: 5
-
-  def perform(follow_request_id); end
-end
diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb
deleted file mode 100644
index d9719f2bf..000000000
--- a/app/workers/after_remote_follow_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class AfterRemoteFollowWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'pull', retry: 5
-
-  def perform(follow_id); end
-end
diff --git a/app/workers/backup_worker.rb b/app/workers/backup_worker.rb
index e4c609d70..7b0b52844 100644
--- a/app/workers/backup_worker.rb
+++ b/app/workers/backup_worker.rb
@@ -9,8 +9,12 @@ class BackupWorker
     backup_id = msg['args'].first
 
     ActiveRecord::Base.connection_pool.with_connection do
-      backup = Backup.find(backup_id)
-      backup&.destroy
+      begin
+        backup = Backup.find(backup_id)
+        backup.destroy
+      rescue ActiveRecord::RecordNotFound
+        true
+      end
     end
   end
 
diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb
deleted file mode 100644
index 1c0f001cf..000000000
--- a/app/workers/notification_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class NotificationWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'push', retry: 5
-
-  def perform(xml, source_account_id, target_account_id); end
-end
diff --git a/app/workers/poll_expiration_notify_worker.rb b/app/workers/poll_expiration_notify_worker.rb
index e08f0c249..64b4cbd7e 100644
--- a/app/workers/poll_expiration_notify_worker.rb
+++ b/app/workers/poll_expiration_notify_worker.rb
@@ -3,7 +3,7 @@
 class PollExpirationNotifyWorker
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed
+  sidekiq_options lock: :until_executed
 
   def perform(poll_id)
     poll = Poll.find(poll_id)
diff --git a/app/workers/post_process_media_worker.rb b/app/workers/post_process_media_worker.rb
new file mode 100644
index 000000000..d3ebda194
--- /dev/null
+++ b/app/workers/post_process_media_worker.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class PostProcessMediaWorker
+  include Sidekiq::Worker
+
+  sidekiq_options retry: 1, dead: false
+
+  sidekiq_retries_exhausted do |msg|
+    media_attachment_id = msg['args'].first
+
+    ActiveRecord::Base.connection_pool.with_connection do
+      begin
+        media_attachment = MediaAttachment.find(media_attachment_id)
+        media_attachment.processing = :failed
+        media_attachment.save
+      rescue ActiveRecord::RecordNotFound
+        true
+      end
+    end
+
+    Sidekiq.logger.error("Processing media attachment #{media_attachment_id} failed with #{msg['error_message']}")
+  end
+
+  def perform(media_attachment_id)
+    media_attachment = MediaAttachment.find(media_attachment_id)
+    media_attachment.processing = :in_progress
+    media_attachment.save
+    media_attachment.file.reprocess_original!
+    media_attachment.processing = :complete
+    media_attachment.save
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb
deleted file mode 100644
index cf3bd8397..000000000
--- a/app/workers/processing_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class ProcessingWorker
-  include Sidekiq::Worker
-
-  sidekiq_options backtrace: true
-
-  def perform(account_id, body); end
-end
diff --git a/app/workers/publish_scheduled_announcement_worker.rb b/app/workers/publish_scheduled_announcement_worker.rb
index efca39d3d..1392efed0 100644
--- a/app/workers/publish_scheduled_announcement_worker.rb
+++ b/app/workers/publish_scheduled_announcement_worker.rb
@@ -5,15 +5,24 @@ class PublishScheduledAnnouncementWorker
   include Redisable
 
   def perform(announcement_id)
-    announcement = Announcement.find(announcement_id)
+    @announcement = Announcement.find(announcement_id)
 
-    announcement.publish! unless announcement.published?
+    refresh_status_ids!
 
-    payload = InlineRenderer.render(announcement, nil, :announcement)
+    @announcement.publish! unless @announcement.published?
+
+    payload = InlineRenderer.render(@announcement, nil, :announcement)
     payload = Oj.dump(event: :announcement, payload: payload)
 
     FeedManager.instance.with_active_accounts do |account|
       redis.publish("timeline:#{account.id}", payload) if redis.exists("subscribed:timeline:#{account.id}")
     end
   end
+
+  private
+
+  def refresh_status_ids!
+    @announcement.status_ids = Status.from_text(@announcement.text).map(&:id)
+    @announcement.save
+  end
 end
diff --git a/app/workers/publish_scheduled_status_worker.rb b/app/workers/publish_scheduled_status_worker.rb
index 850610c4e..ce42f7be7 100644
--- a/app/workers/publish_scheduled_status_worker.rb
+++ b/app/workers/publish_scheduled_status_worker.rb
@@ -3,7 +3,7 @@
 class PublishScheduledStatusWorker
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed
+  sidekiq_options lock: :until_executed
 
   def perform(scheduled_status_id)
     scheduled_status = ScheduledStatus.find(scheduled_status_id)
diff --git a/app/workers/pubsubhubbub/confirmation_worker.rb b/app/workers/pubsubhubbub/confirmation_worker.rb
deleted file mode 100644
index 783a8c95f..000000000
--- a/app/workers/pubsubhubbub/confirmation_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Pubsubhubbub::ConfirmationWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'push', retry: false
-
-  def perform(subscription_id, mode, secret = nil, lease_seconds = nil); end
-end
diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb
deleted file mode 100644
index 1260060bd..000000000
--- a/app/workers/pubsubhubbub/delivery_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Pubsubhubbub::DeliveryWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'push', retry: 3, dead: false
-
-  def perform(subscription_id, payload); end
-end
diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb
deleted file mode 100644
index 75bac5d6f..000000000
--- a/app/workers/pubsubhubbub/distribution_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Pubsubhubbub::DistributionWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'push'
-
-  def perform(stream_entry_ids); end
-end
diff --git a/app/workers/pubsubhubbub/raw_distribution_worker.rb b/app/workers/pubsubhubbub/raw_distribution_worker.rb
deleted file mode 100644
index ece9c80ac..000000000
--- a/app/workers/pubsubhubbub/raw_distribution_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Pubsubhubbub::RawDistributionWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'push'
-
-  def perform(xml, source_account_id); end
-end
diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb
deleted file mode 100644
index b861b5e67..000000000
--- a/app/workers/pubsubhubbub/subscribe_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Pubsubhubbub::SubscribeWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false
-
-  def perform(account_id); end
-end
diff --git a/app/workers/pubsubhubbub/unsubscribe_worker.rb b/app/workers/pubsubhubbub/unsubscribe_worker.rb
deleted file mode 100644
index 0c1c263f6..000000000
--- a/app/workers/pubsubhubbub/unsubscribe_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Pubsubhubbub::UnsubscribeWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'push', retry: false, unique: :until_executed, dead: false
-
-  def perform(account_id); end
-end
diff --git a/app/workers/regeneration_worker.rb b/app/workers/regeneration_worker.rb
index 5c6a040bd..5c13c894f 100644
--- a/app/workers/regeneration_worker.rb
+++ b/app/workers/regeneration_worker.rb
@@ -3,7 +3,7 @@
 class RegenerationWorker
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed
+  sidekiq_options lock: :until_executed
 
   def perform(account_id, _ = :home)
     account = Account.find(account_id)
diff --git a/app/workers/remote_profile_update_worker.rb b/app/workers/remote_profile_update_worker.rb
deleted file mode 100644
index 01e8daf8f..000000000
--- a/app/workers/remote_profile_update_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class RemoteProfileUpdateWorker
-  include Sidekiq::Worker
-
-  sidekiq_options queue: 'pull'
-
-  def perform(account_id, body, resubscribe); end
-end
diff --git a/app/workers/resolve_account_worker.rb b/app/workers/resolve_account_worker.rb
index cd7c4d7dd..2b5be6d1b 100644
--- a/app/workers/resolve_account_worker.rb
+++ b/app/workers/resolve_account_worker.rb
@@ -3,7 +3,7 @@
 class ResolveAccountWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'pull', unique: :until_executed
+  sidekiq_options queue: 'pull', lock: :until_executed
 
   def perform(uri)
     ResolveAccountService.new.call(uri)
diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb
deleted file mode 100644
index 10200b06c..000000000
--- a/app/workers/salmon_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class SalmonWorker
-  include Sidekiq::Worker
-
-  sidekiq_options backtrace: true
-
-  def perform(account_id, body); end
-end
diff --git a/app/workers/scheduler/backup_cleanup_scheduler.rb b/app/workers/scheduler/backup_cleanup_scheduler.rb
index d43660699..d69ca2556 100644
--- a/app/workers/scheduler/backup_cleanup_scheduler.rb
+++ b/app/workers/scheduler/backup_cleanup_scheduler.rb
@@ -3,7 +3,7 @@
 class Scheduler::BackupCleanupScheduler
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     old_backups.reorder(nil).find_each(&:destroy!)
diff --git a/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb b/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
index e5e5f6bc4..94788a85b 100644
--- a/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
+++ b/app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
@@ -3,7 +3,7 @@
 class Scheduler::DoorkeeperCleanupScheduler
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     Doorkeeper::AccessToken.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
diff --git a/app/workers/scheduler/email_scheduler.rb b/app/workers/scheduler/email_scheduler.rb
index 1eeeee412..9a7355524 100644
--- a/app/workers/scheduler/email_scheduler.rb
+++ b/app/workers/scheduler/email_scheduler.rb
@@ -3,7 +3,7 @@
 class Scheduler::EmailScheduler
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   FREQUENCY      = 7.days.freeze
   SIGN_IN_OFFSET = 1.day.freeze
diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb
index 4933f1753..99e3440fe 100644
--- a/app/workers/scheduler/feed_cleanup_scheduler.rb
+++ b/app/workers/scheduler/feed_cleanup_scheduler.rb
@@ -4,7 +4,7 @@ class Scheduler::FeedCleanupScheduler
   include Sidekiq::Worker
   include Redisable
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     clean_home_feeds!
diff --git a/app/workers/scheduler/ip_cleanup_scheduler.rb b/app/workers/scheduler/ip_cleanup_scheduler.rb
index 4f44078d8..6d38b52a2 100644
--- a/app/workers/scheduler/ip_cleanup_scheduler.rb
+++ b/app/workers/scheduler/ip_cleanup_scheduler.rb
@@ -5,7 +5,7 @@ class Scheduler::IpCleanupScheduler
 
   RETENTION_PERIOD = 1.year
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     time_ago = RETENTION_PERIOD.ago
diff --git a/app/workers/scheduler/media_cleanup_scheduler.rb b/app/workers/scheduler/media_cleanup_scheduler.rb
index fb01aa70c..671ebf6e0 100644
--- a/app/workers/scheduler/media_cleanup_scheduler.rb
+++ b/app/workers/scheduler/media_cleanup_scheduler.rb
@@ -3,7 +3,7 @@
 class Scheduler::MediaCleanupScheduler
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     unattached_media.find_each(&:destroy)
diff --git a/app/workers/scheduler/pghero_scheduler.rb b/app/workers/scheduler/pghero_scheduler.rb
index 4453bf2cd..cf5570048 100644
--- a/app/workers/scheduler/pghero_scheduler.rb
+++ b/app/workers/scheduler/pghero_scheduler.rb
@@ -3,7 +3,7 @@
 class Scheduler::PgheroScheduler
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     PgHero.capture_space_stats
diff --git a/app/workers/scheduler/scheduled_statuses_scheduler.rb b/app/workers/scheduler/scheduled_statuses_scheduler.rb
index 9cfe949de..25df3c07d 100644
--- a/app/workers/scheduler/scheduled_statuses_scheduler.rb
+++ b/app/workers/scheduler/scheduled_statuses_scheduler.rb
@@ -3,7 +3,7 @@
 class Scheduler::ScheduledStatusesScheduler
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     publish_scheduled_statuses!
diff --git a/app/workers/scheduler/subscriptions_cleanup_scheduler.rb b/app/workers/scheduler/subscriptions_cleanup_scheduler.rb
deleted file mode 100644
index 75fe681a9..000000000
--- a/app/workers/scheduler/subscriptions_cleanup_scheduler.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Scheduler::SubscriptionsCleanupScheduler
-  include Sidekiq::Worker
-
-  sidekiq_options unique: :until_executed, retry: 0
-
-  def perform; end
-end
diff --git a/app/workers/scheduler/subscriptions_scheduler.rb b/app/workers/scheduler/subscriptions_scheduler.rb
deleted file mode 100644
index 6903cadc7..000000000
--- a/app/workers/scheduler/subscriptions_scheduler.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Scheduler::SubscriptionsScheduler
-  include Sidekiq::Worker
-
-  sidekiq_options unique: :until_executed, retry: 0
-
-  def perform; end
-end
diff --git a/app/workers/scheduler/trending_tags_scheduler.rb b/app/workers/scheduler/trending_tags_scheduler.rb
index 77f0d5747..e9891424e 100644
--- a/app/workers/scheduler/trending_tags_scheduler.rb
+++ b/app/workers/scheduler/trending_tags_scheduler.rb
@@ -3,7 +3,7 @@
 class Scheduler::TrendingTagsScheduler
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     TrendingTags.update! if Setting.trends
diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb
index 881b911be..6113edde1 100644
--- a/app/workers/scheduler/user_cleanup_scheduler.rb
+++ b/app/workers/scheduler/user_cleanup_scheduler.rb
@@ -3,7 +3,7 @@
 class Scheduler::UserCleanupScheduler
   include Sidekiq::Worker
 
-  sidekiq_options unique: :until_executed, retry: 0
+  sidekiq_options lock: :until_executed, retry: 0
 
   def perform
     User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).reorder(nil).find_in_batches do |batch|
diff --git a/app/workers/verify_account_links_worker.rb b/app/workers/verify_account_links_worker.rb
index 901498583..8114d59be 100644
--- a/app/workers/verify_account_links_worker.rb
+++ b/app/workers/verify_account_links_worker.rb
@@ -3,7 +3,7 @@
 class VerifyAccountLinksWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'pull', retry: false, unique: :until_executed
+  sidekiq_options queue: 'pull', retry: false, lock: :until_executed
 
   def perform(account_id)
     account = Account.find(account_id)