about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2019-01-10 19:12:10 +0100
committerThibaut Girka <thib@sitedethib.com>2019-01-10 21:00:30 +0100
commita2a64ecd3e3551707412c47f0d16e484dea25632 (patch)
treebc4e0b8e0ca2a2735f527bff8bd73421c0ff72dd /app
parentfb0c906c717f2b21bb63610742a357850142b522 (diff)
parent70801b850c78d7879182eeba4eae509af42fafeb (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- .eslintrc.yml
  Removed, as upstream removed it.
- app/controllers/admin/statuses_controller.rb
  Minor code cleanup when porting one of our features.
- app/models/account.rb
  Note length validation has changed upstream.
  We now use upstream's validation (dropped legacy glitch-soc
  account metadata stuff) but with configurable limit.
- app/services/post_status_service.rb
  Upstream has added support for scheduled toots, refactoring
  the code a bit. Adapted our changes to this refactoring.
- app/views/stream_entries/_detailed_status.html.haml
  Not a real conflict, changes too close.
- app/views/stream_entries/_simple_status.html.haml
  Not a real conflict, changes too close.
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb11
-rw-r--r--app/controllers/admin/followers_controller.rb6
-rw-r--r--app/controllers/admin/instances_controller.rb27
-rw-r--r--app/controllers/admin/statuses_controller.rb2
-rw-r--r--app/controllers/api/v1/accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/custom_emojis_controller.rb4
-rw-r--r--app/controllers/api/v1/scheduled_statuses_controller.rb77
-rw-r--r--app/controllers/api/v1/statuses_controller.rb9
-rw-r--r--app/controllers/concerns/signature_verification.rb45
-rw-r--r--app/controllers/remote_interaction_controller.rb5
-rw-r--r--app/helpers/admin/filter_helper.rb3
-rw-r--r--app/javascript/mastodon/components/account.js4
-rw-r--r--app/javascript/mastodon/components/media_gallery.js4
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js9
-rw-r--r--app/javascript/mastodon/features/account/components/action_bar.js8
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js23
-rw-r--r--app/javascript/mastodon/features/status/components/action_bar.js9
-rw-r--r--app/javascript/mastodon/features/status/components/card.js6
-rw-r--r--app/javascript/mastodon/initial_state.js1
-rw-r--r--app/javascript/mastodon/locales/ar.json2
-rw-r--r--app/javascript/mastodon/locales/ast.json2
-rw-r--r--app/javascript/mastodon/locales/bg.json2
-rw-r--r--app/javascript/mastodon/locales/ca.json48
-rw-r--r--app/javascript/mastodon/locales/co.json6
-rw-r--r--app/javascript/mastodon/locales/cs.json8
-rw-r--r--app/javascript/mastodon/locales/cy.json2
-rw-r--r--app/javascript/mastodon/locales/da.json10
-rw-r--r--app/javascript/mastodon/locales/de.json6
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json20
-rw-r--r--app/javascript/mastodon/locales/el.json6
-rw-r--r--app/javascript/mastodon/locales/en.json2
-rw-r--r--app/javascript/mastodon/locales/eo.json2
-rw-r--r--app/javascript/mastodon/locales/es.json2
-rw-r--r--app/javascript/mastodon/locales/eu.json50
-rw-r--r--app/javascript/mastodon/locales/fa.json2
-rw-r--r--app/javascript/mastodon/locales/fi.json2
-rw-r--r--app/javascript/mastodon/locales/fr.json4
-rw-r--r--app/javascript/mastodon/locales/gl.json4
-rw-r--r--app/javascript/mastodon/locales/he.json2
-rw-r--r--app/javascript/mastodon/locales/hr.json2
-rw-r--r--app/javascript/mastodon/locales/hu.json2
-rw-r--r--app/javascript/mastodon/locales/hy.json2
-rw-r--r--app/javascript/mastodon/locales/id.json2
-rw-r--r--app/javascript/mastodon/locales/io.json2
-rw-r--r--app/javascript/mastodon/locales/it.json50
-rw-r--r--app/javascript/mastodon/locales/ja.json2
-rw-r--r--app/javascript/mastodon/locales/ka.json2
-rw-r--r--app/javascript/mastodon/locales/ko.json6
-rw-r--r--app/javascript/mastodon/locales/lv.json358
-rw-r--r--app/javascript/mastodon/locales/ms.json2
-rw-r--r--app/javascript/mastodon/locales/nl.json6
-rw-r--r--app/javascript/mastodon/locales/no.json2
-rw-r--r--app/javascript/mastodon/locales/oc.json10
-rw-r--r--app/javascript/mastodon/locales/pl.json4
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json48
-rw-r--r--app/javascript/mastodon/locales/pt.json2
-rw-r--r--app/javascript/mastodon/locales/ro.json172
-rw-r--r--app/javascript/mastodon/locales/ru.json2
-rw-r--r--app/javascript/mastodon/locales/sk.json18
-rw-r--r--app/javascript/mastodon/locales/sl.json2
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json2
-rw-r--r--app/javascript/mastodon/locales/sr.json2
-rw-r--r--app/javascript/mastodon/locales/sv.json2
-rw-r--r--app/javascript/mastodon/locales/ta.json2
-rw-r--r--app/javascript/mastodon/locales/te.json70
-rw-r--r--app/javascript/mastodon/locales/th.json2
-rw-r--r--app/javascript/mastodon/locales/tr.json2
-rw-r--r--app/javascript/mastodon/locales/uk.json2
-rw-r--r--app/javascript/mastodon/locales/whitelist_lv.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json2
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json2
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json2
-rw-r--r--app/javascript/packs/public.js28
-rw-r--r--app/javascript/styles/mastodon/admin.scss14
-rw-r--r--app/javascript/styles/mastodon/components.scss10
-rw-r--r--app/javascript/styles/mastodon/dashboard.scss1
-rw-r--r--app/models/account.rb18
-rw-r--r--app/models/concerns/account_associations.rb1
-rw-r--r--app/models/concerns/account_finder_concern.rb4
-rw-r--r--app/models/instance.rb21
-rw-r--r--app/models/instance_filter.rb17
-rw-r--r--app/models/media_attachment.rb38
-rw-r--r--app/models/relay.rb2
-rw-r--r--app/models/scheduled_status.rb39
-rw-r--r--app/policies/instance_policy.rb2
-rw-r--r--app/serializers/rest/scheduled_status_serializer.rb15
-rw-r--r--app/services/activitypub/fetch_remote_account_service.rb8
-rw-r--r--app/services/activitypub/process_account_service.rb10
-rw-r--r--app/services/app_sign_up_service.rb2
-rw-r--r--app/services/post_status_service.rb176
-rw-r--r--app/services/report_service.rb2
-rw-r--r--app/services/suspend_account_service.rb1
-rw-r--r--app/validators/note_length_validator.rb22
-rw-r--r--app/validators/unreserved_username_validator.rb16
-rw-r--r--app/validators/url_validator.rb4
-rw-r--r--app/views/admin/domain_blocks/_domain_block.html.haml13
-rw-r--r--app/views/admin/domain_blocks/index.html.haml17
-rw-r--r--app/views/admin/followers/index.html.haml2
-rw-r--r--app/views/admin/instances/_instance.html.haml7
-rw-r--r--app/views/admin/instances/index.html.haml48
-rw-r--r--app/views/admin/instances/show.html.haml44
-rw-r--r--app/views/remote_follow/new.html.haml4
-rw-r--r--app/views/remote_interaction/new.html.haml10
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml10
-rw-r--r--app/views/stream_entries/_simple_status.html.haml10
-rw-r--r--app/workers/publish_scheduled_status_worker.rb24
-rw-r--r--app/workers/scheduler/scheduled_statuses_scheduler.rb19
-rw-r--r--app/workers/unfollow_follow_worker.rb4
108 files changed, 1388 insertions, 500 deletions
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 90c70275a..5f307ddee 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -4,14 +4,9 @@ module Admin
   class DomainBlocksController < BaseController
     before_action :set_domain_block, only: [:show, :destroy]
 
-    def index
-      authorize :domain_block, :index?
-      @domain_blocks = DomainBlock.page(params[:page])
-    end
-
     def new
       authorize :domain_block, :create?
-      @domain_block = DomainBlock.new
+      @domain_block = DomainBlock.new(domain: params[:_domain])
     end
 
     def create
@@ -22,7 +17,7 @@ module Admin
       if @domain_block.save
         DomainBlockWorker.perform_async(@domain_block.id)
         log_action :create, @domain_block
-        redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
+        redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
       else
         render :new
       end
@@ -36,7 +31,7 @@ module Admin
       authorize @domain_block, :destroy?
       UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
       log_action :destroy, @domain_block
-      redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
+      redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
     end
 
     private
diff --git a/app/controllers/admin/followers_controller.rb b/app/controllers/admin/followers_controller.rb
index 819628b20..d826f47c5 100644
--- a/app/controllers/admin/followers_controller.rb
+++ b/app/controllers/admin/followers_controller.rb
@@ -8,15 +8,11 @@ module Admin
 
     def index
       authorize :account, :index?
-      @followers = followers.recent.page(params[:page]).per(PER_PAGE)
+      @followers = @account.followers.local.recent.page(params[:page]).per(PER_PAGE)
     end
 
     def set_account
       @account = Account.find(params[:account_id])
     end
-
-    def followers
-      Follow.includes(:account).where(target_account: @account)
-    end
   end
 end
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb
index 6f8eaf65c..431ce6f4d 100644
--- a/app/controllers/admin/instances_controller.rb
+++ b/app/controllers/admin/instances_controller.rb
@@ -4,14 +4,21 @@ module Admin
   class InstancesController < BaseController
     def index
       authorize :instance, :index?
+
       @instances = ordered_instances
     end
 
-    def resubscribe
-      authorize :instance, :resubscribe?
-      params.require(:by_domain)
-      Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
-      redirect_to admin_instances_path
+    def show
+      authorize :instance, :show?
+
+      @instance        = Instance.new(Account.by_domain_accounts.find_by(domain: params[:id]) || DomainBlock.find_by!(domain: params[:id]))
+      @following_count = Follow.where(account: Account.where(domain: params[:id])).count
+      @followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
+      @reports_count   = Report.where(target_account: Account.where(domain: params[:id])).count
+      @blocks_count    = Block.where(target_account: Account.where(domain: params[:id])).count
+      @available       = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
+      @media_storage   = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
+      @domain_block    = DomainBlock.find_by(domain: params[:id])
     end
 
     private
@@ -27,17 +34,11 @@ module Admin
     helper_method :paginated_instances
 
     def ordered_instances
-      paginated_instances.map { |account| Instance.new(account) }
-    end
-
-    def subscribeable_accounts
-      Account.remote.where(protocol: :ostatus).where(domain: params[:by_domain])
+      paginated_instances.map { |resource| Instance.new(resource) }
     end
 
     def filter_params
-      params.permit(
-        :domain_name
-      )
+      params.permit(:limited)
     end
   end
 end
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index 62f49806c..650195034 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -26,7 +26,7 @@ module Admin
       authorize :status, :index?
 
       @statuses = @account.statuses.where(id: params[:id])
-      authorize @statuses[0], :show?
+      authorize @statuses.first, :show?
 
       @form = Form::StatusBatch.new
     end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 6e4084c4e..2ccbc3cbb 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -76,7 +76,7 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def account_params
-    params.permit(:username, :email, :password, :agreement)
+    params.permit(:username, :email, :password, :agreement, :locale)
   end
 
   def check_enabled_registrations
diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb
index f8cd64455..7bac27da4 100644
--- a/app/controllers/api/v1/custom_emojis_controller.rb
+++ b/app/controllers/api/v1/custom_emojis_controller.rb
@@ -4,6 +4,8 @@ class Api::V1::CustomEmojisController < Api::BaseController
   respond_to :json
 
   def index
-    render json: CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer
+    render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
+      ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer)
+    end
   end
 end
diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb
new file mode 100644
index 000000000..9950296f3
--- /dev/null
+++ b/app/controllers/api/v1/scheduled_statuses_controller.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+class Api::V1::ScheduledStatusesController < Api::BaseController
+  include Authorization
+
+  before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
+  before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
+
+  before_action :set_statuses, only: :index
+  before_action :set_status, except: :index
+
+  after_action :insert_pagination_headers, only: :index
+
+  def index
+    render json: @statuses, each_serializer: REST::ScheduledStatusSerializer
+  end
+
+  def show
+    render json: @status, serializer: REST::ScheduledStatusSerializer
+  end
+
+  def update
+    @status.update!(scheduled_status_params)
+    render json: @status, serializer: REST::ScheduledStatusSerializer
+  end
+
+  def destroy
+    @status.destroy!
+    render_empty
+  end
+
+  private
+
+  def set_statuses
+    @statuses = current_account.scheduled_statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
+  end
+
+  def set_status
+    @status = current_account.scheduled_statuses.find(params[:id])
+  end
+
+  def scheduled_status_params
+    params.permit(:scheduled_at)
+  end
+
+  def pagination_params(core_params)
+    params.slice(:limit).permit(:limit).merge(core_params)
+  end
+
+  def insert_pagination_headers
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def next_path
+    if records_continue?
+      api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id)
+    end
+  end
+
+  def prev_path
+    unless @statuses.empty?
+      api_v1_scheduled_statuses_url pagination_params(min_id: pagination_since_id)
+    end
+  end
+
+  def records_continue?
+    @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
+  end
+
+  def pagination_max_id
+    @statuses.last.id
+  end
+
+  def pagination_since_id
+    @statuses.first.id
+  end
+end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 49a52f7a6..29b420c67 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -45,16 +45,17 @@ class Api::V1::StatusesController < Api::BaseController
 
   def create
     @status = PostStatusService.new.call(current_user.account,
-                                         status_params[:status],
-                                         status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
+                                         text: status_params[:status],
+                                         thread: status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
                                          media_ids: status_params[:media_ids],
                                          sensitive: status_params[:sensitive],
                                          spoiler_text: status_params[:spoiler_text],
                                          visibility: status_params[:visibility],
+                                         scheduled_at: status_params[:scheduled_at],
                                          application: doorkeeper_token.application,
                                          idempotency: request.headers['Idempotency-Key'])
 
-    render json: @status, serializer: REST::StatusSerializer
+    render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
   end
 
   def destroy
@@ -77,7 +78,7 @@ class Api::V1::StatusesController < Api::BaseController
   end
 
   def status_params
-    params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, media_ids: [])
+    params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :scheduled_at, media_ids: [])
   end
 
   def pagination_params(core_params)
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 887096e8b..91566c4fa 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -60,23 +60,26 @@ module SignatureVerification
     signature             = Base64.decode64(signature_params['signature'])
     compare_signed_string = build_signed_string(signature_params['headers'])
 
-    if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
-      @signed_request_account = account
-      @signed_request_account
-    elsif account.possibly_stale?
-      account = account.refresh!
+    return account unless verify_signature(account, signature, compare_signed_string).nil?
 
-      if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
-        @signed_request_account = account
-        @signed_request_account
-      else
-        @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
-        @signed_request_account = nil
-      end
-    else
-      @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
+    account_stoplight = Stoplight("source:#{request.ip}") { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
+      .with_fallback { nil }
+      .with_threshold(1)
+      .with_cool_off_time(5.minutes.seconds)
+      .with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
+
+    account = account_stoplight.run
+
+    if account.nil?
+      @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
       @signed_request_account = nil
+      return
     end
+
+    return account unless verify_signature(account, signature, compare_signed_string).nil?
+
+    @signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
+    @signed_request_account = nil
   end
 
   def request_body
@@ -85,6 +88,15 @@ module SignatureVerification
 
   private
 
+  def verify_signature(account, signature, compare_signed_string)
+    if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
+      @signed_request_account = account
+      @signed_request_account
+    end
+  rescue OpenSSL::PKey::RSAError
+    nil
+  end
+
   def build_signed_string(signed_headers)
     signed_headers = 'date' if signed_headers.blank?
 
@@ -131,4 +143,9 @@ module SignatureVerification
       account
     end
   end
+
+  def account_refresh_key(account)
+    return if account.local? || !account.activitypub?
+    ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
+  end
 end
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
index 6861f3f21..d7197d434 100644
--- a/app/controllers/remote_interaction_controller.rb
+++ b/app/controllers/remote_interaction_controller.rb
@@ -5,6 +5,7 @@ class RemoteInteractionController < ApplicationController
 
   layout 'modal'
 
+  before_action :set_interaction_type
   before_action :set_status
   before_action :set_body_classes
   before_action :set_pack
@@ -50,4 +51,8 @@ class RemoteInteractionController < ApplicationController
   def set_pack
     use_pack 'modal'
   end
+
+  def set_interaction_type
+    @interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
+  end
 end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 8807cc784..97beb587f 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -6,8 +6,9 @@ module Admin::FilterHelper
   INVITE_FILTER        = %i(available expired).freeze
   CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
   TAGS_FILTERS         = %i(hidden).freeze
+  INSTANCES_FILTERS    = %i(limited).freeze
 
-  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS
+  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
     new_url = filtered_url_for(link_to_params)
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 2bcea8b67..206030c00 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -68,10 +68,10 @@ class Account extends ImmutablePureComponent {
 
     if (hidden) {
       return (
-        <div>
+        <Fragment>
           {account.get('display_name')}
           {account.get('username')}
-        </div>
+        </Fragment>
       );
     }
 
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index ed0e4ff1b..c507920d0 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -51,6 +51,10 @@ class Item extends React.PureComponent {
     const { index, onClick } = this.props;
 
     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+      if (this.hoverToPlay()) {
+        e.target.pause();
+        e.target.currentTime = 0;
+      }
       e.preventDefault();
       onClick(index);
     }
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index becd44ec0..0995a1490 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -5,7 +5,7 @@ import IconButton from './icon_button';
 import DropdownMenuContainer from '../containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from '../initial_state';
+import { me, isStaff } from '../initial_state';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -30,6 +30,8 @@ const messages = defineMessages({
   pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
   unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
   embed: { id: 'status.embed', defaultMessage: 'Embed' },
+  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' },
 });
 
 const obfuscatedCount = count => {
@@ -182,6 +184,11 @@ class StatusActionBar extends ImmutablePureComponent {
       menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
       menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
       menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
+      if (isStaff) {
+        menu.push(null);
+        menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
+        menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+      }
     }
 
     if (status.get('visibility') === 'direct') {
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index e6ae1a2fd..8ed4c917a 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
 import { NavLink } from 'react-router-dom';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { me } from '../../../initial_state';
+import { me, isStaff  } from '../../../initial_state';
 import { shortNumberFormat } from '../../../utils/numbers';
 
 const messages = defineMessages({
@@ -35,6 +35,7 @@ const messages = defineMessages({
   endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
   unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
   add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
+  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 });
 
 export default @injectIntl
@@ -151,6 +152,11 @@ class ActionBar extends React.PureComponent {
       }
     }
 
+    if (account.get('id') !== me && isStaff) {
+      menu.push(null);
+      menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
+    }
+
     return (
       <div>
         {extraInfo}
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index feecfd0e4..709a3aa96 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -32,6 +32,7 @@ const messages = defineMessages({
   personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
   security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
   menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
+  profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
 });
 
 const mapStateToProps = state => ({
@@ -87,10 +88,29 @@ class GettingStarted extends ImmutablePureComponent {
         <ColumnSubheading key={i++} text={intl.formatMessage(messages.discover)} />,
         <ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
         <ColumnLink key={i++} icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
+      );
+
+      height += 34 + 48*2;
+
+      if (profile_directory) {
+        navItems.push(
+          <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} href='/explore' />
+        );
+
+        height += 48;
+      }
+
+      navItems.push(
         <ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />
       );
 
-      height += 34*2 + 48*2;
+      height += 34;
+    } else if (profile_directory) {
+      navItems.push(
+        <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} href='/explore' />
+      );
+
+      height += 48;
     }
 
     navItems.push(
@@ -136,7 +156,6 @@ class GettingStarted extends ImmutablePureComponent {
 
           <div className='getting-started__footer'>
             <ul>
-              {profile_directory && <li><a href='/explore' target='_blank'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></a> · </li>}
               {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
               {multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
               <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index 3b30d33b2..d3b725283 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -4,7 +4,7 @@ import IconButton from '../../../components/icon_button';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
-import { me } from '../../../initial_state';
+import { me, isStaff } from '../../../initial_state';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -26,6 +26,8 @@ const messages = defineMessages({
   pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
   unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
   embed: { id: 'status.embed', defaultMessage: 'Embed' },
+  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' },
 });
 
 export default @injectIntl
@@ -145,6 +147,11 @@ class ActionBar extends React.PureComponent {
       menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
       menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
       menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
+      if (isStaff) {
+        menu.push(null);
+        menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
+        menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+      }
     }
 
     const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index 235d209b8..8491299ef 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -195,6 +195,12 @@ export default class Card extends React.PureComponent {
           {thumbnail}
         </div>
       );
+    } else {
+      embed = (
+        <div className='status-card__image'>
+          <i className='fa fa-file-text' />
+        </div>
+      );
     }
 
     return (
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index 1f99c508b..6068246ae 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -17,5 +17,6 @@ export const invitesEnabled = getMeta('invites_enabled');
 export const version = getMeta('version');
 export const mascot = getMeta('mascot');
 export const profile_directory = getMeta('profile_directory');
+export const isStaff = getMeta('is_staff');
 
 export default initialState;
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 3781f394e..f558a6ddc 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "التبويقات",
   "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "standalone.public_title": "نظرة على ...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "إلغاء الترقية",
   "status.cannot_reblog": "تعذرت ترقية هذا المنشور",
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index e38c45963..a9407e82d 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Bloquiar a @{name}",
   "status.cancel_reblog_private": "Dexar de compartir",
   "status.cannot_reblog": "Esti artículu nun pue compartise",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index b15e20813..a812f5cb1 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 97568c53a..6b911711e 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Autoritzar",
   "follow_request.reject": "Rebutjar",
   "getting_started.developers": "Desenvolupadors",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Directori de perfils",
   "getting_started.documentation": "Documentació",
   "getting_started.heading": "Començant",
   "getting_started.invite": "Convida gent",
@@ -149,23 +149,23 @@
   "home.column_settings.basic": "Bàsic",
   "home.column_settings.show_reblogs": "Mostrar impulsos",
   "home.column_settings.show_replies": "Mostrar respostes",
-  "introduction.federation.action": "Next",
+  "introduction.federation.action": "Següent",
   "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.text": "Les publicacions públiques d'altres servidors del fedivers apareixeran a la línia de temps federada.",
   "introduction.federation.home.headline": "Home",
-  "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": "Les publicacions de les persones que segueixes apareixeran a la línia de temps Inici. Pots seguir qualsevol persona de qualsevol servidor!",
   "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.",
-  "introduction.interactions.action": "Finish tutorial!",
-  "introduction.interactions.favourite.headline": "Favourite",
-  "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.text": "You can share other people's toots with your followers by boosting them.",
-  "introduction.interactions.reply.headline": "Reply",
-  "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.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.federation.local.text": "Les publicacions públiques de les persones del teu mateix servidor apareixeran a la línia de temps local.",
+  "introduction.interactions.action": "Finalitza el tutorial!",
+  "introduction.interactions.favourite.headline": "Favorit",
+  "introduction.interactions.favourite.text": "Pots desar un toot per a més tard i deixar que l'autor sàpiga que t'ha agradat, marcant-lo com a favorit.",
+  "introduction.interactions.reblog.headline": "Impuls",
+  "introduction.interactions.reblog.text": "Pots compartir amb els teus seguidors els toots d'altres usuaris, impulsant-los.",
+  "introduction.interactions.reply.headline": "Respondre",
+  "introduction.interactions.reply.text": "Pots respondre als toots d'altres persones i als teus propis, que els unirà en una conversa.",
+  "introduction.welcome.action": "Som-hi!",
+  "introduction.welcome.headline": "Primers passos",
+  "introduction.welcome.text": "Benvingut al fedivers! En uns moments podràs emetre missatges i conversar amb els teus amics en una gran varietat de servidors. Però aquest servidor, {domain}, és especial: allotja el teu perfil així que recorda el seu nom.",
   "keyboard_shortcuts.back": "navegar enrera",
   "keyboard_shortcuts.blocked": "per obrir la llista d'usuaris bloquejats",
   "keyboard_shortcuts.boost": "impulsar",
@@ -242,20 +242,20 @@
   "notifications.clear_confirmation": "Estàs segur que vols esborrar permanenment totes les teves notificacions?",
   "notifications.column_settings.alert": "Notificacions d'escriptori",
   "notifications.column_settings.favourite": "Favorits:",
-  "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.advanced": "Mostra totes les categories",
+  "notifications.column_settings.filter_bar.category": "Barra ràpida de filtres",
+  "notifications.column_settings.filter_bar.show": "Mostra",
   "notifications.column_settings.follow": "Nous seguidors:",
   "notifications.column_settings.mention": "Mencions:",
   "notifications.column_settings.push": "Push notificacions",
   "notifications.column_settings.reblog": "Impulsos:",
   "notifications.column_settings.show": "Mostrar en la columna",
   "notifications.column_settings.sound": "Reproduïr so",
-  "notifications.filter.all": "All",
-  "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
-  "notifications.filter.follows": "Follows",
-  "notifications.filter.mentions": "Mentions",
+  "notifications.filter.all": "Tots",
+  "notifications.filter.boosts": "Impulsos",
+  "notifications.filter.favourites": "Favorits",
+  "notifications.filter.follows": "Seguiments",
+  "notifications.filter.mentions": "Mencions",
   "notifications.group": "{count} notificacions",
   "privacy.change": "Ajusta l'estat de privacitat",
   "privacy.direct.long": "Publicar només per als usuaris esmentats",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, un {result} altres {results}}",
   "standalone.public_title": "Una mirada a l'interior ...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Desfer l'impuls",
   "status.cannot_reblog": "Aquesta publicació no pot ser retootejada",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index 5d3204943..0277a513b 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Auturizà",
   "follow_request.reject": "Righjittà",
   "getting_started.developers": "Sviluppatori",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Annuariu di i prufili",
   "getting_started.documentation": "Documentation",
   "getting_started.heading": "Per principià",
   "getting_started.invite": "Invità ghjente",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Statuti",
   "search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}",
   "standalone.public_title": "Una vista à l'internu...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Bluccà @{name}",
   "status.cancel_reblog_private": "Ùn sparte più",
   "status.cannot_reblog": "Stu statutu ùn pò micca esse spartutu",
@@ -341,7 +343,7 @@
   "upload_area.title": "Drag & drop per caricà un fugliale",
   "upload_button.label": "Aghjunghje un media (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Discrive per i malvistosi",
-  "upload_form.focus": "Riquatrà",
+  "upload_form.focus": "Cambià a vista",
   "upload_form.undo": "Sguassà",
   "upload_progress.label": "Caricamentu...",
   "video.close": "Chjudà a video",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 9058f3b0a..31c21cce5 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -12,7 +12,7 @@
   "account.follow": "Sledovat",
   "account.followers": "Sledovatelé",
   "account.followers.empty": "Tohoto uživatele ještě nikdo nesleduje.",
-  "account.follows": "Sleduje",
+  "account.follows": "Sledovaní",
   "account.follows.empty": "Tento uživatel ještě nikoho nesleduje.",
   "account.follows_you": "Sleduje vás",
   "account.hide_reblogs": "Skrýt boosty od uživatele @{name}",
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Autorizovat",
   "follow_request.reject": "Odmítnout",
   "getting_started.developers": "Vývojáři",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Adresář profilů",
   "getting_started.documentation": "Dokumentace",
   "getting_started.heading": "Začínáme",
   "getting_started.invite": "Pozvat lidi",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Tooty",
   "search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledku} other {výsledků}}",
   "standalone.public_title": "Nahlédněte dovnitř...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Zablokovat uživatele @{name}",
   "status.cancel_reblog_private": "Zrušit boost",
   "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý",
@@ -341,7 +343,7 @@
   "upload_area.title": "Přetažením nahrajete",
   "upload_button.label": "Přidat média (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Popis pro zrakově postižené",
-  "upload_form.focus": "Vystřihnout",
+  "upload_form.focus": "Změnit náhled",
   "upload_form.undo": "Smazat",
   "upload_progress.label": "Nahrávám...",
   "video.close": "Zavřít video",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index e386e7ec0..a25497f78 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Tŵtiau",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "Golwg tu fewn...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Blocio @{name}",
   "status.cancel_reblog_private": "Dadfŵstio",
   "status.cannot_reblog": "Ni ellir sbarduno'r tŵt hwn",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 3f350d9f9..60315211a 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -149,7 +149,7 @@
   "home.column_settings.basic": "Grundlæggende",
   "home.column_settings.show_reblogs": "Vis fremhævelser",
   "home.column_settings.show_replies": "Vis svar",
-  "introduction.federation.action": "Next",
+  "introduction.federation.action": "Næste",
   "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",
@@ -251,10 +251,10 @@
   "notifications.column_settings.reblog": "Fremhævelser:",
   "notifications.column_settings.show": "Vis i kolonne",
   "notifications.column_settings.sound": "Afspil lyd",
-  "notifications.filter.all": "All",
+  "notifications.filter.all": "Alle",
   "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
-  "notifications.filter.follows": "Follows",
+  "notifications.filter.favourites": "Favoritter",
+  "notifications.filter.follows": "Følger",
   "notifications.filter.mentions": "Mentions",
   "notifications.group": "{count} notifikationer",
   "privacy.change": "Ændre status privatliv",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Trut",
   "search_results.total": "{count, number} {count, plural, et {result} andre {results}}",
   "standalone.public_title": "Et kig indenfor...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Bloker @{name}",
   "status.cancel_reblog_private": "Fremhæv ikke længere",
   "status.cannot_reblog": "Denne post kan ikke fremhæves",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 73c779646..7d5f0348a 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Erlauben",
   "follow_request.reject": "Ablehnen",
   "getting_started.developers": "Entwickler",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Profilverzeichnis",
   "getting_started.documentation": "Dokumentation",
   "getting_started.heading": "Erste Schritte",
   "getting_started.invite": "Leute einladen",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Beiträge",
   "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
   "standalone.public_title": "Ein kleiner Einblick …",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Blockiere @{name}",
   "status.cancel_reblog_private": "Nicht mehr teilen",
   "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
@@ -341,7 +343,7 @@
   "upload_area.title": "Zum Hochladen hereinziehen",
   "upload_button.label": "Mediendatei hinzufügen (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben",
-  "upload_form.focus": "Zuschneiden",
+  "upload_form.focus": "Thumbnail bearbeiten",
   "upload_form.undo": "Löschen",
   "upload_progress.label": "Wird hochgeladen …",
   "video.close": "Video schließen",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index b31218081..b4181ea05 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -302,6 +302,14 @@
       {
         "defaultMessage": "Embed",
         "id": "status.embed"
+      },
+      {
+        "defaultMessage": "Open moderation interface for @{name}",
+        "id": "status.admin_account"
+      },
+      {
+        "defaultMessage": "Open this status in the moderation interface",
+        "id": "status.admin_status"
       }
     ],
     "path": "app/javascript/mastodon/components/status_action_bar.json"
@@ -595,6 +603,10 @@
         "id": "account.add_or_remove_from_list"
       },
       {
+        "defaultMessage": "Open moderation interface for @{name}",
+        "id": "status.admin_account"
+      },
+      {
         "defaultMessage": "Information below may reflect the user's profile incompletely.",
         "id": "account.disclaimer_full"
       },
@@ -1926,6 +1938,14 @@
       {
         "defaultMessage": "Embed",
         "id": "status.embed"
+      },
+      {
+        "defaultMessage": "Open moderation interface for @{name}",
+        "id": "status.admin_account"
+      },
+      {
+        "defaultMessage": "Open this status in the moderation interface",
+        "id": "status.admin_status"
       }
     ],
     "path": "app/javascript/mastodon/features/status/components/action_bar.json"
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index 5aca43c28..7b4852271 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -132,7 +132,7 @@
   "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": "Προσκάλεσε κόσμο",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Τουτ",
   "search_results.total": "{count, number} {count, plural, ένα {result} υπόλοιπα {results}}",
   "standalone.public_title": "Μια πρώτη γεύση...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Αποκλεισμός @{name}",
   "status.cancel_reblog_private": "Ακύρωσε την προώθηση",
   "status.cannot_reblog": "Αυτή η δημοσίευση δεν μπορεί να προωθηθεί",
@@ -341,7 +343,7 @@
   "upload_area.title": "Drag & drop για να ανεβάσεις",
   "upload_button.label": "Πρόσθεσε πολυμέσα (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Περιέγραψε για όσους & όσες έχουν προβλήματα όρασης",
-  "upload_form.focus": "Περικοπή",
+  "upload_form.focus": "Αλλαγή προεπισκόπησης",
   "upload_form.undo": "Διαγραφή",
   "upload_progress.label": "Ανεβαίνει...",
   "video.close": "Κλείσε το βίντεο",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 87a894657..3bb157aeb 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -297,6 +297,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 5332ab874..8be964a52 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Mesaĝoj",
   "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}",
   "standalone.public_title": "Enrigardo…",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Bloki @{name}",
   "status.cancel_reblog_private": "Eksdiskonigi",
   "status.cannot_reblog": "Ĉi tiu mesaĝo ne diskonigeblas",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index e3c267b40..4f73dbba2 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Un pequeño vistazo...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Des-impulsar",
   "status.cannot_reblog": "Este toot no puede retootearse",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index b882ae2a7..0602fbf9e 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Baimendu",
   "follow_request.reject": "Ukatu",
   "getting_started.developers": "Garatzaileak",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Profil-direktorioa",
   "getting_started.documentation": "Dokumentazioa",
   "getting_started.heading": "Menua",
   "getting_started.invite": "Gonbidatu jendea",
@@ -149,23 +149,23 @@
   "home.column_settings.basic": "Oinarrizkoa",
   "home.column_settings.show_reblogs": "Erakutsi bultzadak",
   "home.column_settings.show_replies": "Erakutsi erantzunak",
-  "introduction.federation.action": "Next",
+  "introduction.federation.action": "Hurrengoa",
   "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.text": "Fedibertsoko beste zerbitzarietako bidalketa publikoak federatutako denbora-lerroan agertuko dira.",
   "introduction.federation.home.headline": "Home",
-  "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": "Jarraitzen dituzun horien mezuak zure hasierako jarioan agertuko dira. Edozein zerbitzariko edonor jarraitu dezakezu!",
   "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.",
-  "introduction.interactions.action": "Finish tutorial!",
-  "introduction.interactions.favourite.headline": "Favourite",
-  "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.text": "You can share other people's toots with your followers by boosting them.",
-  "introduction.interactions.reply.headline": "Reply",
-  "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.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.federation.local.text": "Zure zerbitzari berean dauden horien mezu publikoak denbora-lerro lokalean agertuko dira.",
+  "introduction.interactions.action": "Amaitu tutoriala!",
+  "introduction.interactions.favourite.headline": "Gogokoa",
+  "introduction.interactions.favourite.text": "Toot bat geroko gorde dezakezu, eta egileari gustukoa duzula jakinarazi, hau gogoko bihurtuz.",
+  "introduction.interactions.reblog.headline": "Bultzada",
+  "introduction.interactions.reblog.text": "Beste batzuen mezuak partekatu ditzakezu zure jarraitzaileekin hauei bultzada emanez.",
+  "introduction.interactions.reply.headline": "Erantzun",
+  "introduction.interactions.reply.text": "Besteen mezuei eta zure mezuei ere erantzun diezaiekezu, eta elkarrizketa batean lotuta agertuko dira.",
+  "introduction.welcome.action": "Goazen!",
+  "introduction.welcome.headline": "Lehen urratsak",
+  "introduction.welcome.text": "Ongi etorri fedibertsora! Hemendik gutxira hainbat zerbitzarietan zehar mezuak zabaldu eta lagunekin hitz egin ahal izango duzu. Baina zerbitzari hau hainbat zerbitzarietan zehar. berezia da, hau da zure profila ostatatzen duena, ez ahaztu bere izena.",
   "keyboard_shortcuts.back": "atzera nabigatzeko",
   "keyboard_shortcuts.blocked": "blokeatutako erabiltzaileen zerrenda irekitzeko",
   "keyboard_shortcuts.boost": "bultzada ematea",
@@ -242,20 +242,20 @@
   "notifications.clear_confirmation": "Ziur zure jakinarazpen guztiak behin betirako garbitu nahi dituzula?",
   "notifications.column_settings.alert": "Mahaigaineko jakinarazpenak",
   "notifications.column_settings.favourite": "Gogokoak:",
-  "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.advanced": "Erakutsi kategoria guztiak",
+  "notifications.column_settings.filter_bar.category": "Iragazki azkarraren barra",
+  "notifications.column_settings.filter_bar.show": "Erakutsi",
   "notifications.column_settings.follow": "Jarraitzaile berriak:",
   "notifications.column_settings.mention": "Aipamenak:",
   "notifications.column_settings.push": "Push jakinarazpenak",
   "notifications.column_settings.reblog": "Bultzadak:",
   "notifications.column_settings.show": "Erakutsi zutabean",
   "notifications.column_settings.sound": "Jo soinua",
-  "notifications.filter.all": "All",
-  "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
-  "notifications.filter.follows": "Follows",
-  "notifications.filter.mentions": "Mentions",
+  "notifications.filter.all": "Denak",
+  "notifications.filter.boosts": "Bultzadak",
+  "notifications.filter.favourites": "Gogokoak",
+  "notifications.filter.follows": "Jarraipenak",
+  "notifications.filter.mentions": "Aipamenak",
   "notifications.group": "{count} jakinarazpen",
   "privacy.change": "Doitu mezuaren pribatutasuna",
   "privacy.direct.long": "Bidali aipatutako erabiltzaileei besterik ez",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toot-ak",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "Begiradatxo bat...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Kendu bultzada",
   "status.cannot_reblog": "Mezu honi ezin zaio bultzada eman",
@@ -341,7 +343,7 @@
   "upload_area.title": "Arrastatu eta jaregin igotzeko",
   "upload_button.label": "Gehitu multimedia  (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Deskribatu ikusmen arazoak dituztenentzat",
-  "upload_form.focus": "Moztu",
+  "upload_form.focus": "Aldatu aurrebista",
   "upload_form.undo": "Ezabatu",
   "upload_progress.label": "Igotzen...",
   "video.close": "Itxi bideoa",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index e2790d8b6..b11d88d87 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "بوق‌ها",
   "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
   "standalone.public_title": "نگاهی به کاربران این سرور...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "مسدودسازی @{name}",
   "status.cancel_reblog_private": "حذف بازبوق",
   "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 84638af35..c8d258672 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Tuuttaukset",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "Kurkistus sisälle...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Estä @{name}",
   "status.cancel_reblog_private": "Peru buustaus",
   "status.cannot_reblog": "Tätä julkaisua ei voi buustata",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index c0bd047ce..35e41d7bb 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Accepter",
   "follow_request.reject": "Rejeter",
   "getting_started.developers": "Développeurs",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Annuaire des profils",
   "getting_started.documentation": "Documentation",
   "getting_started.heading": "Pour commencer",
   "getting_started.invite": "Inviter des gens",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Pouets",
   "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
   "standalone.public_title": "Un aperçu …",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Dé-booster",
   "status.cannot_reblog": "Cette publication ne peut être boostée",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 3ffa8ac32..23bbed74d 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count,plural,one {result} outros {results}}",
   "standalone.public_title": "Ollada dentro...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Non promover",
   "status.cannot_reblog": "Esta mensaxe non pode ser promovida",
@@ -341,7 +343,7 @@
   "upload_area.title": "Arrastre e solte para subir",
   "upload_button.label": "Engadir medios (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Describa para deficientes visuais",
-  "upload_form.focus": "Recortar",
+  "upload_form.focus": "Cambiar vista previa",
   "upload_form.undo": "Eliminar",
   "upload_progress.label": "Subindo...",
   "video.close": "Pechar video",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index d0fd75e5f..e27e7f09e 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}",
   "standalone.public_title": "הצצה פנימה...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "לא ניתן להדהד הודעה זו",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index b5685a9a0..71dd5319e 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Ovaj post ne može biti boostan",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index b87dcd597..c2842aea7 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "Betekintés...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Ezen státusz nem rebloggolható",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 2fb96cbf3..691994887 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "Այս պահին…",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Արգելափակել @{name}֊ին",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Այս թութը չի կարող տարածվել",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 81fdbc711..eed61af70 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {hasil} other {hasil}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index e51b074ae..b26fa6c4a 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index b1208f382..1f52d3724 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Autorizza",
   "follow_request.reject": "Rifiuta",
   "getting_started.developers": "Sviluppatori",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Directory del profilo",
   "getting_started.documentation": "Documentazione",
   "getting_started.heading": "Come iniziare",
   "getting_started.invite": "Invita qualcuno",
@@ -149,23 +149,23 @@
   "home.column_settings.basic": "Semplice",
   "home.column_settings.show_reblogs": "Mostra post condivisi",
   "home.column_settings.show_replies": "Mostra risposte",
-  "introduction.federation.action": "Next",
+  "introduction.federation.action": "Avanti",
   "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.text": "I post pubblici provenienti da altri server del fediverse saranno mostrati nella timeline federata.",
   "introduction.federation.home.headline": "Home",
-  "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": "I post scritti da persone che segui saranno mostrati nella timeline home. Puoi seguire chiunque su qualunque 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.",
-  "introduction.interactions.action": "Finish tutorial!",
-  "introduction.interactions.favourite.headline": "Favourite",
-  "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.text": "You can share other people's toots with your followers by boosting them.",
-  "introduction.interactions.reply.headline": "Reply",
-  "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.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.federation.local.text": "I post pubblici scritti da persone sul tuo stesso server saranno mostrati nella timeline locale.",
+  "introduction.interactions.action": "Finisci il tutorial!",
+  "introduction.interactions.favourite.headline": "Apprezza",
+  "introduction.interactions.favourite.text": "Puoi salvare un toot e tenerlo per dopo, e far sapere all'autore che ti è piaciuto, segnandolo come apprezzato.",
+  "introduction.interactions.reblog.headline": "Condividi",
+  "introduction.interactions.reblog.text": "Con la condivisione puoi segnalare i toot di altre persone ai tuoi seguaci .",
+  "introduction.interactions.reply.headline": "Rispondi",
+  "introduction.interactions.reply.text": "Puoi rispondere ai toot, sia a quelli di altri sia ai tuoi, e i toot saranno collegati a formare una conversazione.",
+  "introduction.welcome.action": "Andiamo!",
+  "introduction.welcome.headline": "Primi passi",
+  "introduction.welcome.text": "Benvenuto/a nel fediverse! Tra poco potrai inviare messaggi e parlare con i tuoi amici su una grande varietà di server. Ma questo server, {domain}, è speciale: ospita il tuo profilo, quindi ricordati il suo nome.",
   "keyboard_shortcuts.back": "per tornare indietro",
   "keyboard_shortcuts.blocked": "per aprire l'elenco degli utenti bloccati",
   "keyboard_shortcuts.boost": "per condividere",
@@ -242,20 +242,20 @@
   "notifications.clear_confirmation": "Vuoi davvero cancellare tutte le notifiche?",
   "notifications.column_settings.alert": "Notifiche desktop",
   "notifications.column_settings.favourite": "Apprezzati:",
-  "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.advanced": "Mostra tutte le categorie",
+  "notifications.column_settings.filter_bar.category": "Filtro rapido",
+  "notifications.column_settings.filter_bar.show": "Mostra",
   "notifications.column_settings.follow": "Nuovi seguaci:",
   "notifications.column_settings.mention": "Menzioni:",
   "notifications.column_settings.push": "Notifiche push",
   "notifications.column_settings.reblog": "Post condivisi:",
   "notifications.column_settings.show": "Mostra in colonna",
   "notifications.column_settings.sound": "Riproduci suono",
-  "notifications.filter.all": "All",
-  "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
-  "notifications.filter.follows": "Follows",
-  "notifications.filter.mentions": "Mentions",
+  "notifications.filter.all": "Tutti",
+  "notifications.filter.boosts": "Condivisioni",
+  "notifications.filter.favourites": "Apprezzati",
+  "notifications.filter.follows": "Seguaci",
+  "notifications.filter.mentions": "Menzioni",
   "notifications.group": "{count} notifiche",
   "privacy.change": "Modifica privacy del post",
   "privacy.direct.long": "Invia solo a utenti menzionati",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toot",
   "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}",
   "standalone.public_title": "Un'occhiata all'interno...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Annulla condivisione",
   "status.cannot_reblog": "Questo post non può essere condiviso",
@@ -341,7 +343,7 @@
   "upload_area.title": "Trascina per caricare",
   "upload_button.label": "Aggiungi file multimediale",
   "upload_form.description": "Descrizione per utenti con disabilità visive",
-  "upload_form.focus": "Rifila",
+  "upload_form.focus": "Modifica anteprima",
   "upload_form.undo": "Cancella",
   "upload_progress.label": "Sto caricando...",
   "video.close": "Chiudi video",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 103c1a5de..7ddd95f60 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -297,6 +297,8 @@
   "search_results.statuses": "トゥート",
   "search_results.total": "{count, number}件の結果",
   "standalone.public_title": "今こんな話をしています...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "@{name}さんをブロック",
   "status.cancel_reblog_private": "ブースト解除",
   "status.cannot_reblog": "この投稿はブーストできません",
diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json
index 8a019a476..93a11027a 100644
--- a/app/javascript/mastodon/locales/ka.json
+++ b/app/javascript/mastodon/locales/ka.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "ტუტები",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "შიდა ხედი...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "დაბლოკე @{name}",
   "status.cancel_reblog_private": "ბუსტის მოშორება",
   "status.cannot_reblog": "ეს პოსტი ვერ დაიბუსტება",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 834e037a7..0d707afb1 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -132,7 +132,7 @@
   "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": "초대",
@@ -292,6 +292,8 @@
   "search_results.statuses": "툿",
   "search_results.total": "{count, number}건의 결과",
   "standalone.public_title": "지금 이런 이야기를 하고 있습니다…",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "@{name} 차단",
   "status.cancel_reblog_private": "부스트 취소",
   "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
@@ -341,7 +343,7 @@
   "upload_area.title": "드래그 & 드롭으로 업로드",
   "upload_button.label": "미디어 추가 (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "시각장애인을 위한 설명",
-  "upload_form.focus": "크롭",
+  "upload_form.focus": "미리보기 변경",
   "upload_form.undo": "삭제",
   "upload_progress.label": "업로드 중...",
   "video.close": "동영상 닫기",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
new file mode 100644
index 000000000..0d510d011
--- /dev/null
+++ b/app/javascript/mastodon/locales/lv.json
@@ -0,0 +1,358 @@
+{
+  "account.add_or_remove_from_list": "Add or Remove from lists",
+  "account.badges.bot": "Bot",
+  "account.block": "Block @{name}",
+  "account.block_domain": "Hide everything from {domain}",
+  "account.blocked": "Blocked",
+  "account.direct": "Direct message @{name}",
+  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
+  "account.edit_profile": "Edit profile",
+  "account.endorse": "Feature on profile",
+  "account.follow": "Follow",
+  "account.followers": "Followers",
+  "account.followers.empty": "No one follows this user yet.",
+  "account.follows": "Follows",
+  "account.follows.empty": "This user doesn't follow anyone yet.",
+  "account.follows_you": "Follows you",
+  "account.hide_reblogs": "Hide boosts from @{name}",
+  "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.media": "Media",
+  "account.mention": "Mention @{name}",
+  "account.moved_to": "{name} has moved to:",
+  "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Toots and replies",
+  "account.report": "Report @{name}",
+  "account.requested": "Awaiting approval. Click to cancel follow request",
+  "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
+  "account.unblock": "Unblock @{name}",
+  "account.unblock_domain": "Unhide {domain}",
+  "account.unendorse": "Don't feature on profile",
+  "account.unfollow": "Unfollow",
+  "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.view_full_profile": "View full profile",
+  "alert.unexpected.message": "An unexpected error occurred.",
+  "alert.unexpected.title": "Oops!",
+  "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
+  "column.blocks": "Blocked users",
+  "column.community": "Local timeline",
+  "column.direct": "Direct messages",
+  "column.domain_blocks": "Hidden domains",
+  "column.favourites": "Favourites",
+  "column.follow_requests": "Follow requests",
+  "column.home": "Home",
+  "column.lists": "Lists",
+  "column.mutes": "Muted users",
+  "column.notifications": "Notifications",
+  "column.pins": "Pinned toot",
+  "column.public": "Federated timeline",
+  "column_back_button.label": "Back",
+  "column_header.hide_settings": "Hide settings",
+  "column_header.moveLeft_settings": "Move column to the left",
+  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.pin": "Pin",
+  "column_header.show_settings": "Show settings",
+  "column_header.unpin": "Unpin",
+  "column_subheading.settings": "Settings",
+  "community.column_settings.media_only": "Media Only",
+  "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
+  "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.lock_disclaimer.lock": "locked",
+  "compose_form.placeholder": "What is on your mind?",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "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.spoiler.unmarked": "Text is not hidden",
+  "compose_form.spoiler_placeholder": "Write your warning here",
+  "confirmation_modal.cancel": "Cancel",
+  "confirmations.block.confirm": "Block",
+  "confirmations.block.message": "Are you sure you want to block {name}?",
+  "confirmations.delete.confirm": "Delete",
+  "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.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.mute.confirm": "Mute",
+  "confirmations.mute.message": "Are you sure you want to mute {name}?",
+  "confirmations.redraft.confirm": "Delete & redraft",
+  "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.reply.confirm": "Reply",
+  "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
+  "confirmations.unfollow.confirm": "Unfollow",
+  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+  "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.blocks": "You haven't blocked any users yet.",
+  "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 instances to fill it up",
+  "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",
+  "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.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",
+  "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",
+  "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.",
+  "introduction.interactions.action": "Finish tutorial!",
+  "introduction.interactions.favourite.headline": "Favourite",
+  "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.text": "You can share other people's toots with your followers by boosting them.",
+  "introduction.interactions.reply.headline": "Reply",
+  "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.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.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.direct": "to open direct messages column",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourites": "to open favourites list",
+  "keyboard_shortcuts.federated": "to open federated timeline",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.home": "to open home timeline",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.local": "to open local timeline",
+  "keyboard_shortcuts.mention": "to mention author",
+  "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.pinned": "to open pinned toots list",
+  "keyboard_shortcuts.profile": "to open author's profile",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.requests": "to open follow requests list",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.start": "to open \"get started\" column",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "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.next": "Next",
+  "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among people you follow",
+  "lists.subheading": "Your lists",
+  "loading_indicator.label": "Loading...",
+  "media_gallery.toggle_visible": "Toggle visibility",
+  "missing_indicator.label": "Not found",
+  "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.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.info": "About this instance",
+  "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",
+  "notification.favourite": "{name} favourited your status",
+  "notification.follow": "{name} followed you",
+  "notification.mention": "{name} mentioned you",
+  "notification.reblog": "{name} boosted your status",
+  "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.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.follow": "New followers:",
+  "notifications.column_settings.mention": "Mentions:",
+  "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.filter.follows": "Follows",
+  "notifications.filter.mentions": "Mentions",
+  "notifications.group": "{count} notifications",
+  "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",
+  "regeneration_indicator.label": "Loading…",
+  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancel",
+  "report.forward": "Forward to {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 instance 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_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.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_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
+  "status.block": "Block @{name}",
+  "status.cancel_reblog_private": "Unboost",
+  "status.cannot_reblog": "This post cannot be boosted",
+  "status.delete": "Delete",
+  "status.detailed_status": "Detailed conversation view",
+  "status.direct": "Direct message @{name}",
+  "status.embed": "Embed",
+  "status.favourite": "Favourite",
+  "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.open": "Expand this status",
+  "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
+  "status.read_more": "Read more",
+  "status.reblog": "Boost",
+  "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.reply": "Reply",
+  "status.replyAll": "Reply to thread",
+  "status.report": "Report @{name}",
+  "status.sensitive_toggle": "Click to view",
+  "status.sensitive_warning": "Sensitive content",
+  "status.share": "Share",
+  "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.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
+  "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",
+  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "upload_area.title": "Drag & drop to upload",
+  "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
+  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.focus": "Crop",
+  "upload_form.undo": "Delete",
+  "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound"
+}
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index 9e613ce59..0d510d011 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index d8189d45f..e5f7b0bdf 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Goedkeuren",
   "follow_request.reject": "Afkeuren",
   "getting_started.developers": "Ontwikkelaars",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Gebruikersgids",
   "getting_started.documentation": "Documentatie",
   "getting_started.heading": "Aan de slag",
   "getting_started.invite": "Mensen uitnodigen",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
   "standalone.public_title": "Een kijkje binnenin...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Blokkeer @{name}",
   "status.cancel_reblog_private": "Niet langer boosten",
   "status.cannot_reblog": "Deze toot kan niet geboost worden",
@@ -341,7 +343,7 @@
   "upload_area.title": "Hierin slepen om te uploaden",
   "upload_button.label": "Media toevoegen (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
-  "upload_form.focus": "Bijsnijden",
+  "upload_form.focus": "Voorvertoning aanpassen",
   "upload_form.undo": "Verwijderen",
   "upload_progress.label": "Uploaden...",
   "video.close": "Video sluiten",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 7ffdd78a0..fa08e8d73 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}",
   "standalone.public_title": "En titt inni...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Denne posten kan ikke fremheves",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 8dca0d729..87fbf54c3 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Acceptar",
   "follow_request.reject": "Regetar",
   "getting_started.developers": "Desvelopaires",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Annuari de perfils",
   "getting_started.documentation": "Documentacion",
   "getting_started.heading": "Per començar",
   "getting_started.invite": "Convidar de monde",
@@ -242,9 +242,9 @@
   "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
   "notifications.column_settings.alert": "Notificacions localas",
   "notifications.column_settings.favourite": "Favorits :",
-  "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.advanced": "Mostrar totas las categorias",
+  "notifications.column_settings.filter_bar.category": "Barra de recèrca rapida",
+  "notifications.column_settings.filter_bar.show": "Mostrar",
   "notifications.column_settings.follow": "Nòus seguidors :",
   "notifications.column_settings.mention": "Mencions :",
   "notifications.column_settings.push": "Notificacions",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Tuts",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
   "standalone.public_title": "Una ulhada dedins…",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Blocar @{name}",
   "status.cancel_reblog_private": "Quitar de partejar",
   "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 92133f812..6a1b8a0c6 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -297,6 +297,8 @@
   "search_results.statuses": "Wpisy",
   "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}",
   "standalone.public_title": "Spojrzenie w głąb…",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Zablokuj @{name}",
   "status.cancel_reblog_private": "Cofnij podbicie",
   "status.cannot_reblog": "Ten wpis nie może zostać podbity",
@@ -346,7 +348,7 @@
   "upload_area.title": "Przeciągnij i upuść aby wysłać",
   "upload_button.label": "Dodaj zawartość multimedialną (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
-  "upload_form.focus": "Przytnij",
+  "upload_form.focus": "Dopasuj podgląd",
   "upload_form.undo": "Usuń",
   "upload_progress.label": "Wysyłanie...",
   "video.close": "Zamknij film",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 0efcad486..b0555bd0c 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -132,7 +132,7 @@
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
   "getting_started.developers": "Desenvolvedores",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Diretório de perfis",
   "getting_started.documentation": "Documentação",
   "getting_started.heading": "Primeiros passos",
   "getting_started.invite": "Convide pessoas",
@@ -149,23 +149,23 @@
   "home.column_settings.basic": "Básico",
   "home.column_settings.show_reblogs": "Mostrar compartilhamentos",
   "home.column_settings.show_replies": "Mostrar as respostas",
-  "introduction.federation.action": "Next",
+  "introduction.federation.action": "Próximo",
   "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.text": "Posts públicos de outros servidores do fediverso vão aparecer na timeline global.",
   "introduction.federation.home.headline": "Home",
-  "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": "Posts de pessoas que você segue vão aparecer na sua página inicial. Você pode seguir pessoas de qualquer servidor!",
   "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.",
-  "introduction.interactions.action": "Finish tutorial!",
-  "introduction.interactions.favourite.headline": "Favourite",
-  "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.text": "You can share other people's toots with your followers by boosting them.",
-  "introduction.interactions.reply.headline": "Reply",
-  "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.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.federation.local.text": "Posts públicos de pessoas no mesmo servidor que você vão aparecer na timeline local.",
+  "introduction.interactions.action": "Finalizar o tutorial!",
+  "introduction.interactions.favourite.headline": "Favoritos",
+  "introduction.interactions.favourite.text": "Você pode salvar um toot pra mais tarde, e deixar a pessoa que postou saber que você gostou, favoritando-o.",
+  "introduction.interactions.reblog.headline": "Compartilhamento",
+  "introduction.interactions.reblog.text": "Você pode mostrar toots de outras pessoas aos seus seguidores compartilhando.",
+  "introduction.interactions.reply.headline": "Responder",
+  "introduction.interactions.reply.text": "Você pode responder a toots de outras pessoas e aos seus, e isso vai uni-los em uma conversa.",
+  "introduction.welcome.action": "Vamos!",
+  "introduction.welcome.headline": "Primeiros passos",
+  "introduction.welcome.text": "Boas vindas ao fediverso! Em alguns momentos, você vai poder transmitir mensagens e falar com pessoas amigas através de uma variedade de servidores. Mas esse servidor, {domain}, é especial—é onde o seu perfil está hospedado, então lembre do nome.",
   "keyboard_shortcuts.back": "para navegar de volta",
   "keyboard_shortcuts.blocked": "para abrir a lista de usuários bloqueados",
   "keyboard_shortcuts.boost": "para compartilhar",
@@ -242,20 +242,20 @@
   "notifications.clear_confirmation": "Você tem certeza de que quer limpar todas as suas notificações permanentemente?",
   "notifications.column_settings.alert": "Notificações no computador",
   "notifications.column_settings.favourite": "Favoritos:",
-  "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.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": "Novos seguidores:",
   "notifications.column_settings.mention": "Menções:",
   "notifications.column_settings.push": "Enviar notificações",
   "notifications.column_settings.reblog": "Compartilhamento:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
-  "notifications.filter.all": "All",
-  "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
-  "notifications.filter.follows": "Follows",
-  "notifications.filter.mentions": "Mentions",
+  "notifications.filter.all": "Tudo",
+  "notifications.filter.boosts": "Compartilhamentos",
+  "notifications.filter.favourites": "Favoritos",
+  "notifications.filter.follows": "Seguidores",
+  "notifications.filter.mentions": "Menções",
   "notifications.group": "{count} notificações",
   "privacy.change": "Ajustar a privacidade da mensagem",
   "privacy.direct.long": "Apenas para usuários mencionados",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Dê uma espiada...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Desfazer compartilhamento",
   "status.cannot_reblog": "Esta postagem não pode ser compartilhada",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 049a8901a..d4126704a 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Espreitar lá dentro...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Este post não pode ser partilhado",
diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json
index 211f2e660..f213f8ea3 100644
--- a/app/javascript/mastodon/locales/ro.json
+++ b/app/javascript/mastodon/locales/ro.json
@@ -1,23 +1,23 @@
 {
-  "account.add_or_remove_from_list": "Add or Remove from lists",
+  "account.add_or_remove_from_list": "Adaugă sau Elimină din liste",
   "account.badges.bot": "Bot",
   "account.block": "Blochează @{name}",
   "account.block_domain": "Ascunde tot de la {domain}",
   "account.blocked": "Blocat",
   "account.direct": "Mesaj direct @{name}",
-  "account.disclaimer_full": "Informațiile de mai jos pot reflecta profilul incomplet al utilizatorului.",
+  "account.disclaimer_full": "Informațiile de mai jos pot reflecta profilul utilizatorului incomplet.",
   "account.domain_blocked": "Domeniu ascuns",
   "account.edit_profile": "Editează profilul",
   "account.endorse": "Redistribuie pe profil",
   "account.follow": "Urmărește",
   "account.followers": "Urmăritori",
-  "account.followers.empty": "Nimeni nu urmărește acest utilizator incă.",
+  "account.followers.empty": "Acest utilizator nu are încă urmăritori.",
   "account.follows": "Urmărește",
   "account.follows.empty": "Acest utilizator nu urmărește pe nimeni incă.",
   "account.follows_you": "Te urmărește",
   "account.hide_reblogs": "Ascunde redistribuirile de la @{name}",
-  "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": "Deținerea acestui link a fost verificată la {date}",
+  "account.locked_info": "Acest profil este privat. Această persoană gestioneaz manual cine o urmărește.",
   "account.media": "Media",
   "account.mention": "Menționează @{name}",
   "account.moved_to": "{name} a fost mutat la:",
@@ -27,22 +27,22 @@
   "account.posts": "Postări",
   "account.posts_with_replies": "Postări și replici",
   "account.report": "Raportează @{name}",
-  "account.requested": "Așteaptă aprobare. Apasă pentru a anula cererea de urmărire",
+  "account.requested": "Se așteaptă aprobarea. Apasă pentru a anula cererea de urmărire",
   "account.share": "Distribuie profilul lui @{name}",
   "account.show_reblogs": "Arată redistribuirile de la @{name}",
   "account.unblock": "Deblochează @{name}",
   "account.unblock_domain": "Arată {domain}",
   "account.unendorse": "Nu promova pe profil",
   "account.unfollow": "Nu mai urmări",
-  "account.unmute": "Pornește notificările @{name}",
-  "account.unmute_notifications": "Pornește notificările de la @{name}",
+  "account.unmute": "Activează notificările de la @{name}",
+  "account.unmute_notifications": "Activează notificările de la @{name}",
   "account.view_full_profile": "Vezi profilul complet",
   "alert.unexpected.message": "A apărut o eroare neașteptată.",
   "alert.unexpected.title": "Hopa!",
-  "boost_modal.combo": "Poți apăsa {combo} pentru a sări peste asta data viitoare",
+  "boost_modal.combo": "Poți apăsa {combo} pentru a omite asta data viitoare",
   "bundle_column_error.body": "Ceva nu a funcționat la încărcarea acestui component.",
   "bundle_column_error.retry": "Încearcă din nou",
-  "bundle_column_error.title": "Eoare de rețea",
+  "bundle_column_error.title": "Eroare de rețea",
   "bundle_modal_error.close": "Închide",
   "bundle_modal_error.message": "Ceva nu a funcționat în timupul încărcării acestui component.",
   "bundle_modal_error.retry": "Încearcă din nou",
@@ -70,16 +70,16 @@
   "compose_form.direct_message_warning": "Această postare va fi trimisă doar utilizatorilor menționați.",
   "compose_form.direct_message_warning_learn_more": "Află mai multe",
   "compose_form.hashtag_warning": "Această postare nu va fi listată sub nici un hastag. Doar postările publice pot fi găsite dupa un hastag.",
-  "compose_form.lock_disclaimer": "Contul tău nu este {locked}. Toată lumea te poate urmări pentru a vedea postările doar pentru urmăritori.",
+  "compose_form.lock_disclaimer": "Contul tău nu este {locked}. Oricine te poate urmări fără aprobarea ta și vedea toate postările tale.",
   "compose_form.lock_disclaimer.lock": "privat",
   "compose_form.placeholder": "La ce te gândești?",
   "compose_form.publish": "Postează",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Conținutul media este marcat ca sensibil",
   "compose_form.sensitive.unmarked": "Conținutul media nu este marcat ca sensibil",
-  "compose_form.spoiler.marked": "Textul este ascuns sub advertizare",
+  "compose_form.spoiler.marked": "Textul este ascuns sub o avertizare",
   "compose_form.spoiler.unmarked": "Textul nu este ascuns",
-  "compose_form.spoiler_placeholder": "Scrie adveritzarea aici",
+  "compose_form.spoiler_placeholder": "Scrie averitzarea aici",
   "confirmation_modal.cancel": "Anulează",
   "confirmations.block.confirm": "Blochează",
   "confirmations.block.message": "Ești sigur că vrei să blochezi {name}?",
@@ -88,13 +88,13 @@
   "confirmations.delete_list.confirm": "Șterge",
   "confirmations.delete_list.message": "Ești sigur că vrei să ștergi permanent această listă?",
   "confirmations.domain_block.confirm": "Ascunde tot domeniul",
-  "confirmations.domain_block.message": "Ești absolut sigur că vrei să blochezi complet {domain}? În cele mai multe cazuri raportarea sau oprirea anumitor lucruri este suficientă și de preferat. Nu vei mai vedea nici un conținut de la acest domeniu in nici un flux public sau în notificările tale. Urmăritorii tăi de la acele domenii vor fi retrași.",
+  "confirmations.domain_block.message": "Ești absolut sigur că vrei să blochezi complet {domain}? În cele mai multe cazuri raportarea sau oprirea anumitor lucruri este suficientă și de preferat. Nu vei mai vedea nici un conținut de la acest domeniu in nici un flux public sau în notificările tale. Urmăritorii tăi de la acele domenii vor fi eliminați.",
   "confirmations.mute.confirm": "Oprește",
   "confirmations.mute.message": "Ești sigur că vrei să oprești {name}?",
   "confirmations.redraft.confirm": "Șterge și salvează ca ciornă",
   "confirmations.redraft.message": "Ești sigur că vrei să faci asta? Tot ce ține de această postare, inclusiv răspunsurile vor fi deconectate.",
-  "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": "Răspunde",
+  "confirmations.reply.message": "Răspunzând la asta acum, mesajul pe care îl compui în prezent se va șterge. Ești sigur că vrei să continui?",
   "confirmations.unfollow.confirm": "Nu mai urmări",
   "confirmations.unfollow.message": "Ești sigur că nu mai vrei să îl urmărești pe {name}?",
   "embed.instructions": "Inserează această postare pe site-ul tău adăugând codul de mai jos.",
@@ -113,15 +113,15 @@
   "emoji_button.search_results": "Rezultatele căutării",
   "emoji_button.symbols": "Simboluri",
   "emoji_button.travel": "Călătorii si Locuri",
-  "empty_column.account_timeline": "No toots here!",
+  "empty_column.account_timeline": "Nici o postare aici!",
   "empty_column.blocks": "Nu ai blocat nici un utilizator incă.",
   "empty_column.community": "Fluxul local este gol. Scrie ceva public pentru a împinge bila la vale!",
-  "empty_column.direct": "Nu ai nici un mesaj direct incă. Când trimiți sau primești unul, vor fi afișate aici.",
+  "empty_column.direct": "Nu ai nici un mesaj direct incă. Când trimiți sau primești unul, va fi afișat aici.",
   "empty_column.domain_blocks": "Nu sunt domenii ascunse incă.",
   "empty_column.favourited_statuses": "Nu ai nici o postare favorită încă. Când vei avea, vor fi afișate aici.",
   "empty_column.favourites": "Nimeni nu are această postare adăugată la favorite. Când cineva o va face va fi afișat aici.",
   "empty_column.follow_requests": "Nu ai încă nici o cerere de urmărire. Când vei primi una, va fi afișată aici.",
-  "empty_column.hashtag": "Acest hastag nu a fost folosit încă nicăieri.",
+  "empty_column.hashtag": "Acest hastag nu a fost folosit încă.",
   "empty_column.home": "Fluxul tău este gol. Vizitează {public} sau fă o căutare pentru a începe să cunoști oameni noi.",
   "empty_column.home.public_timeline": "fluxul public",
   "empty_column.list": "Nu este nimic încă în această listă. Când membrii acestei liste vor începe să posteze, va apărea aici.",
@@ -132,77 +132,77 @@
   "follow_request.authorize": "Autorizează",
   "follow_request.reject": "Respinge",
   "getting_started.developers": "Dezvoltatori",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Directorul profilului",
   "getting_started.documentation": "Documentație",
   "getting_started.heading": "Începe",
-  "getting_started.invite": "Invită oameni",
+  "getting_started.invite": "Invită prieteni",
   "getting_started.open_source_notice": "Mastodon este o rețea de socializare de tip open source. Puteți contribuii la dezvoltarea ei sau să semnalați erorile pe GitHub la {github}.",
   "getting_started.security": "Securitate",
-  "getting_started.terms": "Termenii de Utilizare",
-  "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.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",
+  "getting_started.terms": "Termeni de Utilizare",
+  "hashtag.column_header.tag_mode.all": "și {additional}",
+  "hashtag.column_header.tag_mode.any": "sau {additional}",
+  "hashtag.column_header.tag_mode.none": "fără {additional}",
+  "hashtag.column_settings.tag_mode.all": "Toate acestea",
+  "hashtag.column_settings.tag_mode.any": "Oricare din acestea",
+  "hashtag.column_settings.tag_mode.none": "Niciuna din aceastea",
+  "hashtag.column_settings.tag_toggle": "Adaugă etichete adiționale pentru această coloană",
   "home.column_settings.basic": "De bază",
   "home.column_settings.show_reblogs": "Arată redistribuirile",
   "home.column_settings.show_replies": "Arată răspunsurile",
-  "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",
-  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
+  "introduction.federation.action": "Următorul",
+  "introduction.federation.federated.headline": "Federalizat",
+  "introduction.federation.federated.text": "Postările publice de pe alte servere din rețea vor apărea in fluxul global.",
+  "introduction.federation.home.headline": "Acasă",
+  "introduction.federation.home.text": "Postările de la persoanele pe care le urmărești vor apărea in fluxul tău \"Acasă\". Poți urmări pe orice de pe orice 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.",
-  "introduction.interactions.action": "Finish tutorial!",
-  "introduction.interactions.favourite.headline": "Favourite",
-  "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.text": "You can share other people's toots with your followers by boosting them.",
-  "introduction.interactions.reply.headline": "Reply",
-  "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.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.federation.local.text": "Postările publice de la persoanele de pe acest server vor apărea în fluxul local.",
+  "introduction.interactions.action": "Încheie ghidul!",
+  "introduction.interactions.favourite.headline": "Favorite",
+  "introduction.interactions.favourite.text": "Poți salva o postare pentru a fi citită mai târziu și poți lăsa autorul să știe că iți place adăugândo la favorite.",
+  "introduction.interactions.reblog.headline": "Redistribuie",
+  "introduction.interactions.reblog.text": "Poți împărtăși postările altora cu urmăritorii tăi redistribuindule.",
+  "introduction.interactions.reply.headline": "Răspunde",
+  "introduction.interactions.reply.text": "Poți răspunde la postările tale și alte altora, care se vor lărgii în discuții.",
+  "introduction.welcome.action": "Să începem!",
+  "introduction.welcome.headline": "Primii pași",
+  "introduction.welcome.text": "Bun Venit in federație! In câteva momente, vei putea să transmiți mesaje și să participi la discuții cu oameni noi intr-o varietate foarte largă de servere din întreaga lume. Dar în special acest server, {domain},găzduiește profilul tău, deci reține numele acestuia.",
   "keyboard_shortcuts.back": "navighează inapoi",
   "keyboard_shortcuts.blocked": "să deschidă lista utilizatorilor blocați",
   "keyboard_shortcuts.boost": "să redistribuie",
   "keyboard_shortcuts.column": "să focuzeze o postare in una dintre coloane",
   "keyboard_shortcuts.compose": "sa focuzeze zona de compunere",
   "keyboard_shortcuts.description": "Descriere",
-  "keyboard_shortcuts.direct": "sa deschida coloane de mesaje directe",
-  "keyboard_shortcuts.down": "sa fie mutata jos in lista",
-  "keyboard_shortcuts.enter": "sa deschisa status",
-  "keyboard_shortcuts.favourite": "sa adauge la favorite",
-  "keyboard_shortcuts.favourites": "sa deschida lista cu favorite",
-  "keyboard_shortcuts.federated": "sa deschida fluxul global",
-  "keyboard_shortcuts.heading": "Comenzi din tastatură",
-  "keyboard_shortcuts.home": "sa deschida fluxul principal",
+  "keyboard_shortcuts.direct": "să deschidă coloana de mesaje directe",
+  "keyboard_shortcuts.down": "să fie mutată jos in lista",
+  "keyboard_shortcuts.enter": "să deschidă un status",
+  "keyboard_shortcuts.favourite": "să adauge la favorite",
+  "keyboard_shortcuts.favourites": "să deschidă lista cu favorite",
+  "keyboard_shortcuts.federated": "să deschidă fluxul global",
+  "keyboard_shortcuts.heading": "Comenzi rapide",
+  "keyboard_shortcuts.home": "să deschidă fluxul Acasă",
   "keyboard_shortcuts.hotkey": "Prescurtări",
-  "keyboard_shortcuts.legend": "sa afiseze aceasta legenda",
-  "keyboard_shortcuts.local": "sa deschida fluxul local",
-  "keyboard_shortcuts.mention": "sa mentioneze autorul",
-  "keyboard_shortcuts.muted": "sa deschida lista utilizatorilor opriti",
-  "keyboard_shortcuts.my_profile": "sa deschida profilul tau",
-  "keyboard_shortcuts.notifications": "sa deschida coloana cu notificari",
-  "keyboard_shortcuts.pinned": "sa deschida lista postarilor fixate",
-  "keyboard_shortcuts.profile": "sa deschida porfilul autorului",
-  "keyboard_shortcuts.reply": "sa raspunda",
-  "keyboard_shortcuts.requests": "sa deschida lista cu cereri de urmarire",
-  "keyboard_shortcuts.search": "sa focuseze cautarea",
-  "keyboard_shortcuts.start": "sa deschida coloana \"Incepere\"",
-  "keyboard_shortcuts.toggle_hidden": "sa arate/ascunda textul in spatele CW",
-  "keyboard_shortcuts.toot": "sa inceapa o noua postare",
-  "keyboard_shortcuts.unfocus": "sa dezactiveze compunerea/cautarea",
-  "keyboard_shortcuts.up": "sa mute mai sus in lista",
+  "keyboard_shortcuts.legend": "să afișeze această legendă",
+  "keyboard_shortcuts.local": "să deschidă fluxul local",
+  "keyboard_shortcuts.mention": "să menționeze autorul",
+  "keyboard_shortcuts.muted": "să deschidă lista utilizatorilor opriți",
+  "keyboard_shortcuts.my_profile": "să deschidă profilul tău",
+  "keyboard_shortcuts.notifications": "să deschidă coloana cu notificări",
+  "keyboard_shortcuts.pinned": "să deschidă lista postărilor fixate",
+  "keyboard_shortcuts.profile": "să deschidă porfilul autorului",
+  "keyboard_shortcuts.reply": "să răspundă",
+  "keyboard_shortcuts.requests": "să deschidă lista cu cereri de urmărire",
+  "keyboard_shortcuts.search": "să focuseze căutarea",
+  "keyboard_shortcuts.start": "să deschidă coloana \"Începere\"",
+  "keyboard_shortcuts.toggle_hidden": "să arate/ascundă textul in spatele CW",
+  "keyboard_shortcuts.toot": "să înceapă o postare nouă",
+  "keyboard_shortcuts.unfocus": "să dezactiveze zona de compunere/căutare",
+  "keyboard_shortcuts.up": "să mute mai sus în listă",
   "lightbox.close": "Închide",
   "lightbox.next": "Următorul",
   "lightbox.previous": "Precedentul",
   "lists.account.add": "Adaugă în listă",
   "lists.account.remove": "Elimină din listă",
-  "lists.delete": "Șterge listă",
+  "lists.delete": "Șterge lista",
   "lists.edit": "Editează lista",
   "lists.new.create": "Adaugă listă",
   "lists.new.title_placeholder": "Titlu pentru noua listă",
@@ -242,20 +242,20 @@
   "notifications.clear_confirmation": "Ești sigur că vrei să ștergi toate notificările?",
   "notifications.column_settings.alert": "Notificări pe desktop",
   "notifications.column_settings.favourite": "Favorite:",
-  "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.advanced": "Afișează toate categoriile",
+  "notifications.column_settings.filter_bar.category": "Bară de filtrare rapidă",
+  "notifications.column_settings.filter_bar.show": "Arată",
   "notifications.column_settings.follow": "Noi urmăritori:",
   "notifications.column_settings.mention": "Mențiuni:",
   "notifications.column_settings.push": "Notificări push",
   "notifications.column_settings.reblog": "Redistribuite:",
   "notifications.column_settings.show": "Arată în coloană",
   "notifications.column_settings.sound": "Redă sunet",
-  "notifications.filter.all": "All",
-  "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
-  "notifications.filter.follows": "Follows",
-  "notifications.filter.mentions": "Mentions",
+  "notifications.filter.all": "Toate",
+  "notifications.filter.boosts": "Redistribuiri",
+  "notifications.filter.favourites": "Favorite",
+  "notifications.filter.follows": "Urmărește",
+  "notifications.filter.mentions": "Menționări",
   "notifications.group": "{count} notificări",
   "privacy.change": "Cine vede asta",
   "privacy.direct.long": "Postează doar pentru utilizatorii menționați",
@@ -291,7 +291,9 @@
   "search_results.hashtags": "Hashtaguri",
   "search_results.statuses": "Postări",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
-  "standalone.public_title": "Se întâmplă acum",
+  "standalone.public_title": "Se întâmplă acum...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Blochează @{name}",
   "status.cancel_reblog_private": "Nedistribuit",
   "status.cannot_reblog": "Această postare nu poate fi redistribuită",
@@ -310,10 +312,10 @@
   "status.open": "Extinde acest status",
   "status.pin": "Fixează pe profil",
   "status.pinned": "Postare fixată",
-  "status.read_more": "Read more",
+  "status.read_more": "Citește mai mult",
   "status.reblog": "Redistribuie",
   "status.reblog_private": "Redistribuie către audiența originală",
-  "status.reblogged_by": "{name} redistribuit",
+  "status.reblogged_by": "{name} a redistribuit",
   "status.reblogs.empty": "Nimeni nu a redistribuit această postare până acum. Când cineva o va face, va apărea aici.",
   "status.redraft": "Șterge și adaugă la ciorne",
   "status.reply": "Răspunde",
@@ -326,11 +328,11 @@
   "status.show_less_all": "Arată mai puțin pentru toți",
   "status.show_more": "Arată mai mult",
   "status.show_more_all": "Arată mai mult pentru toți",
-  "status.show_thread": "Show thread",
+  "status.show_thread": "Arată topicul",
   "status.unmute_conversation": "Repornește conversația",
   "status.unpin": "Eliberează din profil",
-  "suggestions.dismiss": "Dismiss suggestion",
-  "suggestions.header": "You might be interested in…",
+  "suggestions.dismiss": "Omite sugestia",
+  "suggestions.header": "Ai putea fi interesat de…",
   "tabs_bar.federated_timeline": "Global",
   "tabs_bar.home": "Acasă",
   "tabs_bar.local_timeline": "Local",
@@ -341,7 +343,7 @@
   "upload_area.title": "Trage și eliberează pentru a încărca",
   "upload_button.label": "Adaugă media (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Adaugă o descriere pentru persoanele cu deficiențe de vedere",
-  "upload_form.focus": "Taie",
+  "upload_form.focus": "Schimbă previzualizarea",
   "upload_form.undo": "Șterge",
   "upload_progress.label": "Se Încarcă...",
   "video.close": "Închide video",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 0d10c663a..cb6010898 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Посты",
   "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
   "standalone.public_title": "Прямо сейчас",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Заблокировать @{name}",
   "status.cancel_reblog_private": "Не продвигать",
   "status.cannot_reblog": "Этот статус не может быть продвинут",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index b8a74fadb..d874ed0aa 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -33,11 +33,11 @@
   "account.unblock": "Odblokuj @{name}",
   "account.unblock_domain": "Prestaň skrývať {domain}",
   "account.unendorse": "Nezobrazuj na profile",
-  "account.unfollow": "Prestať nasledovať",
-  "account.unmute": "Prestať ignorovať @{name}",
-  "account.unmute_notifications": "Odtĺmiť notifikácie od @{name}",
+  "account.unfollow": "Prestaň následovať",
+  "account.unmute": "Prestaň ignorovať @{name}",
+  "account.unmute_notifications": "Odtĺm oboznámenia od @{name}",
   "account.view_full_profile": "Pozri celý profil",
-  "alert.unexpected.message": "Vyskytla sa neočakávaná chyba.",
+  "alert.unexpected.message": "Vyskytla sa nečakaná chyba.",
   "alert.unexpected.title": "Oops!",
   "boost_modal.combo": "Nabudúce môžeš kliknúť {combo} pre preskočenie",
   "bundle_column_error.body": "Pri načítaní tohto prvku nastala nejaká chyba.",
@@ -127,12 +127,12 @@
   "empty_column.list": "Tento zoznam je ešte prázdny. Keď ale členovia tohoto zoznamu napíšu nové správy, tak tie sa objavia priamo tu.",
   "empty_column.lists": "Nemáš ešte žiadne zoznamy. Keď nejaký vytvoríš, bude zobrazený práve tu.",
   "empty_column.mutes": "Ešte si nestĺmil žiadných užívateľov.",
-  "empty_column.notifications": "Nemáš ešte žiadne oznámenia. Zapoj sa s niekym do debaty a komunikuj s ostatnými aby diskusia mohla začať.",
-  "empty_column.public": "Ešte tu nič nie je. Napíš niečo verejne alebo začnite sledovať užívateľov z iných Mastodon serverov, aby tu tak niečo pribudlo",
+  "empty_column.notifications": "Ešte nemáš žiadne oznámenia. Začni komunikovať s ostatnými, aby diskusia mohla začať.",
+  "empty_column.public": "Ešte tu nič nie je. Napíš niečo verejne, alebo začni sledovať užívateľov z iných Mastodon serverov, aby tu niečo pribudlo",
   "follow_request.authorize": "Povoľ prístup",
   "follow_request.reject": "Odmietni",
   "getting_started.developers": "Vývojári",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Databáza profilov",
   "getting_started.documentation": "Dokumentácia",
   "getting_started.heading": "Začni tu",
   "getting_started.invite": "Pozvať ľudí",
@@ -292,6 +292,8 @@
   "search_results.statuses": "Hlášky",
   "search_results.total": "{count, number} {count, plural, one {výsledok} many {výsledkov} other {výsledky}}",
   "standalone.public_title": "Náhľad dovnútra...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Blokovať @{name}",
   "status.cancel_reblog_private": "Nezdieľaj",
   "status.cannot_reblog": "Tento príspevok nemôže byť re-tootnutý",
@@ -341,7 +343,7 @@
   "upload_area.title": "Pretiahni a pusť pre nahratie",
   "upload_button.label": "Pridať médiálny súbor (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "Opis pre slabo vidiacich",
-  "upload_form.focus": "Vystrihni",
+  "upload_form.focus": "Pozmeň náhľad",
   "upload_form.undo": "Vymaž",
   "upload_progress.label": "Nahráva sa...",
   "video.close": "Zavrieť video",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index 8b7b4586a..cabad737d 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Tuti",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 310456ca1..c8513dbe1 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}",
   "standalone.public_title": "Pogled iznutra...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Ovaj status ne može da se podrži",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index 01f215540..6e0ac6eca 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Трубе",
   "search_results.total": "{count, number} {count, plural, one {резултат} few {резултата} other {резултата}}",
   "standalone.public_title": "Поглед изнутра...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Блокирај @{name}",
   "status.cancel_reblog_private": "Уклони подршку",
   "status.cannot_reblog": "Овај статус не може да се подржи",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 82fef145e..47ce8497a 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, ett {result} andra {results}}",
   "standalone.public_title": "En titt inuti...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Ta bort knuff",
   "status.cannot_reblog": "Detta inlägg kan inte knuffas",
diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json
index 9e613ce59..0d510d011 100644
--- a/app/javascript/mastodon/locales/ta.json
+++ b/app/javascript/mastodon/locales/ta.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 2df54b6d8..7306ec001 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -1,5 +1,5 @@
 {
-  "account.add_or_remove_from_list": "Add or Remove from lists",
+  "account.add_or_remove_from_list": "జాబితాల నుండి చేర్చు లేదా తీసివేయి",
   "account.badges.bot": "బాట్",
   "account.block": "@{name} ను బ్లాక్ చేయి",
   "account.block_domain": "{domain} నుంచి అన్నీ దాచిపెట్టు",
@@ -17,7 +17,7 @@
   "account.follows_you": "మిమ్మల్ని అనుసరిస్తున్నారు",
   "account.hide_reblogs": "@{name} నుంచి బూస్ట్ లను దాచిపెట్టు",
   "account.link_verified_on": "ఈ లంకె యొక్క యాజమాన్యం {date}న పరీక్షించబడింది",
-  "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
+  "account.locked_info": "ఈ ఖాతా యొక్క గోప్యత స్థితి లాక్ చేయబడి వుంది. ఈ ఖాతాను ఎవరు అనుసరించవచ్చో యజమానే నిర్ణయం తీసుకుంటారు.",
   "account.media": "మీడియా",
   "account.mention": "@{name}ను ప్రస్తావించు",
   "account.moved_to": "{name} ఇక్కడికి మారారు:",
@@ -113,7 +113,7 @@
   "emoji_button.search_results": "శోధన ఫలితాలు",
   "emoji_button.symbols": "చిహ్నాలు",
   "emoji_button.travel": "ప్రయాణం & ప్రదేశాలు",
-  "empty_column.account_timeline": "No toots here!",
+  "empty_column.account_timeline": "ఇక్కడ ఏ టూట్లూ లేవు!No toots here!",
   "empty_column.blocks": "మీరు ఇంకా ఏ వినియోగదారులనూ బ్లాక్ చేయలేదు.",
   "empty_column.community": "స్థానిక కాలక్రమం ఖాళీగా ఉంది. మొదలుపెట్టడానికి బహిరంగంగా ఏదో ఒకటి వ్రాయండి!",
   "empty_column.direct": "మీకు ఇంకా ఏ ప్రత్యక్ష సందేశాలు లేవు. మీరు ఒకదాన్ని పంపినప్పుడు లేదా స్వీకరించినప్పుడు, అది ఇక్కడ చూపబడుతుంది.",
@@ -132,40 +132,40 @@
   "follow_request.authorize": "అనుమతించు",
   "follow_request.reject": "తిరస్కరించు",
   "getting_started.developers": "డెవలపర్లు",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "ప్రొఫైల్ డైరెక్టరీProfile directory",
   "getting_started.documentation": "డాక్యుమెంటేషన్",
   "getting_started.heading": "మొదలుపెడదాం",
   "getting_started.invite": "వ్యక్తులను ఆహ్వానించండి",
   "getting_started.open_source_notice": "మాస్టొడొన్ ఓపెన్ సోర్స్ సాఫ్ట్వేర్. మీరు {github} వద్ద 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.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.tag_mode.all": "ఇవన్నీAll of these",
+  "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": "ప్రత్యుత్తరాలను చూపించు",
-  "introduction.federation.action": "Next",
+  "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.text": "ఫెడివర్స్ లోని ఇతర సర్వర్లకు చెందిన పబ్లిక్ టూట్లు ఫెడరేటెడ్ టైంలైన్ లో కనిపిస్తాయి.",
   "introduction.federation.home.headline": "Home",
-  "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": "Local",
-  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
-  "introduction.interactions.action": "Finish tutorial!",
-  "introduction.interactions.favourite.headline": "Favourite",
-  "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.text": "You can share other people's toots with your followers by boosting them.",
-  "introduction.interactions.reply.headline": "Reply",
-  "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.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.federation.local.text": "ఈ సర్వరుకు చెందిన ఖాతాల పబ్లిక్ టూట్లు లోకల్ టైంలైన్ లో కనిపిస్తాయి.",
+  "introduction.interactions.action": "బోధనను ముగించు!",
+  "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.welcome.action": "ఇక ప్రారంభించు!",
+  "introduction.welcome.headline": "మొదటి మెట్లు",
+  "introduction.welcome.text": "ఫెడివర్స్ కు స్వాగతం! మరి కొంతసేపట్లో మీరు సందేశాలను ప్రసారం చేయవచ్చు మరియు వేర్వేరు సర్వర్లలో వున్న మీ స్నేహితులతో మాట్లాడవచ్చు. కానీ ఈ సర్వరు, {domain}, ప్రత్యేకమైనది - ఇది మీ ప్రొఫైలును హోస్టు చేస్తుంది, కాబట్టి ఈ సర్వరు పేరును గుర్తుంచుకోండి.",
   "keyboard_shortcuts.back": "వెనక్కి తిరిగి వెళ్ళడానికి",
   "keyboard_shortcuts.blocked": "బ్లాక్ చేయబడిన వినియోగదారుల జాబితాను తెరవడానికి",
   "keyboard_shortcuts.boost": "బూస్ట్ చేయడానికి",
@@ -242,20 +242,20 @@
   "notifications.clear_confirmation": "మీరు మీ అన్ని నోటిఫికేషన్లను శాశ్వతంగా తొలగించాలనుకుంటున్నారా?",
   "notifications.column_settings.alert": "డెస్క్టాప్ నోటిఫికేషన్లు",
   "notifications.column_settings.favourite": "ఇష్టపడినవి:",
-  "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.advanced": "అన్ని విభాగాలను చూపించు",
+  "notifications.column_settings.filter_bar.category": "క్విక్ ఫిల్టర్ బార్",
+  "notifications.column_settings.filter_bar.show": "చూపించు",
   "notifications.column_settings.follow": "క్రొత్త అనుచరులు:",
   "notifications.column_settings.mention": "ప్రస్తావనలు:",
   "notifications.column_settings.push": "పుష్ ప్రకటనలు",
   "notifications.column_settings.reblog": "బూస్ట్ లు:",
   "notifications.column_settings.show": "నిలువు వరుసలో చూపు",
   "notifications.column_settings.sound": "ధ్వనిని ప్లే చేయి",
-  "notifications.filter.all": "All",
-  "notifications.filter.boosts": "Boosts",
-  "notifications.filter.favourites": "Favourites",
-  "notifications.filter.follows": "Follows",
-  "notifications.filter.mentions": "Mentions",
+  "notifications.filter.all": "అన్నీ",
+  "notifications.filter.boosts": "బూస్ట్లు",
+  "notifications.filter.favourites": "ఇష్టాలు",
+  "notifications.filter.follows": "అనుసరిస్తున్నవి",
+  "notifications.filter.mentions": "పేర్కొన్నవి",
   "notifications.group": "{count} ప్రకటనలు",
   "privacy.change": "స్టేటస్ గోప్యతను సర్దుబాటు చేయండి",
   "privacy.direct.long": "పేర్కొన్న వినియోగదారులకు మాత్రమే పోస్ట్ చేయి",
@@ -292,6 +292,8 @@
   "search_results.statuses": "టూట్లు",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "లోపలికి ఒక చూపు...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "@{name} ను బ్లాక్ చేయి",
   "status.cancel_reblog_private": "బూస్ట్ను తొలగించు",
   "status.cannot_reblog": "ఈ పోస్ట్ను బూస్ట్ చేయడం సాధ్యం కాదు",
@@ -326,7 +328,7 @@
   "status.show_less_all": "అన్నిటికీ తక్కువ చూపించు",
   "status.show_more": "ఇంకా చూపించు",
   "status.show_more_all": "అన్నిటికీ ఇంకా చూపించు",
-  "status.show_thread": "Show thread",
+  "status.show_thread": "గొలుసును చూపించు",
   "status.unmute_conversation": "సంభాషణను అన్మ్యూట్ చేయి",
   "status.unpin": "ప్రొఫైల్ నుండి పీకివేయు",
   "suggestions.dismiss": "సూచనను రద్దు చేయి",
@@ -341,7 +343,7 @@
   "upload_area.title": "అప్లోడ్ చేయడానికి డ్రాగ్ & డ్రాప్ చేయండి",
   "upload_button.label": "మీడియాను జోడించండి (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_form.description": "దృష్టి లోపమున్న వారి కోసం వివరించండి",
-  "upload_form.focus": "కత్తిరించు",
+  "upload_form.focus": "ప్రివ్యూను మార్చు",
   "upload_form.undo": "తొలగించు",
   "upload_progress.label": "అప్లోడ్ అవుతోంది...",
   "video.close": "వీడియోని మూసివేయి",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 92bb05e7c..2683284f4 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 134285953..5d8fc229e 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {sonuç} other {sonuçlar}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Bu gönderi boost edilemez",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 752112588..606dda89f 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}",
   "standalone.public_title": "A look inside...",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "Цей допис не може бути передмухнутий",
diff --git a/app/javascript/mastodon/locales/whitelist_lv.json b/app/javascript/mastodon/locales/whitelist_lv.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_lv.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 54aa84681..dfa261d6e 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "嘟文",
   "search_results.total": "共 {count, number} 个结果",
   "standalone.public_title": "大家都在干啥?",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "屏蔽 @{name}",
   "status.cancel_reblog_private": "取消转嘟",
   "status.cannot_reblog": "无法转嘟这条嘟文",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 927cf9578..e57aa6d96 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "文章",
   "search_results.total": "{count, number} 項結果",
   "standalone.public_title": "站點一瞥…",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "封鎖 @{name}",
   "status.cancel_reblog_private": "取消轉推",
   "status.cannot_reblog": "這篇文章無法被轉推",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index e5be85ac5..0cbe5da5a 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -292,6 +292,8 @@
   "search_results.statuses": "嘟文",
   "search_results.total": "{count, number} 項結果",
   "standalone.public_title": "站點一瞥…",
+  "status.admin_account": "Open moderation interface for @{name}",
+  "status.admin_status": "Open this status in the moderation interface",
   "status.block": "封鎖 @{name}",
   "status.cancel_reblog_private": "取消轉嘟",
   "status.cannot_reblog": "這篇嘟文無法被轉嘟",
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 47c4bf75c..196d2d02f 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -16,6 +16,17 @@ function main() {
   const Rellax = require('rellax');
   const createHistory = require('history').createBrowserHistory;
 
+  const scrollToDetailedStatus = () => {
+    const history = createHistory();
+    const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
+    const location = history.location;
+
+    if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
+      detailedStatuses[0].scrollIntoView();
+      history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
+    }
+  };
+
   ready(() => {
     const locale = document.documentElement.lang;
 
@@ -59,8 +70,14 @@ function main() {
 
           ReactDOM.render(<MediaContainer locale={locale} components={reactComponents} />, content);
           document.body.appendChild(content);
+          scrollToDetailedStatus();
         })
-        .catch(error => console.error(error));
+        .catch(error => {
+          console.error(error);
+          scrollToDetailedStatus();
+        });
+    } else {
+      scrollToDetailedStatus();
     }
 
     const parallaxComponents = document.querySelectorAll('.parallax');
@@ -68,15 +85,6 @@ function main() {
     if (parallaxComponents.length > 0 ) {
       new Rellax('.parallax', { speed: -1 });
     }
-
-    const history = createHistory();
-    const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
-    const location = history.location;
-
-    if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
-      detailedStatuses[0].scrollIntoView();
-      history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
-    }
   });
 }
 
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 8b111a936..e7124a2c0 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -151,6 +151,20 @@ $no-columns-breakpoint: 600px;
       font-weight: 500;
     }
 
+    .directory__tag a {
+      box-shadow: none;
+    }
+
+    .directory__tag h4 {
+      font-size: 18px;
+      font-weight: 700;
+      color: $primary-text-color;
+      text-transform: none;
+      padding-bottom: 0;
+      margin-bottom: 0;
+      border-bottom: none;
+    }
+
     & > p {
       font-size: 14px;
       line-height: 18px;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index da66ccd95..8c1115e76 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2587,6 +2587,15 @@ a.status-card {
   flex: 0 0 100px;
   background: lighten($ui-base-color, 8%);
   position: relative;
+
+  & > .fa {
+    font-size: 21px;
+    position: absolute;
+    transform-origin: 50% 50%;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+  }
 }
 
 .status-card.horizontal {
@@ -4046,6 +4055,7 @@ a.status-card.compact:hover {
     color: $highlight-text-color;
   }
 
+  .status__content,
   .status__content p {
     color: $inverted-text-color;
   }
diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss
index 1f96e7368..e4564f062 100644
--- a/app/javascript/styles/mastodon/dashboard.scss
+++ b/app/javascript/styles/mastodon/dashboard.scss
@@ -39,6 +39,7 @@
     color: $primary-text-color;
     font-family: $font-display, sans-serif;
     margin-bottom: 20px;
+    line-height: 30px;
   }
 
   &__text {
diff --git a/app/models/account.rb b/app/models/account.rb
index 67d9a583e..1ee63c738 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -79,7 +79,7 @@ class Account < ApplicationRecord
   validates_with UniqueUsernameValidator, if: -> { local? && will_save_change_to_username? }
   validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
   validates :display_name, length: { maximum: MAX_DISPLAY_NAME_LENGTH }, if: -> { local? && will_save_change_to_display_name? }
-  validate :note_length_does_not_exceed_length_limit, if: -> { local? && will_save_change_to_note? }
+  validates :note, note_length: { maximum: MAX_NOTE_LENGTH }, if: -> { local? && will_save_change_to_note? }
   validates :fields, length: { maximum: MAX_FIELDS }, if: -> { local? && will_save_change_to_fields? }
 
   scope :remote, -> { where.not(domain: nil) }
@@ -488,22 +488,6 @@ class Account < ApplicationRecord
     self.public_key  = keypair.public_key.to_pem
   end
 
-  YAML_START = "---\r\n"
-  YAML_END = "\r\n...\r\n"
-
-  def note_length_does_not_exceed_length_limit
-    note_without_metadata = note
-    if note.start_with? YAML_START
-      idx = note.index YAML_END
-      unless idx.nil?
-        note_without_metadata = note[(idx + YAML_END.length) .. -1]
-      end
-    end
-    if note_without_metadata.mb_chars.grapheme_length > MAX_NOTE_LENGTH
-      errors.add(:note, "can't be longer than 500 graphemes")
-    end
-  end
-
   def normalize_domain
     return if local?
 
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index de7f3d525..4e730451a 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -15,6 +15,7 @@ module AccountAssociations
     has_many :mentions, inverse_of: :account, dependent: :destroy
     has_many :notifications, inverse_of: :account, dependent: :destroy
     has_many :conversations, class_name: 'AccountConversation', dependent: :destroy, inverse_of: :account
+    has_many :scheduled_statuses, inverse_of: :account, dependent: :destroy
 
     # Pinned statuses
     has_many :status_pins, inverse_of: :account, dependent: :destroy
diff --git a/app/models/concerns/account_finder_concern.rb b/app/models/concerns/account_finder_concern.rb
index 6b7237e89..7e3bbde09 100644
--- a/app/models/concerns/account_finder_concern.rb
+++ b/app/models/concerns/account_finder_concern.rb
@@ -12,6 +12,10 @@ module AccountFinderConcern
       find_remote(username, domain) || raise(ActiveRecord::RecordNotFound)
     end
 
+    def representative
+      find_local(Setting.site_contact_username.gsub(/\A@/, '')) || Account.local.find_by(suspended: false)
+    end
+
     def find_local(username)
       find_remote(username, nil)
     end
diff --git a/app/models/instance.rb b/app/models/instance.rb
index 6d5c9c2ab..7448d465c 100644
--- a/app/models/instance.rb
+++ b/app/models/instance.rb
@@ -3,10 +3,23 @@
 class Instance
   include ActiveModel::Model
 
-  attr_accessor :domain, :accounts_count
+  attr_accessor :domain, :accounts_count, :domain_block
 
-  def initialize(account)
-    @domain = account.domain
-    @accounts_count = account.accounts_count
+  def initialize(resource)
+    @domain         = resource.domain
+    @accounts_count = resource.accounts_count
+    @domain_block   = resource.is_a?(DomainBlock) ? resource : DomainBlock.find_by(domain: domain)
+  end
+
+  def cached_sample_accounts
+    Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { Account.where(domain: domain).searchable.joins(:account_stat).popular.limit(3) }
+  end
+
+  def to_param
+    domain
+  end
+
+  def cache_key
+    domain
   end
 end
diff --git a/app/models/instance_filter.rb b/app/models/instance_filter.rb
index 5073cf1fa..3483d8cd6 100644
--- a/app/models/instance_filter.rb
+++ b/app/models/instance_filter.rb
@@ -8,21 +8,10 @@ class InstanceFilter
   end
 
   def results
-    scope = Account.remote.by_domain_accounts
-    params.each do |key, value|
-      scope.merge!(scope_for(key, value)) if value.present?
-    end
-    scope
-  end
-
-  private
-
-  def scope_for(key, value)
-    case key.to_s
-    when 'domain_name'
-      Account.matches_domain(value)
+    if params[:limited].present?
+      DomainBlock.order(id: :desc)
     else
-      raise "Unknown filter: #{key}"
+      Account.remote.by_domain_accounts
     end
   end
 end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index a034b55fd..601b14223 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -3,20 +3,21 @@
 #
 # Table name: media_attachments
 #
-#  id                :bigint(8)        not null, primary key
-#  status_id         :bigint(8)
-#  file_file_name    :string
-#  file_content_type :string
-#  file_file_size    :integer
-#  file_updated_at   :datetime
-#  remote_url        :string           default(""), not null
-#  created_at        :datetime         not null
-#  updated_at        :datetime         not null
-#  shortcode         :string
-#  type              :integer          default("image"), not null
-#  file_meta         :json
-#  account_id        :bigint(8)
-#  description       :text
+#  id                  :bigint(8)        not null, primary key
+#  status_id           :bigint(8)
+#  file_file_name      :string
+#  file_content_type   :string
+#  file_file_size      :integer
+#  file_updated_at     :datetime
+#  remote_url          :string           default(""), not null
+#  created_at          :datetime         not null
+#  updated_at          :datetime         not null
+#  shortcode           :string
+#  type                :integer          default("image"), not null
+#  file_meta           :json
+#  account_id          :bigint(8)
+#  description         :text
+#  scheduled_status_id :bigint(8)
 #
 
 class MediaAttachment < ApplicationRecord
@@ -94,8 +95,9 @@ class MediaAttachment < ApplicationRecord
   IMAGE_LIMIT = 8.megabytes
   VIDEO_LIMIT = 40.megabytes
 
-  belongs_to :account, inverse_of: :media_attachments, optional: true
-  belongs_to :status,  inverse_of: :media_attachments, optional: true
+  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
 
   has_attached_file :file,
                     styles: ->(f) { file_styles f },
@@ -112,8 +114,8 @@ class MediaAttachment < ApplicationRecord
   validates :account, presence: true
   validates :description, length: { maximum: 420 }, if: :local?
 
-  scope :attached,   -> { where.not(status_id: nil) }
-  scope :unattached, -> { where(status_id: nil) }
+  scope :attached,   -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
+  scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
   scope :local,      -> { where(remote_url: '') }
   scope :remote,     -> { where.not(remote_url: '') }
 
diff --git a/app/models/relay.rb b/app/models/relay.rb
index 75cb060b2..7478c110d 100644
--- a/app/models/relay.rb
+++ b/app/models/relay.rb
@@ -68,7 +68,7 @@ class Relay < ApplicationRecord
   end
 
   def some_local_account
-    @some_local_account ||= Account.local.find_by(suspended: false)
+    @some_local_account ||= Account.representative
   end
 
   def ensure_disabled
diff --git a/app/models/scheduled_status.rb b/app/models/scheduled_status.rb
new file mode 100644
index 000000000..27f0cbd28
--- /dev/null
+++ b/app/models/scheduled_status.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: scheduled_statuses
+#
+#  id           :bigint(8)        not null, primary key
+#  account_id   :bigint(8)
+#  scheduled_at :datetime
+#  params       :jsonb
+#
+
+class ScheduledStatus < ApplicationRecord
+  include Paginable
+
+  TOTAL_LIMIT = 300
+  DAILY_LIMIT = 25
+
+  belongs_to :account, inverse_of: :scheduled_statuses
+  has_many :media_attachments, inverse_of: :scheduled_status, dependent: :nullify
+
+  validate :validate_future_date
+  validate :validate_total_limit
+  validate :validate_daily_limit
+
+  private
+
+  def validate_future_date
+    errors.add(:scheduled_at, I18n.t('scheduled_statuses.too_soon')) if scheduled_at.present? && scheduled_at <= Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET
+  end
+
+  def validate_total_limit
+    errors.add(:base, I18n.t('scheduled_statuses.over_total_limit', limit: TOTAL_LIMIT)) if account.scheduled_statuses.count >= TOTAL_LIMIT
+  end
+
+  def validate_daily_limit
+    errors.add(:base, I18n.t('scheduled_statuses.over_daily_limit', limit: DAILY_LIMIT)) if account.scheduled_statuses.where('scheduled_at::date = ?::date', scheduled_at).count >= DAILY_LIMIT
+  end
+end
diff --git a/app/policies/instance_policy.rb b/app/policies/instance_policy.rb
index d1956e2de..a73823556 100644
--- a/app/policies/instance_policy.rb
+++ b/app/policies/instance_policy.rb
@@ -5,7 +5,7 @@ class InstancePolicy < ApplicationPolicy
     admin?
   end
 
-  def resubscribe?
+  def show?
     admin?
   end
 end
diff --git a/app/serializers/rest/scheduled_status_serializer.rb b/app/serializers/rest/scheduled_status_serializer.rb
new file mode 100644
index 000000000..5d6311b87
--- /dev/null
+++ b/app/serializers/rest/scheduled_status_serializer.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class REST::ScheduledStatusSerializer < ActiveModel::Serializer
+  attributes :id, :scheduled_at, :params
+
+  has_many :media_attachments, serializer: REST::MediaAttachmentSerializer
+
+  def id
+    object.id.to_s
+  end
+
+  def params
+    object.params.without(:application_id)
+  end
+end
diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb
index 8430d12d5..3c2044941 100644
--- a/app/services/activitypub/fetch_remote_account_service.rb
+++ b/app/services/activitypub/fetch_remote_account_service.rb
@@ -5,8 +5,8 @@ class ActivityPub::FetchRemoteAccountService < BaseService
 
   SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
 
-  # Does a WebFinger roundtrip on each call
-  def call(uri, id: true, prefetched_body: nil, break_on_redirect: false)
+  # Does a WebFinger roundtrip on each call, unless `only_key` is true
+  def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false)
     return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri)
 
     @json = if prefetched_body.nil?
@@ -21,9 +21,9 @@ class ActivityPub::FetchRemoteAccountService < BaseService
     @username = @json['preferredUsername']
     @domain   = Addressable::URI.parse(@uri).normalized_host
 
-    return unless verified_webfinger?
+    return unless only_key || verified_webfinger?
 
-    ActivityPub::ProcessAccountService.new.call(@username, @domain, @json)
+    ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key)
   rescue Oj::ParseError
     nil
   end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index d6480028f..d6c791b44 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -33,8 +33,10 @@ class ActivityPub::ProcessAccountService < BaseService
 
     after_protocol_change! if protocol_changed?
     after_key_change! if key_changed? && !@options[:signed_with_known_key]
-    check_featured_collection! if @account.featured_collection_url.present?
-    check_links! unless @account.fields.empty?
+    unless @options[:only_key]
+      check_featured_collection! if @account.featured_collection_url.present?
+      check_links! unless @account.fields.empty?
+    end
 
     @account
   rescue Oj::ParseError
@@ -54,11 +56,11 @@ class ActivityPub::ProcessAccountService < BaseService
   end
 
   def update_account
-    @account.last_webfingered_at = Time.now.utc
+    @account.last_webfingered_at = Time.now.utc unless @options[:only_key]
     @account.protocol            = :activitypub
 
     set_immediate_attributes!
-    set_fetchable_attributes!
+    set_fetchable_attributes! unless @options[:only_keys]
 
     @account.save_with_optional_media!
   end
diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb
index 1878587e8..d621cc462 100644
--- a/app/services/app_sign_up_service.rb
+++ b/app/services/app_sign_up_service.rb
@@ -4,7 +4,7 @@ class AppSignUpService < BaseService
   def call(app, params)
     return unless allowed_registrations?
 
-    user_params    = params.slice(:email, :password, :agreement)
+    user_params    = params.slice(:email, :password, :agreement, :locale)
     account_params = params.slice(:username)
     user           = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params))
 
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 28ecc848d..2ca92dc50 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -1,77 +1,101 @@
 # frozen_string_literal: true
 
 class PostStatusService < BaseService
+  MIN_SCHEDULE_OFFSET = 5.minutes.freeze
+
   # Post a text status update, fetch and notify remote users mentioned
   # @param [Account] account Account from which to post
-  # @param [String] text Message
-  # @param [Status] in_reply_to Optional status to reply to
   # @param [Hash] options
+  # @option [String] :text Message
+  # @option [Status] :thread Optional status to reply to
   # @option [Boolean] :sensitive
   # @option [String] :visibility
   # @option [String] :spoiler_text
+  # @option [String] :language
+  # @option [String] :scheduled_at
   # @option [Enumerable] :media_ids Optional array of media IDs to attach
   # @option [Doorkeeper::Application] :application
   # @option [String] :idempotency Optional idempotency key
   # @return [Status]
-  def call(account, text, in_reply_to = nil, **options)
-    if options[:idempotency].present?
-      existing_id = redis.get("idempotency:status:#{account.id}:#{options[:idempotency]}")
-      return Status.find(existing_id) if existing_id
+  def call(account, options = {})
+    @account     = account
+    @options     = options
+    @text        = @options[:text] || ''
+    @in_reply_to = @options[:thread]
+
+    return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
+
+    validate_media!
+    preprocess_attributes!
+
+    if scheduled?
+      schedule_status!
+    else
+      process_status!
+      postprocess_status!
+      bump_potential_friendship!
     end
 
-    media  = validate_media!(options[:media_ids])
-    status = nil
-    if text.blank? && options[:spoiler_text].present?
-     text = '.'
-     text = media.find(&:video?) ? '📹' : '🖼' if media.size > 0
+    redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given?
+
+    @status
+  end
+
+  private
+
+  def preprocess_attributes!
+    if @text.blank? && @options[:spoiler_text].present?
+     @text = '.'
+     @text = @media.find(&:video?) ? '📹' : '🖼' if @media.size > 0
     end
+    @visibility   = @options[:visibility] || @account.user&.setting_default_privacy
+    @visibility   = :unlisted if @visibility == :public && @account.silenced
+    @scheduled_at = @options[:scheduled_at]&.to_datetime
+    @scheduled_at = nil if scheduled_in_the_past?
+  end
 
-    visibility = options[:visibility] || account.user&.setting_default_privacy
-    visibility = :unlisted if visibility == :public && account.silenced
+  def process_status!
+    # The following transaction block is needed to wrap the UPDATEs to
+    # the media attachments when the status is created
 
     ApplicationRecord.transaction do
-      status = account.statuses.create!(text: text,
-                                        media_attachments: media || [],
-                                        thread: in_reply_to,
-                                        sensitive: (options[:sensitive].nil? ? account.user&.setting_default_sensitive : options[:sensitive]) || options[:spoiler_text].present?,
-                                        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])
+      @status = @account.statuses.create!(status_attributes)
     end
 
-    process_hashtags_service.call(status)
-    process_mentions_service.call(status)
+    process_hashtags_service.call(@status)
+    process_mentions_service.call(@status)
+  end
 
-    LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
-    DistributionWorker.perform_async(status.id)
+  def schedule_status!
+    if @account.statuses.build(status_attributes).valid?
+      # The following transaction block is needed to wrap the UPDATEs to
+      # the media attachments when the scheduled status is created
 
-    unless status.local_only?
-      Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
-      ActivityPub::DistributionWorker.perform_async(status.id)
+      ApplicationRecord.transaction do
+        @status = @account.scheduled_statuses.create!(scheduled_status_attributes)
+      end
+    else
+      raise ActiveRecord::RecordInvalid
     end
+  end
 
-    if options[:idempotency].present?
-      redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id)
+  def postprocess_status!
+    LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text?
+    DistributionWorker.perform_async(@status.id)
+    unless @status.local_only?
+      Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
+      ActivityPub::DistributionWorker.perform_async(@status.id)
     end
-
-    bump_potential_friendship(account, status)
-
-    status
   end
 
-  private
-
-  def validate_media!(media_ids)
-    return if media_ids.blank? || !media_ids.is_a?(Enumerable)
+  def validate_media!
+    return if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
 
-    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4
+    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4
 
-    media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i))
+    @media = MediaAttachment.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(&:video?)
-
-    media
+    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?)
   end
 
   def language_from_option(str)
@@ -90,10 +114,68 @@ class PostStatusService < BaseService
     Redis.current
   end
 
-  def bump_potential_friendship(account, status)
-    return if !status.reply? || account.id == status.in_reply_to_account_id
+  def scheduled?
+    @scheduled_at.present?
+  end
+
+  def idempotency_key
+    "idempotency:status:#{@account.id}:#{@options[:idempotency]}"
+  end
+
+  def idempotency_given?
+    @options[:idempotency].present?
+  end
+
+  def idempotency_duplicate
+    if scheduled?
+      @account.schedule_statuses.find(@idempotency_duplicate)
+    else
+      @account.statuses.find(@idempotency_duplicate)
+    end
+  end
+
+  def idempotency_duplicate?
+    @idempotency_duplicate = redis.get(idempotency_key)
+  end
+
+  def scheduled_in_the_past?
+    @scheduled_at.present? && @scheduled_at <= Time.now.utc + MIN_SCHEDULE_OFFSET
+  end
+
+  def bump_potential_friendship!
+    return if !@status.reply? || @account.id == @status.in_reply_to_account_id
     ActivityTracker.increment('activity:interactions')
-    return if account.following?(status.in_reply_to_account_id)
-    PotentialFriendshipTracker.record(account.id, status.in_reply_to_account_id, :reply)
+    return if @account.following?(@status.in_reply_to_account_id)
+    PotentialFriendshipTracker.record(@account.id, @status.in_reply_to_account_id, :reply)
+  end
+
+  def status_attributes
+    {
+      text: @text,
+      media_attachments: @media || [],
+      thread: @in_reply_to,
+      sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?,
+      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],
+    }
+  end
+
+  def scheduled_status_attributes
+    {
+      scheduled_at: @scheduled_at,
+      media_attachments: @media || [],
+      params: scheduled_options,
+    }
+  end
+
+  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
+    end
   end
 end
diff --git a/app/services/report_service.rb b/app/services/report_service.rb
index 057d05ab9..1bcc1c0d5 100644
--- a/app/services/report_service.rb
+++ b/app/services/report_service.rb
@@ -52,6 +52,6 @@ class ReportService < BaseService
   end
 
   def some_local_account
-    @some_local_account ||= Account.local.where(suspended: false).first
+    @some_local_account ||= Account.representative
   end
 end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 6ab6b2901..1bc2314de 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -20,6 +20,7 @@ class SuspendAccountService < BaseService
     owned_lists
     passive_relationships
     report_notes
+    scheduled_statuses
     status_pins
     stream_entries
     subscriptions
diff --git a/app/validators/note_length_validator.rb b/app/validators/note_length_validator.rb
new file mode 100644
index 000000000..5ff6df6df
--- /dev/null
+++ b/app/validators/note_length_validator.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class NoteLengthValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    record.errors.add(attribute, I18n.t('statuses.over_character_limit', max: options[:maximum])) if too_long?(value)
+  end
+
+  private
+
+  def too_long?(value)
+    countable_text(value).mb_chars.grapheme_length > options[:maximum]
+  end
+
+  def countable_text(value)
+    return '' if value.nil?
+
+    value.dup.tap do |new_text|
+      new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23)
+      new_text.gsub!(Account::MENTION_RE, '@\2')
+    end
+  end
+end
diff --git a/app/validators/unreserved_username_validator.rb b/app/validators/unreserved_username_validator.rb
index c2311a89a..634ceb06e 100644
--- a/app/validators/unreserved_username_validator.rb
+++ b/app/validators/unreserved_username_validator.rb
@@ -2,20 +2,22 @@
 
 class UnreservedUsernameValidator < ActiveModel::Validator
   def validate(account)
-    return if account.username.nil?
-    account.errors.add(:username, I18n.t('accounts.reserved_username')) if reserved_username?(account.username)
+    @username = account.username
+    return if @username.nil?
+
+    account.errors.add(:username, I18n.t('accounts.reserved_username')) if reserved_username?
   end
 
   private
 
-  def pam_controlled?(value)
+  def pam_controlled?
     return false unless Devise.pam_authentication && Devise.pam_controlled_service
-    Rpam2.account(Devise.pam_controlled_service, value).present?
+    Rpam2.account(Devise.pam_controlled_service, @username).present?
   end
 
-  def reserved_username?(value)
-    return true if pam_controlled?(value)
+  def reserved_username?
+    return true if pam_controlled?
     return false unless Setting.reserved_usernames
-    Setting.reserved_usernames.include?(value.downcase)
+    Setting.reserved_usernames.include?(@username.downcase)
   end
 end
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index f39560d90..d95a03fbf 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -8,7 +8,7 @@ class UrlValidator < ActiveModel::EachValidator
   private
 
   def compliant?(url)
-    parsed_url = Addressable::URI.parse(url).normalize
-    !parsed_url.nil? && %w(http https).include?(parsed_url.scheme) && parsed_url.host
+    parsed_url = Addressable::URI.parse(url)
+    parsed_url && %w(http https).include?(parsed_url.scheme) && parsed_url.host
   end
 end
diff --git a/app/views/admin/domain_blocks/_domain_block.html.haml b/app/views/admin/domain_blocks/_domain_block.html.haml
deleted file mode 100644
index 7bfea3574..000000000
--- a/app/views/admin/domain_blocks/_domain_block.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%tr
-  %td
-    %samp= domain_block.domain
-  %td.severity
-    = t("admin.domain_blocks.severities.#{domain_block.severity}")
-  %td.reject_media
-    - if domain_block.reject_media? || domain_block.suspend?
-      %i.fa.fa-check
-  %td.reject_reports
-    - if domain_block.reject_reports? || domain_block.suspend?
-      %i.fa.fa-check
-  %td
-    = table_link_to 'undo', t('admin.domain_blocks.undo'), admin_domain_block_path(domain_block)
diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml
deleted file mode 100644
index 4c5221c42..000000000
--- a/app/views/admin/domain_blocks/index.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- content_for :page_title do
-  = t('admin.domain_blocks.title')
-
-.table-wrapper
-  %table.table
-    %thead
-      %tr
-        %th= t('admin.domain_blocks.domain')
-        %th= t('admin.domain_blocks.severity')
-        %th= t('admin.domain_blocks.reject_media')
-        %th= t('admin.domain_blocks.reject_reports')
-        %th
-    %tbody
-      = render @domain_blocks
-
-= paginate @domain_blocks
-= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
diff --git a/app/views/admin/followers/index.html.haml b/app/views/admin/followers/index.html.haml
index 31b321bac..25f1f290f 100644
--- a/app/views/admin/followers/index.html.haml
+++ b/app/views/admin/followers/index.html.haml
@@ -23,6 +23,6 @@
         %th= t('admin.accounts.most_recent_activity')
         %th
     %tbody
-      = render partial: 'admin/accounts/account', collection: @followers.map(&:account)
+      = render partial: 'admin/accounts/account', collection: @followers
 
 = paginate @followers
diff --git a/app/views/admin/instances/_instance.html.haml b/app/views/admin/instances/_instance.html.haml
deleted file mode 100644
index e36ebae47..000000000
--- a/app/views/admin/instances/_instance.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-%tr
-  %td
-    = link_to instance.domain, admin_accounts_path(by_domain: instance.domain)
-  %td.count
-    = instance.accounts_count
-  %td
-    = table_link_to 'paper-plane-o', t('admin.accounts.resubscribe'), resubscribe_admin_instances_url(by_domain: instance.domain), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml
index 3314ce077..ce35b5db4 100644
--- a/app/views/admin/instances/index.html.haml
+++ b/app/views/admin/instances/index.html.haml
@@ -1,23 +1,39 @@
 - content_for :page_title do
   = t('admin.instances.title')
 
-= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
-  .fields-group
-    - %i(domain_name).each do |key|
-      .input.string.optional
-        = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}")
+.filters
+  .filter-subset
+    %strong= t('admin.instances.moderation.title')
+    %ul
+      %li= filter_link_to t('admin.instances.moderation.all'), limited: nil
+      %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
 
-    .actions
-      %button= t('admin.instances.search')
-      = link_to t('admin.instances.reset'), admin_instances_path, class: 'button negative'
+  %div{ style: 'flex: 1 1 auto; text-align: right' }
+    = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
 
-.table-wrapper
-  %table.table
-    %thead
-      %tr
-        %th= t('admin.instances.domain_name')
-        %th= t('admin.instances.account_count')
-    %tbody
-      = render @instances
+%hr.spacer/
+
+- @instances.each do |instance|
+  .directory__tag
+    = link_to admin_instance_path(instance) do
+      %h4
+        = instance.domain
+        %small
+          = t('admin.instances.known_accounts', count: instance.accounts_count)
+
+          - if instance.domain_block
+            - if !instance.domain_block.noop?
+              &bull;
+              = t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
+            - if instance.domain_block.reject_media?
+              &bull;
+              = t('admin.domain_blocks.rejecting_media')
+            - if instance.domain_block.reject_reports?
+              &bull;
+              = t('admin.domain_blocks.rejecting_reports')
+
+      .avatar-stack
+        - instance.cached_sample_accounts.each do |account|
+          = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'
 
 = paginate paginated_instances
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
new file mode 100644
index 000000000..c7992a490
--- /dev/null
+++ b/app/views/admin/instances/show.html.haml
@@ -0,0 +1,44 @@
+- content_for :page_title do
+  = @instance.domain
+
+.dashboard__counters
+  %div
+    %div
+      .dashboard__counters__num= number_with_delimiter @following_count
+      .dashboard__counters__label= t 'admin.instances.total_followed_by_them'
+  %div
+    %div
+      .dashboard__counters__num= number_with_delimiter @followers_count
+      .dashboard__counters__label= t 'admin.instances.total_followed_by_us'
+  %div
+    %div
+      .dashboard__counters__num= number_to_human_size @media_storage
+      .dashboard__counters__label= t 'admin.instances.total_storage'
+  %div
+    %div
+      .dashboard__counters__num= number_with_delimiter @blocks_count
+      .dashboard__counters__label= t 'admin.instances.total_blocked_by_us'
+  %div
+    %div
+      .dashboard__counters__num= number_with_delimiter @reports_count
+      .dashboard__counters__label= t 'admin.instances.total_reported'
+  %div
+    %div
+      .dashboard__counters__num
+        - if @available
+          = fa_icon 'check'
+        - else
+          = fa_icon 'times'
+      .dashboard__counters__label= t 'admin.instances.delivery_available'
+
+%hr.spacer/
+
+%div{ style: 'overflow: hidden' }
+  %div{ style: 'float: left' }
+    = link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button'
+
+  %div{ style: 'float: right' }
+    - if @domain_block
+      = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@domain_block), class: 'button'
+    - else
+      = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'
diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml
index 9b679015f..5cf6977ba 100644
--- a/app/views/remote_follow/new.html.haml
+++ b/app/views/remote_follow/new.html.haml
@@ -16,4 +16,6 @@
     .actions
       = f.button :button, t('remote_follow.proceed'), type: :submit
 
-    %p.hint.subtle-hint= t('remote_follow.no_account_html', sign_up_path: open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started')
+    %p.hint.subtle-hint
+      = t('remote_follow.reason_html', instance: site_hostname)
+      = t('remote_follow.no_account_html', sign_up_path: open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started')
diff --git a/app/views/remote_interaction/new.html.haml b/app/views/remote_interaction/new.html.haml
index 7357546b6..a0b106814 100644
--- a/app/views/remote_interaction/new.html.haml
+++ b/app/views/remote_interaction/new.html.haml
@@ -1,6 +1,6 @@
 .form-container
   .follow-prompt
-    %h2= t('remote_interaction.prompt')
+    %h2= t("remote_interaction.#{@interaction_type}.prompt")
 
     .public-layout
       .activity-stream.activity-stream--highlighted
@@ -9,9 +9,13 @@
   = simple_form_for @remote_follow, as: :remote_follow, url: remote_interaction_path(@status) do |f|
     = render 'shared/error_messages', object: @remote_follow
 
+    = hidden_field_tag :type, @interaction_type
+
     = f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' }
 
     .actions
-      = f.button :button, t('remote_interaction.proceed'), type: :submit
+      = f.button :button, t("remote_interaction.#{@interaction_type}.proceed"), type: :submit
 
-    %p.hint.subtle-hint= t('remote_follow.no_account_html', sign_up_path: open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started')
+    %p.hint.subtle-hint
+      = t('remote_follow.reason_html', instance: site_hostname)
+      = t('remote_follow.no_account_html', sign_up_path: open_registrations? ? new_user_registration_path : 'https://joinmastodon.org/#getting-started')
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index 447667c5a..9298ecbb0 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -28,8 +28,8 @@
       = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description
     - else
       = react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
-  - elsif status.preview_cards.first
-    = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json
+  - elsif status.preview_card
+    = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
 
   .detailed-status__meta
     %data.dt-published{ value: status.created_at.to_time.iso8601 }
@@ -43,7 +43,7 @@
       - else
         = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
       ·
-    = link_to remote_interaction_path(status), class: 'modal-button detailed-status__link' do
+    = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
       - if status.in_reply_to_id.nil?
         = fa_icon('reply')
       - else
@@ -58,12 +58,12 @@
       %span.detailed-status__link<
         = fa_icon('lock')
     - else
-      = link_to remote_interaction_path(status), class: 'modal-button detailed-status__link' do
+      = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
         = fa_icon('retweet')
         %span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true
         = " "
     ·
-    = link_to remote_interaction_path(status), class: 'modal-button detailed-status__link' do
+    = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
       = fa_icon('star')
       %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true
       = " "
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index 3a4dacc06..1d44be791 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -27,27 +27,29 @@
     .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
       = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
 
-  - unless status.media_attachments.empty?
+  - if !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
       = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description
     - else
       = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
+  - elsif status.preview_card
+    = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
 
   .status__action-bar
     .status__action-bar__counter
-      = link_to remote_interaction_path(status), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+      = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
         - if status.in_reply_to_id.nil?
           = fa_icon 'reply fw'
         - else
           = fa_icon 'reply-all fw'
       .status__action-bar__counter__label= obscured_counter status.replies_count
-    = link_to remote_interaction_path(status), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+    = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
       - if status.public_visibility? || status.unlisted_visibility?
         = fa_icon 'retweet fw'
       - elsif status.private_visibility?
         = fa_icon 'lock fw'
       - else
         = fa_icon 'envelope fw'
-    = link_to remote_interaction_path(status), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+    = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
       = fa_icon 'star fw'
diff --git a/app/workers/publish_scheduled_status_worker.rb b/app/workers/publish_scheduled_status_worker.rb
new file mode 100644
index 000000000..641fcc61c
--- /dev/null
+++ b/app/workers/publish_scheduled_status_worker.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class PublishScheduledStatusWorker
+  include Sidekiq::Worker
+
+  def perform(scheduled_status_id)
+    scheduled_status = ScheduledStatus.find(scheduled_status_id)
+    scheduled_status.destroy!
+
+    PostStatusService.new.call(
+      scheduled_status.account,
+      options_with_objects(scheduled_status.params.with_indifferent_access)
+    )
+  rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
+    true
+  end
+
+  def options_with_objects(options)
+    options.tap do |options_hash|
+      options_hash[:application] = Doorkeeper::Application.find(options_hash.delete(:application_id)) if options[:application_id]
+      options_hash[:thread]      = Status.find(options_hash.delete(:in_reply_to_id)) if options_hash[:in_reply_to_id]
+    end
+  end
+end
diff --git a/app/workers/scheduler/scheduled_statuses_scheduler.rb b/app/workers/scheduler/scheduled_statuses_scheduler.rb
new file mode 100644
index 000000000..1772a246b
--- /dev/null
+++ b/app/workers/scheduler/scheduled_statuses_scheduler.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class Scheduler::ScheduledStatusesScheduler
+  include Sidekiq::Worker
+
+  sidekiq_options unique: :until_executed, retry: 0
+
+  def perform
+    due_statuses.find_each do |scheduled_status|
+      PublishScheduledStatusWorker.perform_at(scheduled_status.scheduled_at, scheduled_status.id)
+    end
+  end
+
+  private
+
+  def due_statuses
+    ScheduledStatus.where('scheduled_at <= ?', Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET)
+  end
+end
diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb
index a2133bb8c..50d3bf034 100644
--- a/app/workers/unfollow_follow_worker.rb
+++ b/app/workers/unfollow_follow_worker.rb
@@ -10,9 +10,9 @@ class UnfollowFollowWorker
     old_target_account = Account.find(old_target_account_id)
     new_target_account = Account.find(new_target_account_id)
 
-    UnfollowService.new.call(follower_account, old_target_account)
     FollowService.new.call(follower_account, new_target_account)
-  rescue ActiveRecord::RecordNotFound
+    UnfollowService.new.call(follower_account, old_target_account)
+  rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
     true
   end
 end