about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2019-08-20 12:09:11 +0200
committerGitHub <noreply@github.com>2019-08-20 12:09:11 +0200
commitbce46f2057b06e78958a42821f3ce18c945de88d (patch)
treed2ac3d5fc7bfcf13f1dc19a91d42feb8c6a945da /app
parentb859eb001717dfc62aebb8eba47b84c75aebe4ef (diff)
parentfae9e34484e7f68b59e7738edfe7344d3790ddfe (diff)
Merge pull request #1199 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app')
-rw-r--r--app/chewy/accounts_index.rb43
-rw-r--r--app/chewy/tags_index.rb37
-rw-r--r--app/controllers/about_controller.rb36
-rw-r--r--app/controllers/accounts_controller.rb17
-rw-r--r--app/controllers/activitypub/replies_controller.rb11
-rw-r--r--app/controllers/application_controller.rb7
-rw-r--r--app/controllers/concerns/signature_verification.rb15
-rw-r--r--app/controllers/follower_accounts_controller.rb2
-rw-r--r--app/controllers/following_accounts_controller.rb2
-rw-r--r--app/controllers/home_controller.rb16
-rw-r--r--app/controllers/instance_actors_controller.rb2
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/media_proxy_controller.rb2
-rw-r--r--app/controllers/public_timelines_controller.rb7
-rw-r--r--app/controllers/shares_controller.rb18
-rw-r--r--app/controllers/tags_controller.rb5
-rw-r--r--app/helpers/application_helper.rb21
-rw-r--r--app/javascript/flavours/glitch/components/status.js42
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/character_counter.js25
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js41
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/publisher.js1
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload.js84
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload_form.js3
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload_progress.js9
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js6
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_container.js6
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js188
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js11
-rw-r--r--app/javascript/flavours/glitch/packs/public.js9
-rw-r--r--app/javascript/flavours/glitch/styles/basics.scss14
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss34
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss21
-rw-r--r--app/javascript/flavours/glitch/styles/components/media.scss5
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss130
-rw-r--r--app/javascript/flavours/glitch/styles/mastodon-light/diff.scss9
-rw-r--r--app/javascript/flavours/glitch/styles/tables.scss67
-rw-r--r--app/javascript/flavours/glitch/styles/widgets.scss15
-rw-r--r--app/javascript/flavours/glitch/util/async-components.js4
-rw-r--r--app/javascript/flavours/glitch/util/numbers.js4
-rw-r--r--app/javascript/flavours/glitch/util/resize_image.js2
-rw-r--r--app/javascript/mastodon/actions/app.js10
-rw-r--r--app/javascript/mastodon/components/status.js36
-rw-r--r--app/javascript/mastodon/containers/media_container.js13
-rw-r--r--app/javascript/mastodon/features/compose/components/upload.js85
-rw-r--r--app/javascript/mastodon/features/compose/components/upload_form.js3
-rw-r--r--app/javascript/mastodon/features/compose/components/upload_progress.js9
-rw-r--r--app/javascript/mastodon/features/compose/containers/upload_container.js6
-rw-r--r--app/javascript/mastodon/features/ui/components/document_title.js41
-rw-r--r--app/javascript/mastodon/features/ui/components/focal_point_modal.js184
-rw-r--r--app/javascript/mastodon/features/ui/index.js18
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js4
-rw-r--r--app/javascript/mastodon/features/video/index.js11
-rw-r--r--app/javascript/mastodon/initial_state.js1
-rw-r--r--app/javascript/mastodon/locales/ar.json19
-rw-r--r--app/javascript/mastodon/locales/ast.json13
-rw-r--r--app/javascript/mastodon/locales/bg.json13
-rw-r--r--app/javascript/mastodon/locales/bn.json13
-rw-r--r--app/javascript/mastodon/locales/ca.json19
-rw-r--r--app/javascript/mastodon/locales/co.json31
-rw-r--r--app/javascript/mastodon/locales/cs.json19
-rw-r--r--app/javascript/mastodon/locales/cy.json13
-rw-r--r--app/javascript/mastodon/locales/da.json13
-rw-r--r--app/javascript/mastodon/locales/de.json19
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json88
-rw-r--r--app/javascript/mastodon/locales/el.json19
-rw-r--r--app/javascript/mastodon/locales/en.json13
-rw-r--r--app/javascript/mastodon/locales/eo.json41
-rw-r--r--app/javascript/mastodon/locales/es.json39
-rw-r--r--app/javascript/mastodon/locales/et.json402
-rw-r--r--app/javascript/mastodon/locales/eu.json19
-rw-r--r--app/javascript/mastodon/locales/fa.json21
-rw-r--r--app/javascript/mastodon/locales/fi.json13
-rw-r--r--app/javascript/mastodon/locales/fr.json19
-rw-r--r--app/javascript/mastodon/locales/gl.json19
-rw-r--r--app/javascript/mastodon/locales/he.json13
-rw-r--r--app/javascript/mastodon/locales/hi.json13
-rw-r--r--app/javascript/mastodon/locales/hr.json13
-rw-r--r--app/javascript/mastodon/locales/hu.json19
-rw-r--r--app/javascript/mastodon/locales/hy.json23
-rw-r--r--app/javascript/mastodon/locales/id.json13
-rw-r--r--app/javascript/mastodon/locales/io.json13
-rw-r--r--app/javascript/mastodon/locales/it.json25
-rw-r--r--app/javascript/mastodon/locales/ja.json19
-rw-r--r--app/javascript/mastodon/locales/ka.json13
-rw-r--r--app/javascript/mastodon/locales/kk.json13
-rw-r--r--app/javascript/mastodon/locales/ko.json19
-rw-r--r--app/javascript/mastodon/locales/lt.json13
-rw-r--r--app/javascript/mastodon/locales/lv.json13
-rw-r--r--app/javascript/mastodon/locales/ms.json13
-rw-r--r--app/javascript/mastodon/locales/nl.json13
-rw-r--r--app/javascript/mastodon/locales/no.json13
-rw-r--r--app/javascript/mastodon/locales/oc.json19
-rw-r--r--app/javascript/mastodon/locales/pl.json13
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json13
-rw-r--r--app/javascript/mastodon/locales/pt.json13
-rw-r--r--app/javascript/mastodon/locales/ro.json13
-rw-r--r--app/javascript/mastodon/locales/ru.json25
-rw-r--r--app/javascript/mastodon/locales/sk.json39
-rw-r--r--app/javascript/mastodon/locales/sl.json21
-rw-r--r--app/javascript/mastodon/locales/sq.json13
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json13
-rw-r--r--app/javascript/mastodon/locales/sr.json13
-rw-r--r--app/javascript/mastodon/locales/sv.json201
-rw-r--r--app/javascript/mastodon/locales/ta.json13
-rw-r--r--app/javascript/mastodon/locales/te.json13
-rw-r--r--app/javascript/mastodon/locales/th.json31
-rw-r--r--app/javascript/mastodon/locales/tr.json13
-rw-r--r--app/javascript/mastodon/locales/uk.json329
-rw-r--r--app/javascript/mastodon/locales/whitelist_et.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json17
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json13
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json139
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/missed_updates.js21
-rw-r--r--app/javascript/mastodon/utils/numbers.js4
-rw-r--r--app/javascript/mastodon/utils/resize_image.js4
-rw-r--r--app/javascript/packs/public.js9
-rw-r--r--app/javascript/styles/mastodon/basics.scss2
-rw-r--r--app/javascript/styles/mastodon/components.scss186
-rw-r--r--app/javascript/styles/mastodon/tables.scss67
-rw-r--r--app/javascript/styles/mastodon/widgets.scss25
-rw-r--r--app/lib/search_query_parser.rb19
-rw-r--r--app/lib/search_query_transformer.rb2
-rw-r--r--app/models/account.rb6
-rw-r--r--app/models/account_stat.rb2
-rw-r--r--app/models/concerns/account_avatar.rb2
-rw-r--r--app/models/concerns/account_counters.rb3
-rw-r--r--app/models/concerns/account_header.rb2
-rw-r--r--app/models/concerns/attachmentable.rb2
-rw-r--r--app/models/custom_emoji.rb2
-rw-r--r--app/models/domain_block.rb1
-rw-r--r--app/models/form/admin_settings.rb4
-rw-r--r--app/models/invite.rb3
-rw-r--r--app/models/media_attachment.rb4
-rw-r--r--app/models/preview_card.rb2
-rw-r--r--app/models/status.rb7
-rw-r--r--app/models/tag.rb16
-rw-r--r--app/models/trending_tags.rb3
-rw-r--r--app/serializers/activitypub/note_serializer.rb2
-rw-r--r--app/serializers/initial_state_serializer.rb6
-rw-r--r--app/serializers/rss/account_serializer.rb8
-rw-r--r--app/services/account_search_service.rb189
-rw-r--r--app/services/search_service.rb10
-rw-r--r--app/services/tag_search_service.rb82
-rw-r--r--app/views/about/blocks.html.haml48
-rw-r--r--app/views/accounts/show.html.haml37
-rw-r--r--app/views/admin/settings/edit.html.haml6
-rw-r--r--app/views/application/_sidebar.html.haml10
-rw-r--r--app/views/home/index.html.haml2
-rw-r--r--app/views/invites/_form.html.haml3
-rw-r--r--app/views/invites/_invite.html.haml3
-rw-r--r--app/views/invites/index.html.haml1
-rw-r--r--app/views/layouts/public.html.haml3
-rw-r--r--app/views/public_timelines/show.html.haml1
-rw-r--r--app/views/settings/featured_tags/index.html.haml4
-rw-r--r--app/views/shares/show.html.haml2
-rw-r--r--app/views/tags/show.html.haml1
157 files changed, 3261 insertions, 1043 deletions
diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb
new file mode 100644
index 000000000..b814e009e
--- /dev/null
+++ b/app/chewy/accounts_index.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class AccountsIndex < Chewy::Index
+  settings index: { refresh_interval: '5m' }, analysis: {
+    analyzer: {
+      content: {
+        tokenizer: 'whitespace',
+        filter: %w(lowercase asciifolding cjk_width),
+      },
+
+      edge_ngram: {
+        tokenizer: 'edge_ngram',
+        filter: %w(lowercase asciifolding cjk_width),
+      },
+    },
+
+    tokenizer: {
+      edge_ngram: {
+        type: 'edge_ngram',
+        min_gram: 1,
+        max_gram: 15,
+      },
+    },
+  }
+
+  define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do
+    root date_detection: false do
+      field :id, type: 'long'
+
+      field :display_name, type: 'text', analyzer: 'content' do
+        field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
+      end
+
+      field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
+        field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
+      end
+
+      field :following_count, type: 'long', value: ->(account) { account.following.local.count }
+      field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
+      field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
+    end
+  end
+end
diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb
new file mode 100644
index 000000000..300fc128f
--- /dev/null
+++ b/app/chewy/tags_index.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class TagsIndex < Chewy::Index
+  settings index: { refresh_interval: '15m' }, analysis: {
+    analyzer: {
+      content: {
+        tokenizer: 'keyword',
+        filter: %w(lowercase asciifolding cjk_width),
+      },
+
+      edge_ngram: {
+        tokenizer: 'edge_ngram',
+        filter: %w(lowercase asciifolding cjk_width),
+      },
+    },
+
+    tokenizer: {
+      edge_ngram: {
+        type: 'edge_ngram',
+        min_gram: 2,
+        max_gram: 15,
+      },
+    },
+  }
+
+  define_type ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } do
+    root date_detection: false do
+      field :name, type: 'text', analyzer: 'content' do
+        field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
+      end
+
+      field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
+      field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } }
+      field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
+    end
+  end
+end
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index f41e52aae..7b0438127 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -4,10 +4,12 @@ class AboutController < ApplicationController
   before_action :set_pack
   layout 'public'
 
-  before_action :require_open_federation!, only: [:show, :more]
+  before_action :require_open_federation!, only: [:show, :more, :blocks]
+  before_action :check_blocklist_enabled, only: [:blocks]
+  before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required?
   before_action :set_body_classes, only: :show
   before_action :set_instance_presenter
-  before_action :set_expires_in
+  before_action :set_expires_in, only: [:show, :more, :terms]
 
   skip_before_action :require_functional!, only: [:more, :terms]
 
@@ -19,12 +21,40 @@ class AboutController < ApplicationController
 
   def terms; end
 
+  def blocks
+    @show_rationale = Setting.show_domain_blocks_rationale == 'all'
+    @show_rationale |= Setting.show_domain_blocks_rationale == 'users' && !current_user.nil? && current_user.functional?
+    @blocks = DomainBlock.with_user_facing_limitations.order('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain').to_a
+  end
+
   private
 
   def require_open_federation!
     not_found if whitelist_mode?
   end
 
+  def check_blocklist_enabled
+    not_found if Setting.show_domain_blocks == 'disabled'
+  end
+
+  def blocklist_account_required?
+    Setting.show_domain_blocks == 'users'
+  end
+
+  def block_severity_text(block)
+    if block.severity == 'suspend'
+      I18n.t('domain_blocks.suspension')
+    else
+      limitations = []
+      limitations << I18n.t('domain_blocks.media_block') if block.reject_media?
+      limitations << I18n.t('domain_blocks.silence') if block.severity == 'silence'
+      limitations.join(', ')
+    end
+  end
+
+  helper_method :block_severity_text
+  helper_method :public_fetch_mode?
+
   def new_user
     User.new.tap do |user|
       user.build_account
@@ -35,7 +65,7 @@ class AboutController < ApplicationController
   helper_method :new_user
 
   def set_pack
-    use_pack 'common'
+    use_pack 'public'
   end
 
   def set_instance_presenter
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 1a876b831..817e5e832 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -19,6 +19,7 @@ class AccountsController < ApplicationController
 
         @pinned_statuses   = []
         @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
+        @featured_hashtags = @account.featured_tags.order(statuses_count: :desc)
 
         if current_account && @account.blocking?(current_account)
           @statuses = []
@@ -28,6 +29,7 @@ class AccountsController < ApplicationController
         @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
         @statuses        = filtered_status_page(params)
         @statuses        = cache_collection(@statuses, Status)
+        @rss_url         = rss_url
 
         unless @statuses.empty?
           @older_url = older_url if @statuses.last.id > filtered_statuses.last.id
@@ -38,8 +40,9 @@ class AccountsController < ApplicationController
       format.rss do
         expires_in 0, public: true
 
-        @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status)
-        render xml: RSS::AccountSerializer.render(@account, @statuses)
+        @statuses = filtered_statuses.without_reblogs.without_replies.limit(PAGE_SIZE)
+        @statuses = cache_collection(@statuses, Status)
+        render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
       end
 
       format.json do
@@ -97,6 +100,14 @@ class AccountsController < ApplicationController
     params[:username]
   end
 
+  def rss_url
+    if tag_requested?
+      short_account_tag_url(@account, params[:tag], format: 'rss')
+    else
+      short_account_url(@account, format: 'rss')
+    end
+  end
+
   def older_url
     pagination_url(max_id: @statuses.last.id)
   end
@@ -126,7 +137,7 @@ class AccountsController < ApplicationController
   end
 
   def tag_requested?
-    request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
+    request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
   end
 
   def filtered_status_page(params)
diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb
index ab755ed4e..c62061555 100644
--- a/app/controllers/activitypub/replies_controller.rb
+++ b/app/controllers/activitypub/replies_controller.rb
@@ -27,7 +27,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
   end
 
   def set_replies
-    @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
+    @replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
     @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
     @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
   end
@@ -38,7 +38,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
       type: :unordered,
       part_of: account_status_replies_url(@account, @status),
       next: next_page,
-      items: @replies.map { |status| status.local ? status : status.id }
+      items: @replies.map { |status| status.local ? status : status.uri }
     )
 
     return page if page_requested?
@@ -55,16 +55,17 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
   end
 
   def next_page
+    only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
     account_status_replies_url(
       @account,
       @status,
       page: true,
-      min_id: @replies&.last&.id,
-      other_accounts: !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
+      min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id,
+      only_other_accounts: only_other_accounts
     )
   end
 
   def page_params
-    params_slice(:other_accounts, :min_id).merge(page: true)
+    params_slice(:only_other_accounts, :min_id).merge(page: true)
   end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 19efc8838..5f88838e4 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -26,10 +26,13 @@ class ApplicationController < ActionController::Base
   rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
   rescue_from ActionController::UnknownFormat, with: :not_acceptable
   rescue_from Mastodon::NotPermittedError, with: :forbidden
+  rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
 
   before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
   before_action :require_functional!, if: :user_signed_in?
 
+  skip_before_action :verify_authenticity_token, only: :raise_not_found
+
   def raise_not_found
     raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
   end
@@ -163,6 +166,10 @@ class ApplicationController < ActionController::Base
     respond_with_error(406)
   end
 
+  def internal_server_error
+    respond_with_error(500)
+  end
+
   def single_user_mode?
     @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
   end
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 7b251cf80..ce353f1de 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -23,6 +23,19 @@ module SignatureVerification
     @signature_verification_failure_code || 401
   end
 
+  def signature_key_id
+    raw_signature    = request.headers['Signature']
+    signature_params = {}
+
+    raw_signature.split(',').each do |part|
+      parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
+      next if parsed_parts.nil? || parsed_parts.size != 3
+      signature_params[parsed_parts[1]] = parsed_parts[2]
+    end
+
+    signature_params['keyId']
+  end
+
   def signed_request_account
     return @signed_request_account if defined?(@signed_request_account)
 
@@ -154,7 +167,7 @@ module SignatureVerification
       .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) }
+      .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
       .run
   end
 
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index e2ba9bf00..4641a8bb9 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -7,6 +7,8 @@ class FollowerAccountsController < ApplicationController
   before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
   before_action :set_cache_headers
 
+  skip_around_action :set_locale, if: -> { request.format == :json }
+
   def index
     respond_to do |format|
       format.html do
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 49f1f3218..6e80554fb 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -7,6 +7,8 @@ class FollowingAccountsController < ApplicationController
   before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
   before_action :set_cache_headers
 
+  skip_around_action :set_locale, if: -> { request.format == :json }
+
   def index
     respond_to do |format|
       format.html do
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index a09aed801..efdb1d226 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -5,7 +5,6 @@ class HomeController < ApplicationController
 
   before_action :set_pack
   before_action :set_referrer_policy_header
-  before_action :set_initial_state_json
 
   def index
     @body_classes = 'app-body'
@@ -45,21 +44,6 @@ class HomeController < ApplicationController
     use_pack 'home'
   end
 
-  def set_initial_state_json
-    serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
-    @initial_state_json   = serializable_resource.to_json
-  end
-
-  def initial_state_params
-    {
-      settings: Web::Setting.find_by(user: current_user)&.data || {},
-      push_subscription: current_account.user.web_push_subscription(current_session),
-      current_account: current_account,
-      token: current_session.token,
-      admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
-    }
-  end
-
   def default_redirect_path
     if request.path.start_with?('/web') || whitelist_mode?
       new_user_session_path
diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb
index 41f33602e..6f02d6a35 100644
--- a/app/controllers/instance_actors_controller.rb
+++ b/app/controllers/instance_actors_controller.rb
@@ -3,6 +3,8 @@
 class InstanceActorsController < ApplicationController
   include AccountControllerConcern
 
+  skip_around_action :set_locale
+
   def show
     expires_in 10.minutes, public: true
     render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 639002964..0b3c082dc 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -48,7 +48,7 @@ class InvitesController < ApplicationController
   end
 
   def resource_params
-    params.require(:invite).permit(:max_uses, :expires_in, :autofollow)
+    params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment)
   end
 
   def set_body_classes
diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb
index 8da6c6fe0..558cd6e30 100644
--- a/app/controllers/media_proxy_controller.rb
+++ b/app/controllers/media_proxy_controller.rb
@@ -7,6 +7,8 @@ class MediaProxyController < ApplicationController
 
   before_action :authenticate_user!, if: :whitelist_mode?
 
+  rescue_from ActiveRecord::RecordInvalid, with: :not_found
+
   def show
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb
index 940b2f7cd..eb5bb191b 100644
--- a/app/controllers/public_timelines_controller.rb
+++ b/app/controllers/public_timelines_controller.rb
@@ -9,12 +9,7 @@ class PublicTimelinesController < ApplicationController
   before_action :set_body_classes
   before_action :set_instance_presenter
 
-  def show
-    @initial_state_json = ActiveModelSerializers::SerializableResource.new(
-      InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token),
-      serializer: InitialStateSerializer
-    ).to_json
-  end
+  def show; end
 
   private
 
diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb
index ada4eec54..e13e7e8b6 100644
--- a/app/controllers/shares_controller.rb
+++ b/app/controllers/shares_controller.rb
@@ -7,26 +7,10 @@ class SharesController < ApplicationController
   before_action :set_pack
   before_action :set_body_classes
 
-  def show
-    serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
-    @initial_state_json   = serializable_resource.to_json
-  end
+  def show; end
 
   private
 
-  def initial_state_params
-    text = [params[:title], params[:text], params[:url]].compact.join(' ')
-
-    {
-      settings: Web::Setting.find_by(user: current_user)&.data || {},
-      push_subscription: current_account.user.web_push_subscription(current_session),
-      current_account: current_account,
-      token: current_session.token,
-      admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
-      text: text,
-    }
-  end
-
   def set_pack
     use_pack 'share'
   end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index d6bb28eb5..c447a3a2b 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -18,11 +18,6 @@ class TagsController < ApplicationController
       format.html do
         use_pack 'about'
         expires_in 0, public: true
-
-        @initial_state_json = ActiveModelSerializers::SerializableResource.new(
-          InitialStatePresenter.new(settings: {}, token: current_session&.token),
-          serializer: InitialStateSerializer
-        ).to_json
       end
 
       format.rss do
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7ae1e5d0b..6940c8535 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -123,4 +123,25 @@ module ApplicationHelper
     text = word_wrap(text, line_width: line_width - 2, break_sequence: break_sequence)
     text.split("\n").map { |line| '> ' + line }.join("\n")
   end
+
+  def render_initial_state
+    state_params = {
+      settings: {
+        known_fediverse: Setting.show_known_fediverse_at_about_page,
+      },
+
+      text: [params[:title], params[:text], params[:url]].compact.join(' '),
+    }
+
+    if user_signed_in?
+      state_params[:settings]          = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {})
+      state_params[:push_subscription] = current_account.user.web_push_subscription(current_session)
+      state_params[:current_account]   = current_account
+      state_params[:token]             = current_session.token
+      state_params[:admin]             = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
+    end
+
+    json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
+    content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
+  end
 end
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 170efad04..88994c2ac 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -486,13 +486,30 @@ class Status extends ImmutablePureComponent {
       return null;
     }
 
+    const handlers = {
+      reply: this.handleHotkeyReply,
+      favourite: this.handleHotkeyFavourite,
+      boost: this.handleHotkeyBoost,
+      mention: this.handleHotkeyMention,
+      open: this.handleHotkeyOpen,
+      openProfile: this.handleHotkeyOpenProfile,
+      moveUp: this.handleHotkeyMoveUp,
+      moveDown: this.handleHotkeyMoveDown,
+      toggleSpoiler: this.handleExpandedToggle,
+      bookmark: this.handleHotkeyBookmark,
+      toggleCollapse: this.handleHotkeyCollapse,
+      toggleSensitive: this.handleHotkeyToggleSensitive,
+    };
+
     if (hidden) {
       return (
-        <div ref={this.handleRef}>
-          {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
-          {' '}
-          {status.get('content')}
-        </div>
+        <HotKeys handlers={handlers}>
+          <div ref={this.handleRef} className='status focusable' tabIndex='0'>
+            {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
+            {' '}
+            {status.get('content')}
+          </div>
+        </HotKeys>
       );
     }
 
@@ -628,21 +645,6 @@ class Status extends ImmutablePureComponent {
       rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') });
     }
 
-    const handlers = {
-      reply: this.handleHotkeyReply,
-      favourite: this.handleHotkeyFavourite,
-      boost: this.handleHotkeyBoost,
-      mention: this.handleHotkeyMention,
-      open: this.handleHotkeyOpen,
-      openProfile: this.handleHotkeyOpenProfile,
-      moveUp: this.handleHotkeyMoveUp,
-      moveDown: this.handleHotkeyMoveDown,
-      toggleSpoiler: this.handleExpandedToggle,
-      bookmark: this.handleHotkeyBookmark,
-      toggleCollapse: this.handleHotkeyCollapse,
-      toggleSensitive: this.handleHotkeyToggleSensitive,
-    };
-
     const computedClass = classNames('status', `status-${status.get('visibility')}`, {
       collapsed: isCollapsed,
       'has-background': isCollapsed && background,
diff --git a/app/javascript/flavours/glitch/features/compose/components/character_counter.js b/app/javascript/flavours/glitch/features/compose/components/character_counter.js
new file mode 100644
index 000000000..0ecfc9141
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { length } from 'stringz';
+
+export default class CharacterCounter extends React.PureComponent {
+
+  static propTypes = {
+    text: PropTypes.string.isRequired,
+    max: PropTypes.number.isRequired,
+  };
+
+  checkRemainingText (diff) {
+    if (diff < 0) {
+      return <span className='character-counter character-counter--over'>{diff}</span>;
+    }
+
+    return <span className='character-counter'>{diff}</span>;
+  }
+
+  render () {
+    const diff = this.props.max - length(this.props.text);
+    return this.checkRemainingText(diff);
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index 3d9002fe4..6e07998ec 100644
--- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -15,6 +15,8 @@ import { countableText } from 'flavours/glitch/util/counter';
 import OptionsContainer from '../containers/options_container';
 import Publisher from './publisher';
 import TextareaIcons from './textarea_icons';
+import { maxChars } from 'flavours/glitch/util/initial_state';
+import CharacterCounter from './character_counter';
 
 const messages = defineMessages({
   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -119,14 +121,8 @@ class ComposeForm extends ImmutablePureComponent {
 
     // Submit unless there are media with missing descriptions
     if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) {
-      const firstWithoutDescription = media.findIndex(item => !item.get('description'));
-      if (uploadForm) {
-        const inputs = uploadForm.querySelectorAll('.composer--upload_form--item input');
-        if (inputs.length == media.size && firstWithoutDescription !== -1) {
-          inputs[firstWithoutDescription].focus();
-        }
-      }
-      onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null);
+      const firstWithoutDescription = media.find(item => !item.get('description'));
+      onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'));
     } else if (onSubmit) {
       onSubmit(this.context.router ? this.context.router.history : null);
     }
@@ -298,6 +294,8 @@ class ComposeForm extends ImmutablePureComponent {
 
     let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia);
 
+    const countText = `${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`;
+
     return (
       <div className='composer'>
         <WarningContainer />
@@ -347,19 +345,24 @@ class ComposeForm extends ImmutablePureComponent {
           </div>
         </AutosuggestTextarea>
 
-        <OptionsContainer
-          advancedOptions={advancedOptions}
-          disabled={isSubmitting}
-          onChangeVisibility={onChangeVisibility}
-          onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness}
-          onUpload={onPaste}
-          privacy={privacy}
-          sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)}
-          spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler}
-        />
+        <div className='composer--options-wrapper'>
+          <OptionsContainer
+            advancedOptions={advancedOptions}
+            disabled={isSubmitting}
+            onChangeVisibility={onChangeVisibility}
+            onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness}
+            onUpload={onPaste}
+            privacy={privacy}
+            sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)}
+            spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler}
+          />
+          <div className='compose--counter-wrapper'>
+            <CharacterCounter text={countText} max={maxChars} />
+          </div>
+        </div>
 
         <Publisher
-          countText={`${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`}
+          countText={countText}
           disabled={disabledButton}
           onSecondarySubmit={handleSecondarySubmit}
           onSubmit={handleSubmit}
diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js
index e283b32b9..f5eafc6fd 100644
--- a/app/javascript/flavours/glitch/features/compose/components/publisher.js
+++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js
@@ -49,7 +49,6 @@ class Publisher extends ImmutablePureComponent {
 
     return (
       <div className={computedClass}>
-        <span className='count'>{diff}</span>
         {sideArm && sideArm !== 'none' ? (
           <Button
             className='side_arm'
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js
index 84edf664e..f89145a52 100644
--- a/app/javascript/flavours/glitch/features/compose/components/upload.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload.js
@@ -4,18 +4,12 @@ import PropTypes from 'prop-types';
 import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { FormattedMessage } from 'react-intl';
 import classNames from 'classnames';
 import Icon from 'flavours/glitch/components/icon';
 import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
-const messages = defineMessages({
-  description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
-});
-
-//  The component.
-export default @injectIntl
-class Upload extends ImmutablePureComponent {
+export default class Upload extends ImmutablePureComponent {
 
   static contextTypes = {
     router: PropTypes.object,
@@ -23,30 +17,10 @@ class Upload extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
-    intl: PropTypes.object.isRequired,
     onUndo: PropTypes.func.isRequired,
-    onDescriptionChange: PropTypes.func.isRequired,
     onOpenFocalPoint: PropTypes.func.isRequired,
-    onSubmit: PropTypes.func.isRequired,
-  };
-
-  state = {
-    hovered: false,
-    focused: false,
-    dirtyDescription: null,
   };
 
-  handleKeyDown = (e) => {
-    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
-      this.handleSubmit();
-    }
-  }
-
-  handleSubmit = () => {
-    this.handleInputBlur();
-    this.props.onSubmit(this.context.router.history);
-  }
-
   handleUndoClick = e => {
     e.stopPropagation();
     this.props.onUndo(this.props.media.get('id'));
@@ -57,69 +31,21 @@ class Upload extends ImmutablePureComponent {
     this.props.onOpenFocalPoint(this.props.media.get('id'));
   }
 
-  handleInputChange = e => {
-    this.setState({ dirtyDescription: e.target.value });
-  }
-
-  handleMouseEnter = () => {
-    this.setState({ hovered: true });
-  }
-
-  handleMouseLeave = () => {
-    this.setState({ hovered: false });
-  }
-
-  handleInputFocus = () => {
-    this.setState({ focused: true });
-  }
-
-  handleClick = () => {
-    this.setState({ focused: true });
-  }
-
-  handleInputBlur = () => {
-    const { dirtyDescription } = this.state;
-
-    this.setState({ focused: false, dirtyDescription: null });
-
-    if (dirtyDescription !== null) {
-      this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription);
-    }
-  }
-
   render () {
     const { intl, media } = this.props;
-    const active          = this.state.hovered || this.state.focused || isUserTouching();
-    const description     = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
-    const computedClass   = classNames('composer--upload_form--item', { active });
     const focusX = media.getIn(['meta', 'focus', 'x']);
     const focusY = media.getIn(['meta', 'focus', 'y']);
     const x = ((focusX /  2) + .5) * 100;
     const y = ((focusY / -2) + .5) * 100;
 
     return (
-      <div className={computedClass} tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
+      <div className='composer--upload_form--item' tabIndex='0' role='button'>
         <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12, }) }}>
           {({ scale }) => (
             <div style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
-              <div className={classNames('composer--upload_form--actions', { active })}>
+              <div className={classNames('composer--upload_form--actions', { active: true })}>
                 <button className='icon-button' onClick={this.handleUndoClick}><Icon icon='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
-                {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
-              </div>
-
-              <div className={classNames('composer--upload_form--description', { active })}>
-                <label>
-                  <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
-                  <textarea
-                    placeholder={intl.formatMessage(messages.description)}
-                    value={description}
-                    maxLength={420}
-                    onFocus={this.handleInputFocus}
-                    onChange={this.handleInputChange}
-                    onBlur={this.handleInputBlur}
-                    onKeyDown={this.handleKeyDown}
-                  />
-                </label>
+                <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
               </div>
             </div>
           )}
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js
index 35880ddcc..43039c674 100644
--- a/app/javascript/flavours/glitch/features/compose/components/upload_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js
@@ -4,6 +4,7 @@ import UploadProgressContainer from '../containers/upload_progress_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import UploadContainer from '../containers/upload_container';
 import SensitiveButtonContainer from '../containers/sensitive_button_container';
+import { FormattedMessage } from 'react-intl';
 
 export default class UploadForm extends ImmutablePureComponent {
   static propTypes = {
@@ -15,7 +16,7 @@ export default class UploadForm extends ImmutablePureComponent {
 
     return (
       <div className='composer--upload_form'>
-        <UploadProgressContainer />
+        <UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} />
 
         {mediaIds.size > 0 && (
           <div className='content'>
diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js
index 264c563f2..b00612983 100644
--- a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js
@@ -2,7 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
-import { FormattedMessage } from 'react-intl';
 import Icon from 'flavours/glitch/components/icon';
 
 export default class UploadProgress extends React.PureComponent {
@@ -10,10 +9,12 @@ export default class UploadProgress extends React.PureComponent {
   static propTypes = {
     active: PropTypes.bool,
     progress: PropTypes.number,
+    icon: PropTypes.string.isRequired,
+    message: PropTypes.node.isRequired,
   };
 
   render () {
-    const { active, progress } = this.props;
+    const { active, progress, icon, message } = this.props;
 
     if (!active) {
       return null;
@@ -21,10 +22,10 @@ export default class UploadProgress extends React.PureComponent {
 
     return (
       <div className='composer--upload_form--progress'>
-        <Icon icon='upload' />
+        <Icon icon={icon} />
 
         <div className='message'>
-          <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
+          {message}
 
           <div className='backdrop'>
             <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
index 199d43913..18e2b2f39 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
@@ -25,6 +25,8 @@ const messages = defineMessages({
                                 defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
   missingDescriptionConfirm: {  id: 'confirmations.missing_media_description.confirm',
                                 defaultMessage: 'Send anyway' },
+  missingDescriptionEdit:    {  id: 'confirmations.missing_media_description.edit',
+                                defaultMessage: 'Edit media' },
 });
 
 //  State mapping.
@@ -112,11 +114,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(changeComposeVisibility(value));
   },
 
-  onMediaDescriptionConfirm(routerHistory) {
+  onMediaDescriptionConfirm(routerHistory, mediaId) {
     dispatch(openModal('CONFIRM', {
       message: intl.formatMessage(messages.missingDescriptionMessage),
       confirm: intl.formatMessage(messages.missingDescriptionConfirm),
       onConfirm: () => dispatch(submitCompose(routerHistory)),
+      secondary: intl.formatMessage(messages.missingDescriptionEdit),
+      onSecondary: () => dispatch(openModal('FOCAL_POINT', { id: mediaId })),
       onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)),
     }));
   },
diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
index d6bff63ac..f687fae99 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import Upload from '../components/upload';
-import { undoUploadCompose, changeUploadCompose } from 'flavours/glitch/actions/compose';
+import { undoUploadCompose } from 'flavours/glitch/actions/compose';
 import { openModal } from 'flavours/glitch/actions/modal';
 import { submitCompose } from 'flavours/glitch/actions/compose';
 
@@ -14,10 +14,6 @@ const mapDispatchToProps = dispatch => ({
     dispatch(undoUploadCompose(id));
   },
 
-  onDescriptionChange: (id, description) => {
-    dispatch(changeUploadCompose(id, { description }));
-  },
-
   onOpenFocalPoint: id => {
     dispatch(openModal('FOCAL_POINT', { id }));
   },
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
index 57c92cc66..c4cc18f94 100644
--- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
@@ -1,11 +1,26 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
-import ImageLoader from './image_loader';
 import classNames from 'classnames';
 import { changeUploadCompose } from 'flavours/glitch/actions/compose';
 import { getPointerPosition } from 'flavours/glitch/features/video';
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+import IconButton from 'flavours/glitch/components/icon_button';
+import Button from 'flavours/glitch/components/button';
+import Video from 'flavours/glitch/features/video';
+import Textarea from 'react-textarea-autosize';
+import UploadProgress from 'flavours/glitch/features/compose/components/upload_progress';
+import CharacterCounter from 'flavours/glitch/features/compose/components/character_counter';
+import { length } from 'stringz';
+import { Tesseract as fetchTesseract } from 'flavours/glitch/util/async-components';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+  apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
+  placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
+});
 
 const mapStateToProps = (state, { id }) => ({
   media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
@@ -13,17 +28,26 @@ const mapStateToProps = (state, { id }) => ({
 
 const mapDispatchToProps = (dispatch, { id }) => ({
 
-  onSave: (x, y) => {
-    dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
+  onSave: (description, x, y) => {
+    dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
   },
 
 });
 
-@connect(mapStateToProps, mapDispatchToProps)
-export default class FocalPointModal extends ImmutablePureComponent {
+const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
+  .replace(/\n/g, ' ')
+  .replace(/\*\*\*\*\*\*/g, '\n\n');
+
+const assetHost = process.env.CDN_HOST || '';
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+class FocalPointModal extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
   };
 
   state = {
@@ -32,6 +56,9 @@ export default class FocalPointModal extends ImmutablePureComponent {
     focusX: 0,
     focusY: 0,
     dragging: false,
+    description: '',
+    dirty: false,
+    progress: 0,
   };
 
   componentWillMount () {
@@ -57,6 +84,14 @@ export default class FocalPointModal extends ImmutablePureComponent {
     this.setState({ dragging: true });
   }
 
+  handleTouchStart = e => {
+    document.addEventListener('touchmove', this.handleMouseMove);
+    document.addEventListener('touchend', this.handleTouchEnd);
+
+    this.updatePosition(e);
+    this.setState({ dragging: true });
+  }
+
   handleMouseMove = e => {
     this.updatePosition(e);
   }
@@ -66,7 +101,13 @@ export default class FocalPointModal extends ImmutablePureComponent {
     document.removeEventListener('mouseup', this.handleMouseUp);
 
     this.setState({ dragging: false });
-    this.props.onSave(this.state.focusX, this.state.focusY);
+  }
+
+  handleTouchEnd = () => {
+    document.removeEventListener('touchmove', this.handleMouseMove);
+    document.removeEventListener('touchend', this.handleTouchEnd);
+
+    this.setState({ dragging: false });
   }
 
   updatePosition = e => {
@@ -74,46 +115,145 @@ export default class FocalPointModal extends ImmutablePureComponent {
     const focusX   = (x - .5) *  2;
     const focusY   = (y - .5) * -2;
 
-    this.setState({ x, y, focusX, focusY });
+    this.setState({ x, y, focusX, focusY, dirty: true });
   }
 
   updatePositionFromMedia = media => {
-    const focusX = media.getIn(['meta', 'focus', 'x']);
-    const focusY = media.getIn(['meta', 'focus', 'y']);
+    const focusX      = media.getIn(['meta', 'focus', 'x']);
+    const focusY      = media.getIn(['meta', 'focus', 'y']);
+    const description = media.get('description') || '';
 
     if (focusX && focusY) {
       const x = (focusX /  2) + .5;
       const y = (focusY / -2) + .5;
 
-      this.setState({ x, y, focusX, focusY });
+      this.setState({
+        x,
+        y,
+        focusX,
+        focusY,
+        description,
+        dirty: false,
+      });
     } else {
-      this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 });
+      this.setState({
+        x: 0.5,
+        y: 0.5,
+        focusX: 0,
+        focusY: 0,
+        description,
+        dirty: false,
+      });
     }
   }
 
+  handleChange = e => {
+    this.setState({ description: e.target.value, dirty: true });
+  }
+
+  handleSubmit = () => {
+    this.props.onSave(this.state.description, this.state.focusX, this.state.focusY);
+    this.props.onClose();
+  }
+
   setRef = c => {
     this.node = c;
   }
 
-  render () {
+  handleTextDetection = () => {
     const { media } = this.props;
-    const { x, y, dragging } = this.state;
+
+    this.setState({ detecting: true });
+
+    fetchTesseract().then(({ TesseractWorker }) => {
+      const worker = new TesseractWorker({
+        workerPath: `${assetHost}/packs/ocr/worker.min.js`,
+        corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`,
+        langPath: `${assetHost}/ocr/lang-data`,
+      });
+
+      worker.recognize(media.get('url'))
+        .progress(({ progress }) => this.setState({ progress }))
+        .finally(() => worker.terminate())
+        .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
+        .catch(() => this.setState({ detecting: false }));
+    }).catch(() => this.setState({ detecting: false }));
+  }
+
+  render () {
+    const { media, intl, onClose } = this.props;
+    const { x, y, dragging, description, dirty, detecting, progress } = this.state;
 
     const width  = media.getIn(['meta', 'original', 'width']) || null;
     const height = media.getIn(['meta', 'original', 'height']) || null;
+    const focals = ['image', 'gifv'].includes(media.get('type'));
+
+    const previewRatio  = 16/9;
+    const previewWidth  = 200;
+    const previewHeight = previewWidth / previewRatio;
 
     return (
-      <div className='modal-root__modal video-modal focal-point-modal'>
-        <div className={classNames('focal-point', { dragging })} ref={this.setRef}>
-          <ImageLoader
-            previewSrc={media.get('preview_url')}
-            src={media.get('url')}
-            width={width}
-            height={height}
-          />
-
-          <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
-          <div className='focal-point__overlay' onMouseDown={this.handleMouseDown} />
+      <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
+        <div className='report-modal__target'>
+          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
+          <FormattedMessage id='upload_modal.edit_media' defaultMessage='Edit media' />
+        </div>
+
+        <div className='report-modal__container'>
+          <div className='report-modal__comment'>
+            {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
+
+            <label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label>
+
+            <div className='setting-text__wrapper'>
+              <Textarea
+                id='upload-modal__description'
+                className='setting-text light'
+                value={detecting ? '…' : description}
+                onChange={this.handleChange}
+                disabled={detecting}
+                autoFocus
+              />
+
+              <div className='setting-text__modifiers'>
+                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={<FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />} />
+              </div>
+            </div>
+
+            <div className='setting-text__toolbar'>
+              <button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button>
+              <CharacterCounter max={420} text={detecting ? '' : description} />
+            </div>
+
+            <Button disabled={!dirty || detecting || length(description) > 420} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
+          </div>
+
+          <div className='focal-point-modal__content'>
+            {focals && (
+              <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
+                {media.get('type') === 'image' && <img src={media.get('url')} width={width} height={height} alt='' />}
+                {media.get('type') === 'gifv' && <video src={media.get('url')} width={width} height={height} loop muted autoPlay />}
+
+                <div className='focal-point__preview'>
+                  <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
+                  <div style={{ width: previewWidth, height: previewHeight, backgroundImage: `url(${media.get('preview_url')})`, backgroundSize: 'cover', backgroundPosition: `${x * 100}% ${y * 100}%` }} />
+                </div>
+
+                <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
+                <div className='focal-point__overlay' />
+              </div>
+            )}
+
+            {['audio', 'video'].includes(media.get('type')) && (
+              <Video
+                preview={media.get('preview_url')}
+                blurhash={media.get('blurhash')}
+                src={media.get('url')}
+                detailed
+                editable
+              />
+            )}
+          </div>
         </div>
       </div>
     );
diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index 112f9d101..6d5162519 100644
--- a/app/javascript/flavours/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -101,6 +101,7 @@ export default class Video extends React.PureComponent {
     fullwidth: PropTypes.bool,
     detailed: PropTypes.bool,
     inline: PropTypes.bool,
+    editable: PropTypes.bool,
     cacheWidth: PropTypes.func,
     intl: PropTypes.object.isRequired,
     visible: PropTypes.bool,
@@ -393,7 +394,7 @@ export default class Video extends React.PureComponent {
   }
 
   render () {
-    const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link } = this.props;
+    const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link, editable } = this.props;
     const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
     const progress = (currentTime / duration) * 100;
     const playerStyle = {};
@@ -401,7 +402,7 @@ export default class Video extends React.PureComponent {
     const volumeWidth = (muted) ? 0 : volume * this.volWidth;
     const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume);
 
-    const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, letterbox, 'full-width': fullwidth });
+    const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth });
 
     let { width, height } = this.props;
 
@@ -443,7 +444,7 @@ export default class Video extends React.PureComponent {
       >
         <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} />
 
-        {revealed && <video
+        {(revealed || editable) && <video
           ref={this.setVideoRef}
           src={src}
           poster={preview}
@@ -465,7 +466,7 @@ export default class Video extends React.PureComponent {
           onVolumeChange={this.handleVolumeChange}
         />}
 
-        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}>
+        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
           <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
             <span className='spoiler-button__overlay__label'>{warning}</span>
           </button>
@@ -508,7 +509,7 @@ export default class Video extends React.PureComponent {
             </div>
 
             <div className='video-player__buttons right'>
-              {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye-slash' /></button>}
+              {(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye-slash' /></button>}
               {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
               {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>}
               <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index 9f88b0e04..72725d20b 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -104,6 +104,15 @@ function main() {
 
     delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
+
+    delegate(document, '.blocks-table button.icon-button', 'click', function(e) {
+      e.preventDefault();
+
+      const classList = this.firstElementChild.classList;
+      classList.toggle('fa-chevron-down');
+      classList.toggle('fa-chevron-up');
+      this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden');
+    });
   });
 }
 
diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss
index 550b7fdfc..4de3955a6 100644
--- a/app/javascript/flavours/glitch/styles/basics.scss
+++ b/app/javascript/flavours/glitch/styles/basics.scss
@@ -133,3 +133,17 @@ button {
     outline: 0 !important;
   }
 }
+
+.layout-single-column .app-holder {
+  &,
+  & > div {
+    min-height: 100vh;
+  }
+}
+
+.layout-multiple-columns .app-holder {
+  &,
+  & > div {
+    height: 100%;
+  }
+}
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index 1044b13c1..c4fa4f654 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -2,6 +2,18 @@
   padding: 10px;
 }
 
+.character-counter {
+  cursor: default;
+  font-family: $font-sans-serif, sans-serif;
+  font-size: 14px;
+  font-weight: 600;
+  color: $lighter-text-color;
+
+  &.character-counter--over {
+    color: $warning-red;
+  }
+}
+
 .no-reduce-motion .composer--spoiler {
   transition: height 0.4s ease, opacity 0.4s ease;
 }
@@ -489,12 +501,18 @@
   background: $simple-background-color;
 }
 
-.composer--options {
+.composer--options-wrapper {
   padding: 10px;
   background: darken($simple-background-color, 8%);
-  box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05);
   border-radius: 0 0 4px 4px;
   height: 27px;
+  display: flex;
+  justify-content: space-between;
+  flex: 0 0 auto;
+}
+
+.composer--options {
+  display: flex;
   flex: 0 0 auto;
 
   & > * {
@@ -519,6 +537,11 @@
   }
 }
 
+.compose--counter-wrapper {
+  align-self: center;
+  margin-right: 4px;
+}
+
 .composer--options--dropdown {
   &.open {
     & > .value {
@@ -589,13 +612,6 @@
   justify-content: flex-end;
   flex: 0 0 auto;
 
-  & > .count {
-    display: inline-block;
-    margin: 0 16px 0 8px;
-    font-size: 16px;
-    line-height: 36px;
-  }
-
   & > .primary {
     display: inline-block;
     margin: 0;
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 6942170f2..f453a046e 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -3,6 +3,27 @@
   -ms-overflow-style: -ms-autohiding-scrollbar;
 }
 
+.link-button {
+  display: block;
+  font-size: 15px;
+  line-height: 20px;
+  color: $ui-highlight-color;
+  border: 0;
+  background: transparent;
+  padding: 0;
+  cursor: pointer;
+
+  &:hover,
+  &:active {
+    text-decoration: underline;
+  }
+
+  &:disabled {
+    color: $ui-primary-color;
+    cursor: default;
+  }
+}
+
 .button {
   background-color: darken($ui-highlight-color, 3%);
   border: 10px none;
diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss
index 8b5d0486d..39ffcae9d 100644
--- a/app/javascript/flavours/glitch/styles/components/media.scss
+++ b/app/javascript/flavours/glitch/styles/components/media.scss
@@ -338,6 +338,11 @@
   position: relative;
   background: $base-shadow-color;
   max-width: 100%;
+  border-radius: 4px;
+
+  &.editable {
+    border-radius: 0;
+  }
 
   &:focus {
     outline: 0;
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index a98efee9f..ec32c9114 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -528,7 +528,8 @@
   }
 }
 
-.report-modal__statuses {
+.report-modal__statuses,
+.focal-point-modal__content {
   flex: 1 1 auto;
   min-height: 20vh;
   max-height: 80vh;
@@ -544,6 +545,12 @@
   }
 }
 
+.focal-point-modal__content {
+  @media screen and (max-width: 480px) {
+    max-height: 40vh;
+  }
+}
+
 .report-modal__comment {
   padding: 20px;
   border-right: 1px solid $ui-secondary-color;
@@ -565,16 +572,56 @@
     padding: 10px;
     font-family: inherit;
     font-size: 14px;
-    resize: vertical;
+    resize: none;
     border: 0;
     outline: 0;
     border-radius: 4px;
     border: 1px solid $ui-secondary-color;
-    margin-bottom: 20px;
+    min-height: 100px;
+    max-height: 50vh;
+    margin-bottom: 10px;
 
     &:focus {
       border: 1px solid darken($ui-secondary-color, 8%);
     }
+
+    &__wrapper {
+      background: $white;
+      border: 1px solid $ui-secondary-color;
+      margin-bottom: 10px;
+      border-radius: 4px;
+
+      .setting-text {
+        border: 0;
+        margin-bottom: 0;
+        border-radius: 0;
+
+        &:focus {
+          border: 0;
+        }
+      }
+
+      &__modifiers {
+        color: $inverted-text-color;
+        font-family: inherit;
+        font-size: 14px;
+        background: $white;
+      }
+    }
+
+    &__toolbar {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 20px;
+    }
+  }
+
+  .setting-text-label {
+    display: block;
+    color: $inverted-text-color;
+    font-size: 14px;
+    font-weight: 500;
+    margin-bottom: 10px;
   }
 
   .setting-toggle {
@@ -598,15 +645,6 @@
   }
 }
 
-.report-modal__target {
-  padding: 20px;
-
-  .media-modal__close {
-    top: 19px;
-    right: 15px;
-  }
-}
-
 .actions-modal {
   .status {
     overflow-y: auto;
@@ -725,6 +763,15 @@
   }
 }
 
+.report-modal__target {
+  padding: 15px;
+
+  .media-modal__close {
+    top: 14px;
+    right: 15px;
+  }
+}
+
 .embed-modal {
   max-width: 80vw;
   max-height: 80vh;
@@ -787,19 +834,23 @@
 
 .focal-point {
   position: relative;
-  cursor: pointer;
+  cursor: move;
   overflow: hidden;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: $base-shadow-color;
 
-  &.dragging {
-    cursor: move;
-  }
-
-  img {
-    max-width: 80vw;
+  img,
+  video {
+    display: block;
     max-height: 80vh;
-    width: auto;
+    width: 100%;
     height: auto;
-    margin: auto;
+    margin: 0;
+    object-fit: contain;
+    background: $base-shadow-color;
   }
 
   &__reticle {
@@ -819,6 +870,43 @@
     top: 0;
     left: 0;
   }
+
+  &__preview {
+    position: absolute;
+    bottom: 10px;
+    right: 10px;
+    z-index: 2;
+    cursor: move;
+    transition: opacity 0.1s ease;
+
+    &:hover {
+      opacity: 0.5;
+    }
+
+    strong {
+      color: $primary-text-color;
+      font-size: 14px;
+      font-weight: 500;
+      display: block;
+      margin-bottom: 5px;
+    }
+
+    div {
+      border-radius: 4px;
+      box-shadow: 0 0 14px rgba($base-shadow-color, 0.2);
+    }
+  }
+
+  @media screen and (max-width: 480px) {
+    img,
+    video {
+      max-height: 100%;
+    }
+
+    &__preview {
+      display: none;
+    }
+  }
 }
 
 .filtered-status-info {
diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
index 7da8edbde..35a8ce7a3 100644
--- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
@@ -135,13 +135,12 @@
     }
   }
 
-  .composer--options {
+  .composer--options-wrapper {
     background: lighten($ui-base-color, 10%);
-    box-shadow: unset;
+  }
 
-    & > hr {
-      display: none;
-    }
+  .composer--options > hr {
+    display: none;
   }
 
   .composer--options--dropdown--content--item {
diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index 154844665..bf67388f0 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -237,3 +237,70 @@ a.table-action-link {
     }
   }
 }
+
+.blocks-table {
+  width: 100%;
+  max-width: 100%;
+  border-spacing: 0;
+  border-collapse: collapse;
+  table-layout: fixed;
+  border: 1px solid darken($ui-base-color, 8%);
+
+  thead {
+    border: 1px solid darken($ui-base-color, 8%);
+    background: darken($ui-base-color, 4%);
+    font-weight: 500;
+
+    th.severity-column {
+      width: 120px;
+    }
+
+    th.button-column {
+      width: 23px;
+    }
+  }
+
+  tbody > tr {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-bottom: 0;
+    background: darken($ui-base-color, 4%);
+
+    &:hover {
+      background: darken($ui-base-color, 2%);
+    }
+
+    &.even {
+      background: $ui-base-color;
+
+      &:hover {
+        background: lighten($ui-base-color, 2%);
+      }
+    }
+
+    &.rationale {
+      background: lighten($ui-base-color, 4%);
+      border-top: 0;
+
+      &:hover {
+        background: lighten($ui-base-color, 6%);
+      }
+
+      &.hidden {
+        display: none;
+      }
+    }
+
+    td:first-child {
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+
+  th,
+  td {
+    padding: 8px;
+    line-height: 18px;
+    vertical-align: top;
+    text-align: left;
+  }
+}
diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss
index acaf5b024..e207113be 100644
--- a/app/javascript/flavours/glitch/styles/widgets.scss
+++ b/app/javascript/flavours/glitch/styles/widgets.scss
@@ -109,6 +109,15 @@
   box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
 }
 
+.placeholder-widget {
+  padding: 16px;
+  border-radius: 4px;
+  border: 2px dashed $dark-text-color;
+  text-align: center;
+  color: $darker-text-color;
+  margin-bottom: 10px;
+}
+
 .contact-widget,
 .landing-page__information.contact-widget {
   box-sizing: border-box;
@@ -521,6 +530,12 @@ $fluid-breakpoint: $maximum-width + 20px;
   a {
     font-size: 14px;
     line-height: 20px;
+  }
+}
+
+.notice-widget,
+.placeholder-widget {
+  a {
     text-decoration: none;
     font-weight: 500;
     color: $ui-highlight-color;
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
index f2aeda834..8f2e4c6e4 100644
--- a/app/javascript/flavours/glitch/util/async-components.js
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -153,3 +153,7 @@ export function ListAdder () {
 export function Search () {
   return import(/*webpackChunkName: "features/glitch/async/search" */'flavours/glitch/features/search');
 }
+
+export function Tesseract () {
+  return import(/*webpackChunkName: "tesseract" */'tesseract.js');
+}
diff --git a/app/javascript/flavours/glitch/util/numbers.js b/app/javascript/flavours/glitch/util/numbers.js
index fdd8269ae..f7e4ceb93 100644
--- a/app/javascript/flavours/glitch/util/numbers.js
+++ b/app/javascript/flavours/glitch/util/numbers.js
@@ -4,7 +4,9 @@ import { FormattedNumber } from 'react-intl';
 export const shortNumberFormat = number => {
   if (number < 1000) {
     return <FormattedNumber value={number} />;
-  } else {
+  } else if (number < 1000000) {
     return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
+  } else {
+    return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>;
   }
 };
diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js
index a8ec5f3fa..d566edb03 100644
--- a/app/javascript/flavours/glitch/util/resize_image.js
+++ b/app/javascript/flavours/glitch/util/resize_image.js
@@ -71,7 +71,7 @@ const processImage = (img, { width, height, orientation, type = 'image/png' }) =
   // and return an all-white image instead. Assume reading failed if the resized
   // image is perfectly white.
   const imageData = context.getImageData(0, 0, width, height);
-  if (imageData.every(value => value === 255)) {
+  if (imageData.data.every(value => value === 255)) {
     throw 'Failed to read from canvas';
   }
 
diff --git a/app/javascript/mastodon/actions/app.js b/app/javascript/mastodon/actions/app.js
new file mode 100644
index 000000000..414968f7d
--- /dev/null
+++ b/app/javascript/mastodon/actions/app.js
@@ -0,0 +1,10 @@
+export const APP_FOCUS   = 'APP_FOCUS';
+export const APP_UNFOCUS = 'APP_UNFOCUS';
+
+export const focusApp = () => ({
+  type: APP_FOCUS,
+});
+
+export const unfocusApp = () => ({
+  type: APP_UNFOCUS,
+});
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 9b1035649..735cab007 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -278,12 +278,27 @@ class Status extends ImmutablePureComponent {
       return null;
     }
 
+    const handlers = this.props.muted ? {} : {
+      reply: this.handleHotkeyReply,
+      favourite: this.handleHotkeyFavourite,
+      boost: this.handleHotkeyBoost,
+      mention: this.handleHotkeyMention,
+      open: this.handleHotkeyOpen,
+      openProfile: this.handleHotkeyOpenProfile,
+      moveUp: this.handleHotkeyMoveUp,
+      moveDown: this.handleHotkeyMoveDown,
+      toggleHidden: this.handleHotkeyToggleHidden,
+      toggleSensitive: this.handleHotkeyToggleSensitive,
+    };
+
     if (hidden) {
       return (
-        <div ref={this.handleRef}>
-          {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
-          {status.get('content')}
-        </div>
+        <HotKeys handlers={handlers}>
+          <div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'>
+            {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
+            {status.get('content')}
+          </div>
+        </HotKeys>
       );
     }
 
@@ -394,19 +409,6 @@ class Status extends ImmutablePureComponent {
       statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
     }
 
-    const handlers = this.props.muted ? {} : {
-      reply: this.handleHotkeyReply,
-      favourite: this.handleHotkeyFavourite,
-      boost: this.handleHotkeyBoost,
-      mention: this.handleHotkeyMention,
-      open: this.handleHotkeyOpen,
-      openProfile: this.handleHotkeyOpenProfile,
-      moveUp: this.handleHotkeyMoveUp,
-      moveDown: this.handleHotkeyMoveDown,
-      toggleHidden: this.handleHotkeyToggleHidden,
-      toggleSensitive: this.handleHotkeyToggleSensitive,
-    };
-
     return (
       <HotKeys handlers={handlers}>
         <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
index 48492f43d..8fddb6f54 100644
--- a/app/javascript/mastodon/containers/media_container.js
+++ b/app/javascript/mastodon/containers/media_container.js
@@ -7,6 +7,7 @@ import MediaGallery from '../components/media_gallery';
 import Video from '../features/video';
 import Card from '../features/status/components/card';
 import Poll from 'mastodon/components/poll';
+import Hashtag from 'mastodon/components/hashtag';
 import ModalRoot from '../components/modal_root';
 import { getScrollbarWidth } from '../features/ui/components/modal_root';
 import MediaModal from '../features/ui/components/media_modal';
@@ -15,7 +16,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
-const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll };
+const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag };
 
 export default class MediaContainer extends PureComponent {
 
@@ -62,12 +63,13 @@ export default class MediaContainer extends PureComponent {
           {[].map.call(components, (component, i) => {
             const componentName = component.getAttribute('data-component');
             const Component = MEDIA_COMPONENTS[componentName];
-            const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props'));
+            const { media, card, poll, hashtag, ...props } = JSON.parse(component.getAttribute('data-props'));
 
             Object.assign(props, {
-              ...(media ? { media: fromJS(media) } : {}),
-              ...(card  ? { card:  fromJS(card)  } : {}),
-              ...(poll  ? { poll:  fromJS(poll)  } : {}),
+              ...(media   ? { media:   fromJS(media)   } : {}),
+              ...(card    ? { card:    fromJS(card)    } : {}),
+              ...(poll    ? { poll:    fromJS(poll)    } : {}),
+              ...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
 
               ...(componentName === 'Video' ? {
                 onOpenVideo: this.handleOpenVideo,
@@ -81,6 +83,7 @@ export default class MediaContainer extends PureComponent {
               component,
             );
           })}
+
           <ModalRoot onClose={this.handleCloseMedia}>
             {this.state.media && (
               <MediaModal
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index 629cbc36a..b9f0fbe3a 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -4,16 +4,11 @@ import PropTypes from 'prop-types';
 import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { FormattedMessage } from 'react-intl';
 import classNames from 'classnames';
 import Icon from 'mastodon/components/icon';
 
-const messages = defineMessages({
-  description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
-});
-
-export default @injectIntl
-class Upload extends ImmutablePureComponent {
+export default class Upload extends ImmutablePureComponent {
 
   static contextTypes = {
     router: PropTypes.object,
@@ -21,30 +16,10 @@ class Upload extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
-    intl: PropTypes.object.isRequired,
     onUndo: PropTypes.func.isRequired,
-    onDescriptionChange: PropTypes.func.isRequired,
     onOpenFocalPoint: PropTypes.func.isRequired,
-    onSubmit: PropTypes.func.isRequired,
-  };
-
-  state = {
-    hovered: false,
-    focused: false,
-    dirtyDescription: null,
   };
 
-  handleKeyDown = (e) => {
-    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
-      this.handleSubmit();
-    }
-  }
-
-  handleSubmit = () => {
-    this.handleInputBlur();
-    this.props.onSubmit(this.context.router.history);
-  }
-
   handleUndoClick = e => {
     e.stopPropagation();
     this.props.onUndo(this.props.media.get('id'));
@@ -55,69 +30,21 @@ class Upload extends ImmutablePureComponent {
     this.props.onOpenFocalPoint(this.props.media.get('id'));
   }
 
-  handleInputChange = e => {
-    this.setState({ dirtyDescription: e.target.value });
-  }
-
-  handleMouseEnter = () => {
-    this.setState({ hovered: true });
-  }
-
-  handleMouseLeave = () => {
-    this.setState({ hovered: false });
-  }
-
-  handleInputFocus = () => {
-    this.setState({ focused: true });
-  }
-
-  handleClick = () => {
-    this.setState({ focused: true });
-  }
-
-  handleInputBlur = () => {
-    const { dirtyDescription } = this.state;
-
-    this.setState({ focused: false, dirtyDescription: null });
-
-    if (dirtyDescription !== null) {
-      this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription);
-    }
-  }
-
   render () {
-    const { intl, media } = this.props;
-    const active          = this.state.hovered || this.state.focused;
-    const description     = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
+    const { media } = this.props;
     const focusX = media.getIn(['meta', 'focus', 'x']);
     const focusY = media.getIn(['meta', 'focus', 'y']);
     const x = ((focusX /  2) + .5) * 100;
     const y = ((focusY / -2) + .5) * 100;
 
     return (
-      <div className='compose-form__upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
+      <div className='compose-form__upload' tabIndex='0' role='button'>
         <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
           {({ scale }) => (
             <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
-              <div className={classNames('compose-form__upload__actions', { active })}>
+              <div className={classNames('compose-form__upload__actions', { active: true })}>
                 <button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
-                {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
-              </div>
-
-              <div className={classNames('compose-form__upload-description', { active })}>
-                <label>
-                  <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
-
-                  <textarea
-                    placeholder={intl.formatMessage(messages.description)}
-                    value={description}
-                    maxLength={420}
-                    onFocus={this.handleInputFocus}
-                    onChange={this.handleInputChange}
-                    onBlur={this.handleInputBlur}
-                    onKeyDown={this.handleKeyDown}
-                  />
-                </label>
+                <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
               </div>
             </div>
           )}
diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js
index 9ff2aa0fa..c6eac554e 100644
--- a/app/javascript/mastodon/features/compose/components/upload_form.js
+++ b/app/javascript/mastodon/features/compose/components/upload_form.js
@@ -4,6 +4,7 @@ import UploadProgressContainer from '../containers/upload_progress_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import UploadContainer from '../containers/upload_container';
 import SensitiveButtonContainer from '../containers/sensitive_button_container';
+import { FormattedMessage } from 'react-intl';
 
 export default class UploadForm extends ImmutablePureComponent {
 
@@ -16,7 +17,7 @@ export default class UploadForm extends ImmutablePureComponent {
 
     return (
       <div className='compose-form__upload-wrapper'>
-        <UploadProgressContainer />
+        <UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} />
 
         <div className='compose-form__uploads-wrapper'>
           {mediaIds.map(id => (
diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js
index cbe58f573..b0bfe0c9a 100644
--- a/app/javascript/mastodon/features/compose/components/upload_progress.js
+++ b/app/javascript/mastodon/features/compose/components/upload_progress.js
@@ -2,7 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
-import { FormattedMessage } from 'react-intl';
 import Icon from 'mastodon/components/icon';
 
 export default class UploadProgress extends React.PureComponent {
@@ -10,10 +9,12 @@ export default class UploadProgress extends React.PureComponent {
   static propTypes = {
     active: PropTypes.bool,
     progress: PropTypes.number,
+    icon: PropTypes.string.isRequired,
+    message: PropTypes.node.isRequired,
   };
 
   render () {
-    const { active, progress } = this.props;
+    const { active, progress, icon, message } = this.props;
 
     if (!active) {
       return null;
@@ -22,11 +23,11 @@ export default class UploadProgress extends React.PureComponent {
     return (
       <div className='upload-progress'>
         <div className='upload-progress__icon'>
-          <Icon id='upload' />
+          <Icon id={icon} />
         </div>
 
         <div className='upload-progress__message'>
-          <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
+          {message}
 
           <div className='upload-progress__backdrop'>
             <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
diff --git a/app/javascript/mastodon/features/compose/containers/upload_container.js b/app/javascript/mastodon/features/compose/containers/upload_container.js
index b6d81f03a..342b0c2a9 100644
--- a/app/javascript/mastodon/features/compose/containers/upload_container.js
+++ b/app/javascript/mastodon/features/compose/containers/upload_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import Upload from '../components/upload';
-import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose';
+import { undoUploadCompose } from '../../../actions/compose';
 import { openModal } from '../../../actions/modal';
 import { submitCompose } from '../../../actions/compose';
 
@@ -14,10 +14,6 @@ const mapDispatchToProps = dispatch => ({
     dispatch(undoUploadCompose(id));
   },
 
-  onDescriptionChange: (id, description) => {
-    dispatch(changeUploadCompose(id, { description }));
-  },
-
   onOpenFocalPoint: id => {
     dispatch(openModal('FOCAL_POINT', { id }));
   },
diff --git a/app/javascript/mastodon/features/ui/components/document_title.js b/app/javascript/mastodon/features/ui/components/document_title.js
new file mode 100644
index 000000000..cd081b20c
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/document_title.js
@@ -0,0 +1,41 @@
+import { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { title } from 'mastodon/initial_state';
+
+const mapStateToProps = state => ({
+  unread: state.getIn(['missed_updates', 'unread']),
+});
+
+export default @connect(mapStateToProps)
+class DocumentTitle extends PureComponent {
+
+  static propTypes = {
+    unread: PropTypes.number.isRequired,
+  };
+
+  componentDidMount () {
+    this._sideEffects();
+  }
+
+  componentDidUpdate() {
+    this._sideEffects();
+  }
+
+  _sideEffects () {
+    const { unread } = this.props;
+
+    if (unread > 99) {
+      document.title = `(*) ${title}`;
+    } else if (unread > 0) {
+      document.title = `(${unread}) ${title}`;
+    } else {
+      document.title = title;
+    }
+  }
+
+  render () {
+    return null;
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index 7488a3598..e0ef1a066 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
@@ -1,11 +1,26 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
-import ImageLoader from './image_loader';
 import classNames from 'classnames';
 import { changeUploadCompose } from '../../../actions/compose';
 import { getPointerPosition } from '../../video';
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+import IconButton from 'mastodon/components/icon_button';
+import Button from 'mastodon/components/button';
+import Video from 'mastodon/features/video';
+import Textarea from 'react-textarea-autosize';
+import UploadProgress from 'mastodon/features/compose/components/upload_progress';
+import CharacterCounter from 'mastodon/features/compose/components/character_counter';
+import { length } from 'stringz';
+import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' },
+  apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
+  placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
+});
 
 const mapStateToProps = (state, { id }) => ({
   media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
@@ -13,17 +28,26 @@ const mapStateToProps = (state, { id }) => ({
 
 const mapDispatchToProps = (dispatch, { id }) => ({
 
-  onSave: (x, y) => {
-    dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
+  onSave: (description, x, y) => {
+    dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
   },
 
 });
 
+const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
+  .replace(/\n/g, ' ')
+  .replace(/\*\*\*\*\*\*/g, '\n\n');
+
+const assetHost = process.env.CDN_HOST || '';
+
 export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
 class FocalPointModal extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
   };
 
   state = {
@@ -32,6 +56,9 @@ class FocalPointModal extends ImmutablePureComponent {
     focusX: 0,
     focusY: 0,
     dragging: false,
+    description: '',
+    dirty: false,
+    progress: 0,
   };
 
   componentWillMount () {
@@ -57,6 +84,14 @@ class FocalPointModal extends ImmutablePureComponent {
     this.setState({ dragging: true });
   }
 
+  handleTouchStart = e => {
+    document.addEventListener('touchmove', this.handleMouseMove);
+    document.addEventListener('touchend', this.handleTouchEnd);
+
+    this.updatePosition(e);
+    this.setState({ dragging: true });
+  }
+
   handleMouseMove = e => {
     this.updatePosition(e);
   }
@@ -66,7 +101,13 @@ class FocalPointModal extends ImmutablePureComponent {
     document.removeEventListener('mouseup', this.handleMouseUp);
 
     this.setState({ dragging: false });
-    this.props.onSave(this.state.focusX, this.state.focusY);
+  }
+
+  handleTouchEnd = () => {
+    document.removeEventListener('touchmove', this.handleMouseMove);
+    document.removeEventListener('touchend', this.handleTouchEnd);
+
+    this.setState({ dragging: false });
   }
 
   updatePosition = e => {
@@ -74,46 +115,145 @@ class FocalPointModal extends ImmutablePureComponent {
     const focusX   = (x - .5) *  2;
     const focusY   = (y - .5) * -2;
 
-    this.setState({ x, y, focusX, focusY });
+    this.setState({ x, y, focusX, focusY, dirty: true });
   }
 
   updatePositionFromMedia = media => {
-    const focusX = media.getIn(['meta', 'focus', 'x']);
-    const focusY = media.getIn(['meta', 'focus', 'y']);
+    const focusX      = media.getIn(['meta', 'focus', 'x']);
+    const focusY      = media.getIn(['meta', 'focus', 'y']);
+    const description = media.get('description') || '';
 
     if (focusX && focusY) {
       const x = (focusX /  2) + .5;
       const y = (focusY / -2) + .5;
 
-      this.setState({ x, y, focusX, focusY });
+      this.setState({
+        x,
+        y,
+        focusX,
+        focusY,
+        description,
+        dirty: false,
+      });
     } else {
-      this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 });
+      this.setState({
+        x: 0.5,
+        y: 0.5,
+        focusX: 0,
+        focusY: 0,
+        description,
+        dirty: false,
+      });
     }
   }
 
+  handleChange = e => {
+    this.setState({ description: e.target.value, dirty: true });
+  }
+
+  handleSubmit = () => {
+    this.props.onSave(this.state.description, this.state.focusX, this.state.focusY);
+    this.props.onClose();
+  }
+
   setRef = c => {
     this.node = c;
   }
 
-  render () {
+  handleTextDetection = () => {
     const { media } = this.props;
-    const { x, y, dragging } = this.state;
+
+    this.setState({ detecting: true });
+
+    fetchTesseract().then(({ TesseractWorker }) => {
+      const worker = new TesseractWorker({
+        workerPath: `${assetHost}/packs/ocr/worker.min.js`,
+        corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`,
+        langPath: `${assetHost}/ocr/lang-data`,
+      });
+
+      worker.recognize(media.get('url'))
+        .progress(({ progress }) => this.setState({ progress }))
+        .finally(() => worker.terminate())
+        .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
+        .catch(() => this.setState({ detecting: false }));
+    }).catch(() => this.setState({ detecting: false }));
+  }
+
+  render () {
+    const { media, intl, onClose } = this.props;
+    const { x, y, dragging, description, dirty, detecting, progress } = this.state;
 
     const width  = media.getIn(['meta', 'original', 'width']) || null;
     const height = media.getIn(['meta', 'original', 'height']) || null;
+    const focals = ['image', 'gifv'].includes(media.get('type'));
+
+    const previewRatio  = 16/9;
+    const previewWidth  = 200;
+    const previewHeight = previewWidth / previewRatio;
 
     return (
-      <div className='modal-root__modal video-modal focal-point-modal'>
-        <div className={classNames('focal-point', { dragging })} ref={this.setRef}>
-          <ImageLoader
-            previewSrc={media.get('preview_url')}
-            src={media.get('url')}
-            width={width}
-            height={height}
-          />
-
-          <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
-          <div className='focal-point__overlay' onMouseDown={this.handleMouseDown} />
+      <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
+        <div className='report-modal__target'>
+          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
+          <FormattedMessage id='upload_modal.edit_media' defaultMessage='Edit media' />
+        </div>
+
+        <div className='report-modal__container'>
+          <div className='report-modal__comment'>
+            {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
+
+            <label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label>
+
+            <div className='setting-text__wrapper'>
+              <Textarea
+                id='upload-modal__description'
+                className='setting-text light'
+                value={detecting ? '…' : description}
+                onChange={this.handleChange}
+                disabled={detecting}
+                autoFocus
+              />
+
+              <div className='setting-text__modifiers'>
+                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={<FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />} />
+              </div>
+            </div>
+
+            <div className='setting-text__toolbar'>
+              <button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button>
+              <CharacterCounter max={420} text={detecting ? '' : description} />
+            </div>
+
+            <Button disabled={!dirty || detecting || length(description) > 420} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
+          </div>
+
+          <div className='focal-point-modal__content'>
+            {focals && (
+              <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
+                {media.get('type') === 'image' && <img src={media.get('url')} width={width} height={height} alt='' />}
+                {media.get('type') === 'gifv' && <video src={media.get('url')} width={width} height={height} loop muted autoPlay />}
+
+                <div className='focal-point__preview'>
+                  <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
+                  <div style={{ width: previewWidth, height: previewHeight, backgroundImage: `url(${media.get('preview_url')})`, backgroundSize: 'cover', backgroundPosition: `${x * 100}% ${y * 100}%` }} />
+                </div>
+
+                <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
+                <div className='focal-point__overlay' />
+              </div>
+            )}
+
+            {['audio', 'video'].includes(media.get('type')) && (
+              <Video
+                preview={media.get('preview_url')}
+                blurhash={media.get('blurhash')}
+                src={media.get('url')}
+                detailed
+                editable
+              />
+            )}
+          </div>
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index d1a3dc949..f0c3eff83 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -15,9 +15,11 @@ import { expandHomeTimeline } from '../../actions/timelines';
 import { expandNotifications } from '../../actions/notifications';
 import { fetchFilters } from '../../actions/filters';
 import { clearHeight } from '../../actions/height_cache';
+import { focusApp, unfocusApp } from 'mastodon/actions/app';
 import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
 import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
+import DocumentTitle from './components/document_title';
 import {
   Compose,
   Status,
@@ -226,7 +228,7 @@ class UI extends React.PureComponent {
     draggingOver: false,
   };
 
-  handleBeforeUnload = (e) => {
+  handleBeforeUnload = e => {
     const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props;
 
     if (isComposing && (hasComposingText || hasMediaAttachments)) {
@@ -237,6 +239,14 @@ class UI extends React.PureComponent {
     }
   }
 
+  handleWindowFocus = () => {
+    this.props.dispatch(focusApp());
+  }
+
+  handleWindowBlur = () => {
+    this.props.dispatch(unfocusApp());
+  }
+
   handleLayoutChange = () => {
     // The cached heights are no longer accurate, invalidate
     this.props.dispatch(clearHeight());
@@ -314,6 +324,8 @@ class UI extends React.PureComponent {
   }
 
   componentWillMount () {
+    window.addEventListener('focus', this.handleWindowFocus, false);
+    window.addEventListener('blur', this.handleWindowBlur, false);
     window.addEventListener('beforeunload', this.handleBeforeUnload, false);
 
     document.addEventListener('dragenter', this.handleDragEnter, false);
@@ -343,7 +355,10 @@ class UI extends React.PureComponent {
   }
 
   componentWillUnmount () {
+    window.removeEventListener('focus', this.handleWindowFocus);
+    window.removeEventListener('blur', this.handleWindowBlur);
     window.removeEventListener('beforeunload', this.handleBeforeUnload);
+
     document.removeEventListener('dragenter', this.handleDragEnter);
     document.removeEventListener('dragover', this.handleDragOver);
     document.removeEventListener('drop', this.handleDrop);
@@ -502,6 +517,7 @@ class UI extends React.PureComponent {
           <LoadingBarContainer className='loading-bar' />
           <ModalContainer />
           <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
+          <DocumentTitle />
         </div>
       </HotKeys>
     );
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 6e8ed163a..0a07aa75e 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -133,3 +133,7 @@ export function ListAdder () {
 export function Search () {
   return import(/*webpackChunkName: "features/search" */'../../search');
 }
+
+export function Tesseract () {
+  return import(/*webpackChunkName: "tesseract" */'tesseract.js');
+}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 0acdd198d..da48c165e 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -101,6 +101,7 @@ class Video extends React.PureComponent {
     onCloseVideo: PropTypes.func,
     detailed: PropTypes.bool,
     inline: PropTypes.bool,
+    editable: PropTypes.bool,
     cacheWidth: PropTypes.func,
     visible: PropTypes.bool,
     onToggleVisibility: PropTypes.func,
@@ -375,7 +376,7 @@ class Video extends React.PureComponent {
   }
 
   render () {
-    const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link } = this.props;
+    const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props;
     const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
     const progress = (currentTime / duration) * 100;
 
@@ -413,7 +414,7 @@ class Video extends React.PureComponent {
     return (
       <div
         role='menuitem'
-        className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen })}
+        className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable })}
         style={playerStyle}
         ref={this.setPlayerRef}
         onMouseEnter={this.handleMouseEnter}
@@ -423,7 +424,7 @@ class Video extends React.PureComponent {
       >
         <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} />
 
-        {revealed && <video
+        {(revealed || editable) && <video
           ref={this.setVideoRef}
           src={src}
           poster={preview}
@@ -445,7 +446,7 @@ class Video extends React.PureComponent {
           onVolumeChange={this.handleVolumeChange}
         />}
 
-        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}>
+        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
           <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
             <span className='spoiler-button__overlay__label'>{warning}</span>
           </button>
@@ -489,7 +490,7 @@ class Video extends React.PureComponent {
             </div>
 
             <div className='video-player__buttons right'>
-              {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
+              {(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
               {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
               {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
               <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index 8db5f59af..deebe1815 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -24,5 +24,6 @@ export const forceSingleColumn = !getMeta('advanced_layout');
 export const useBlurhash = getMeta('use_blurhash');
 export const usePendingItems = getMeta('use_pending_items');
 export const showTrends = getMeta('trends');
+export const title = getMeta('title');
 
 export default initialState;
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index d62ee90c2..e7a21523a 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -4,6 +4,7 @@
   "account.block": "حظر @{name}",
   "account.block_domain": "إخفاء كل شيئ قادم من اسم النطاق {domain}",
   "account.blocked": "محظور",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "رسالة خاصة إلى @{name}",
   "account.domain_blocked": "النطاق مخفي",
   "account.edit_profile": "تعديل الملف الشخصي",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
   "alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.",
   "alert.unexpected.title": "المعذرة!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "يمكنك/ي ضغط {combo} لتخطّي هذه في المرّة القادمة",
   "bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
   "bundle_column_error.retry": "إعادة المحاولة",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "أساسية",
   "home.column_settings.show_reblogs": "عرض الترقيات",
   "home.column_settings.show_replies": "عرض الردود",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "تحديث في الوقت الحالي",
   "intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}",
   "intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}",
   "intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}",
@@ -251,10 +253,11 @@
   "navigation_bar.profile_directory": "دليل المستخدِمين",
   "navigation_bar.public_timeline": "الخيط العام الموحد",
   "navigation_bar.security": "الأمان",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "أُعجِب {name} بمنشورك",
   "notification.follow": "{name} يتابعك",
   "notification.mention": "{name} ذكرك",
-  "notification.poll": "A poll you have voted in has ended",
+  "notification.poll": "لقد إنتها تصويت شاركت فيه",
   "notification.reblog": "{name} قام بترقية تبويقك",
   "notifications.clear": "امسح الإخطارات",
   "notifications.clear_confirmation": "أمتأكد من أنك تود مسح جل الإخطارات الخاصة بك و المتلقاة إلى حد الآن ؟",
@@ -316,7 +319,7 @@
   "search_results.accounts": "أشخاص",
   "search_results.hashtags": "الوُسوم",
   "search_results.statuses": "التبويقات",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "البحث في التبويقات عن طريق المحتوى ليس مفعل في خادم ماستدون هذا.",
   "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
   "status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "لحظات متبقية",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
   "upload_area.title": "اسحب ثم أفلت للرفع",
   "upload_button.label": "إضافة وسائط ({formats})",
   "upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.",
   "upload_error.poll": "لا يمكن إدراج ملفات في استطلاعات الرأي.",
   "upload_form.description": "وصف للمعاقين بصريا",
-  "upload_form.focus": "قص",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "حذف",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "يرفع...",
   "video.close": "إغلاق الفيديو",
   "video.exit_fullscreen": "الخروج من وضع الشاشة المليئة",
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index 3ae4e5e5e..c9b5d6061 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -4,6 +4,7 @@
   "account.block": "Bloquiar a @{name}",
   "account.block_domain": "Anubrir tolo de {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Unviar un mensaxe direutu a @{name}",
   "account.domain_blocked": "Dominiu anubríu",
   "account.edit_profile": "Editar el perfil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Unmute notifications from @{name}",
   "alert.unexpected.message": "Asocedió un fallu inesperáu.",
   "alert.unexpected.title": "¡Ups!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Pues primir {combo} pa saltar esto la próxima vegada",
   "bundle_column_error.body": "Something went wrong while loading this component.",
   "bundle_column_error.retry": "Try again",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Llinia temporal federada",
   "navigation_bar.security": "Seguranza",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} siguióte",
   "notification.mention": "{name} mentóte",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "El borrador va perdese si coles de Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Descripción pa discapacitaos visuales",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Desaniciar",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Xubiendo...",
   "video.close": "Zarrar el videu",
   "video.exit_fullscreen": "Colar de la pantalla completa",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 4c97fe1fc..3cb5900f4 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -4,6 +4,7 @@
   "account.block": "Блокирай",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Редактирай профила си",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Unmute notifications from @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "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",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Публичен канал",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} хареса твоята публикация",
   "notification.follow": "{name} те последва",
   "notification.mention": "{name} те спомена",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Добави медия",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Отмяна",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploading...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json
index 358f994f3..ee79b0edd 100644
--- a/app/javascript/mastodon/locales/bn.json
+++ b/app/javascript/mastodon/locales/bn.json
@@ -4,6 +4,7 @@
   "account.block": "@{name} কে বন্ধ করুন",
   "account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন",
   "account.blocked": "বন্ধ করা হয়েছে",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "@{name} এর কাছে সরকারি লেখা পাঠাতে",
   "account.domain_blocked": "ওয়েবসাইট সরিয়ে ফেলা হয়েছে",
   "account.edit_profile": "নিজের পাতা সম্পাদনা করতে",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন",
   "alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।",
   "alert.unexpected.title": "ওহো!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "পরেরবার আপনি {combo} চাপ দিলে এটার শেষে চলে যেতে পারবেন",
   "bundle_column_error.body": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।",
   "bundle_column_error.retry": "আবার চেষ্টা করুন",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "নিজস্ব পাতার তালিকা",
   "navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা",
   "navigation_bar.security": "নিরাপত্তা",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন",
   "notification.follow": "{name} আপনাকে অনুসরণ করেছেন",
   "notification.mention": "{name} আপনাকে উল্লেখ করেছেন",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "সময় বাকি আছে",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।",
   "upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে",
   "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।",
   "upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।",
   "upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে",
-  "upload_form.focus": "সাধারণ দেখাটি পরিবর্তন করতে",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "মুছে ফেলতে",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "যুক্ত করতে পাঠানো হচ্ছে...",
   "video.close": "ভিডিওটি বন্ধ করতে",
   "video.exit_fullscreen": "পূর্ণ পর্দা থেকে বাইরে বের হতে",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 09f8838e9..4554ff04e 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -4,6 +4,7 @@
   "account.block": "Bloqueja @{name}",
   "account.block_domain": "Amaga-ho tot de {domain}",
   "account.blocked": "Bloquejat",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Missatge directe @{name}",
   "account.domain_blocked": "Domini ocult",
   "account.edit_profile": "Editar el perfil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Activar notificacions de @{name}",
   "alert.unexpected.message": "S'ha produït un error inesperat.",
   "alert.unexpected.title": "Vaja!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
   "bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
   "bundle_column_error.retry": "Torna-ho a provar",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Bàsic",
   "home.column_settings.show_reblogs": "Mostrar impulsos",
   "home.column_settings.show_replies": "Mostrar respostes",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Actualització en temps real",
   "intervals.full.days": "{number, plural, one {# dia} other {# dies}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
   "intervals.full.minutes": "{number, plural, one {# minut} other {# minuts}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Nova llista",
   "lists.search": "Cercar entre les persones que segueixes",
   "lists.subheading": "Les teves llistes",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
   "missing_indicator.label": "No trobat",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Directori de perfils",
   "navigation_bar.public_timeline": "Línia de temps federada",
   "navigation_bar.security": "Seguretat",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} ha afavorit el teu estat",
   "notification.follow": "{name} et segueix",
   "notification.mention": "{name} t'ha esmentat",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Gent",
   "search_results.hashtags": "Etiquetes",
   "search_results.statuses": "Toots",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "La cerca de toots pel seu contingut no està habilitada en aquest servidor Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
   "status.admin_account": "Obre l'interfície de moderació per a @{name}",
   "status.admin_status": "Obre aquest toot a la interfície de moderació",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments restants",
   "time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {gent}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.",
   "upload_area.title": "Arrossega i deixa anar per a carregar",
   "upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "S'ha superat el límit de càrrega d'arxius.",
   "upload_error.poll": "No es permet l'enviament de fitxers en les enquestes.",
   "upload_form.description": "Descriure els problemes visuals",
-  "upload_form.focus": "Modificar la previsualització",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Esborra",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Pujant...",
   "video.close": "Tancar el vídeo",
   "video.exit_fullscreen": "Sortir de pantalla completa",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index 7a1ff863b..428c993a6 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -1,9 +1,10 @@
 {
-  "account.add_or_remove_from_list": "Aghjustà o toglie da e liste",
+  "account.add_or_remove_from_list": "Aghjunghje o toglie da e liste",
   "account.badges.bot": "Bot",
   "account.block": "Bluccà @{name}",
   "account.block_domain": "Piattà tuttu da {domain}",
   "account.blocked": "Bluccatu",
+  "account.cancel_follow_request": "Annullà a dumanda d'abbunamentu",
   "account.direct": "Missaghju direttu @{name}",
   "account.domain_blocked": "Duminiu piattatu",
   "account.edit_profile": "Mudificà u prufile",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Ùn piattà più nutificazione da @{name}",
   "alert.unexpected.message": "Un prublemu inaspettatu hè accadutu.",
   "alert.unexpected.title": "Uups!",
+  "autosuggest_hashtag.per_week": "{count} per settimana",
   "boost_modal.combo": "Pudete appughjà nant'à {combo} per saltà quessa a prussima volta",
   "bundle_column_error.body": "C'hè statu un prublemu caricandu st'elementu.",
   "bundle_column_error.retry": "Pruvà torna",
@@ -71,7 +73,7 @@
   "compose_form.lock_disclaimer": "U vostru contu ùn hè micca {locked}. Tuttu u mondu pò seguitavi è vede i vostri statuti privati.",
   "compose_form.lock_disclaimer.lock": "privatu",
   "compose_form.placeholder": "À chè pensate?",
-  "compose_form.poll.add_option": "Aghjustà una scelta",
+  "compose_form.poll.add_option": "Aghjunghje scelta",
   "compose_form.poll.duration": "Durata di u scandagliu",
   "compose_form.poll.option_placeholder": "Scelta {number}",
   "compose_form.poll.remove_option": "Toglie sta scelta",
@@ -123,8 +125,8 @@
   "empty_column.community": "Ùn c'hè nunda indè a linea lucale. Scrivete puru qualcosa!",
   "empty_column.direct": "Ùn avete ancu nisun missaghju direttu. S'è voi mandate o ricevete unu, u vidarete quì.",
   "empty_column.domain_blocks": "Ùn c'hè manc'un duminiu bluccatu avà.",
-  "empty_column.favourited_statuses": "Ùn avete manc'unu statutu favuritu. Quandu aghjusterate unu à i vostri favuriti, sarà mustratu quì.",
-  "empty_column.favourites": "Nisunu hà aghjustatu stu statutu à i so favuriti. Quandu qualch'unu farà quessa, u so contu sarà mustratu quì.",
+  "empty_column.favourited_statuses": "Ùn avete manc'unu statutu favuritu. Quandu aghjunghjerate unu à i vostri favuriti, sarà mustratu quì.",
+  "empty_column.favourites": "Nisunu hà aghjuntu stu statutu à i so favuriti. Quandu qualch'unu farà quessa, u so contu sarà mustratu quì.",
   "empty_column.follow_requests": "Ùn avete manc'una dumanda d'abbunamentu. Quandu averete una, sarà mustrata quì.",
   "empty_column.hashtag": "Ùn c'hè ancu nunda quì.",
   "empty_column.home": "A vostr'accolta hè viota! Pudete andà nant'à {public} o pruvà a ricerca per truvà parsone da siguità.",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Bàsichi",
   "home.column_settings.show_reblogs": "Vede e spartere",
   "home.column_settings.show_replies": "Vede e risposte",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Mette à ghjornu in tempu reale",
   "intervals.full.days": "{number, plural, one {# ghjornu} other {# ghjorni}}",
   "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}",
@@ -218,11 +220,11 @@
   "lists.delete": "Supprime a lista",
   "lists.edit": "Mudificà a lista",
   "lists.edit.submit": "Cambià u titulu",
-  "lists.new.create": "Aghjustà una lista",
+  "lists.new.create": "Aghjunghje",
   "lists.new.title_placeholder": "Titulu di a lista",
   "lists.search": "Circà indè i vostr'abbunamenti",
   "lists.subheading": "E vo liste",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
   "loading_indicator.label": "Caricamentu...",
   "media_gallery.toggle_visible": "Cambià a visibilità",
   "missing_indicator.label": "Micca trovu",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Annuariu di i prufili",
   "navigation_bar.public_timeline": "Linea pubblica glubale",
   "navigation_bar.security": "Sicurità",
+  "notification.and_n_others": "è {count, plural, one {# altru} other {# altri}}",
   "notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti",
   "notification.follow": "{name} v'hà seguitatu",
   "notification.mention": "{name} v'hà mintuvatu",
@@ -281,7 +284,7 @@
   "poll.refresh": "Attualizà",
   "poll.total_votes": "{count, plural, one {# votu} other {# voti}}",
   "poll.vote": "Vutà",
-  "poll_button.add_poll": "Aghjustà un scandagliu",
+  "poll_button.add_poll": "Aghjunghje",
   "poll_button.remove_poll": "Toglie u scandagliu",
   "privacy.change": "Mudificà a cunfidenzialità di u statutu",
   "privacy.direct.long": "Mandà solu à quelli chì so mintuvati",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Ghjente",
   "search_results.hashtags": "Hashtag",
   "search_results.statuses": "Statuti",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "A ricerca di i cuntinuti di i statuti ùn hè micca attivata nant'à stu servore Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}",
   "status.admin_account": "Apre l'interfaccia di muderazione per @{name}",
   "status.admin_status": "Apre stu statutu in l'interfaccia di muderazione",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Ci fermanu qualchi mumentu",
   "time_remaining.seconds": "{number, plural, one {# siconda ferma} other {# siconde fermanu}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} parlanu",
+  "trends.refresh": "Attualizà",
   "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.",
   "upload_area.title": "Drag & drop per caricà un fugliale",
   "upload_button.label": "Aghjunghje un media (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Limita di caricamentu di fugliali trapassata.",
   "upload_error.poll": "Ùn si pò micca caricà fugliali cù i scandagli.",
   "upload_form.description": "Discrive per i malvistosi",
-  "upload_form.focus": "Cambià a vista",
+  "upload_form.edit": "Mudificà",
   "upload_form.undo": "Sguassà",
+  "upload_modal.analyzing_picture": "Analisi di u ritrattu…",
+  "upload_modal.apply": "Affettà",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Ditettà testu da u ritrattu",
+  "upload_modal.edit_media": "Cambià media",
+  "upload_modal.hint": "Cliccate o sguillate u chjerchju nant'à a vista per sceglie u puntu fucale chì sarà sempre in vista indè tutte e miniature.",
+  "upload_modal.preview_label": "Vista ({ratio})",
   "upload_progress.label": "Caricamentu...",
   "video.close": "Chjudà a video",
   "video.exit_fullscreen": "Caccià u pienu screnu",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 020fd35b0..46a57b3b8 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -4,6 +4,7 @@
   "account.block": "Zablokovat uživatele @{name}",
   "account.block_domain": "Skrýt vše z {domain}",
   "account.blocked": "Blokován/a",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Poslat přímou zprávu uživateli @{name}",
   "account.domain_blocked": "Doména skryta",
   "account.edit_profile": "Upravit profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Odkrýt oznámení od uživatele @{name}",
   "alert.unexpected.message": "Objevila se neočekávaná chyba.",
   "alert.unexpected.title": "Jejda!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Příště můžete pro přeskočení kliknout na {combo}",
   "bundle_column_error.body": "Při načítání tohoto komponentu se něco pokazilo.",
   "bundle_column_error.retry": "Zkuste to znovu",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Základní",
   "home.column_settings.show_reblogs": "Zobrazit boosty",
   "home.column_settings.show_replies": "Zobrazit odpovědi",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Aktualizovat v reálném čase",
   "intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dne} other {# dní}}",
   "intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minuty} other {# minut}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Název nového seznamu",
   "lists.search": "Hledejte mezi lidmi, které sledujete",
   "lists.subheading": "Vaše seznamy",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
   "loading_indicator.label": "Načítám…",
   "media_gallery.toggle_visible": "Přepínat viditelnost",
   "missing_indicator.label": "Nenalezeno",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Adresář profilů",
   "navigation_bar.public_timeline": "Federovaná časová osa",
   "navigation_bar.security": "Zabezpečení",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} si oblíbil/a váš toot",
   "notification.follow": "{name} vás začal/a sledovat",
   "notification.mention": "{name} vás zmínil/a",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Lidé",
   "search_results.hashtags": "Hashtagy",
   "search_results.statuses": "Tooty",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Vyhledávání tootů podle jejich obsahu není na tomto serveru Mastodon povoleno.",
   "search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledku} other {výsledků}}",
   "status.admin_account": "Otevřít moderátorské rozhraní pro uživatele @{name}",
   "status.admin_status": "Otevřít tento toot v moderátorském rozhraní",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Zbývá několik sekund",
   "time_remaining.seconds": "{number, plural, one {Zbývá # sekunda} few {Zbývají # sekundy} many {Zbývá # sekundy} other {Zbývá # sekund}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {člověk} few {lidé} many {lidí} other {lidí}} hovoří",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Váš koncept se ztratí, pokud Mastodon opustíte.",
   "upload_area.title": "Přetažením nahrajete",
   "upload_button.label": "Přidat média (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Byl překročen limit nahraných souborů.",
   "upload_error.poll": "Nahrávání souborů není povoleno u anket.",
   "upload_form.description": "Popis pro zrakově postižené",
-  "upload_form.focus": "Změnit náhled",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Smazat",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Nahrávám…",
   "video.close": "Zavřít video",
   "video.exit_fullscreen": "Ukončit celou obrazovku",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index 9de3efda8..0bd6f19d2 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -4,6 +4,7 @@
   "account.block": "Blocio @{name}",
   "account.block_domain": "Cuddio popeth rhag {domain}",
   "account.blocked": "Blociwyd",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Neges breifat @{name}",
   "account.domain_blocked": "Parth wedi ei guddio",
   "account.edit_profile": "Golygu proffil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Dad-dawelu hysbysiadau o @{name}",
   "alert.unexpected.message": "Digwyddodd gwall annisgwyl.",
   "alert.unexpected.title": "Wps!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Mae modd gwasgu {combo} er mwyn sgipio hyn tro nesa",
   "bundle_column_error.body": "Aeth rhywbeth o'i le tra'n llwytho'r elfen hon.",
   "bundle_column_error.retry": "Ceisiwch eto",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Cyfeiriadur Proffil",
   "navigation_bar.public_timeline": "Ffrwd y ffederasiwn",
   "navigation_bar.security": "Diogelwch",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "hoffodd {name} eich tŵt",
   "notification.follow": "dilynodd {name} chi",
   "notification.mention": "Soniodd {name} amdanoch chi",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Munudau ar ôl",
   "time_remaining.seconds": "{number, plural, one {# eiliad} other {# o eiliadau}} ar ôl",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} yn siarad",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.",
   "upload_area.title": "Llusgwch & gollwing i uwchlwytho",
   "upload_button.label": "Ychwanegwch gyfryngau (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Wedi mynd heibio'r uchafswm terfyn uwchlwytho.",
   "upload_error.poll": "Nid oes modd uwchlwytho ffeiliau â phleidleisiau.",
   "upload_form.description": "Disgrifio i'r rheini a nam ar ei golwg",
-  "upload_form.focus": "Newid rhagolwg",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Dileu",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uwchlwytho...",
   "video.close": "Cau fideo",
   "video.exit_fullscreen": "Gadael sgrîn llawn",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 17080c41e..b021f9aa1 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -4,6 +4,7 @@
   "account.block": "Bloker @{name}",
   "account.block_domain": "Skjul alt fra {domain}",
   "account.blocked": "Blokeret",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Send en direkte besked til @{name}",
   "account.domain_blocked": "Domænet er blevet skjult",
   "account.edit_profile": "Rediger profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Fjern dæmpningen af notifikationer fra @{name}",
   "alert.unexpected.message": "Der opstod en uventet fejl.",
   "alert.unexpected.title": "Ups!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Du kan trykke {combo} for at springe dette over næste gang",
   "bundle_column_error.body": "Noget gik galt under indlæsningen af dette komponent.",
   "bundle_column_error.retry": "Prøv igen",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Fælles tidslinje",
   "navigation_bar.security": "Sikkerhed",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favoriserede din status",
   "notification.follow": "{name} fulgte dig",
   "notification.mention": "{name} nævnte dig",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} snakker",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Din kladde vil gå tabt hvis du forlader Mastodon.",
   "upload_area.title": "Træk og slip for at uploade",
   "upload_button.label": "Tilføj medie (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Beskriv for de svagtseende",
-  "upload_form.focus": "Beskær",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Slet",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploader...",
   "video.close": "Luk video",
   "video.exit_fullscreen": "Gå ud af fuldskærm",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 4ae785270..28b41baf7 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -4,6 +4,7 @@
   "account.block": "@{name} blockieren",
   "account.block_domain": "Alles von {domain} verstecken",
   "account.blocked": "Blockiert",
+  "account.cancel_follow_request": "Folgeanfrage abbrechen",
   "account.direct": "Direktnachricht an @{name}",
   "account.domain_blocked": "Domain versteckt",
   "account.edit_profile": "Profil bearbeiten",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
   "alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
   "alert.unexpected.title": "Hoppla!",
+  "autosuggest_hashtag.per_week": "{count} pro Woche",
   "boost_modal.combo": "Drücke {combo}, um dieses Fenster zu überspringen",
   "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
   "bundle_column_error.retry": "Erneut versuchen",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Einfach",
   "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
   "home.column_settings.show_replies": "Antworten anzeigen",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "In Echtzeit aktualisieren",
   "intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}",
   "intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}",
   "intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Neuer Titel der Liste",
   "lists.search": "Suche nach Leuten denen du folgst",
   "lists.subheading": "Deine Listen",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
   "loading_indicator.label": "Wird geladen …",
   "media_gallery.toggle_visible": "Sichtbarkeit umschalten",
   "missing_indicator.label": "Nicht gefunden",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profilverzeichnis",
   "navigation_bar.public_timeline": "Föderierte Zeitleiste",
   "navigation_bar.security": "Sicherheit",
+  "notification.and_n_others": "und {count, plural, one {# andere Person} other {# andere Personen}}",
   "notification.favourite": "{name} hat deinen Beitrag favorisiert",
   "notification.follow": "{name} folgt dir",
   "notification.mention": "{name} hat dich erwähnt",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Personen",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Beiträge",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Die Suche für Beiträge nach ihrem Inhalt ist auf diesem Mastodon-Server deaktiviert.",
   "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
   "status.admin_account": "Öffne Moderationsoberfläche für @{name}",
   "status.admin_status": "Öffne Beitrag in der Moderationsoberfläche",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Schließt in Kürze",
   "time_remaining.seconds": "{number, plural, one {# Sekunde} other {# Sekunden}} verbleibend",
   "trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber",
+  "trends.refresh": "Aktualisieren",
   "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.",
   "upload_area.title": "Zum Hochladen hereinziehen",
   "upload_button.label": "Mediendatei hinzufügen ({formats})",
   "upload_error.limit": "Dateiupload-Limit erreicht.",
   "upload_error.poll": "Dateiuploads sind in Kombination mit Umfragen nicht erlaubt.",
   "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben",
-  "upload_form.focus": "Vorschaubild bearbeiten",
+  "upload_form.edit": "Bearbeiten",
   "upload_form.undo": "Löschen",
+  "upload_modal.analyzing_picture": "Analysiere Bild…",
+  "upload_modal.apply": "Übernehmen",
+  "upload_modal.description_placeholder": "Franz jagt im komplett verwahrlosten Taxi quer durch Bayern",
+  "upload_modal.detect_text": "Text aus Bild erkennen",
+  "upload_modal.edit_media": "Medien bearbeiten",
+  "upload_modal.hint": "Klicke oder ziehe den Kreis auf die Vorschau, um den Brennpunkt auszuwählen, der immer auf allen Vorschaubilder angezeigt wird.",
+  "upload_modal.preview_label": "Vorschau ({ratio})",
   "upload_progress.label": "Wird hochgeladen …",
   "video.close": "Video schließen",
   "video.exit_fullscreen": "Vollbild verlassen",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 8c8c89115..246c9bd0e 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -74,6 +74,15 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "{count} per week",
+        "id": "autosuggest_hashtag.per_week"
+      }
+    ],
+    "path": "app/javascript/mastodon/components/autosuggest_hashtag.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Back",
         "id": "column_back_button.label"
       }
@@ -578,6 +587,10 @@
         "id": "account.follow"
       },
       {
+        "defaultMessage": "Cancel follow request",
+        "id": "account.cancel_follow_request"
+      },
+      {
         "defaultMessage": "Awaiting approval. Click to cancel follow request",
         "id": "account.requested"
       },
@@ -804,6 +817,10 @@
       {
         "defaultMessage": "Muted words",
         "id": "navigation_bar.filters"
+      },
+      {
+        "defaultMessage": "Logout",
+        "id": "navigation_bar.logout"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/components/action_bar.json"
@@ -1073,6 +1090,15 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Uploading…",
+        "id": "upload_progress.label"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/compose/components/upload_form.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Uploading...",
         "id": "upload_progress.label"
       }
@@ -1082,16 +1108,12 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Describe for the visually impaired",
-        "id": "upload_form.description"
-      },
-      {
         "defaultMessage": "Delete",
         "id": "upload_form.undo"
       },
       {
-        "defaultMessage": "Crop",
-        "id": "upload_form.focus"
+        "defaultMessage": "Edit",
+        "id": "upload_form.edit"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/components/upload.json"
@@ -1295,6 +1317,15 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Refresh",
+        "id": "trends.refresh"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/getting_started/components/trends.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Home",
         "id": "tabs_bar.home"
       },
@@ -1873,6 +1904,10 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "and {count, plural, one {# other} other {# others}}",
+        "id": "notification.and_n_others"
+      },
+      {
         "defaultMessage": "{name} followed you",
         "id": "notification.follow"
       },
@@ -2237,6 +2272,47 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Close",
+        "id": "lightbox.close"
+      },
+      {
+        "defaultMessage": "Apply",
+        "id": "upload_modal.apply"
+      },
+      {
+        "defaultMessage": "A quick brown fox jumps over the lazy dog",
+        "id": "upload_modal.description_placeholder"
+      },
+      {
+        "defaultMessage": "Edit media",
+        "id": "upload_modal.edit_media"
+      },
+      {
+        "defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+        "id": "upload_modal.hint"
+      },
+      {
+        "defaultMessage": "Describe for the visually impaired",
+        "id": "upload_form.description"
+      },
+      {
+        "defaultMessage": "Analyzing picture…",
+        "id": "upload_modal.analyzing_picture"
+      },
+      {
+        "defaultMessage": "Detect text from picture",
+        "id": "upload_modal.detect_text"
+      },
+      {
+        "defaultMessage": "Preview ({ratio})",
+        "id": "upload_modal.preview_label"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/focal_point_modal.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Follow requests",
         "id": "navigation_bar.follow_requests"
       }
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index df85c025f..68c59817f 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -4,6 +4,7 @@
   "account.block": "Αποκλισμός @{name}",
   "account.block_domain": "Απόκρυψε τα πάντα από το {domain}",
   "account.blocked": "Αποκλεισμένος/η",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Προσωπικό μήνυμα προς @{name}",
   "account.domain_blocked": "Κρυμμένος τομέας",
   "account.edit_profile": "Επεξεργασία προφίλ",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Διακοπή αποσιώπησης ειδοποιήσεων του/της @{name}",
   "alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.",
   "alert.unexpected.title": "Εεπ!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Μπορείς να πατήσεις {combo} για να το προσπεράσεις αυτό την επόμενη φορά",
   "bundle_column_error.body": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.",
   "bundle_column_error.retry": "Δοκίμασε ξανά",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Βασικά",
   "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων",
   "home.column_settings.show_replies": "Εμφάνιση απαντήσεων",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Ζωντανή ενημέρωση",
   "intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}",
   "intervals.full.hours": "{number, plural, one {# ώρα} other {# ώρες}}",
   "intervals.full.minutes": "{number, plural, one {# λεπτό} other {# λεπτά}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Τίτλος νέας λίστα",
   "lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς",
   "lists.subheading": "Οι λίστες σου",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# νέο} other {# νέα}}",
   "loading_indicator.label": "Φορτώνει...",
   "media_gallery.toggle_visible": "Εναλλαγή ορατότητας",
   "missing_indicator.label": "Δε βρέθηκε",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Κατάλογος λογαριασμών",
   "navigation_bar.public_timeline": "Ομοσπονδιακή ροή",
   "navigation_bar.security": "Ασφάλεια",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "Ο/Η {name} σημείωσε ως αγαπημένη την κατάστασή σου",
   "notification.follow": "Ο/Η {name} σε ακολούθησε",
   "notification.mention": "Ο/Η {name} σε ανέφερε",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Άνθρωποι",
   "search_results.hashtags": "Ταμπέλες",
   "search_results.statuses": "Τουτ",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Η αναζήτηση τουτ βάσει του περιεχόμενού τους δεν είναι ενεργοποιημένη σε αυτό τον κόμβο.",
   "search_results.total": "{count, number} {count, plural, zero {αποτελέσματα} one {αποτέλεσμα} other {αποτελέσματα}}",
   "status.admin_account": "Άνοιγμα λειτουργίας διαμεσολάβησης για τον/την @{name}",
   "status.admin_status": "Άνοιγμα αυτής της δημοσίευσης στη λειτουργία διαμεσολάβησης",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Απομένουν στιγμές",
   "time_remaining.seconds": "απομένουν {number, plural, one {# δευτερόλεπτο} other {# δευτερόλεπτα}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} μιλάνε",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Το προσχέδιό σου θα χαθεί αν φύγεις από το Mastodon.",
   "upload_area.title": "Drag & drop για να ανεβάσεις",
   "upload_button.label": "Πρόσθεσε πολυμέσα (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Υπέρβαση ορίου μεγέθους ανεβασμένων αρχείων.",
   "upload_error.poll": "Στις δημοσκοπήσεις δεν επιτρέπεται η μεταφόρτωση αρχείου.",
   "upload_form.description": "Περιέγραψε για όσους & όσες έχουν προβλήματα όρασης",
-  "upload_form.focus": "Αλλαγή προεπισκόπησης",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Διαγραφή",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Ανεβαίνει...",
   "video.close": "Κλείσε το βίντεο",
   "video.exit_fullscreen": "Έξοδος από πλήρη οθόνη",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 7bed98530..628ede3e3 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -4,6 +4,7 @@
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edit profile",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Unmute notifications from @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "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",
@@ -256,6 +258,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federated timeline",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} followed you",
   "notification.mention": "{name} mentioned you",
@@ -375,14 +378,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media ({formats})",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Change preview",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Delete",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploading...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index ddc694252..763c31bb8 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -4,6 +4,7 @@
   "account.block": "Bloki @{name}",
   "account.block_domain": "Kaŝi ĉion de {domain}",
   "account.blocked": "Blokita",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Rekte mesaĝi @{name}",
   "account.domain_blocked": "Domajno kaŝita",
   "account.edit_profile": "Redakti profilon",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Malsilentigi sciigojn de @{name}",
   "alert.unexpected.message": "Neatendita eraro okazis.",
   "alert.unexpected.title": "Ups!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje",
   "bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.",
   "bundle_column_error.retry": "Bonvolu reprovi",
@@ -72,12 +74,12 @@
   "compose_form.lock_disclaimer.lock": "ŝlosita",
   "compose_form.placeholder": "Pri kio vi pensas?",
   "compose_form.poll.add_option": "Aldoni elekto",
-  "compose_form.poll.duration": "Balotenketo daŭro",
+  "compose_form.poll.duration": "Balotenketa daŭro",
   "compose_form.poll.option_placeholder": "elekto {number}",
   "compose_form.poll.remove_option": "Forigi ĉi tiu elekton",
   "compose_form.publish": "Hup",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.hide": "Marki aŭdovidaĵojn kiel tiklaj",
+  "compose_form.sensitive.hide": "Marki la aŭdovidaĵojn kiel tiklaj",
   "compose_form.sensitive.marked": "Aŭdovidaĵo markita tikla",
   "compose_form.sensitive.unmarked": "Aŭdovidaĵo ne markita tikla",
   "compose_form.spoiler.marked": "Teksto kaŝita malantaŭ averto",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Bazaj agordoj",
   "home.column_settings.show_reblogs": "Montri diskonigojn",
   "home.column_settings.show_replies": "Montri respondojn",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Ĝisdatigo en realtempa",
   "intervals.full.days": "{number, plural, one {# tago} other {# tagoj}}",
   "intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Titolo de la nova listo",
   "lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
   "lists.subheading": "Viaj listoj",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count,plural, one {# nova ero} other {# novaj eroj}}",
   "loading_indicator.label": "Ŝargado…",
   "media_gallery.toggle_visible": "Baskuligi videblecon",
   "missing_indicator.label": "Ne trovita",
@@ -251,10 +253,11 @@
   "navigation_bar.profile_directory": "Profilujo",
   "navigation_bar.public_timeline": "Fratara tempolinio",
   "navigation_bar.security": "Sekureco",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} stelumis vian mesaĝon",
   "notification.follow": "{name} eksekvis vin",
   "notification.mention": "{name} menciis vin",
-  "notification.poll": "Balotenketo ke vi balotis estas finita",
+  "notification.poll": "Partoprenita balotenketo finiĝis",
   "notification.reblog": "{name} diskonigis vian mesaĝon",
   "notifications.clear": "Forviŝi sciigojn",
   "notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?",
@@ -265,7 +268,7 @@
   "notifications.column_settings.filter_bar.show": "Montri",
   "notifications.column_settings.follow": "Novaj sekvantoj:",
   "notifications.column_settings.mention": "Mencioj:",
-  "notifications.column_settings.poll": "Balotenketo rezulto:",
+  "notifications.column_settings.poll": "Balotenketaj rezultoj:",
   "notifications.column_settings.push": "Puŝsciigoj",
   "notifications.column_settings.reblog": "Diskonigoj:",
   "notifications.column_settings.show": "Montri en kolumno",
@@ -275,7 +278,7 @@
   "notifications.filter.favourites": "Stelumoj",
   "notifications.filter.follows": "Sekvoj",
   "notifications.filter.mentions": "Mencioj",
-  "notifications.filter.polls": "Balotenketoj rezultoj",
+  "notifications.filter.polls": "Balotenketaj rezultoj",
   "notifications.group": "{count} sciigoj",
   "poll.closed": "Finita",
   "poll.refresh": "Aktualigi",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Homoj",
   "search_results.hashtags": "Kradvortoj",
   "search_results.statuses": "Mesaĝoj",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Serĉi mesaĝojn laŭ enhavo ne estas ebligita en ĉi tiu Mastodon-servilo.",
   "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}",
   "status.admin_account": "Malfermi la kontrolan interfacon por @{name}",
   "status.admin_status": "Malfermi ĉi tiun mesaĝon en la kontrola interfaco",
@@ -364,20 +367,28 @@
   "tabs_bar.local_timeline": "Loka tempolinio",
   "tabs_bar.notifications": "Sciigoj",
   "tabs_bar.search": "Serĉi",
-  "time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restanta",
-  "time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restanta",
-  "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restanta",
-  "time_remaining.moments": "Momento restanta",
-  "time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundoj}} restanta",
+  "time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restas",
+  "time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restas",
+  "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restas",
+  "time_remaining.moments": "Momenteto restas",
+  "time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundoj}} restas",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persono} other {personoj}} parolas",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.",
   "upload_area.title": "Altreni kaj lasi por alŝuti",
   "upload_button.label": "Aldoni aŭdovidaĵon (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Limo de dosiera alŝutado transpasita.",
-  "upload_error.poll": "Alŝuto de dosiero ne permisita kun balotenketo",
+  "upload_error.poll": "Alŝuto de dosiero ne permesita kun balotenketo.",
   "upload_form.description": "Priskribi por misvidantaj homoj",
-  "upload_form.focus": "Antaŭvido de ŝanĝo",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Forigi",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Alŝutado…",
   "video.close": "Fermi videon",
   "video.exit_fullscreen": "Eksigi plenekrana",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index dc42bc7ef..415faf509 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -1,21 +1,22 @@
 {
-  "account.add_or_remove_from_list": "Agregar o eliminar de las listas",
+  "account.add_or_remove_from_list": "Agregar o eliminar de listas",
   "account.badges.bot": "Bot",
-  "account.block": "Bloquear",
+  "account.block": "Bloquear a @{name}",
   "account.block_domain": "Ocultar todo de {domain}",
   "account.blocked": "Bloqueado",
-  "account.direct": "Direct Message @{name}",
+  "account.cancel_follow_request": "Cancel follow request",
+  "account.direct": "Mensaje directo a @{name}",
   "account.domain_blocked": "Dominio oculto",
   "account.edit_profile": "Editar perfil",
   "account.endorse": "Mostrar en perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
-  "account.followers.empty": "Nadie sigue a este usuario todavía.",
+  "account.followers.empty": "Todavía nadie sigue a este usuario.",
   "account.follows": "Sigue",
   "account.follows.empty": "Este usuario todavía no sigue a nadie.",
   "account.follows_you": "Te sigue",
   "account.hide_reblogs": "Ocultar retoots de @{name}",
-  "account.link_verified_on": "El proprietario de este link fue verificado el {date}",
+  "account.link_verified_on": "El proprietario de este link fue comprobado el {date}",
   "account.locked_info": "El estado de privacidad de esta cuenta està configurado como bloqueado. El proprietario debe revisar manualmente quien puede seguirle.",
   "account.media": "Multimedia",
   "account.mention": "Mencionar a @{name}",
@@ -37,7 +38,8 @@
   "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
   "alert.unexpected.message": "Hubo un error inesperado.",
   "alert.unexpected.title": "¡Ups!",
-  "boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
+  "autosuggest_hashtag.per_week": "{count} per week",
+  "boost_modal.combo": "Puedes hacer clic en {combo} para saltar este aviso la próxima vez",
   "bundle_column_error.body": "Algo salió mal al cargar este componente.",
   "bundle_column_error.retry": "Inténtalo de nuevo",
   "bundle_column_error.title": "Error de red",
@@ -47,17 +49,17 @@
   "column.blocks": "Usuarios bloqueados",
   "column.community": "Línea de tiempo local",
   "column.direct": "Mensajes directos",
-  "column.domain_blocks": "Dominios ocultos",
+  "column.domain_blocks": "Dominios ocultados",
   "column.favourites": "Favoritos",
   "column.follow_requests": "Solicitudes de seguimiento",
   "column.home": "Inicio",
   "column.lists": "Listas",
   "column.mutes": "Usuarios silenciados",
   "column.notifications": "Notificaciones",
-  "column.pins": "Toot fijado",
-  "column.public": "Historia federada",
+  "column.pins": "Toots fijados",
+  "column.public": "Línea de tiempo federada",
   "column_back_button.label": "Atrás",
-  "column_header.hide_settings": "Ocultar ajustes",
+  "column_header.hide_settings": "Ocultar configuración",
   "column_header.moveLeft_settings": "Mover columna a la izquierda",
   "column_header.moveRight_settings": "Mover columna a la derecha",
   "column_header.pin": "Fijar",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Básico",
   "home.column_settings.show_reblogs": "Mostrar retoots",
   "home.column_settings.show_replies": "Mostrar respuestas",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Actualizar en tiempo real",
   "intervals.full.days": "{number, plural, one {# día} other {# días}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Título de la nueva lista",
   "lists.search": "Buscar entre la gente a la que sigues",
   "lists.subheading": "Tus listas",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
   "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "Cambiar visibilidad",
   "missing_indicator.label": "No encontrado",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Directorio de perfiles",
   "navigation_bar.public_timeline": "Historia federada",
   "navigation_bar.security": "Seguridad",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} marcó tu estado como favorito",
   "notification.follow": "{name} te empezó a seguir",
   "notification.mention": "{name} te ha mencionado",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Gente",
   "search_results.hashtags": "Etiquetas",
   "search_results.statuses": "Toots",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Buscar toots por su contenido no está disponible en este servidor de Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "status.admin_account": "Abrir interfaz de moderación para @{name}",
   "status.admin_status": "Abrir este estado en la interfaz de moderación",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Momentos restantes",
   "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
   "upload_area.title": "Arrastra y suelta para subir",
   "upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Límite de subida de archivos excedido.",
   "upload_error.poll": "Subida de archivos no permitida con encuestas.",
   "upload_form.description": "Describir para los usuarios con dificultad visual",
-  "upload_form.focus": "Recortar",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Borrar",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Subiendo…",
   "video.close": "Cerrar video",
   "video.exit_fullscreen": "Salir de pantalla completa",
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
new file mode 100644
index 000000000..0d708eac2
--- /dev/null
+++ b/app/javascript/mastodon/locales/et.json
@@ -0,0 +1,402 @@
+{
+  "account.add_or_remove_from_list": "Lisa või Eemalda nimekirjadest",
+  "account.badges.bot": "Robot",
+  "account.block": "Blokeeri @{name}",
+  "account.block_domain": "Peida kõik domeenist {domain}",
+  "account.blocked": "Blokeeritud",
+  "account.cancel_follow_request": "Cancel follow request",
+  "account.direct": "Otsesõnum @{name}",
+  "account.domain_blocked": "Domeen peidetud",
+  "account.edit_profile": "Muuda profiili",
+  "account.endorse": "Too profiilil esile",
+  "account.follow": "Jälgi",
+  "account.followers": "Jälgijad",
+  "account.followers.empty": "Keegi ei jälgi seda kasutajat veel.",
+  "account.follows": "Jälgib",
+  "account.follows.empty": "See kasutaja ei jälgi veel kedagi.",
+  "account.follows_you": "Jälgib sind",
+  "account.hide_reblogs": "Peida upitused kasutajalt @{name}",
+  "account.link_verified_on": "Selle lingi autorsust kontrolliti {date}",
+  "account.locked_info": "Selle konto privaatsus on lukustatud. Omanik vaatab manuaalselt üle, kes teda jägida saab.",
+  "account.media": "Meedia",
+  "account.mention": "Maini @{name}",
+  "account.moved_to": "{name} on kolinud:",
+  "account.mute": "Vaigista @{name}",
+  "account.mute_notifications": "Vaigista teated kasutajalt @{name}",
+  "account.muted": "Vaigistatud",
+  "account.posts": "Tuututused",
+  "account.posts_with_replies": "Tuututused ja vastused",
+  "account.report": "Raporteeri @{name}",
+  "account.requested": "Ootab kinnitust. Klõpsa jälgimise soovi tühistamiseks",
+  "account.share": "Jaga @{name} profiili",
+  "account.show_reblogs": "Näita kasutaja @{name} upitusi",
+  "account.unblock": "Eemalda blokeering @{name}",
+  "account.unblock_domain": "Tee {domain} nähtavaks",
+  "account.unendorse": "Ära kuva profiilil",
+  "account.unfollow": "Ära jälgi",
+  "account.unmute": "Ära vaigista @{name}",
+  "account.unmute_notifications": "Ära vaigista teateid kasutajalt @{name}",
+  "alert.unexpected.message": "Tekkis ootamatu viga.",
+  "alert.unexpected.title": "Oih!",
+  "autosuggest_hashtag.per_week": "{count} per week",
+  "boost_modal.combo": "Saad vajutada {combo}, et see järgmine kord vahele jätta",
+  "bundle_column_error.body": "Mindagi läks valesti selle komponendi laadimisel.",
+  "bundle_column_error.retry": "Proovi uuesti",
+  "bundle_column_error.title": "Võrgu viga",
+  "bundle_modal_error.close": "Sulge",
+  "bundle_modal_error.message": "Selle komponendi laadimisel läks midagi viltu.",
+  "bundle_modal_error.retry": "Proovi uuesti",
+  "column.blocks": "Blokeeritud kasutajad",
+  "column.community": "Kohalik ajajoon",
+  "column.direct": "Otsesõnumid",
+  "column.domain_blocks": "Peidetud domeenid",
+  "column.favourites": "Lemmikud",
+  "column.follow_requests": "Jälgimistaotlused",
+  "column.home": "Kodu",
+  "column.lists": "Nimekirjad",
+  "column.mutes": "Vaigistatud kasutajad",
+  "column.notifications": "Teated",
+  "column.pins": "Kinnitatud upitused",
+  "column.public": "Föderatiivne ajajoon",
+  "column_back_button.label": "Tagasi",
+  "column_header.hide_settings": "Peida sätted",
+  "column_header.moveLeft_settings": "Liiguta tulp vasakule",
+  "column_header.moveRight_settings": "Liiguta tulp paremale",
+  "column_header.pin": "Kinnita",
+  "column_header.show_settings": "Näita sätteid",
+  "column_header.unpin": "Eemalda kinnitus",
+  "column_subheading.settings": "Sätted",
+  "community.column_settings.media_only": "Ainult meedia",
+  "compose_form.direct_message_warning": "See tuut saadetakse ainult mainitud kasutajatele.",
+  "compose_form.direct_message_warning_learn_more": "Vaata veel",
+  "compose_form.hashtag_warning": "Seda tuuti ei kuvata ühegi sildi all, sest see on kirjendamata. Ainult avalikud tuutid on sildi järgi otsitavad.",
+  "compose_form.lock_disclaimer": "Sinu konto ei ole {locked}. Igaüks saab sind jälgida ja näha su ainult-jälgijatele postitusi.",
+  "compose_form.lock_disclaimer.lock": "lukus",
+  "compose_form.placeholder": "Millest mõtled?",
+  "compose_form.poll.add_option": "Lisa valik",
+  "compose_form.poll.duration": "Küsitluse kestus",
+  "compose_form.poll.option_placeholder": "Valik {number}",
+  "compose_form.poll.remove_option": "Eemalda see valik",
+  "compose_form.publish": "Tuut",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive.hide": "Märgista meedia tundlikuks",
+  "compose_form.sensitive.marked": "Meedia on sensitiivseks märgitud",
+  "compose_form.sensitive.unmarked": "Meedia ei ole sensitiivseks märgitud",
+  "compose_form.spoiler.marked": "Tekst on hoiatuse taha peidetud",
+  "compose_form.spoiler.unmarked": "Tekst ei ole peidetud",
+  "compose_form.spoiler_placeholder": "Kirjuta oma hoiatus siia",
+  "confirmation_modal.cancel": "Katkesta",
+  "confirmations.block.block_and_report": "Blokeeri & Teata",
+  "confirmations.block.confirm": "Blokeeri",
+  "confirmations.block.message": "Oled kindel, et soovid blokkida {name}?",
+  "confirmations.delete.confirm": "Kustuta",
+  "confirmations.delete.message": "Oled kindel, et soovid selle staatuse kustutada?",
+  "confirmations.delete_list.confirm": "Kustuta",
+  "confirmations.delete_list.message": "Oled kindel, et soovid selle nimekirja püsivalt kustutada?",
+  "confirmations.domain_block.confirm": "Peida terve domeen",
+  "confirmations.domain_block.message": "Oled ikka päris kindel, et soovid blokeerida terve  {domain}? Enamikul juhtudel piisab mõnest sihitud blokist või vaigistusest, mis on eelistatav. Sa ei näe selle domeeni sisu üheski avalikus ajajoones või teadetes. Sinu jälgijad sellest domeenist eemaldatakse.",
+  "confirmations.mute.confirm": "Vaigista",
+  "confirmations.mute.message": "Oled kindel, et soovid {name} vaigistada?",
+  "confirmations.redraft.confirm": "Kustuta & taasalusta",
+  "confirmations.redraft.message": "Oled kindel, et soovid selle staatuse kustutada ja alustada uuesti? Lemmikud ja upitused lähevad kaotsi ja vastused originaaalpostitusele jäävad orvuks.",
+  "confirmations.reply.confirm": "Vasta",
+  "confirmations.reply.message": "Kohene vastamine kirjutab üle sõnumi, mida hetkel koostad. Oled kindel, et soovid jätkata?",
+  "confirmations.unfollow.confirm": "Ära jälgi",
+  "confirmations.unfollow.message": "Oled kindel, et ei soovi jälgida {name}?",
+  "embed.instructions": "Manusta see staatus oma veebilehele, kopeerides alloleva koodi.",
+  "embed.preview": "Nii näeb see välja:",
+  "emoji_button.activity": "Tegevus",
+  "emoji_button.custom": "Mugandatud",
+  "emoji_button.flags": "Lipud",
+  "emoji_button.food": "Toit & Jook",
+  "emoji_button.label": "Sisesta emoji",
+  "emoji_button.nature": "Loodus",
+  "emoji_button.not_found": "Ei ole emojosi!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objektid",
+  "emoji_button.people": "Inimesed",
+  "emoji_button.recent": "Tihti kasutatud",
+  "emoji_button.search": "Otsi...",
+  "emoji_button.search_results": "Otsitulemused",
+  "emoji_button.symbols": "Sümbolid",
+  "emoji_button.travel": "Reisimine & Kohad",
+  "empty_column.account_timeline": "Siin tuute ei ole!",
+  "empty_column.account_unavailable": "Profiil pole saadaval",
+  "empty_column.blocks": "Sa ei ole veel ühtegi kasutajat blokeerinud.",
+  "empty_column.community": "Kohalik ajajoon on tühi. Kirjuta midagi avalikult, et pall veerema saada!",
+  "empty_column.direct": "Sul ei veel otsesõnumeid. Kui saadad või võtad mõne vastu, ilmuvad nad siia.",
+  "empty_column.domain_blocks": "Siin ei ole veel peidetud domeene.",
+  "empty_column.favourited_statuses": "Sul pole veel lemmikuid tuute. Kui märgid mõne, näed neid siin.",
+  "empty_column.favourites": "Keegi pole veel seda tuuti lemmikuks märkinud. Kui seegi seda teeb, näed seda siin.",
+  "empty_column.follow_requests": "Sul pole veel ühtegi jälgimise taotlust. Kui saad mõne, näed seda siin.",
+  "empty_column.hashtag": "Selle sildiga pole veel midagi.",
+  "empty_column.home": "Sinu kodu ajajoon on tühi! Külasta {public} või kasuta otsingut alustamaks ja kohtamaks teisi kasutajaid.",
+  "empty_column.home.public_timeline": "avalik ajajoon",
+  "empty_column.list": "Siin nimstus pole veel midagi. Kui selle nimistu liikmed postitavad uusi staatusi, näed neid siin.",
+  "empty_column.lists": "Sul ei ole veel ühtegi nimekirja. Kui lood mõne, näed seda siin.",
+  "empty_column.mutes": "Sa pole veel ühtegi kasutajat vaigistanud.",
+  "empty_column.notifications": "Sul ei ole veel teateid. Suhtle teistega alustamaks vestlust.",
+  "empty_column.public": "Siin pole midagi! Kirjuta midagi avalikut või jälgi ise kasutajaid täitmaks seda ruumi",
+  "follow_request.authorize": "Autoriseeri",
+  "follow_request.reject": "Hülga",
+  "getting_started.developers": "Arendajad",
+  "getting_started.directory": "Profiili kataloog",
+  "getting_started.documentation": "Dokumentatsioon",
+  "getting_started.heading": "Alustamine",
+  "getting_started.invite": "Kutsu inimesi",
+  "getting_started.open_source_notice": "Mastodon on avatud lähtekoodiga tarkvara. Saad panustada või teatada probleemidest GitHubis {github}.",
+  "getting_started.security": "Turvalisus",
+  "getting_started.terms": "Kasutustingimused",
+  "hashtag.column_header.tag_mode.all": "ja {additional}",
+  "hashtag.column_header.tag_mode.any": "või {additional}",
+  "hashtag.column_header.tag_mode.none": "ilma {additional}",
+  "hashtag.column_settings.select.no_options_message": "Soovitusi ei leitud",
+  "hashtag.column_settings.select.placeholder": "Sisesta sildid…",
+  "hashtag.column_settings.tag_mode.all": "Kõik need",
+  "hashtag.column_settings.tag_mode.any": "Mõni neist",
+  "hashtag.column_settings.tag_mode.none": "Mitte ükski neist",
+  "hashtag.column_settings.tag_toggle": "Kaasa lisamärked selle tulba jaoks",
+  "home.column_settings.basic": "Peamine",
+  "home.column_settings.show_reblogs": "Näita upitusi",
+  "home.column_settings.show_replies": "Näita vastuseid",
+  "home.column_settings.update_live": "Uuenda reaalajas",
+  "intervals.full.days": "{number, plural, one {# päev} other {# päevad}}",
+  "intervals.full.hours": "{number, plural, one {# tund} other {# tundi}}",
+  "intervals.full.minutes": "{number, plural, one {# minut} other {# minutit}}",
+  "introduction.federation.action": "Järgmine",
+  "introduction.federation.federated.headline": "Föderatiivne",
+  "introduction.federation.federated.text": "Avalikud postitused teistest föderatsiooni serveritest kuvatakse föderatiivsel ajajoonel.",
+  "introduction.federation.home.headline": "Kodu",
+  "introduction.federation.home.text": "Inimest postitused keda jälgid kuvatakse sinu koduajajoonel. Saad jälgida igaüht igas serveris!",
+  "introduction.federation.local.headline": "Kohalik",
+  "introduction.federation.local.text": "Samas serveris olevate inimeste postitused kuvatakse kohalikul ajajoonel.",
+  "introduction.interactions.action": "Välju õpetusest!",
+  "introduction.interactions.favourite.headline": "Lemmik",
+  "introduction.interactions.favourite.text": "Saad tuuti salvestada ja anda autorile teada, et meeldis märkides selle lemmikuks.",
+  "introduction.interactions.reblog.headline": "Upita",
+  "introduction.interactions.reblog.text": "Saad jagada teiste inimeste tuute oma jälgijatega upitades neid.",
+  "introduction.interactions.reply.headline": "Vasta",
+  "introduction.interactions.reply.text": "Saad vastata teiste ja enda tuutidele, mis ühendab nad kokku aruteluks.",
+  "introduction.welcome.action": "Lähme!",
+  "introduction.welcome.headline": "Esimesed sammud",
+  "introduction.welcome.text": "Teretulemast fediversumisse! Mõne aja pärast saad avaldada sõnumeid ja rääkida oma sõpradega läbi laia valiku serverite. Aga see server, {domain}, on eriline—ta majutab sinu profiili. Seega jäta ta nimi meelde.",
+  "keyboard_shortcuts.back": "tagasiminekuks",
+  "keyboard_shortcuts.blocked": "avamaks blokeeritud kasutajate nimistut",
+  "keyboard_shortcuts.boost": "upitamiseks",
+  "keyboard_shortcuts.column": "fokuseerimaks staatust ühele tulpadest",
+  "keyboard_shortcuts.compose": "fokuseerimaks tekstikoostamise alale",
+  "keyboard_shortcuts.description": "Kirjeldus",
+  "keyboard_shortcuts.direct": "avamaks otsesõnumite tulpa",
+  "keyboard_shortcuts.down": "liikumaks nimstus alla",
+  "keyboard_shortcuts.enter": "staatuse avamiseks",
+  "keyboard_shortcuts.favourite": "lemmikuks märkimiseks",
+  "keyboard_shortcuts.favourites": "avamaks lemmikute nimistut",
+  "keyboard_shortcuts.federated": "avamaks föderatsiooni ajajoont",
+  "keyboard_shortcuts.heading": "Klaviatuuri kiirkäsud",
+  "keyboard_shortcuts.home": "avamaks kodu ajajoont",
+  "keyboard_shortcuts.hotkey": "Kiirklahv",
+  "keyboard_shortcuts.legend": "selle legendi kuvamiseks",
+  "keyboard_shortcuts.local": "avamaks kohalikku ajajoont",
+  "keyboard_shortcuts.mention": "autori mainimiseks",
+  "keyboard_shortcuts.muted": "avamaks vaigistatud kasutajate nimistut",
+  "keyboard_shortcuts.my_profile": "avamaks profiili",
+  "keyboard_shortcuts.notifications": "avamaks teadete tulpa",
+  "keyboard_shortcuts.pinned": "avamaks kinnitatud tuutide nimistut",
+  "keyboard_shortcuts.profile": "avamaks autori profiili",
+  "keyboard_shortcuts.reply": "vastamiseks",
+  "keyboard_shortcuts.requests": "avamaks jälgimistaotluste nimistut",
+  "keyboard_shortcuts.search": "otsingu fokuseerimiseks",
+  "keyboard_shortcuts.start": "avamaks \"Alusta\" tulpa",
+  "keyboard_shortcuts.toggle_hidden": "näitamaks/peitmaks teksti CW taga",
+  "keyboard_shortcuts.toggle_sensitivity": "et peita/näidata meediat",
+  "keyboard_shortcuts.toot": "alustamaks täiesti uut tuuti",
+  "keyboard_shortcuts.unfocus": "tekstiala/otsingu koostamise mittefokuseerimiseks",
+  "keyboard_shortcuts.up": "liikumaks nimistus üles",
+  "lightbox.close": "Sulge",
+  "lightbox.next": "Järgmine",
+  "lightbox.previous": "Eelmine",
+  "lightbox.view_context": "Vaata konteksti",
+  "lists.account.add": "Lisa nimistusse",
+  "lists.account.remove": "Eemalda nimistust",
+  "lists.delete": "Kustuta nimistu",
+  "lists.edit": "Muuda nimistut",
+  "lists.edit.submit": "Muuda pealkiri",
+  "lists.new.create": "Lisa nimistu",
+  "lists.new.title_placeholder": "Uus nimistu pealkiri",
+  "lists.search": "Otsi sinu poolt jälgitavate inimese hulgast",
+  "lists.subheading": "Sinu nimistud",
+  "load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
+  "loading_indicator.label": "Laeb..",
+  "media_gallery.toggle_visible": "Lülita nähtavus",
+  "missing_indicator.label": "Ei leitud",
+  "missing_indicator.sublabel": "Seda ressurssi ei leitud",
+  "mute_modal.hide_notifications": "Kas peita teated sellelt kasutajalt?",
+  "navigation_bar.apps": "Mobiilrakendused",
+  "navigation_bar.blocks": "Blokeeritud kasutajad",
+  "navigation_bar.community_timeline": "Kohalik ajajoon",
+  "navigation_bar.compose": "Koosta uus tuut",
+  "navigation_bar.direct": "Otsesõnumid",
+  "navigation_bar.discover": "Avasta",
+  "navigation_bar.domain_blocks": "Peidetud domeenid",
+  "navigation_bar.edit_profile": "Muuda profiili",
+  "navigation_bar.favourites": "Lemmikud",
+  "navigation_bar.filters": "Vaigistatud sõnad",
+  "navigation_bar.follow_requests": "Jälgimistaotlused",
+  "navigation_bar.follows_and_followers": "Jälgitud ja jälgijad",
+  "navigation_bar.info": "Selle serveri kohta",
+  "navigation_bar.keyboard_shortcuts": "Kiirklahvid",
+  "navigation_bar.lists": "Nimistud",
+  "navigation_bar.logout": "Logi välja",
+  "navigation_bar.mutes": "Vaigistatud kasutajad",
+  "navigation_bar.personal": "Isiklik",
+  "navigation_bar.pins": "Kinnitatud tuutid",
+  "navigation_bar.preferences": "Eelistused",
+  "navigation_bar.profile_directory": "Profiilikataloog",
+  "navigation_bar.public_timeline": "Föderatiivne ajajoon",
+  "navigation_bar.security": "Turvalisus",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+  "notification.favourite": "{name} märkis su staatuse lemmikuks",
+  "notification.follow": "{name} jälgib sind",
+  "notification.mention": "{name} mainis sind",
+  "notification.poll": "Küsitlus, milles osalesid, on lõppenud",
+  "notification.reblog": "{name} upitas su staatust",
+  "notifications.clear": "Puhasta teated",
+  "notifications.clear_confirmation": "Oled kindel, et soovid püsivalt kõik oma teated puhastada?",
+  "notifications.column_settings.alert": "Töölauateated",
+  "notifications.column_settings.favourite": "Lemmikud:",
+  "notifications.column_settings.filter_bar.advanced": "Kuva kõik kategooriad",
+  "notifications.column_settings.filter_bar.category": "Kiirfiltri riba",
+  "notifications.column_settings.filter_bar.show": "Kuva",
+  "notifications.column_settings.follow": "Uued jälgijad:",
+  "notifications.column_settings.mention": "Mainimised:",
+  "notifications.column_settings.poll": "Küsitluse tulemused:",
+  "notifications.column_settings.push": "Push teated",
+  "notifications.column_settings.reblog": "Upitused:",
+  "notifications.column_settings.show": "Kuva tulbas",
+  "notifications.column_settings.sound": "Mängi heli",
+  "notifications.filter.all": "Kõik",
+  "notifications.filter.boosts": "Upitused",
+  "notifications.filter.favourites": "Lemmikud",
+  "notifications.filter.follows": "Jälgib",
+  "notifications.filter.mentions": "Mainimised",
+  "notifications.filter.polls": "Küsitluse tulemused",
+  "notifications.group": "{count} teated",
+  "poll.closed": "Suletud",
+  "poll.refresh": "Värskenda",
+  "poll.total_votes": "{count, plural, one {# hääl} other {# hääli}}",
+  "poll.vote": "Hääleta",
+  "poll_button.add_poll": "Lisa küsitlus",
+  "poll_button.remove_poll": "Eemalda küsitlus",
+  "privacy.change": "Muuda staatuse privaatsust",
+  "privacy.direct.long": "Postita ainult mainitud kasutajatele",
+  "privacy.direct.short": "Otsene",
+  "privacy.private.long": "Postita ainult jälgijatele",
+  "privacy.private.short": "Ainult jälgijatele",
+  "privacy.public.long": "Postita avalikele ajajoontele",
+  "privacy.public.short": "Avalik",
+  "privacy.unlisted.long": "Ära postita avalikele ajajoontele",
+  "privacy.unlisted.short": "Määramata",
+  "regeneration_indicator.label": "Laeb…",
+  "regeneration_indicator.sublabel": "Sinu kodu voog on ettevalmistamisel!",
+  "relative_time.days": "{number}p",
+  "relative_time.hours": "{number}t",
+  "relative_time.just_now": "nüüd",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Tühista",
+  "report.forward": "Edasta kasutajale {target}",
+  "report.forward_hint": "See kasutaja on teisest serverist. Kas saadan anonümiseeritud koopia sellest teatest sinna ka?",
+  "report.hint": "See teade saadetakse sinu serveri moderaatoritele. Te saate lisada selgituse selle kohta, miks selle kasutaja kohta teate esitasite, siin:",
+  "report.placeholder": "Lisaks kommentaarid",
+  "report.submit": "Saada",
+  "report.target": "Teatamine {target} kohta",
+  "search.placeholder": "Otsi",
+  "search_popout.search_format": "Täiustatud otsiformaat",
+  "search_popout.tips.full_text": "Lihtne tekst toob esile staatused mida olete kirjutanud, lisanud lemmikuks, upitanud või olete seal mainitud, ning lisaks veel kattuvad kasutajanimed, kuvanimed ja sildid.",
+  "search_popout.tips.hashtag": "silt",
+  "search_popout.tips.status": "staatus",
+  "search_popout.tips.text": "Lihtne tekst toob esile kattuvad kuvanimed, kasutajanimed ning sildid",
+  "search_popout.tips.user": "kasutaja",
+  "search_results.accounts": "Inimesed",
+  "search_results.hashtags": "Sildid",
+  "search_results.statuses": "Tuudid",
+  "search_results.statuses_fts_disabled": "Tuutsude otsimine nende sisu järgi ei ole sellel Mastodoni serveril sisse lülitatud.",
+  "search_results.total": "{count, number} {count, plural, one {tulemus} other {tulemust}}",
+  "status.admin_account": "Ava moderaatoriliides kasutajale @{name}",
+  "status.admin_status": "Ava see staatus moderaatoriliites",
+  "status.block": "Blokeeri @{name}",
+  "status.cancel_reblog_private": "Äraupita",
+  "status.cannot_reblog": "Seda postitust ei saa upitada",
+  "status.copy": "Kopeeri link staatusesse",
+  "status.delete": "Kustuta",
+  "status.detailed_status": "Detailne vestluskuva",
+  "status.direct": "Otsesõnum @{name}",
+  "status.embed": "Sängita",
+  "status.favourite": "Lemmik",
+  "status.filtered": "Filtreeritud",
+  "status.load_more": "Lae veel",
+  "status.media_hidden": "Meedia peidetud",
+  "status.mention": "Mainimine @{name}",
+  "status.more": "Veel",
+  "status.mute": "Vaigista @{name}",
+  "status.mute_conversation": "Vaigista vestlus",
+  "status.open": "Laienda see staatus",
+  "status.pin": "Kinnita profiilile",
+  "status.pinned": "Kinnitatud tuut",
+  "status.read_more": "Loe veel",
+  "status.reblog": "Upita",
+  "status.reblog_private": "Upita algsele publikule",
+  "status.reblogged_by": "{name} upitatud",
+  "status.reblogs.empty": "Keegi pole seda tuuti veel upitanud. Kui keegi upitab, näed seda siin.",
+  "status.redraft": "Kustuta & alga uuesti",
+  "status.reply": "Vasta",
+  "status.replyAll": "Vasta lõimele",
+  "status.report": "Raport @{name}",
+  "status.sensitive_warning": "Tundlik sisu",
+  "status.share": "Jaga",
+  "status.show_less": "Näita vähem",
+  "status.show_less_all": "Näita vähem kõigile",
+  "status.show_more": "Näita veel",
+  "status.show_more_all": "Näita enam kõigile",
+  "status.show_thread": "Kuva lõim",
+  "status.unmute_conversation": "Ära vaigista vestlust",
+  "status.unpin": "Kinnita profiililt lahti",
+  "suggestions.dismiss": "Eira soovitust",
+  "suggestions.header": "Sind võib huvitada…",
+  "tabs_bar.federated_timeline": "Föderatiivne",
+  "tabs_bar.home": "Kodu",
+  "tabs_bar.local_timeline": "Kohalik",
+  "tabs_bar.notifications": "Teated",
+  "tabs_bar.search": "Otsi",
+  "time_remaining.days": "{number, plural, one {# päev} other {# päeva}} left",
+  "time_remaining.hours": "{number, plural, one {# tund} other {# tundi}} left",
+  "time_remaining.minutes": "{number, plural, one {# minut} other {# minutit}} left",
+  "time_remaining.moments": "Hetked jäänud",
+  "time_remaining.seconds": "{number, plural, one {# sekund} other {# sekundit}} left",
+  "trends.count_by_accounts": "{count} {rawCount, plural, one {inimene} other {inimesed}} talking",
+  "trends.refresh": "Refresh",
+  "ui.beforeunload": "Sinu mustand läheb kaotsi, kui lahkud Mastodonist.",
+  "upload_area.title": "Lohista & aseta üleslaadimiseks",
+  "upload_button.label": "Lisa meedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
+  "upload_error.limit": "Faili üleslaadimise limiit ületatud.",
+  "upload_error.poll": "Küsitlustes pole faili üleslaadimine lubatud.",
+  "upload_form.description": "Kirjelda vaegnägijatele",
+  "upload_form.edit": "Edit",
+  "upload_form.undo": "Kustuta",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
+  "upload_progress.label": "Laeb üles....",
+  "video.close": "Sulge video",
+  "video.exit_fullscreen": "Välju täisekraanist",
+  "video.expand": "Suurenda video",
+  "video.fullscreen": "Täisekraan",
+  "video.hide": "Peida video",
+  "video.mute": "Vaigista heli",
+  "video.pause": "Paus",
+  "video.play": "Mängi",
+  "video.unmute": "Taasta heli"
+}
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 0c078840a..da16bf669 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -4,6 +4,7 @@
   "account.block": "Blokeatu @{name}",
   "account.block_domain": "Ezkutatu {domain} domeinuko guztia",
   "account.blocked": "Blokeatuta",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Mezu zuzena @{name}(r)i",
   "account.domain_blocked": "Ezkutatutako domeinua",
   "account.edit_profile": "Aldatu profila",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Desmututu @{name}(r)en jakinarazpenak",
   "alert.unexpected.message": "Ustekabeko errore bat gertatu da.",
   "alert.unexpected.title": "Ene!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "{combo} sakatu dezakezu hurrengoan hau saltatzeko",
   "bundle_column_error.body": "Zerbait okerra gertatu da osagai hau kargatzean.",
   "bundle_column_error.retry": "Saiatu berriro",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Oinarrizkoa",
   "home.column_settings.show_reblogs": "Erakutsi bultzadak",
   "home.column_settings.show_replies": "Erakutsi erantzunak",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Eguneratu denbora errealean",
   "intervals.full.days": "{number, plural, one {egun #} other {# egun}}",
   "intervals.full.hours": "{number, plural, one {ordu #} other {# ordu}}",
   "intervals.full.minutes": "{number, plural, one {minutu #} other {# minutu}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Zerrenda berriaren izena",
   "lists.search": "Bilatu jarraitzen dituzun pertsonen artean",
   "lists.subheading": "Zure zerrendak",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {eleentuberri #} other {# elementu berri}}",
   "loading_indicator.label": "Kargatzen...",
   "media_gallery.toggle_visible": "Txandakatu ikusgaitasuna",
   "missing_indicator.label": "Ez aurkitua",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profilen direktorioa",
   "navigation_bar.public_timeline": "Federatutako denbora-lerroa",
   "navigation_bar.security": "Segurtasuna",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name}(e)k zure mezua gogoko du",
   "notification.follow": "{name}(e)k jarraitzen zaitu",
   "notification.mention": "{name}(e)k aipatu zaitu",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Jendea",
   "search_results.hashtags": "Traolak",
   "search_results.statuses": "Toot-ak",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Mastodon zerbitzari honek ez du Toot-en edukiaren bilaketa gaitu.",
   "search_results.total": "{count, number} {count, plural, one {emaitza} other {emaitzak}}",
   "status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea",
   "status.admin_status": "Ireki mezu hau moderazio interfazean",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Amaitzekotan",
   "time_remaining.seconds": "{number, plural, one {segundo #} other {# segundo}} amaitzeko",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} hitz egiten",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Zure zirriborroa galduko da Mastodon uzten baduzu.",
   "upload_area.title": "Arrastatu eta jaregin igotzeko",
   "upload_button.label": "Gehitu multimedia  (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Fitxategi igoera muga gaindituta.",
   "upload_error.poll": "Ez da inkestetan fitxategiak igotzea onartzen.",
   "upload_form.description": "Deskribatu ikusmen arazoak dituztenentzat",
-  "upload_form.focus": "Aldatu aurrebista",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Ezabatu",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Igotzen...",
   "video.close": "Itxi bideoa",
   "video.exit_fullscreen": "Irten pantaila osotik",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 41143bcc8..9804cd4c8 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -1,9 +1,10 @@
 {
-  "account.add_or_remove_from_list": "افزودن یا حذف از فهرست‌ها",
+  "account.add_or_remove_from_list": "افزودن یا برداشتن از فهرست",
   "account.badges.bot": "ربات",
   "account.block": "مسدودسازی @{name}",
   "account.block_domain": "پنهان‌سازی همه چیز از سرور {domain}",
   "account.blocked": "مسدودشده",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "پیغام خصوصی به @{name}",
   "account.domain_blocked": "دامین پنهان‌شده",
   "account.edit_profile": "ویرایش نمایه",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "باصداکردن اعلان‌ها از طرف @{name}",
   "alert.unexpected.message": "خطای پیش‌بینی‌نشده‌ای رخ داد.",
   "alert.unexpected.title": "ای وای!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
   "bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
   "bundle_column_error.retry": "تلاش دوباره",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "اصلی",
   "home.column_settings.show_reblogs": "نمایش بازبوق‌ها",
   "home.column_settings.show_replies": "نمایش پاسخ‌ها",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "به‌روزرسانی لحظه‌ای",
   "intervals.full.days": "{number, plural, one {# روز} other {# روز}}",
   "intervals.full.hours": "{number, plural, one {# ساعت} other {# ساعت}}",
   "intervals.full.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "نام فهرست تازه",
   "lists.search": "بین کسانی که پی می‌گیرید بگردید",
   "lists.subheading": "فهرست‌های شما",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# مورد تازه} other {# مورد تازه}}",
   "loading_indicator.label": "بارگیری...",
   "media_gallery.toggle_visible": "تغییر پیدایی",
   "missing_indicator.label": "پیدا نشد",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "فهرست گزیدهٔ کاربران",
   "navigation_bar.public_timeline": "نوشته‌های همه‌جا",
   "navigation_bar.security": "امنیت",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "‫{name}‬ نوشتهٔ شما را پسندید",
   "notification.follow": "‫{name}‬ پیگیر شما شد",
   "notification.mention": "‫{name}‬ از شما نام برد",
@@ -316,7 +319,7 @@
   "search_results.accounts": "افراد",
   "search_results.hashtags": "هشتگ‌ها",
   "search_results.statuses": "بوق‌ها",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "جستجوی محتوای بوق‌ها در این سرور ماستدون ممکن نیست.",
   "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
   "status.admin_account": "محیط مدیریت مربوط به @{name} را باز کن",
   "status.admin_status": "این نوشته را در محیط مدیریت باز کن",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "زمان باقی‌مانده",
   "time_remaining.seconds": "{number, plural, one {# ثانیه} other {# ثانیه}} باقی مانده",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {نفر نوشته است} other {نفر نوشته‌اند}}",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "اگر از ماستدون خارج شوید پیش‌نویس شما پاک خواهد شد.",
   "upload_area.title": "برای بارگذاری به این‌جا بکشید",
   "upload_button.label": "افزودن عکس و ویدیو (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "از حد مجاز باگذاری فراتر رفتید.",
   "upload_error.poll": "باگذاری پرونده در نظرسنجی‌ها ممکن نیست.",
   "upload_form.description": "نوشتهٔ توضیحی برای کم‌بینایان و نابینایان",
-  "upload_form.focus": "تغییر پیش‌نمایش",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "حذف",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "بارگذاری...",
   "video.close": "بستن ویدیو",
   "video.exit_fullscreen": "خروج از حالت تمام صفحه",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 05495d5d7..baed4f0a5 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -4,6 +4,7 @@
   "account.block": "Estä @{name}",
   "account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}",
   "account.blocked": "Estetty",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Viesti käyttäjälle @{name}",
   "account.domain_blocked": "Verkko-osoite piilotettu",
   "account.edit_profile": "Muokkaa",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Poista mykistys käyttäjän @{name} ilmoituksilta",
   "alert.unexpected.message": "Tapahtui odottamaton virhe.",
   "alert.unexpected.title": "Hups!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Ensi kerralla voit ohittaa tämän painamalla {combo}",
   "bundle_column_error.body": "Jokin meni vikaan komponenttia ladattaessa.",
   "bundle_column_error.retry": "Yritä uudestaan",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Yleinen aikajana",
   "navigation_bar.security": "Tunnukset",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} tykkäsi tilastasi",
   "notification.follow": "{name} seurasi sinua",
   "notification.mention": "{name} mainitsi sinut",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.",
   "upload_area.title": "Lataa raahaamalla ja pudottamalla tähän",
   "upload_button.label": "Lisää mediaa",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Anna kuvaus näkörajoitteisia varten",
-  "upload_form.focus": "Rajaa",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Peru",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Ladataan...",
   "video.close": "Sulje video",
   "video.exit_fullscreen": "Poistu koko näytön tilasta",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index f4db2e7a1..12025baff 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -4,6 +4,7 @@
   "account.block": "Bloquer @{name}",
   "account.block_domain": "Tout masquer venant de {domain}",
   "account.blocked": "Bloqué",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Envoyer un message direct à @{name}",
   "account.domain_blocked": "Domaine caché",
   "account.edit_profile": "Modifier le profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Réactiver les notifications de @{name}",
   "alert.unexpected.message": "Une erreur inattendue s’est produite.",
   "alert.unexpected.title": "Oups !",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
   "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
   "bundle_column_error.retry": "Réessayer",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Basique",
   "home.column_settings.show_reblogs": "Afficher les partages",
   "home.column_settings.show_replies": "Afficher les réponses",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Mettre à jour en temps réel",
   "intervals.full.days": "{number, plural, one {# jour} other {# jours}}",
   "intervals.full.hours": "{number, plural, one {# heure} other {# heures}}",
   "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Titre de la nouvelle liste",
   "lists.search": "Rechercher parmi les gens que vous suivez",
   "lists.subheading": "Vos listes",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# nouvel item} other {# nouveaux items}}",
   "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "Modifier la visibilité",
   "missing_indicator.label": "Non trouvé",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Annuaire des profils",
   "navigation_bar.public_timeline": "Fil public global",
   "navigation_bar.security": "Sécurité",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} a ajouté à ses favoris :",
   "notification.follow": "{name} vous suit",
   "notification.mention": "{name} vous a mentionné :",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Comptes",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Pouets",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "La recherche de pouets par leur contenu n'est pas activée sur ce serveur Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
   "status.admin_account": "Ouvrir l'interface de modération pour @{name}",
   "status.admin_status": "Ouvrir ce statut dans l'interface de modération",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Encore quelques instants",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} restantes",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {personne} other {personnes}} discutent",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
   "upload_area.title": "Glissez et déposez pour envoyer",
   "upload_button.label": "Joindre un média (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Taille maximale d'envoi de fichier dépassée.",
   "upload_error.poll": "L'envoi de fichiers n'est pas autorisé avec les sondages.",
   "upload_form.description": "Décrire pour les malvoyant·e·s",
-  "upload_form.focus": "Modifier l’aperçu",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Supprimer",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Envoi en cours…",
   "video.close": "Fermer la vidéo",
   "video.exit_fullscreen": "Quitter le plein écran",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 2605f61f8..fcbc16017 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -4,6 +4,7 @@
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Ocultar calquer contido de {domain}",
   "account.blocked": "Bloqueada",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Mensaxe directa @{name}",
   "account.domain_blocked": "Dominio agochado",
   "account.edit_profile": "Editar perfil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Desbloquear as notificacións de @{name}",
   "alert.unexpected.message": "Aconteceu un fallo non agardado.",
   "alert.unexpected.title": "Vaia!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Pulse {combo} para saltar esto a próxima vez",
   "bundle_column_error.body": "Houbo un fallo mentras se cargaba este compoñente.",
   "bundle_column_error.retry": "Inténteo de novo",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Básico",
   "home.column_settings.show_reblogs": "Mostrar repeticións",
   "home.column_settings.show_replies": "Mostrar respostas",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Actualizar en tempo real",
   "intervals.full.days": "{number, plural,one {# día} other {# días}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Novo título da lista",
   "lists.search": "Procurar entre a xente que segues",
   "lists.subheading": "As túas listas",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# novo elemento} other {# novos elementos}}",
   "loading_indicator.label": "Cargando...",
   "media_gallery.toggle_visible": "Ocultar",
   "missing_indicator.label": "Non atopado",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Directorio de perfil",
   "navigation_bar.public_timeline": "Liña temporal federada",
   "navigation_bar.security": "Seguridade",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} marcou como favorito o seu estado",
   "notification.follow": "{name} está a seguila",
   "notification.mention": "{name} mencionoute",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Xente",
   "search_results.hashtags": "Etiquetas",
   "search_results.statuses": "Toots",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Non está activada neste servidor Mastodon a busca de toots polo seu contido.",
   "search_results.total": "{count, number} {count,plural,one {result} outros {results}}",
   "status.admin_account": "Abrir interface de moderación para @{name}",
   "status.admin_status": "Abrir este estado na interface de moderación",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Está rematando",
   "time_remaining.seconds": "{number, plural, one {# segundo} other {# segundos}} restantes",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} outras {people}} conversando",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "O borrador perderase se sae de Mastodon.",
   "upload_area.title": "Arrastre e solte para subir",
   "upload_button.label": "Engadir medios ({formats})",
   "upload_error.limit": "Excedeu o límite de subida de ficheiros.",
   "upload_error.poll": "Non se poden subir ficheiros nas sondaxes.",
   "upload_form.description": "Describa para deficientes visuais",
-  "upload_form.focus": "Cambiar vista previa",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Eliminar",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Subindo...",
   "video.close": "Pechar video",
   "video.exit_fullscreen": "Saír da pantalla completa",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 99bb87a5f..e17d451ca 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -4,6 +4,7 @@
   "account.block": "חסימת @{name}",
   "account.block_domain": "להסתיר הכל מהקהילה {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "עריכת פרופיל",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}",
   "alert.unexpected.message": "אירעה שגיאה בלתי צפויה.",
   "alert.unexpected.title": "אופס!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
   "bundle_column_error.body": "משהו השתבש בעת הצגת הרכיב הזה.",
   "bundle_column_error.retry": "לנסות שוב",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "ציר זמן בין-קהילתי",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "חצרוצך חובב על ידי {name}",
   "notification.follow": "{name} במעקב אחרייך",
   "notification.mention": "אוזכרת על ידי {name}",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
   "upload_area.title": "ניתן להעלות על ידי Drag & drop",
   "upload_button.label": "הוספת מדיה",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "תיאור לכבדי ראיה",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "ביטול",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "עולה...",
   "video.close": "סגירת וידאו",
   "video.exit_fullscreen": "יציאה ממסך מלא",
diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json
index d4d9e5f64..980b4e457 100644
--- a/app/javascript/mastodon/locales/hi.json
+++ b/app/javascript/mastodon/locales/hi.json
@@ -4,6 +4,7 @@
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edit profile",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Unmute notifications from @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "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",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federated timeline",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} followed you",
   "notification.mention": "{name} mentioned you",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "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_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Delete",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploading...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 273b70d07..d718915c8 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -4,6 +4,7 @@
   "account.block": "Blokiraj @{name}",
   "account.block_domain": "Sakrij sve sa {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Uredi profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Unmute notifications from @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
   "bundle_column_error.body": "Something went wrong while loading this component.",
   "bundle_column_error.retry": "Try again",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federalni timeline",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} je lajkao tvoj status",
   "notification.follow": "{name} te sada slijedi",
   "notification.mention": "{name} te je spomenuo",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Povuci i spusti kako bi uploadao",
   "upload_button.label": "Dodaj media",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Poništi",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploadam...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 38d30efe4..f06e748a8 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -4,6 +4,7 @@
   "account.block": "@{name} letiltása",
   "account.block_domain": "Minden elrejtése innen: {domain}",
   "account.blocked": "Letiltva",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Közvetlen üzenet @{name} számára",
   "account.domain_blocked": "Rejtett domain",
   "account.edit_profile": "Profil szerkesztése",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "@{name} némított értesítéseinek feloldása",
   "alert.unexpected.message": "Váratlan hiba történt.",
   "alert.unexpected.title": "Hoppá!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Hogy átugord ezt következő alkalommal, használd {combo}",
   "bundle_column_error.body": "Hiba történt a komponens betöltése közben.",
   "bundle_column_error.retry": "Próbáld újra",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Alapértelmezések",
   "home.column_settings.show_reblogs": "Megtolások mutatása",
   "home.column_settings.show_replies": "Válaszok mutatása",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Frissítés valós időben",
   "intervals.full.days": "{number, plural, one {# nap} other {# nap}}",
   "intervals.full.hours": "{number, plural, one {# óra} other {# óra}}",
   "intervals.full.minutes": "{number, plural, one {# perc} other {# perc}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Új lista címe",
   "lists.search": "Keresés a követett személyek között",
   "lists.subheading": "Listáid",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# új elem} other {# új elem}}",
   "loading_indicator.label": "Betöltés...",
   "media_gallery.toggle_visible": "Láthatóság állítása",
   "missing_indicator.label": "Nincs találat",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profilok",
   "navigation_bar.public_timeline": "Föderációs idővonal",
   "navigation_bar.security": "Biztonság",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} kedvencnek jelölte egy tülködet",
   "notification.follow": "{name} követ téged",
   "notification.mention": "{name} megemlített",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Emberek",
   "search_results.hashtags": "Hashtagek",
   "search_results.statuses": "Tülkök",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Ezen a szerveren nem engedélyezett a tülkök tartalom szerinti keresése.",
   "search_results.total": "{count, number} {count, plural, one {találat} other {találat}}",
   "status.admin_account": "Moderáció megnyitása @{name} felhasználóhoz",
   "status.admin_status": "Tülk megnyitása moderációra",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Pillanatok vannak hátra",
   "time_remaining.seconds": "{number, plural, one {# másodperc} other {# másodperc}} van hátra",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {résztvevő} other {résztvevő}} beszélget",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "A piszkozatod el fog veszni, ha elhagyod a Mastodon-t.",
   "upload_area.title": "Húzd ide a feltöltéshez",
   "upload_button.label": "Média hozzáadása (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Túllépted a fájl feltöltési limitet.",
   "upload_error.poll": "Szavazásnál nem lehet fájlt feltölteni.",
   "upload_form.description": "Leírás látáskorlátozottak számára",
-  "upload_form.focus": "Előnézet megváltoztatása",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Mégsem",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Feltöltés...",
   "video.close": "Videó bezárása",
   "video.exit_fullscreen": "Kilépés teljes képernyőből",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 801d34380..47e9ee68b 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -1,15 +1,16 @@
 {
   "account.add_or_remove_from_list": "Add or Remove from lists",
-  "account.badges.bot": "Bot",
+  "account.badges.bot": "Բոտ",
   "account.block": "Արգելափակել @{name}֊ին",
   "account.block_domain": "Թաքցնել ամենը հետեւյալ տիրույթից՝ {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Խմբագրել անձնական էջը",
   "account.endorse": "Feature on profile",
   "account.follow": "Հետեւել",
-  "account.followers": "Հետեւվողներ",
+  "account.followers": "Հետեւողներ",
   "account.followers.empty": "No one follows this user yet.",
   "account.follows": "Հետեւում է",
   "account.follows.empty": "This user doesn't follow anyone yet.",
@@ -36,7 +37,8 @@
   "account.unmute": "Ապալռեցնել @{name}֊ին",
   "account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից",
   "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.title": "Վա՜յ",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Կարող ես սեղմել {combo}՝ սա հաջորդ անգամ բաց թողնելու համար",
   "bundle_column_error.body": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանվեց։",
   "bundle_column_error.retry": "Կրկին փորձել",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Դաշնային հոսք",
   "navigation_bar.security": "Անվտանգություն",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} հավանեց թութդ",
   "notification.follow": "{name} սկսեց հետեւել քեզ",
   "notification.mention": "{name} նշեց քեզ",
@@ -313,7 +316,7 @@
   "search_popout.tips.status": "թութ",
   "search_popout.tips.text": "Հասարակ տեքստը կվերադարձնի համընկնող անուններ, օգտանուններ ու պիտակներ",
   "search_popout.tips.user": "օգտատեր",
-  "search_results.accounts": "People",
+  "search_results.accounts": "Մարդիկ",
   "search_results.hashtags": "Hashtags",
   "search_results.statuses": "Toots",
   "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
@@ -363,21 +366,29 @@
   "tabs_bar.home": "Հիմնական",
   "tabs_bar.local_timeline": "Տեղական",
   "tabs_bar.notifications": "Ծանուցումներ",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Փնտրել",
   "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
   "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
   "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։",
   "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար",
   "upload_button.label": "Ավելացնել մեդիա",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Նկարագրություն ավելացրու տեսողական խնդիրներ ունեցողների համար",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Հետարկել",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Վերբեռնվում է…",
   "video.close": "Փակել  տեսագրությունը",
   "video.exit_fullscreen": "Անջատել լիաէկրան դիտումը",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index daa87f955..0757e8ff3 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -4,6 +4,7 @@
   "account.block": "Blokir @{name}",
   "account.block_domain": "Sembunyikan segalanya dari {domain}",
   "account.blocked": "Terblokir",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domain disembunyikan",
   "account.edit_profile": "Ubah profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Munculkan notifikasi dari @{name}",
   "alert.unexpected.message": "Terjadi kesalahan yang tidak terduga.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
   "bundle_column_error.body": "Kesalahan terjadi saat memuat komponen ini.",
   "bundle_column_error.retry": "Coba lagi",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Linimasa gabungan",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} menyukai status anda",
   "notification.follow": "{name} mengikuti anda",
   "notification.mention": "{name} mentioned you",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Naskah anda akan hilang jika anda keluar dari Mastodon.",
   "upload_area.title": "Seret & lepaskan untuk mengunggah",
   "upload_button.label": "Tambahkan media",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Deskripsikan untuk mereka yang tidak bisa melihat dengan jelas",
-  "upload_form.focus": "Potong",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Undo",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Mengunggah...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Keluar dari layar penuh",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 864d49995..ff096f5cf 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -4,6 +4,7 @@
   "account.block": "Blokusar @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Modifikar profilo",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Unmute notifications from @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo",
   "bundle_column_error.body": "Something went wrong while loading this component.",
   "bundle_column_error.retry": "Try again",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federata tempolineo",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favorizis tua mesajo",
   "notification.follow": "{name} sequeskis tu",
   "notification.mention": "{name} mencionis tu",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Tranar faligar por kargar",
   "upload_button.label": "Adjuntar kontenajo",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Desfacar",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Kargante...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 7925cef8c..0e791e13d 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -4,6 +4,7 @@
   "account.block": "Blocca @{name}",
   "account.block_domain": "Nascondi tutto da {domain}",
   "account.blocked": "Bloccato",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Invia messaggio privato a @{name}",
   "account.domain_blocked": "Dominio nascosto",
   "account.edit_profile": "Modifica profilo",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Non silenziare più le notifiche da @{name}",
   "alert.unexpected.message": "Si è verificato un errore inatteso.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per settimana",
   "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta",
   "bundle_column_error.body": "E' avvenuto un errore durante il caricamento di questo componente.",
   "bundle_column_error.retry": "Riprova",
@@ -66,7 +68,7 @@
   "column_subheading.settings": "Impostazioni",
   "community.column_settings.media_only": "Solo media",
   "compose_form.direct_message_warning": "Questo toot sarà mandato solo a tutti gli utenti menzionati.",
-  "compose_form.direct_message_warning_learn_more": "Per saperne di piu'",
+  "compose_form.direct_message_warning_learn_more": "Per saperne di più",
   "compose_form.hashtag_warning": "Questo toot non è listato, quindi non sarà trovato nelle ricerche per hashtag. Solo i toot pubblici possono essere cercati per hashtag.",
   "compose_form.lock_disclaimer": "Il tuo account non è {bloccato}. Chiunque può decidere di seguirti per vedere i tuoi post per soli seguaci.",
   "compose_form.lock_disclaimer.lock": "bloccato",
@@ -137,7 +139,7 @@
   "follow_request.authorize": "Autorizza",
   "follow_request.reject": "Rifiuta",
   "getting_started.developers": "Sviluppatori",
-  "getting_started.directory": "Directory del profilo",
+  "getting_started.directory": "Directory dei profili",
   "getting_started.documentation": "Documentazione",
   "getting_started.heading": "Come iniziare",
   "getting_started.invite": "Invita qualcuno",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Semplice",
   "home.column_settings.show_reblogs": "Mostra post condivisi",
   "home.column_settings.show_replies": "Mostra risposte",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Aggiornama in tempo reale",
   "intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}",
   "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Titolo della nuova lista",
   "lists.search": "Cerca tra le persone che segui",
   "lists.subheading": "Le tue liste",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# nuovo oggetto} other {# nuovi oggetti}}",
   "loading_indicator.label": "Caricamento...",
   "media_gallery.toggle_visible": "Imposta visibilità",
   "missing_indicator.label": "Non trovato",
@@ -239,7 +241,7 @@
   "navigation_bar.favourites": "Apprezzati",
   "navigation_bar.filters": "Parole silenziate",
   "navigation_bar.follow_requests": "Richieste di amicizia",
-  "navigation_bar.follows_and_followers": "Follows and followers",
+  "navigation_bar.follows_and_followers": "Seguiti e seguaci",
   "navigation_bar.info": "Informazioni su questo server",
   "navigation_bar.keyboard_shortcuts": "Tasti di scelta rapida",
   "navigation_bar.lists": "Liste",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Directory dei profili",
   "navigation_bar.public_timeline": "Timeline federata",
   "navigation_bar.security": "Sicurezza",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} ha apprezzato il tuo post",
   "notification.follow": "{name} ha iniziato a seguirti",
   "notification.mention": "{name} ti ha menzionato",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Gente",
   "search_results.hashtags": "Hashtag",
   "search_results.statuses": "Toot",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "La ricerca di toot per il loro contenuto non è abilitata su questo server Mastodon.",
   "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}",
   "status.admin_account": "Apri interfaccia di moderazione per @{name}",
   "status.admin_status": "Apri questo status nell'interfaccia di moderazione",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Restano pochi istanti",
   "time_remaining.seconds": "{number, plural, one {# secondo} other {# secondi}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persona ne sta} other {persone ne stanno}} parlando",
+  "trends.refresh": "Aggiorna",
   "ui.beforeunload": "La bozza andrà persa se esci da Mastodon.",
   "upload_area.title": "Trascina per caricare",
   "upload_button.label": "Aggiungi file multimediale",
   "upload_error.limit": "Limite al caricamento di file superato.",
   "upload_error.poll": "Caricamento file non consentito nei sondaggi.",
   "upload_form.description": "Descrizione per utenti con disabilità visive",
-  "upload_form.focus": "Modifica anteprima",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Cancella",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Clicca o trascina il cerchio sull'anteprima per scegliere il punto focale che sarà sempre visualizzato su tutte le miniature.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Sto caricando...",
   "video.close": "Chiudi video",
   "video.exit_fullscreen": "Esci da modalità a schermo intero",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 3c6d71835..d28fe4247 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -4,6 +4,7 @@
   "account.block": "@{name}さんをブロック",
   "account.block_domain": "{domain}全体を非表示",
   "account.blocked": "ブロック済み",
+  "account.cancel_follow_request": "フォローリクエストを取り消す",
   "account.direct": "@{name}さんにダイレクトメッセージ",
   "account.domain_blocked": "ドメイン非表示中",
   "account.edit_profile": "プロフィール編集",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
   "alert.unexpected.message": "不明なエラーが発生しました。",
   "alert.unexpected.title": "エラー!",
+  "autosuggest_hashtag.per_week": "{count} 回 / 週",
   "boost_modal.combo": "次からは{combo}を押せばスキップできます",
   "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
   "bundle_column_error.retry": "再試行",
@@ -160,7 +162,7 @@
   "home.column_settings.basic": "基本設定",
   "home.column_settings.show_reblogs": "ブースト表示",
   "home.column_settings.show_replies": "返信表示",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "リアルタイムで更新",
   "intervals.full.days": "{number}日",
   "intervals.full.hours": "{number}時間",
   "intervals.full.minutes": "{number}分",
@@ -226,7 +228,7 @@
   "lists.new.title_placeholder": "新規リスト名",
   "lists.search": "フォローしている人の中から検索",
   "lists.subheading": "あなたのリスト",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count} 件の新着",
   "loading_indicator.label": "読み込み中...",
   "media_gallery.toggle_visible": "表示切り替え",
   "missing_indicator.label": "見つかりません",
@@ -256,6 +258,7 @@
   "navigation_bar.public_timeline": "連合タイムライン",
   "navigation_bar.misc": "その他",
   "navigation_bar.security": "セキュリティ",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
   "notification.follow": "{name}さんにフォローされました",
   "notification.mention": "{name}さんがあなたに返信しました",
@@ -321,7 +324,7 @@
   "search_results.accounts": "人々",
   "search_results.hashtags": "ハッシュタグ",
   "search_results.statuses": "トゥート",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "このサーバーではトゥート本文での検索は利用できません。",
   "search_results.total": "{count, number}件の結果",
   "status.admin_account": "@{name} のモデレーション画面を開く",
   "status.admin_status": "このトゥートをモデレーション画面で開く",
@@ -375,14 +378,22 @@
   "time_remaining.moments": "まもなく終了",
   "time_remaining.seconds": "残り{number}秒",
   "trends.count_by_accounts": "{count}人がトゥート",
+  "trends.refresh": "更新",
   "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
   "upload_area.title": "ドラッグ&ドロップでアップロード",
   "upload_button.label": "メディアを追加 ({formats})",
   "upload_error.limit": "アップロードできる上限を超えています。",
   "upload_error.poll": "アンケートではファイルをアップロードできません。",
   "upload_form.description": "視覚障害者のための説明",
-  "upload_form.focus": "プレビューを変更",
+  "upload_form.edit": "編集",
   "upload_form.undo": "削除",
+  "upload_modal.analyzing_picture": "画像を解析中…",
+  "upload_modal.apply": "適用",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "画像からテキストを検出",
+  "upload_modal.edit_media": "メディアを編集",
+  "upload_modal.hint": "画像をクリックするか円をドラッグすると全てのサムネイルで注目する場所を選ぶことができます",
+  "upload_modal.preview_label": "プレビュー ({ratio})",
   "upload_progress.label": "アップロード中...",
   "video.close": "動画を閉じる",
   "video.exit_fullscreen": "全画面を終了する",
diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json
index a78543476..fecfb519c 100644
--- a/app/javascript/mastodon/locales/ka.json
+++ b/app/javascript/mastodon/locales/ka.json
@@ -4,6 +4,7 @@
   "account.block": "დაბლოკე @{name}",
   "account.block_domain": "დაიმალოს ყველაფერი დომენიდან {domain}",
   "account.blocked": "დაიბლოკა",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "პირდაპირი წერილი @{name}-ს",
   "account.domain_blocked": "დომენი დამალულია",
   "account.edit_profile": "პროფილის ცვლილება",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "ნუღარ აჩუმებ შეტყობინებებს @{name}-სგან",
   "alert.unexpected.message": "წარმოიშვა მოულოდნელი შეცდომა.",
   "alert.unexpected.title": "უპს!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "შეგიძლიათ დააჭიროთ {combo}-ს რათა შემდეგ ჯერზე გამოტოვოთ ეს",
   "bundle_column_error.body": "ამ კომპონენტის ჩატვირთვისას რაღაც აირია.",
   "bundle_column_error.retry": "სცადეთ კიდევ ერთხელ",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "ფედერალური თაიმლაინი",
   "navigation_bar.security": "უსაფრთხოება",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name}-მა თქვენი სტატუსი აქცია ფავორიტად",
   "notification.follow": "{name} გამოგყვათ",
   "notification.mention": "{name}-მა გასახელათ",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} საუბრობს",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.",
   "upload_area.title": "გადმოწიეთ და ჩააგდეთ ასატვირთათ",
   "upload_button.label": "მედიის დამატება",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "აღწერილობა ვიზუალურად უფასურისთვის",
-  "upload_form.focus": "კროპი",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "გაუქმება",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "იტვირთება...",
   "video.close": "ვიდეოს დახურვა",
   "video.exit_fullscreen": "სრულ ეკრანზე ჩვენების გათიშვა",
diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json
index 9514d68a9..8710ae90b 100644
--- a/app/javascript/mastodon/locales/kk.json
+++ b/app/javascript/mastodon/locales/kk.json
@@ -4,6 +4,7 @@
   "account.block": "Бұғаттау @{name}",
   "account.block_domain": "Домендегі барлығын бұғатта {domain}",
   "account.blocked": "Бұғатталды",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Жеке хат @{name}",
   "account.domain_blocked": "Домен жабық",
   "account.edit_profile": "Профильді өңдеу",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "@{name} ескертпелерін көрсету",
   "alert.unexpected.message": "Бір нәрсе дұрыс болмады.",
   "alert.unexpected.title": "Өй!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Келесіде өткізіп жіберу үшін басыңыз {combo}",
   "bundle_column_error.body": "Бұл компонентті жүктеген кезде бір қате пайда болды.",
   "bundle_column_error.retry": "Қайтадан көріңіз",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Жаһандық желі",
   "navigation_bar.security": "Қауіпсіздік",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} жазбаңызды таңдаулыға қосты",
   "notification.follow": "{name} сізге жазылды",
   "notification.mention": "{name} сізді атап өтті",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Қалған уақыт",
   "time_remaining.seconds": "{number, plural, one {# секунд} other {# секунд}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} жазған екен",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Mastodon желісінен шықсаңыз, нобайыңыз сақталмайды.",
   "upload_area.title": "Жүктеу үшін сүйреп әкеліңіз",
   "upload_button.label": "Медиа қосу (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Файл жүктеу лимитінен асып кеттіңіз.",
   "upload_error.poll": "Сауалнамамен бірге файл жүктеуге болмайды.",
   "upload_form.description": "Көру қабілеті нашар адамдар үшін сипаттаңыз",
-  "upload_form.focus": "Превьюді өзгерту",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Өшіру",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Жүктеп жатыр...",
   "video.close": "Видеоны жабу",
   "video.exit_fullscreen": "Толық экраннан шық",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index e71631938..ac6a3ca91 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -4,6 +4,7 @@
   "account.block": "@{name}을 차단",
   "account.block_domain": "{domain} 전체를 숨김",
   "account.blocked": "차단 됨",
+  "account.cancel_follow_request": "팔로우 요청 취소",
   "account.direct": "@{name}으로부터의 다이렉트 메시지",
   "account.domain_blocked": "도메인 숨겨짐",
   "account.edit_profile": "프로필 편집",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "@{name}의 알림 뮤트 해제",
   "alert.unexpected.message": "예측하지 못한 에러가 발생했습니다.",
   "alert.unexpected.title": "앗!",
+  "autosuggest_hashtag.per_week": "주간 {count}회",
   "boost_modal.combo": "{combo}를 누르면 다음부터 이 과정을 건너뛸 수 있습니다",
   "bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
   "bundle_column_error.retry": "다시 시도",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "기본 설정",
   "home.column_settings.show_reblogs": "부스트 표시",
   "home.column_settings.show_replies": "답글 표시",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "실시간 업데이트",
   "intervals.full.days": "{number} 일",
   "intervals.full.hours": "{number} 시간",
   "intervals.full.minutes": "{number} 분",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "새 리스트의 이름",
   "lists.search": "팔로우 중인 사람들 중에서 찾기",
   "lists.subheading": "당신의 리스트",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count}개의 새 항목",
   "loading_indicator.label": "불러오는 중...",
   "media_gallery.toggle_visible": "표시 전환",
   "missing_indicator.label": "찾을 수 없습니다",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "프로필 디렉토리",
   "navigation_bar.public_timeline": "연합 타임라인",
   "navigation_bar.security": "보안",
+  "notification.and_n_others": "그리고 {count}개의 기타 항목",
   "notification.favourite": "{name}님이 즐겨찾기 했습니다",
   "notification.follow": "{name}님이 나를 팔로우 했습니다",
   "notification.mention": "{name}님이 답글을 보냈습니다",
@@ -316,7 +319,7 @@
   "search_results.accounts": "사람",
   "search_results.hashtags": "해시태그",
   "search_results.statuses": "툿",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "이 마스토돈 서버에선 툿의 내용을 통한 검색이 활성화 되어 있지 않습니다.",
   "search_results.total": "{count, number}건의 결과",
   "status.admin_account": "@{name}에 대한 모더레이션 인터페이스 열기",
   "status.admin_status": "모더레이션 인터페이스에서 이 게시물 열기",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "남은 시간",
   "time_remaining.seconds": "{number} 초 남음",
   "trends.count_by_accounts": "{count} 명의 사람들이 말하고 있습니다",
+  "trends.refresh": "새로고침",
   "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
   "upload_area.title": "드래그 & 드롭으로 업로드",
   "upload_button.label": "미디어 추가 (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "파일 업로드 제한에 도달했습니다.",
   "upload_error.poll": "파일 업로드는 투표와 함께 첨부할 수 없습니다.",
   "upload_form.description": "시각장애인을 위한 설명",
-  "upload_form.focus": "미리보기 변경",
+  "upload_form.edit": "편집",
   "upload_form.undo": "삭제",
+  "upload_modal.analyzing_picture": "이미지 분석 중…",
+  "upload_modal.apply": "적용",
+  "upload_modal.description_placeholder": "다람쥐 헌 쳇바퀴 타고파",
+  "upload_modal.detect_text": "이미지에서 텍스트 추출",
+  "upload_modal.edit_media": "미디어 편집",
+  "upload_modal.hint": "미리보기를 클릭하거나 드래그 해서 포컬 포인트를 맞추세요. 이 점은 썸네일에 항상 보여질 부분을 나타냅니다.",
+  "upload_modal.preview_label": "미리보기 ({ratio})",
   "upload_progress.label": "업로드 중...",
   "video.close": "동영상 닫기",
   "video.exit_fullscreen": "전체화면 나가기",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 919129cc5..b844e2898 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -4,6 +4,7 @@
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edit profile",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Unmute notifications from @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "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",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federated timeline",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} followed you",
   "notification.mention": "{name} mentioned you",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media ({formats})",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Delete",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploading...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 5328f15c5..b4e45a854 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -4,6 +4,7 @@
   "account.block": "Bloķēt @{name}",
   "account.block_domain": "Slēpt visu no {domain}",
   "account.blocked": "Bloķēts",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Privātā ziņa @{name}",
   "account.domain_blocked": "Domēns ir paslēpts",
   "account.edit_profile": "Labot profilu",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Rādīt paziņojumus no lietotāja @{name}",
   "alert.unexpected.message": "Negaidīta kļūda.",
   "alert.unexpected.title": "Ups!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Nospied {combo} lai izlaistu šo nākamreiz",
   "bundle_column_error.body": "Kaut kas nogāja greizi ielādējot šo komponenti.",
   "bundle_column_error.retry": "Mēģini vēlreiz",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federated timeline",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} followed you",
   "notification.mention": "{name} mentioned you",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media ({formats})",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Delete",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploading...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index ad72b3233..556204753 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -4,6 +4,7 @@
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Edit profile",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Unmute notifications from @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "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",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federated timeline",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} followed you",
   "notification.mention": "{name} mentioned you",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
   "upload_area.title": "Drag & drop to upload",
   "upload_button.label": "Add media ({formats})",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Delete",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploading...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index d7f428193..6cfcf6bd6 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -4,6 +4,7 @@
   "account.block": "Blokkeer @{name}",
   "account.block_domain": "Verberg alles van {domain}",
   "account.blocked": "Geblokkeerd",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domein verborgen",
   "account.edit_profile": "Profiel bewerken",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "@{name} meldingen niet langer negeren",
   "alert.unexpected.message": "Er deed zich een onverwachte fout voor",
   "alert.unexpected.title": "Oeps!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
   "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
   "bundle_column_error.retry": "Opnieuw proberen",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Gebruikersgids",
   "navigation_bar.public_timeline": "Globale tijdlijn",
   "navigation_bar.security": "Beveiliging",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} voegde jouw toot als favoriet toe",
   "notification.follow": "{name} volgt jou nu",
   "notification.mention": "{name} vermeldde jou",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Nog enkele ogenblikken resterend",
   "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} te gaan",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persoon praat} other {mensen praten}} hierover",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
   "upload_area.title": "Hiernaar toe slepen om te uploaden",
   "upload_button.label": "Media toevoegen ({formats})",
   "upload_error.limit": "Uploadlimiet van bestand overschreden.",
   "upload_error.poll": "Het uploaden van bestanden is in polls niet toegestaan.",
   "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
-  "upload_form.focus": "Voorvertoning aanpassen",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Verwijderen",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploaden...",
   "video.close": "Video sluiten",
   "video.exit_fullscreen": "Volledig scherm sluiten",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index ea722a01e..b310efd69 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -4,6 +4,7 @@
   "account.block": "Blokkér @{name}",
   "account.block_domain": "Skjul alt fra {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Rediger profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Vis varsler fra @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
   "bundle_column_error.body": "Noe gikk galt mens denne komponenten lastet.",
   "bundle_column_error.retry": "Prøv igjen",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Felles tidslinje",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} likte din status",
   "notification.follow": "{name} fulgte deg",
   "notification.mention": "{name} nevnte deg",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.",
   "upload_area.title": "Dra og slipp for å laste opp",
   "upload_button.label": "Legg til media",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Beskriv for synshemmede",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Angre",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Laster opp...",
   "video.close": "Lukk video",
   "video.exit_fullscreen": "Lukk fullskjerm",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 34804da20..76b578021 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -4,6 +4,7 @@
   "account.block": "Blocar @{name}",
   "account.block_domain": "Tot amagar del domeni {domain}",
   "account.blocked": "Blocat",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Escriure un MP a @{name}",
   "account.domain_blocked": "Domeni amagat",
   "account.edit_profile": "Modificar lo perfil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Mostrar las notificacions de @{name}",
   "alert.unexpected.message": "Una error s’es producha.",
   "alert.unexpected.title": "Ops !",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
   "bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
   "bundle_column_error.retry": "Tornar ensajar",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Basic",
   "home.column_settings.show_reblogs": "Mostrar los partatges",
   "home.column_settings.show_replies": "Mostrar las responsas",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Actualizacion en dirècte",
   "intervals.full.days": "{number, plural, one {# jorn} other {# jorns}}",
   "intervals.full.hours": "{number, plural, one {# ora} other {# oras}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} other {# minutas}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Títol de la nòva lista",
   "lists.search": "Cercar demest lo monde que seguètz",
   "lists.subheading": "Vòstras listas",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}",
   "loading_indicator.label": "Cargament…",
   "media_gallery.toggle_visible": "Modificar la visibilitat",
   "missing_indicator.label": "Pas trobat",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Annuari de perfils",
   "navigation_bar.public_timeline": "Flux public global",
   "navigation_bar.security": "Seguretat",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} a ajustat a sos favorits",
   "notification.follow": "{name} vos sèc",
   "notification.mention": "{name} vos a mencionat",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Gents",
   "search_results.hashtags": "Etiquetas",
   "search_results.statuses": "Tuts",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "La recèrca de tuts per lor contengut es pas activada sus aqueste servidor Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
   "status.admin_account": "Dobrir l’interfàcia de moderacion per @{name}",
   "status.admin_status": "Dobrir aqueste estatut dins l’interfàcia de moderacion",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments restants",
   "time_remaining.seconds": "demòra{number, plural, one { # segonda} other {n # segondas}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} ne charra other {people}} ne charran",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
   "upload_area.title": "Lisatz e depausatz per mandar",
   "upload_button.label": "Ajustar un mèdia (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Talha maximum pels mandadís subrepassada.",
   "upload_error.poll": "Lo mandadís de fichièr es pas autorizat pels sondatges.",
   "upload_form.description": "Descripcion pels mal vesents",
-  "upload_form.focus": "Modificar l’apercebut",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Suprimir",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Mandadís…",
   "video.close": "Tampar la vidèo",
   "video.exit_fullscreen": "Sortir plen ecran",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index d96ceb064..0793dbe01 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -4,6 +4,7 @@
   "account.block": "Blokuj @{name}",
   "account.block_domain": "Blokuj wszystko z {domain}",
   "account.blocked": "Zablokowany(-a)",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Wyślij wiadomość bezpośrednią do @{name}",
   "account.domain_blocked": "Ukryto domenę",
   "account.edit_profile": "Edytuj profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}",
   "alert.unexpected.message": "Wystąpił nieoczekiwany błąd.",
   "alert.unexpected.title": "O nie!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
   "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
   "bundle_column_error.retry": "Spróbuj ponownie",
@@ -256,6 +258,7 @@
   "navigation_bar.profile_directory": "Katalog profilów",
   "navigation_bar.public_timeline": "Globalna oś czasu",
   "navigation_bar.security": "Bezpieczeństwo",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} dodał(a) Twój wpis do ulubionych",
   "notification.follow": "{name} zaczął(-ęła) Cię śledzić",
   "notification.mention": "{name} wspomniał(a) o tobie",
@@ -375,14 +378,22 @@
   "time_remaining.moments": "Pozostała chwila",
   "time_remaining.seconds": "{number, plural, one {Pozostała # sekunda} few {Pozostały # sekundy} many {Pozostało # sekund} other {Pozostało # sekund}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {osoba rozmawia} few {osoby rozmawiają} other {osób rozmawia}} o tym",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
   "upload_area.title": "Przeciągnij i upuść aby wysłać",
   "upload_button.label": "Dodaj zawartość multimedialną (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Przekroczono limit plików do wysłania.",
   "upload_error.poll": "Dołączanie plików nie dozwolone z głosowaniami.",
   "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
-  "upload_form.focus": "Dopasuj podgląd",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Usuń",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Wysyłanie…",
   "video.close": "Zamknij film",
   "video.exit_fullscreen": "Opuść tryb pełnoekranowy",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 1fb700874..523378276 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -4,6 +4,7 @@
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo de {domain}",
   "account.blocked": "Bloqueado",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domínio escondido",
   "account.edit_profile": "Editar perfil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
   "alert.unexpected.message": "Um erro inesperado ocorreu.",
   "alert.unexpected.title": "Eita!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
   "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
   "bundle_column_error.retry": "Tente novamente",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Diretório de perfis",
   "navigation_bar.public_timeline": "Global",
   "navigation_bar.security": "Segurança",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} adicionou a sua postagem aos favoritos",
   "notification.follow": "{name} te seguiu",
   "notification.mention": "{name} te mencionou",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Momentos restantes",
   "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {pessoa} other {pessoas}} falando sobre",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Limite de envio de arquivos excedido.",
   "upload_error.poll": "Envio de arquivos não é permitido com enquetes.",
   "upload_form.description": "Descreva a imagem para deficientes visuais",
-  "upload_form.focus": "Ajustar foco",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Remover",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Salvando...",
   "video.close": "Fechar vídeo",
   "video.exit_fullscreen": "Sair da tela cheia",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index c6ea3f847..7ce628422 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -4,6 +4,7 @@
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo do domínio {domain}",
   "account.blocked": "Bloqueado",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Mensagem directa @{name}",
   "account.domain_blocked": "Domínio escondido",
   "account.edit_profile": "Editar perfil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Deixar de silenciar @{name}",
   "alert.unexpected.message": "Ocorreu um erro inesperado.",
   "alert.unexpected.title": "Bolas!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
   "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
   "bundle_column_error.retry": "Tente de novo",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Directório de perfis",
   "navigation_bar.public_timeline": "Cronologia federada",
   "navigation_bar.security": "Segurança",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} adicionou o teu estado aos favoritos",
   "notification.follow": "{name} começou a seguir-te",
   "notification.mention": "{name} mencionou-te",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Momentos restantes",
   "time_remaining.seconds": "{número, plural, um {# second} outro {# seconds}} faltam",
   "trends.count_by_accounts": "{count} {rawCount, plural, uma {person} outra {people}} a falar",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "O teu rascunho será perdido se abandonares o Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar media",
   "upload_error.limit": "Limite máximo do ficheiro a carregar excedido.",
   "upload_error.poll": "Carregamento de ficheiros não é permitido em votações.",
   "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais",
-  "upload_form.focus": "Alterar previsualização",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Apagar",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "A enviar...",
   "video.close": "Fechar vídeo",
   "video.exit_fullscreen": "Sair de full screen",
diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json
index ac10d4678..141ccd5ab 100644
--- a/app/javascript/mastodon/locales/ro.json
+++ b/app/javascript/mastodon/locales/ro.json
@@ -4,6 +4,7 @@
   "account.block": "Blochează @{name}",
   "account.block_domain": "Ascunde tot de la {domain}",
   "account.blocked": "Blocat",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Mesaj direct @{name}",
   "account.domain_blocked": "Domeniu ascuns",
   "account.edit_profile": "Editează profilul",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Activează notificările de la @{name}",
   "alert.unexpected.message": "A apărut o eroare neașteptată.",
   "alert.unexpected.title": "Hopa!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "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",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Flux global",
   "navigation_bar.security": "Securitate",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} a adăugat statusul tău la favorite",
   "notification.follow": "{name} te urmărește",
   "notification.mention": "{name} te-a menționat",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} vorbesc",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Postarea se va pierde dacă părăsești pagina.",
   "upload_area.title": "Trage și eliberează pentru a încărca",
   "upload_button.label": "Adaugă media (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Adaugă o descriere pentru persoanele cu deficiențe de vedere",
-  "upload_form.focus": "Schimbă previzualizarea",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Șterge",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Se Încarcă...",
   "video.close": "Închide video",
   "video.exit_fullscreen": "Închide",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 8a7a39a06..afc064a6b 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -4,6 +4,7 @@
   "account.block": "Блокировать",
   "account.block_domain": "Блокировать все с {domain}",
   "account.blocked": "Заблокирован(а)",
+  "account.cancel_follow_request": "Отменить запрос",
   "account.direct": "Написать @{name}",
   "account.domain_blocked": "Домен скрыт",
   "account.edit_profile": "Изменить профиль",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Показывать уведомления от @{name}",
   "alert.unexpected.message": "Что-то пошло не так.",
   "alert.unexpected.title": "Ой!",
+  "autosuggest_hashtag.per_week": "{count} / неделю",
   "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
   "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
   "bundle_column_error.retry": "Попробовать снова",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Основные",
   "home.column_settings.show_reblogs": "Показывать продвижения",
   "home.column_settings.show_replies": "Показывать ответы",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Обновлять в реальном времени",
   "intervals.full.days": "{number, plural, one {# день} few {# дня} other {# дней}}",
   "intervals.full.hours": "{number, plural, one {# час} few {# часа} other {# часов}}",
   "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} other {# минут}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Заголовок списка",
   "lists.search": "Искать из ваших подписок",
   "lists.subheading": "Ваши списки",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}",
   "loading_indicator.label": "Загрузка...",
   "media_gallery.toggle_visible": "Показать/скрыть",
   "missing_indicator.label": "Не найдено",
@@ -251,9 +253,10 @@
   "navigation_bar.profile_directory": "Каталог профилей",
   "navigation_bar.public_timeline": "Глобальная лента",
   "navigation_bar.security": "Безопасность",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} понравился Ваш статус",
-  "notification.follow": "{name} подписался(-лась) на Вас",
-  "notification.mention": "{name} упомянул(а) Вас",
+  "notification.follow": "{name} подписался (-лась) на вас",
+  "notification.mention": "{name} упомянул(а) вас",
   "notification.poll": "Опрос, в котором вы приняли участие, завершился",
   "notification.reblog": "{name} продвинул(а) Ваш статус",
   "notifications.clear": "Очистить уведомления",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Люди",
   "search_results.hashtags": "Хэштеги",
   "search_results.statuses": "Посты",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Поиск постов по их контенту не поддерживается на этом сервере Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
   "status.admin_account": "Открыть интерфейс модератора для @{name}",
   "status.admin_status": "Открыть этот статус в интерфейсе модератора",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "остались считанные мгновения",
   "time_remaining.seconds": "{number, plural, one {осталась # секунду} few {осталось # секунды} many {осталось # секунд} other {осталось # секунд}}",
   "trends.count_by_accounts": "Популярно у {count} {rawCount, plural, one {человека} few {человек} many {человек} other {человек}}",
+  "trends.refresh": "Обновить",
   "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.",
   "upload_area.title": "Перетащите сюда, чтобы загрузить",
   "upload_button.label": "Добавить медиаконтент",
   "upload_error.limit": "Достигнут лимит загруженных файлов.",
   "upload_error.poll": "К опросам нельзя прикреплять файлы.",
-  "upload_form.description": "Описать для людей с нарушениями зрения",
-  "upload_form.focus": "Обрезать",
+  "upload_form.description": "Добавьте описание для людей с нарушениями зрения:",
+  "upload_form.edit": "Изменить",
   "upload_form.undo": "Отменить",
+  "upload_modal.analyzing_picture": "Обработка изображения…",
+  "upload_modal.apply": "Применить",
+  "upload_modal.description_placeholder": "На дворе трава, на траве дрова",
+  "upload_modal.detect_text": "Найти текст на картинке",
+  "upload_modal.edit_media": "Изменение медиа",
+  "upload_modal.hint": "Нажмите и перетащите круг в предпросмотре в точку фокуса, которая всегда будет видна на эскизах.",
+  "upload_modal.preview_label": "Предпросмотр ({ratio})",
   "upload_progress.label": "Загрузка...",
   "video.close": "Закрыть видео",
   "video.exit_fullscreen": "Покинуть полноэкранный режим",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 3cc2cbaa7..4a7625aae 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -4,6 +4,7 @@
   "account.block": "Blokuj @{name}",
   "account.block_domain": "Ukry všetko z {domain}",
   "account.blocked": "Blokovaný/á",
+  "account.cancel_follow_request": "Zruš požiadanie o sledovanie",
   "account.direct": "Súkromná správa pre @{name}",
   "account.domain_blocked": "Doména ukrytá",
   "account.edit_profile": "Uprav profil",
@@ -23,7 +24,7 @@
   "account.mute": "Ignorovať @{name}",
   "account.mute_notifications": "Stĺm oboznámenia od @{name}",
   "account.muted": "Utíšený/á",
-  "account.posts": "Príspevky",
+  "account.posts": "Príspevkov",
   "account.posts_with_replies": "Príspevky aj s odpoveďami",
   "account.report": "Nahlás @{name}",
   "account.requested": "Čaká na schválenie. Klikni pre zrušenie žiadosti",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Zruš stĺmenie oboznámení od @{name}",
   "alert.unexpected.message": "Vyskytla sa nečakaná chyba.",
   "alert.unexpected.title": "Ups!",
+  "autosuggest_hashtag.per_week": "{count} týždenne",
   "boost_modal.combo": "Nabudúce môžeš kliknúť {combo} pre preskočenie",
   "bundle_column_error.body": "Pri načítaní tohto prvku nastala nejaká chyba.",
   "bundle_column_error.retry": "Skús to znova",
@@ -137,10 +139,10 @@
   "follow_request.authorize": "Povoľ prístup",
   "follow_request.reject": "Odmietni",
   "getting_started.developers": "Vývojári",
-  "getting_started.directory": "Databáza profilov",
+  "getting_started.directory": "Zoznam profilov",
   "getting_started.documentation": "Dokumentácia",
   "getting_started.heading": "Začni tu",
-  "getting_started.invite": "Pozvať ľudí",
+  "getting_started.invite": "Pozvi ľudí",
   "getting_started.open_source_notice": "Mastodon je softvér s otvoreným kódom. Nahlásiť chyby, alebo prispievať môžeš na GitHube v {github}.",
   "getting_started.security": "Zabezpečenie",
   "getting_started.terms": "Podmienky prevozu",
@@ -156,10 +158,10 @@
   "home.column_settings.basic": "Základné",
   "home.column_settings.show_reblogs": "Zobraziť povýšené",
   "home.column_settings.show_replies": "Ukázať odpovede",
-  "home.column_settings.update_live": "Update in real-time",
-  "intervals.full.days": "{number, plural, one {# deň} few {# dní} many {# dní} other {# dni}}",
-  "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodiny}}",
-  "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minúty}}",
+  "home.column_settings.update_live": "Aktualizuj v reálnom čase",
+  "intervals.full.days": "{number, plural, one {# deň} few {# dní} many {# dní} other {# dní}}",
+  "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodín}}",
+  "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minút}}",
   "introduction.federation.action": "Ďalej",
   "introduction.federation.federated.headline": "Federovaná",
   "introduction.federation.federated.text": "Verejné príspevky z ostatných serverov vo fediverse budú zobrazené vo federovanej časovej osi.",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Názov nového zoznamu",
   "lists.search": "Vyhľadávaj medzi užívateľmi, ktorých sleduješ",
   "lists.subheading": "Tvoje zoznamy",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# nová položka} other {# nových položiek}}",
   "loading_indicator.label": "Načítam...",
   "media_gallery.toggle_visible": "Zapni/Vypni viditeľnosť",
   "missing_indicator.label": "Nenájdené",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Katalóg profilov",
   "navigation_bar.public_timeline": "Federovaná časová os",
   "navigation_bar.security": "Zabezbečenie",
+  "notification.and_n_others": "a {count, plural,one {# ostatní} other {# ostatných}}",
   "notification.favourite": "{name} si obľúbil/a tvoj príspevok",
   "notification.follow": "{name} ťa začal/a následovať",
   "notification.mention": "{name} ťa spomenul/a",
@@ -278,8 +281,8 @@
   "notifications.filter.polls": "Výsledky ankiet",
   "notifications.group": "{count} oboznámení",
   "poll.closed": "Uzatvorená",
-  "poll.refresh": "Aktualizuj",
-  "poll.total_votes": "{count, plural, one {# hlas} few {# hlasov} many {# hlasov} other {# hlasy}}",
+  "poll.refresh": "Obnov",
+  "poll.total_votes": "{count, plural, one {# hlas} few {# hlasov} many {# hlasov} other {# hlasov}}",
   "poll.vote": "Hlasuj",
   "poll_button.add_poll": "Pridaj anketu",
   "poll_button.remove_poll": "Odstráň anketu",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Ľudia",
   "search_results.hashtags": "Haštagy",
   "search_results.statuses": "Príspevky",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Vyhľadávanie v obsahu príspevkov nieje na tomto Mastodon serveri povolené.",
   "search_results.total": "{count, number} {count, plural, one {výsledok} many {výsledkov} other {výsledky}}",
   "status.admin_account": "Otvor moderovacie rozhranie užívateľa @{name}",
   "status.admin_status": "Otvor tento príspevok v moderovacom rozhraní",
@@ -364,20 +367,28 @@
   "tabs_bar.local_timeline": "Miestna",
   "tabs_bar.notifications": "Oboznámenia",
   "tabs_bar.search": "Hľadaj",
-  "time_remaining.days": "Ostáva {number, plural, one {# deň} few {# dní} many {# dní} other {# dni}}",
+  "time_remaining.days": "Ostáva {number, plural, one {# deň} few {# dní} many {# dní} other {# dní}}",
   "time_remaining.hours": "Ostáva {number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodiny}}",
   "time_remaining.minutes": "Ostáva {number, plural, one {# minúta} few {# minút} many {# minút} other {# minúty}}",
   "time_remaining.moments": "Ostáva už iba chviľka",
-  "time_remaining.seconds": "Ostáva {number, plural, one {# sekunda} few {# sekúnd} many {# sekúnd} other {# sekundy}}",
+  "time_remaining.seconds": "Ostáva {number, plural, one {# sekunda} few {# sekúnd} many {# sekúnd} other {# sekúnd}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {človek vraví} other {ľudia vravia}}",
+  "trends.refresh": "Obnov",
   "ui.beforeunload": "Čo máš rozpísané sa stratí, ak opustíš Mastodon.",
   "upload_area.title": "Pretiahni a pusť pre nahratie",
   "upload_button.label": "Pridaj médiálny súbor (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Limit pre nahrávanie súborov bol prekročený.",
   "upload_error.poll": "Nahrávanie súborov pri anketách nieje možné.",
   "upload_form.description": "Opis pre slabo vidiacich",
-  "upload_form.focus": "Pozmeň náhľad",
+  "upload_form.edit": "Uprav",
   "upload_form.undo": "Vymaž",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Použi",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Uprav médiá",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Nahráva sa...",
   "video.close": "Zavri video",
   "video.exit_fullscreen": "Vypni zobrazenie na celú obrazovku",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index f79a7051a..8a5a0d4bb 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -4,6 +4,7 @@
   "account.block": "Blokiraj @{name}",
   "account.block_domain": "Skrij vse iz {domain}",
   "account.blocked": "Blokirano",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Neposredno sporočilo @{name}",
   "account.domain_blocked": "Skrita domena",
   "account.edit_profile": "Uredi profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Vklopi obvestila od @{name}",
   "alert.unexpected.message": "Zgodila se je nepričakovana napaka.",
   "alert.unexpected.title": "Uups!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Če želite preskočiti to, lahko pritisnete {combo}",
   "bundle_column_error.body": "Med nalaganjem te komponente je prišlo do napake.",
   "bundle_column_error.retry": "Poskusi ponovno",
@@ -132,7 +134,7 @@
   "empty_column.list": "Na tem seznamu ni ničesar. Ko bodo člani tega seznama objavili nove statuse, se bodo pojavili tukaj.",
   "empty_column.lists": "Nimate seznamov. Ko ga boste ustvarili, se bo prikazal tukaj.",
   "empty_column.mutes": "Niste utišali še nobenega uporabnika.",
-  "empty_column.notifications": "Nimate še nobenih obvestil. Poveži se z drugimi, da začnete pogovor.",
+  "empty_column.notifications": "Nimate še nobenih obvestil. Povežite se z drugimi, da začnete pogovor.",
   "empty_column.public": "Tukaj ni ničesar! Da ga napolnite, napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih strežnikov",
   "follow_request.authorize": "Overi",
   "follow_request.reject": "Zavrni",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "Osnovno",
   "home.column_settings.show_reblogs": "Pokaži spodbude",
   "home.column_settings.show_replies": "Pokaži odgovore",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "Posodabljaj v realnem času",
   "intervals.full.days": "{number, plural, one {# dan} two {# dni} few {# dni} other {# dni}}",
   "intervals.full.hours": "{number, plural, one {# ura} two {# uri} few {# ure} other {# ur}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Nov naslov seznama",
   "lists.search": "Išči med ljudmi, katerim sledite",
   "lists.subheading": "Vaši seznami",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# nov element} other {# novih elementov}}",
   "loading_indicator.label": "Nalaganje...",
   "media_gallery.toggle_visible": "Preklopi vidljivost",
   "missing_indicator.label": "Ni najdeno",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Imenik profilov",
   "navigation_bar.public_timeline": "Združena časovnica",
   "navigation_bar.security": "Varnost",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} je vzljubil/a vaš status",
   "notification.follow": "{name} vam sledi",
   "notification.mention": "{name} vas je omenil/a",
@@ -316,7 +319,7 @@
   "search_results.accounts": "Ljudje",
   "search_results.hashtags": "Ključniki",
   "search_results.statuses": "Tuti",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "Iskanje tutov po njihovi vsebini ni omogočeno na tem strežniku Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {rezultat} other {rezultatov}}",
   "status.admin_account": "Odpri vmesnik za moderiranje za @{name}",
   "status.admin_status": "Odpri status v vmesniku za moderiranje",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Preostali trenutki",
   "time_remaining.seconds": "{number, plural, one {# sekunda} other {# sekund}} je ostalo",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {oseba} other {ljudi}} govori",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.",
   "upload_area.title": "Za pošiljanje povlecite in spustite",
   "upload_button.label": "Dodaj medije ({formats})",
   "upload_error.limit": "Omejitev prenosa datoteke je presežena.",
   "upload_error.poll": "Prenos datoteke z anketami ni dovoljen.",
   "upload_form.description": "Opišite za slabovidne",
-  "upload_form.focus": "Spremeni predogled",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Izbriši",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Pošiljanje...",
   "video.close": "Zapri video",
   "video.exit_fullscreen": "Izhod iz celozaslonskega načina",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 21d45f2e8..9877ca93f 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -4,6 +4,7 @@
   "account.block": "Blloko @{name}",
   "account.block_domain": "Fshih gjithçka prej {domain}",
   "account.blocked": "E bllokuar",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Mesazh i drejtpërdrejt për @{name}",
   "account.domain_blocked": "Përkatësi e fshehur",
   "account.edit_profile": "Përpunoni profilin",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Hiqua ndalimin e shfaqjes njoftimeve nga @{name}",
   "alert.unexpected.message": "Ndodhi një gabim të papritur.",
   "alert.unexpected.title": "Hëm!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Mund të shtypni {combo}, që të anashkalohet kjo herës tjetër",
   "bundle_column_error.body": "Diç shkoi ters teksa ngarkohej ky përbërës.",
   "bundle_column_error.retry": "Riprovoni",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Rrjedhë kohore të federuarish",
   "navigation_bar.security": "Siguri",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} parapëlqeu gjendjen tuaj",
   "notification.follow": "{name} zuri t’ju ndjekë",
   "notification.mention": "{name} ju ka përmendur",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, një {person} {people} të tjerë} po flasin",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Skica juaj do të humbë nëse dilni nga Mastodon-i.",
   "upload_area.title": "Merreni & vëreni që të ngarkohet",
   "upload_button.label": "Shtoni media (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "U tejkalua kufi ngarkimi kartelash.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Përshkruajeni për persona me probleme shikimi",
-  "upload_form.focus": "Ndryshoni parapamjen",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Fshije",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Po ngarkohet…",
   "video.close": "Mbylle videon",
   "video.exit_fullscreen": "Dil nga mënyra Sa Krejt Ekrani",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 55bae4cdd..e60e2c7e8 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -4,6 +4,7 @@
   "account.block": "Blokiraj korisnika @{name}",
   "account.block_domain": "Sakrij sve sa domena {domain}",
   "account.blocked": "Blocked",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Direct Message @{name}",
   "account.domain_blocked": "Domain hidden",
   "account.edit_profile": "Izmeni profil",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}",
   "alert.unexpected.message": "An unexpected error occurred.",
   "alert.unexpected.title": "Oops!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put",
   "bundle_column_error.body": "Nešto je pošlo po zlu prilikom učitavanja ove komponente.",
   "bundle_column_error.retry": "Pokušajte ponovo",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federisana lajna",
   "navigation_bar.security": "Security",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} je stavio Vaš status kao omiljeni",
   "notification.follow": "{name} Vas je zapratio",
   "notification.mention": "{name} Vas je pomenuo",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Ako napustite Mastodont, izgubićete napisani nacrt.",
   "upload_area.title": "Prevucite ovde da otpremite",
   "upload_button.label": "Dodaj multimediju",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Opiši za slabovide osobe",
-  "upload_form.focus": "Crop",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Opozovi",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Otpremam...",
   "video.close": "Zatvori video",
   "video.exit_fullscreen": "Napusti ceo ekran",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index a4ae9fcaa..82833630c 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -4,6 +4,7 @@
   "account.block": "Блокирај @{name}",
   "account.block_domain": "Сакриј све са домена {domain}",
   "account.blocked": "Блокиран",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Директна порука @{name}",
   "account.domain_blocked": "Домен сакривен",
   "account.edit_profile": "Измени профил",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "Укључи назад обавештења од корисника @{name}",
   "alert.unexpected.message": "Појавила се неочекивана грешка.",
   "alert.unexpected.title": "Упс!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут",
   "bundle_column_error.body": "Нешто је пошло по злу приликом учитавања ове компоненте.",
   "bundle_column_error.retry": "Покушајте поново",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Здружена временска линија",
   "navigation_bar.security": "Безбедност",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} је ставио/ла Ваш статус као омиљени",
   "notification.follow": "{name} Вас је запратио/ла",
   "notification.mention": "{name} Вас је поменуо/ла",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {човек} other {људи}} прича",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Ако напустите Мастодонт, изгубићете написани нацрт.",
   "upload_area.title": "Превуците овде да отпремите",
   "upload_button.label": "Додај мултимедију (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Опишите за особе са оштећеним видом",
-  "upload_form.focus": "Подесите",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Обриши",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Отпремам...",
   "video.close": "Затвори видео",
   "video.exit_fullscreen": "Напусти цео екран",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index fda5c4d57..db28900ba 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -4,52 +4,54 @@
   "account.block": "Blockera @{name}",
   "account.block_domain": "Dölj allt från {domain}",
   "account.blocked": "Blockerad",
-  "account.direct": "Direktmeddelande @{name}",
+  "account.cancel_follow_request": "Cancel follow request",
+  "account.direct": "Skicka ett direktmeddelande till @{name}",
   "account.domain_blocked": "Domän dold",
   "account.edit_profile": "Redigera profil",
-  "account.endorse": "Feature on profile",
+  "account.endorse": "Visa upp på profil",
   "account.follow": "Följ",
   "account.followers": "Följare",
   "account.followers.empty": "Ingen följer denna användaren än.",
   "account.follows": "Följer",
-  "account.follows.empty": "This user doesn't follow anyone yet.",
+  "account.follows.empty": "Den här användaren följer inte någon ännu.",
   "account.follows_you": "Följer dig",
   "account.hide_reblogs": "Dölj knuffar från @{name}",
-  "account.link_verified_on": "Ownership of this link was checked on {date}",
+  "account.link_verified_on": "Ägarskapet för det här kontot kontrollerades den {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": "Nämna @{name}",
   "account.moved_to": "{name} har flyttat till:",
   "account.mute": "Tysta @{name}",
   "account.mute_notifications": "Stäng av notifieringar från @{name}",
-  "account.muted": "Nertystad",
+  "account.muted": "Tystad",
   "account.posts": "Inlägg",
   "account.posts_with_replies": "Toots och svar",
   "account.report": "Rapportera @{name}",
   "account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan",
-  "account.share": "Dela @{name}'s profil",
+  "account.share": "Dela @{name}s profil",
   "account.show_reblogs": "Visa knuffar från @{name}",
   "account.unblock": "Avblockera @{name}",
-  "account.unblock_domain": "Ta fram {domain}",
-  "account.unendorse": "Don't feature on profile",
+  "account.unblock_domain": "Sluta dölja {domain}",
+  "account.unendorse": "Visa inte upp på profil",
   "account.unfollow": "Sluta följa",
-  "account.unmute": "Ta bort tystad @{name}",
+  "account.unmute": "Sluta tysta @{name}",
   "account.unmute_notifications": "Återaktivera notifikationer från @{name}",
   "alert.unexpected.message": "Ett oväntat fel uppstod.",
-  "alert.unexpected.title": "Whups!",
+  "alert.unexpected.title": "Hoppsan!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång",
   "bundle_column_error.body": "Något gick fel när du laddade denna komponent.",
   "bundle_column_error.retry": "Försök igen",
   "bundle_column_error.title": "Nätverksfel",
   "bundle_modal_error.close": "Stäng",
-  "bundle_modal_error.message": "Något gick fel när du laddade denna komponent.",
+  "bundle_modal_error.message": "Något gick fel när denna komponent laddades.",
   "bundle_modal_error.retry": "Försök igen",
   "column.blocks": "Blockerade användare",
   "column.community": "Lokal tidslinje",
-  "column.direct": "Direktmeddelande",
+  "column.direct": "Direktmeddelanden",
   "column.domain_blocks": "Dolda domäner",
   "column.favourites": "Favoriter",
-  "column.follow_requests": "Följ förfrågningar",
+  "column.follow_requests": "Följförfrågningar",
   "column.home": "Hem",
   "column.lists": "Listor",
   "column.mutes": "Tystade användare",
@@ -65,64 +67,64 @@
   "column_header.unpin": "Ångra fäst",
   "column_subheading.settings": "Inställningar",
   "community.column_settings.media_only": "Enbart media",
-  "compose_form.direct_message_warning": "Denna toot kommer endast att skickas nämnda nämnda användare.",
+  "compose_form.direct_message_warning": "Denna toot kommer endast att skickas till nämnda användare.",
   "compose_form.direct_message_warning_learn_more": "Visa mer",
   "compose_form.hashtag_warning": "Denna toot kommer inte att listas under någon hashtag eftersom den är onoterad. Endast offentliga toots kan sökas med hashtag.",
-  "compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vemsomhelst kan följa dig och även se dina inlägg skrivna för endast dina följare.",
+  "compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vem som helst kan följa dig och även se dina inlägg som bara är för följare.",
   "compose_form.lock_disclaimer.lock": "låst",
   "compose_form.placeholder": "Vad funderar du på?",
-  "compose_form.poll.add_option": "Add a choice",
-  "compose_form.poll.duration": "Poll duration",
-  "compose_form.poll.option_placeholder": "Choice {number}",
-  "compose_form.poll.remove_option": "Remove this choice",
-  "compose_form.publish": "Toot",
+  "compose_form.poll.add_option": "Nytt alternativ",
+  "compose_form.poll.duration": "Varaktighet för omröstning",
+  "compose_form.poll.option_placeholder": "Alternativ {number}",
+  "compose_form.poll.remove_option": "Ta bort alternativ",
+  "compose_form.publish": "Tut",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.hide": "Mark media as sensitive",
+  "compose_form.sensitive.hide": "Markera media som känsligt",
   "compose_form.sensitive.marked": "Media har markerats som känsligt",
   "compose_form.sensitive.unmarked": "Media har inte markerats som känsligt",
   "compose_form.spoiler.marked": "Texten har dolts bakom en varning",
   "compose_form.spoiler.unmarked": "Texten är inte dold",
   "compose_form.spoiler_placeholder": "Skriv din varning här",
   "confirmation_modal.cancel": "Ångra",
-  "confirmations.block.block_and_report": "Block & Report",
+  "confirmations.block.block_and_report": "Blockera & rapportera",
   "confirmations.block.confirm": "Blockera",
   "confirmations.block.message": "Är du säker att du vill blockera {name}?",
   "confirmations.delete.confirm": "Ta bort",
   "confirmations.delete.message": "Är du säker att du vill ta bort denna status?",
-  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.confirm": "Ta bort",
   "confirmations.delete_list.message": "Är du säker på att du vill radera denna lista permanent?",
-  "confirmations.domain_block.confirm": "Blockera hela domänen",
+  "confirmations.domain_block.confirm": "Dölj hela domänen",
   "confirmations.domain_block.message": "Är du verkligen säker på att du vill blockera hela {domain}? I de flesta fall är några riktade blockeringar eller nedtystade konton tillräckligt och att föredra. Du kommer sluta se innehåll från {domain}-domänen i den allmänna tidslinjen och i dina egna notifieringar. Du kommer även sluta följa alla eventuella följare du har från {domain}.",
   "confirmations.mute.confirm": "Tysta",
   "confirmations.mute.message": "Är du säker du vill tysta ner {name}?",
   "confirmations.redraft.confirm": "Radera och gör om",
   "confirmations.redraft.message": "Är du säker på att du vill radera meddelandet och göra om det? Du kommer förlora alla svar, knuffar och favoriter som hänvisar till meddelandet.",
-  "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": "Svara",
+  "confirmations.reply.message": "Om du svarar nu kommer det att ersätta meddelandet du håller på att skriva. Är du säker på att du vill fortsätta?",
   "confirmations.unfollow.confirm": "Sluta följa",
   "confirmations.unfollow.message": "Är du säker på att du vill sluta följa {name}?",
   "embed.instructions": "Bädda in den här statusen på din webbplats genom att kopiera koden nedan.",
-  "embed.preview": "Här ser du hur det kommer att se ut:",
+  "embed.preview": "Så här kommer det att se ut:",
   "emoji_button.activity": "Aktivitet",
-  "emoji_button.custom": "Specialgjord",
+  "emoji_button.custom": "Anpassad",
   "emoji_button.flags": "Flaggor",
-  "emoji_button.food": "Mat & Dryck",
+  "emoji_button.food": "Mat & dryck",
   "emoji_button.label": "Lägg till emoji",
   "emoji_button.nature": "Natur",
   "emoji_button.not_found": "Inga emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objekt",
-  "emoji_button.people": "Människor",
+  "emoji_button.people": "Personer",
   "emoji_button.recent": "Ofta använda",
   "emoji_button.search": "Sök...",
   "emoji_button.search_results": "Sökresultat",
   "emoji_button.symbols": "Symboler",
-  "emoji_button.travel": "Resor & Platser",
-  "empty_column.account_timeline": "No toots here!",
-  "empty_column.account_unavailable": "Profile unavailable",
-  "empty_column.blocks": "You haven't blocked any users yet.",
-  "empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att få bollen att rulla!",
-  "empty_column.direct": "Du har inga direktmeddelanden än. När du skickar eller tar emot kommer den att dyka upp här.",
-  "empty_column.domain_blocks": "There are no hidden domains yet.",
+  "emoji_button.travel": "Resor & platser",
+  "empty_column.account_timeline": "Inga inlägg här!",
+  "empty_column.account_unavailable": "Profilen är inte tillgänglig",
+  "empty_column.blocks": "Du har ännu inte blockerat några användare.",
+  "empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att sätta bollen i rullning!",
+  "empty_column.direct": "Du har inga direktmeddelanden än. När du skickar eller tar emot ett kommer det att dyka upp här.",
+  "empty_column.domain_blocks": "Det finns ännu inga dolda domäner.",
   "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.",
@@ -131,84 +133,84 @@
   "empty_column.home.public_timeline": "den publika tidslinjen",
   "empty_column.list": "Det finns inget i denna lista än. När medlemmar i denna lista lägger till nya statusar kommer de att visas här.",
   "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.mutes": "Du har ännu inte tystat några användare.",
   "empty_column.notifications": "Du har inga meddelanden än. Interagera med andra för att starta konversationen.",
   "empty_column.public": "Det finns inget här! Skriv något offentligt, eller följ manuellt användarna från andra instanser för att fylla på det",
   "follow_request.authorize": "Godkänn",
   "follow_request.reject": "Avvisa",
   "getting_started.developers": "Utvecklare",
-  "getting_started.directory": "Profile directory",
-  "getting_started.documentation": "Documentation",
+  "getting_started.directory": "Profilkatalog",
+  "getting_started.documentation": "Dokumentation",
   "getting_started.heading": "Kom igång",
   "getting_started.invite": "Skicka inbjudningar",
   "getting_started.open_source_notice": "Mastodon är programvara med öppen källkod. Du kan bidra eller rapportera problem via GitHub på {github}.",
   "getting_started.security": "Säkerhet",
   "getting_started.terms": "Användarvillkor",
-  "hashtag.column_header.tag_mode.all": "and {additional}",
-  "hashtag.column_header.tag_mode.any": "or {additional}",
-  "hashtag.column_header.tag_mode.none": "without {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
-  "hashtag.column_settings.tag_mode.all": "All of these",
-  "hashtag.column_settings.tag_mode.any": "Any of these",
+  "hashtag.column_header.tag_mode.all": "och {additional}",
+  "hashtag.column_header.tag_mode.any": "eller {additional}",
+  "hashtag.column_header.tag_mode.none": "utan {additional}",
+  "hashtag.column_settings.select.no_options_message": "Inga förslag hittades",
+  "hashtag.column_settings.select.placeholder": "Ange hashtags …",
+  "hashtag.column_settings.tag_mode.all": "Alla dessa",
+  "hashtag.column_settings.tag_mode.any": "Någon av dessa",
   "hashtag.column_settings.tag_mode.none": "Ingen av dessa",
   "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
   "home.column_settings.basic": "Grundläggande",
   "home.column_settings.show_reblogs": "Visa knuffar",
   "home.column_settings.show_replies": "Visa svar",
-  "home.column_settings.update_live": "Update in real-time",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
-  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
+  "home.column_settings.update_live": "Uppdatera i realtid",
+  "intervals.full.days": "{number, plural, one {# dag} other {# dagar}}",
+  "intervals.full.hours": "{hours, plural, one {# timme} other {# timmar}}",
+  "intervals.full.minutes": "{minutes, plural, one {1 minut} other {# minuter}}",
   "introduction.federation.action": "Nästa",
   "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.headline": "Hem",
   "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 toot-orial!",
+  "introduction.interactions.action": "Slutför introduktionsguide!",
   "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.action": "Sätt igång!",
   "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": "att navigera tillbaka",
-  "keyboard_shortcuts.blocked": "to open blocked users list",
-  "keyboard_shortcuts.boost": "att knuffa",
-  "keyboard_shortcuts.column": "att fokusera en status i en av kolumnerna",
-  "keyboard_shortcuts.compose": "att fokusera komponera text fältet",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.direct": "to open direct messages column",
-  "keyboard_shortcuts.down": "att flytta ner i listan",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "att favorisera",
-  "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": "Snabbvalstangent",
-  "keyboard_shortcuts.legend": "att visa denna översikt",
-  "keyboard_shortcuts.local": "to open local timeline",
-  "keyboard_shortcuts.mention": "att nämna författaren",
-  "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": "att svara",
-  "keyboard_shortcuts.requests": "to open follow requests list",
-  "keyboard_shortcuts.search": "att fokusera sökfältet",
-  "keyboard_shortcuts.start": "to open \"get started\" column",
-  "keyboard_shortcuts.toggle_hidden": "att visa/gömma text bakom CW",
-  "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
-  "keyboard_shortcuts.toot": "att börja en helt ny toot",
-  "keyboard_shortcuts.unfocus": "att avfokusera komponera text fält / sökfält",
-  "keyboard_shortcuts.up": "att flytta upp i listan",
+  "introduction.welcome.text": "Välkommen till fediverse! Om några ögonblick kommer du kunna sända ut meddelanden och prata med dina vänner över en mängd servrar. Men den här servern, {domain}, är speciell — den är hem åt din profil, så kom ihåg vad den heter.",
+  "keyboard_shortcuts.back": "för att gå bakåt",
+  "keyboard_shortcuts.blocked": "för att öppna listan över blockerade användare",
+  "keyboard_shortcuts.boost": "för att knuffa",
+  "keyboard_shortcuts.column": "för att fokusera en status i en av kolumnerna",
+  "keyboard_shortcuts.compose": "för att fokusera skrivfältet",
+  "keyboard_shortcuts.description": "Beskrivning",
+  "keyboard_shortcuts.direct": "för att öppna Direktmeddelanden",
+  "keyboard_shortcuts.down": "för att flytta nedåt i listan",
+  "keyboard_shortcuts.enter": "för att öppna en status",
+  "keyboard_shortcuts.favourite": "för att sätta som favorit",
+  "keyboard_shortcuts.favourites": "för att öppna Favoriter",
+  "keyboard_shortcuts.federated": "för att öppna Förenad tidslinje",
+  "keyboard_shortcuts.heading": "Tangentbordsgenvägar",
+  "keyboard_shortcuts.home": "för att öppna Hem-tidslinjen",
+  "keyboard_shortcuts.hotkey": "Kommando",
+  "keyboard_shortcuts.legend": "för att visa denna översikt",
+  "keyboard_shortcuts.local": "för att öppna Lokal tidslinje",
+  "keyboard_shortcuts.mention": "för att nämna skaparen",
+  "keyboard_shortcuts.muted": "för att öppna listan över tystade användare",
+  "keyboard_shortcuts.my_profile": "för att öppna din profil",
+  "keyboard_shortcuts.notifications": "för att öppna Meddelanden",
+  "keyboard_shortcuts.pinned": "för att öppna Nålade toots",
+  "keyboard_shortcuts.profile": "för att öppna skaparens profil",
+  "keyboard_shortcuts.reply": "för att svara",
+  "keyboard_shortcuts.requests": "för att öppna Följförfrågningar",
+  "keyboard_shortcuts.search": "för att fokusera sökfältet",
+  "keyboard_shortcuts.start": "för att öppna \"Kom igång\"-kolumnen",
+  "keyboard_shortcuts.toggle_hidden": "för att visa/gömma text bakom CW",
+  "keyboard_shortcuts.toggle_sensitivity": "för att visa/gömma media",
+  "keyboard_shortcuts.toot": "för att påbörja en helt ny toot",
+  "keyboard_shortcuts.unfocus": "för att avfokusera skrivfält/sökfält",
+  "keyboard_shortcuts.up": "för att flytta uppåt i listan",
   "lightbox.close": "Stäng",
   "lightbox.next": "Nästa",
   "lightbox.previous": "Tidigare",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "Ny listrubrik",
   "lists.search": "Sök bland personer du följer",
   "lists.subheading": "Dina listor",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, other {# objekt}}",
   "loading_indicator.label": "Laddar...",
   "media_gallery.toggle_visible": "Växla synlighet",
   "missing_indicator.label": "Hittades inte",
@@ -241,7 +243,7 @@
   "navigation_bar.follow_requests": "Följförfrågningar",
   "navigation_bar.follows_and_followers": "Follows and followers",
   "navigation_bar.info": "Om denna instans",
-  "navigation_bar.keyboard_shortcuts": "Tangentbordsgenvägar",
+  "navigation_bar.keyboard_shortcuts": "Kortkommandon",
   "navigation_bar.lists": "Listor",
   "navigation_bar.logout": "Logga ut",
   "navigation_bar.mutes": "Tystade användare",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Förenad tidslinje",
   "navigation_bar.security": "Säkerhet",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} favoriserade din status",
   "notification.follow": "{name} följer dig",
   "notification.mention": "{name} nämnde dig",
@@ -340,7 +343,7 @@
   "status.pin": "Fäst i profil",
   "status.pinned": "Fäst toot",
   "status.read_more": "Läs mer",
-  "status.reblog": "Knuff",
+  "status.reblog": "Knuffa",
   "status.reblog_private": "Knuffa till de ursprungliga åhörarna",
   "status.reblogged_by": "{name} knuffade",
   "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
@@ -364,20 +367,28 @@
   "tabs_bar.local_timeline": "Lokal",
   "tabs_bar.notifications": "Meddelanden",
   "tabs_bar.search": "Sök",
-  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
-  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
-  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.days": "{number, plural, one {# dag} other {# dagar}} kvar",
+  "time_remaining.hours": "{hours, plural, one {# timme} other {# timmar}} kvar",
+  "time_remaining.minutes": "{minutes, plural, one {1 minut} other {# minuter}} kvar",
   "time_remaining.moments": "Moments remaining",
-  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
+  "time_remaining.seconds": "{hours, plural, one {# sekund} other {# sekunder}} kvar",
   "trends.count_by_accounts": "{count} {rawCount, plural, en {person} andra {people}} pratar",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.",
   "upload_area.title": "Dra & släpp för att ladda upp",
   "upload_button.label": "Lägg till media",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "Beskriv för synskadade",
-  "upload_form.focus": "Beskär",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Ta bort",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Laddar upp...",
   "video.close": "Stäng video",
   "video.exit_fullscreen": "Stäng helskärm",
diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json
index 87163e660..7fa7db98b 100644
--- a/app/javascript/mastodon/locales/ta.json
+++ b/app/javascript/mastodon/locales/ta.json
@@ -4,6 +4,7 @@
   "account.block": "Block @{name}",
   "account.block_domain": "எல்லாவற்றையும் மறைக்க {domain}",
   "account.blocked": "தடைமுட்டுகள்",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "நேரடி செய்தி @{name}",
   "account.domain_blocked": "டொமைன் மறைக்கப்பட்டது",
   "account.edit_profile": "சுயவிவரத்தைத் திருத்தவும்",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "அறிவிப்புகளை அகற்றவும் @{name}",
   "alert.unexpected.message": "எதிர் பாராத பிழை ஏற்பட்டு விட்டது.",
   "alert.unexpected.title": "அச்சச்சோ!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "நீங்கள் அழுத்தவும் {combo} அடுத்த முறை தவிர்க்கவும்",
   "bundle_column_error.body": "இந்த கூறுகளை ஏற்றும்போது ஏதோ தவறு ஏற்பட்டது.",
   "bundle_column_error.retry": "மீண்டும் முயற்சி செய்",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "கூட்டாட்சி காலக்கெடு",
   "navigation_bar.security": "பத்திரம்",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} ஆர்வம் கொண்டவர், உங்கள் நிலை",
   "notification.follow": "{name} நீங்கள் தொடர்ந்து வந்தீர்கள்",
   "notification.mention": "{name} நீங்கள் குறிப்பிட்டுள்ளீர்கள்",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "தருணங்கள் மீதமுள்ளன",
   "time_remaining.seconds": "{number, plural, one {# second} மற்ற {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} மற்ற {people}} உரையாடு",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "நீங்கள் வெளியே சென்றால் உங்கள் வரைவு இழக்கப்படும் மஸ்தோடோன்.",
   "upload_area.title": "பதிவேற்ற & இழுக்கவும்",
   "upload_button.label": "மீடியாவைச் சேர்க்கவும் (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "கோப்பு பதிவேற்ற வரம்பு மீறப்பட்டது.",
   "upload_error.poll": "கோப்பு பதிவேற்றம் அனுமதிக்கப்படவில்லை.",
   "upload_form.description": "பார்வையற்ற விவரிக்கவும்",
-  "upload_form.focus": "மாற்றம் முன்னோட்டம்",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Delete",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "ஏற்றுகிறது ...",
   "video.close": "வீடியோவை மூடு",
   "video.exit_fullscreen": "முழு திரையில் இருந்து வெளியேறவும்",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index ccb608812..0f7a617bb 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -4,6 +4,7 @@
   "account.block": "@{name} ను బ్లాక్ చేయి",
   "account.block_domain": "{domain} నుంచి అన్నీ దాచిపెట్టు",
   "account.blocked": "బ్లాక్ అయినవి",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "@{name}కు నేరుగా సందేశం పంపు",
   "account.domain_blocked": "డొమైన్ దాచిపెట్టబడినది",
   "account.edit_profile": "ప్రొఫైల్ని సవరించండి",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "@{name} నుంచి ప్రకటనలపై మ్యూట్ ని తొలగించు",
   "alert.unexpected.message": "అనుకోని తప్పు జరిగినది.",
   "alert.unexpected.title": "అయ్యో!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "మీరు తదుపరిసారి దీనిని దాటవేయడానికి {combo} నొక్కవచ్చు",
   "bundle_column_error.body": "ఈ భాగం లోడ్ అవుతున్నప్పుడు ఏదో తప్పు జరిగింది.",
   "bundle_column_error.retry": "మళ్ళీ ప్రయత్నించండి",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "సమాఖ్య కాలక్రమం",
   "navigation_bar.security": "భద్రత",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} మీ స్టేటస్ ను ఇష్టపడ్డారు",
   "notification.follow": "{name} మిమ్మల్ని అనుసరిస్తున్నారు",
   "notification.mention": "{name} మిమ్మల్ని ప్రస్తావించారు",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "కొన్ని క్షణాలు మాత్రమే మిగిలి ఉన్నాయి",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} మాట్లాడుతున్నారు",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "మీరు మాస్టొడొన్ను వదిలివేస్తే మీ డ్రాఫ్ట్లు పోతాయి.",
   "upload_area.title": "అప్లోడ్ చేయడానికి డ్రాగ్ & డ్రాప్ చేయండి",
   "upload_button.label": "మీడియాను జోడించండి (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "దృష్టి లోపమున్న వారి కోసం వివరించండి",
-  "upload_form.focus": "ప్రివ్యూను మార్చు",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "తొలగించు",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "అప్లోడ్ అవుతోంది...",
   "video.close": "వీడియోని మూసివేయి",
   "video.exit_fullscreen": "పూర్తి స్క్రీన్ నుండి నిష్క్రమించు",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index e8d7a27ed..5bf79ded8 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -4,6 +4,7 @@
   "account.block": "ปิดกั้น @{name}",
   "account.block_domain": "ซ่อนทุกอย่างจาก {domain}",
   "account.blocked": "ปิดกั้นอยู่",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "ส่งข้อความโดยตรงถึง @{name}",
   "account.domain_blocked": "ซ่อนโดเมนอยู่",
   "account.edit_profile": "แก้ไขโปรไฟล์",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "เลิกปิดเสียงการแจ้งเตือนจาก @{name}",
   "alert.unexpected.message": "เกิดข้อผิดพลาดที่ไม่คาดคิด",
   "alert.unexpected.title": "อุปส์!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "คุณสามารถกด {combo} เพื่อข้ามสิ่งนี้ในครั้งถัดไป",
   "bundle_column_error.body": "มีบางอย่างผิดพลาดขณะโหลดส่วนประกอบนี้",
   "bundle_column_error.retry": "ลองอีกครั้ง",
@@ -72,7 +74,7 @@
   "compose_form.lock_disclaimer.lock": "ล็อคอยู่",
   "compose_form.placeholder": "คุณกำลังคิดอะไรอยู่?",
   "compose_form.poll.add_option": "เพิ่มทางเลือก",
-  "compose_form.poll.duration": "ระยะเวลาการหยั่งเสียง",
+  "compose_form.poll.duration": "ระยะเวลาโพล",
   "compose_form.poll.option_placeholder": "ทางเลือก {number}",
   "compose_form.poll.remove_option": "เอาทางเลือกนี้ออก",
   "compose_form.publish": "โพสต์",
@@ -156,13 +158,13 @@
   "home.column_settings.basic": "พื้นฐาน",
   "home.column_settings.show_reblogs": "แสดงการดัน",
   "home.column_settings.show_replies": "แสดงการตอบกลับ",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "อัปเดตตามเวลาจริง",
   "intervals.full.days": "{number, plural, other {# วัน}}",
   "intervals.full.hours": "{number, plural, other {# ชั่วโมง}}",
   "intervals.full.minutes": "{number, plural, other {# นาที}}",
   "introduction.federation.action": "ถัดไป",
   "introduction.federation.federated.headline": "ที่ติดต่อกับภายนอก",
-  "introduction.federation.federated.text": "โพสต์สาธารณะจากเซิร์ฟเวอร์อื่น ๆ ของ Fediverse จะปรากฏในเส้นเวลาที่ติดต่อกับภายนอก",
+  "introduction.federation.federated.text": "โพสต์สาธารณะจากเซิร์ฟเวอร์อื่น ๆ ของเฟดิเวิร์สจะปรากฏในเส้นเวลาที่ติดต่อกับภายนอก",
   "introduction.federation.home.headline": "หน้าแรก",
   "introduction.federation.home.text": "โพสต์จากผู้คนที่คุณติดตามจะปรากฏในฟีดหน้าแรกของคุณ คุณสามารถติดตามใครก็ตามในเซิร์ฟเวอร์ใดก็ตาม!",
   "introduction.federation.local.headline": "ในเว็บ",
@@ -222,7 +224,7 @@
   "lists.new.title_placeholder": "ชื่อเรื่องรายการใหม่",
   "lists.search": "ค้นหาในหมู่ผู้คนที่คุณติดตาม",
   "lists.subheading": "รายการของคุณ",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, other {# รายการใหม่}}",
   "loading_indicator.label": "กำลังโหลด...",
   "media_gallery.toggle_visible": "เปิด/ปิดการมองเห็น",
   "missing_indicator.label": "ไม่พบ",
@@ -251,10 +253,11 @@
   "navigation_bar.profile_directory": "ไดเรกทอรีโปรไฟล์",
   "navigation_bar.public_timeline": "เส้นเวลาที่ติดต่อกับภายนอก",
   "navigation_bar.security": "ความปลอดภัย",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} ได้ชื่นชอบสถานะของคุณ",
   "notification.follow": "{name} ได้ติดตามคุณ",
   "notification.mention": "{name} ได้กล่าวถึงคุณ",
-  "notification.poll": "การหยั่งเสียงที่คุณได้ลงคะแนนได้สิ้นสุดแล้ว",
+  "notification.poll": "โพลที่คุณได้ลงคะแนนได้สิ้นสุดแล้ว",
   "notification.reblog": "{name} ได้ดันสถานะของคุณ",
   "notifications.clear": "ล้างการแจ้งเตือน",
   "notifications.clear_confirmation": "คุณแน่ใจหรือไม่ว่าต้องการล้างการแจ้งเตือนทั้งหมดของคุณอย่างถาวร?",
@@ -265,7 +268,7 @@
   "notifications.column_settings.filter_bar.show": "แสดง",
   "notifications.column_settings.follow": "ผู้ติดตามใหม่:",
   "notifications.column_settings.mention": "การกล่าวถึง:",
-  "notifications.column_settings.poll": "ผลลัพธ์การหยั่งเสียง:",
+  "notifications.column_settings.poll": "ผลลัพธ์โพล:",
   "notifications.column_settings.push": "การแจ้งเตือนแบบผลัก",
   "notifications.column_settings.reblog": "การดัน:",
   "notifications.column_settings.show": "แสดงในคอลัมน์",
@@ -275,14 +278,14 @@
   "notifications.filter.favourites": "รายการโปรด",
   "notifications.filter.follows": "การติดตาม",
   "notifications.filter.mentions": "การกล่าวถึง",
-  "notifications.filter.polls": "ผลลัพธ์การหยั่งเสียง",
+  "notifications.filter.polls": "ผลลัพธ์โพล",
   "notifications.group": "{count} การแจ้งเตือน",
   "poll.closed": "ปิดแล้ว",
   "poll.refresh": "รีเฟรช",
   "poll.total_votes": "{count, plural, other {# การลงคะแนน}}",
   "poll.vote": "ลงคะแนน",
-  "poll_button.add_poll": "เพิ่มการหยั่งเสียง",
-  "poll_button.remove_poll": "เอาการหยั่งเสียงออก",
+  "poll_button.add_poll": "เพิ่มโพล",
+  "poll_button.remove_poll": "เอาโพลออก",
   "privacy.change": "ปรับเปลี่ยนความเป็นส่วนตัวของสถานะ",
   "privacy.direct.long": "โพสต์ไปยังผู้ใช้ที่กล่าวถึงเท่านั้น",
   "privacy.direct.short": "โดยตรง",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "ช่วงเวลาที่เหลือ",
   "time_remaining.seconds": "เหลืออีก {number, plural, other {# วินาที}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "แบบร่างของคุณจะหายไปหากคุณออกจาก Mastodon",
   "upload_area.title": "ลากแล้วปล่อยเพื่ออัปโหลด",
   "upload_button.label": "เพิ่มสื่อ (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "เกินขีดจำกัดการอัปโหลดไฟล์",
   "upload_error.poll": "ไม่อนุญาตให้อัปโหลดไฟล์กับการลงคะแนน",
   "upload_form.description": "อธิบายสำหรับผู้บกพร่องทางการมองเห็น",
-  "upload_form.focus": "ตัวอย่างการเปลี่ยนแปลง",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "ลบ",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "กำลังอัปโหลด...",
   "video.close": "ปิดวิดีโอ",
   "video.exit_fullscreen": "ออกจากเต็มหน้าจอ",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 0ea015cc6..3638b0582 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -4,6 +4,7 @@
   "account.block": "Engelle @{name}",
   "account.block_domain": "{domain} alanından her şeyi gizle",
   "account.blocked": "Engellenmiş",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Mesaj gönder : @{name}",
   "account.domain_blocked": "Alan adı gizlendi",
   "account.edit_profile": "Profili düzenle",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "@{name} kullanıcısından bildirimleri aç",
   "alert.unexpected.message": "Beklenmedik bir hata oluştu.",
   "alert.unexpected.title": "Hay aksi!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz",
   "bundle_column_error.body": "Bu bileşen yüklenirken bir şeyler ters gitti.",
   "bundle_column_error.retry": "Tekrar deneyin",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "Federe zaman tüneli",
   "navigation_bar.security": "Güvenlik",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} senin durumunu favorilere ekledi",
   "notification.follow": "{name} seni takip ediyor",
   "notification.mention": "{name} mentioned you",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Mastodon'dan ayrılırsanız taslağınız kaybolacak.",
   "upload_area.title": "Karşıya yükleme için sürükle bırak yapınız",
   "upload_button.label": "Görsel ekle",
   "upload_error.limit": "Dosya yükleme sınırı aşıldı.",
   "upload_error.poll": "Anketlerde dosya yüklemesine izin verilmez.",
   "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Kırp",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Geri al",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Yükleniyor...",
   "video.close": "Videoyu kapat",
   "video.exit_fullscreen": "Tam ekrandan çık",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 17e8cb49f..69bf016f9 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -1,22 +1,23 @@
 {
-  "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}",
   "account.blocked": "Заблоковані",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "Пряме повідомлення @{name}",
   "account.domain_blocked": "Домен приховано",
   "account.edit_profile": "Редагувати профіль",
   "account.endorse": "Feature on profile",
   "account.follow": "Підписатися",
   "account.followers": "Підписники",
-  "account.followers.empty": "No one follows this user yet.",
+  "account.followers.empty": "Ніхто ще не підписався на цього користувача.",
   "account.follows": "Підписки",
-  "account.follows.empty": "This user doesn't follow anyone yet.",
+  "account.follows.empty": "Цей користувач ще ні на кого не підписався.",
   "account.follows_you": "Підписаний(-а) на Вас",
   "account.hide_reblogs": "Сховати передмухи від @{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": "Права власності на це посилання були перевірені {date}",
+  "account.locked_info": "Статус конфіденційності цього облікового запису встановлено у заблокований. Власник вручну переглядає, хто може за ним стежити.",
   "account.media": "Медіа",
   "account.mention": "Згадати @{name}",
   "account.moved_to": "{name} переїхав на:",
@@ -29,21 +30,22 @@
   "account.requested": "Очікує підтвердження. Натисніть щоб відмінити запит",
   "account.share": "Поширити профіль @{name}",
   "account.show_reblogs": "Показати передмухи від @{name}",
-  "account.unblock": "Розблокувати",
+  "account.unblock": "Розблокувати @{name}",
   "account.unblock_domain": "Розблокувати {domain}",
   "account.unendorse": "Don't feature on profile",
   "account.unfollow": "Відписатися",
-  "account.unmute": "Зняти глушення @{name}",
+  "account.unmute": "Зняти глушення з @{name}",
   "account.unmute_notifications": "Показувати сповіщення від @{name}",
   "alert.unexpected.message": "Трапилась неочікувана помилка.",
   "alert.unexpected.title": "Ой!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
-  "bundle_column_error.body": "Щось пішло не так при завантаженні компоненту.",
-  "bundle_column_error.retry": "Спробуйте ще",
+  "bundle_column_error.body": "Щось пішло не так під час завантаження компоненту.",
+  "bundle_column_error.retry": "Спробуйте ще раз",
   "bundle_column_error.title": "Помилка мережі",
   "bundle_modal_error.close": "Закрити",
-  "bundle_modal_error.message": "Щось пішло не так при завантаженні компоненту.",
-  "bundle_modal_error.retry": "Спробувати ще",
+  "bundle_modal_error.message": "Щось пішло не так під час завантаження компоненту.",
+  "bundle_modal_error.retry": "Спробувати ще раз",
   "column.blocks": "Заблоковані користувачі",
   "column.community": "Локальна стрічка",
   "column.direct": "Прямі повідомлення",
@@ -58,7 +60,7 @@
   "column.public": "Глобальна стрічка",
   "column_back_button.label": "Назад",
   "column_header.hide_settings": "Приховати налаштування",
-  "column_header.moveLeft_settings": "Move column to the left",
+  "column_header.moveLeft_settings": "Змістити колонку вліво",
   "column_header.moveRight_settings": "Змістити колонку вправо",
   "column_header.pin": "Закріпити",
   "column_header.show_settings": "Показати налаштування",
@@ -66,25 +68,25 @@
   "column_subheading.settings": "Налаштування",
   "community.column_settings.media_only": "Тільки медіа",
   "compose_form.direct_message_warning": "Цей дмух буде видимий тільки згаданим користувачам.",
-  "compose_form.direct_message_warning_learn_more": "Дізнатись більше",
-  "compose_form.hashtag_warning": "Цей дмух не буде відображений у жодній стрічці хештеґу, так як він прихований. Тільки публічні дмухи можуть бути знайдені за хештеґом.",
+  "compose_form.direct_message_warning_learn_more": "Дізнатися більше",
+  "compose_form.hashtag_warning": "Цей дмух не буде відображений у жодній стрічці хештеґу, оскільки він прихований. Тільки публічні дмухи можуть бути знайдені за хештеґом.",
   "compose_form.lock_disclaimer": "Ваш акаунт не {locked}. Кожен може підписатися на Вас та бачити Ваші приватні пости.",
   "compose_form.lock_disclaimer.lock": "приватний",
   "compose_form.placeholder": "Що у Вас на думці?",
-  "compose_form.poll.add_option": "Add a choice",
-  "compose_form.poll.duration": "Poll duration",
-  "compose_form.poll.option_placeholder": "Choice {number}",
-  "compose_form.poll.remove_option": "Remove this choice",
+  "compose_form.poll.add_option": "Додати варіант",
+  "compose_form.poll.duration": "Тривалість опитування",
+  "compose_form.poll.option_placeholder": "Варіант {number}",
+  "compose_form.poll.remove_option": "Видалити цей варіант",
   "compose_form.publish": "Дмухнути",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.hide": "Mark media as sensitive",
-  "compose_form.sensitive.marked": "Медіа відмічене <b>несприйнятливим</b>",
-  "compose_form.sensitive.unmarked": "Медіа відмічене сприйнятливим",
-  "compose_form.spoiler.marked": "Текст приховано за попередженням",
+  "compose_form.sensitive.hide": "Позначити медіа як дражливе",
+  "compose_form.sensitive.marked": "Медіа відмічене як дражливе",
+  "compose_form.sensitive.unmarked": "Медіа не відмічене як дражливе",
+  "compose_form.spoiler.marked": "Текст приховано під попередженням",
   "compose_form.spoiler.unmarked": "Текст видимий",
-  "compose_form.spoiler_placeholder": "Попередження щодо прихованого тексту",
+  "compose_form.spoiler_placeholder": "Напишіть своє попередження тут",
   "confirmation_modal.cancel": "Відмінити",
-  "confirmations.block.block_and_report": "Block & Report",
+  "confirmations.block.block_and_report": "Заблокувати та поскаржитися",
   "confirmations.block.confirm": "Заблокувати",
   "confirmations.block.message": "Ви впевнені, що хочете заблокувати {name}?",
   "confirmations.delete.confirm": "Видалити",
@@ -95,13 +97,13 @@
   "confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів. Ви не зможете бачити контент з цього домену у будь-яких стрічках або ваших сповіщеннях. Ваші підписники з цього домену будуть відписані від вас.",
   "confirmations.mute.confirm": "Заглушити",
   "confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?",
-  "confirmations.redraft.confirm": "Видалити і перестворити",
+  "confirmations.redraft.confirm": "Видалити та перестворити",
   "confirmations.redraft.message": "Ви впевнені, що хочете видалити допис і перестворити його? Ви втратите всі відповіді, передмухи та вподобайки допису.",
-  "confirmations.reply.confirm": "Reply",
-  "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
+  "confirmations.reply.confirm": "Відповісти",
+  "confirmations.reply.message": "Поточна відповідь перезапише повідомлення, яке ви зараз пишете. Ви впевнені, що хочете продовжити?",
   "confirmations.unfollow.confirm": "Відписатися",
   "confirmations.unfollow.message": "Ви впевнені, що хочете відписатися від {name}?",
-  "embed.instructions": "Інтегруйте цей статус на вашому вебсайті, скопіювавши код нижче.",
+  "embed.instructions": "Вбудуйте цей статус до вашого вебсайту, скопіювавши код нижче.",
   "embed.preview": "Ось як він виглядатиме:",
   "emoji_button.activity": "Заняття",
   "emoji_button.custom": "Особливі",
@@ -113,125 +115,125 @@
   "emoji_button.objects": "Предмети",
   "emoji_button.people": "Люди",
   "emoji_button.recent": "Часто використовувані",
-  "emoji_button.search": "Знайти...",
+  "emoji_button.search": "Шукати...",
   "emoji_button.search_results": "Результати пошуку",
   "emoji_button.symbols": "Символи",
   "emoji_button.travel": "Подорожі",
-  "empty_column.account_timeline": "No toots here!",
-  "empty_column.account_unavailable": "Profile unavailable",
-  "empty_column.blocks": "You haven't blocked any users yet.",
+  "empty_column.account_timeline": "Тут дмухалок немає!",
+  "empty_column.account_unavailable": "Профіль недоступний",
+  "empty_column.blocks": "Ви ще не заблокували жодного користувача.",
   "empty_column.community": "Локальна стрічка пуста. Напишіть щось, щоб розігріти народ!",
   "empty_column.direct": "У вас ще немає прямих повідомлень. Коли ви відправите чи отримаєте якесь, воно з'явиться тут.",
-  "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.domain_blocks": "Тут поки немає прихованих доменів.",
+  "empty_column.favourited_statuses": "У вас ще немає вподобаних дмухів. Коли ви щось вподобаєте, воно з'явиться тут.",
+  "empty_column.favourites": "Ніхто ще не вподобав цього дмуху. Коли хтось це зробить, вони з'являться тут.",
+  "empty_column.follow_requests": "У вас ще немає запитів на підписку. Коли ви їх отримаєте, вони з'являться тут.",
   "empty_column.hashtag": "Дописів з цим хештегом поки не існує.",
   "empty_column.home": "Ви поки ні на кого не підписані. Погортайте {public}, або скористуйтесь пошуком, щоб освоїтися та познайомитися з іншими користувачами.",
   "empty_column.home.public_timeline": "публічні стрічки",
   "empty_column.list": "Немає нічого в цьому списку. Коли його учасники дмухнуть нові статуси, вони з'являться тут.",
-  "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
-  "empty_column.mutes": "You haven't muted any users yet.",
+  "empty_column.lists": "У вас ще немає списків. Коли ви їх створите, вони з'являться тут.",
+  "empty_column.mutes": "Ви ще не заглушили жодного користувача.",
   "empty_column.notifications": "У вас ще немає сповіщень. Переписуйтесь з іншими користувачами, щоб почати розмову.",
   "empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших інстанцій, щоб заповнити стрічку",
   "follow_request.authorize": "Авторизувати",
   "follow_request.reject": "Відмовити",
   "getting_started.developers": "Розробникам",
-  "getting_started.directory": "Profile directory",
+  "getting_started.directory": "Каталог профілів",
   "getting_started.documentation": "Документація",
   "getting_started.heading": "Ласкаво просимо",
   "getting_started.invite": "Запросіть людей",
-  "getting_started.open_source_notice": "Mastodon - програма з відкритим вихідним кодом. Ви можете допомогти проекту, або повідомити про проблеми на GitHub за адресою {github}.",
+  "getting_started.open_source_notice": "Mastodon — програма з відкритим сирцевим кодом. Ви можете допомогти проекту, або повідомити про проблеми на 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.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
-  "hashtag.column_settings.tag_mode.all": "All of these",
-  "hashtag.column_settings.tag_mode.any": "Any of these",
-  "hashtag.column_settings.tag_mode.none": "None of these",
-  "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
+  "hashtag.column_header.tag_mode.all": "та {additional}",
+  "hashtag.column_header.tag_mode.any": "або {additional}",
+  "hashtag.column_header.tag_mode.none": "без {additional}",
+  "hashtag.column_settings.select.no_options_message": "Не знайдено пропозицій",
+  "hashtag.column_settings.select.placeholder": "Введіть хештеґи…",
+  "hashtag.column_settings.tag_mode.all": "Усі ці",
+  "hashtag.column_settings.tag_mode.any": "Який-небудь з цих",
+  "hashtag.column_settings.tag_mode.none": "Жоден з цих",
+  "hashtag.column_settings.tag_toggle": "Додайте додаткові теґи до цього стовпчика",
   "home.column_settings.basic": "Основні",
   "home.column_settings.show_reblogs": "Показувати передмухи",
   "home.column_settings.show_replies": "Показувати відповіді",
-  "home.column_settings.update_live": "Update in real-time",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
-  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
-  "introduction.federation.action": "Next",
-  "introduction.federation.federated.headline": "Federated",
-  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
-  "introduction.federation.home.headline": "Home",
-  "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 toot-orial!",
-  "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.",
+  "home.column_settings.update_live": "Оновлювати в реальному часі",
+  "intervals.full.days": "{number, plural, one {# день} few {# дні} other {# днів}}",
+  "intervals.full.hours": "{number, plural, one {# година} few {# години} other {# годин}}",
+  "intervals.full.minutes": "{number, plural, one {# хвилина} few {# хвилини} other {# хвилин}}",
+  "introduction.federation.action": "Далі",
+  "introduction.federation.federated.headline": "Глобальна",
+  "introduction.federation.federated.text": "Публічні пости з інших серверів федіверсу будуть з'являтися у глобальній стрічці.",
+  "introduction.federation.home.headline": "Головна",
+  "introduction.federation.home.text": "Пости від людей, за якими ви слідкуєте, з'являться у Вашій домашній стрічці. Ви можете слідкувати за кожним на будь-якому сервері!",
+  "introduction.federation.local.headline": "Локальна",
+  "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": "to open blocked users list",
+  "keyboard_shortcuts.blocked": "відкрити список заблокованих користувачів",
   "keyboard_shortcuts.boost": "передмухувати",
   "keyboard_shortcuts.column": "фокусуватися на одній з колонок",
   "keyboard_shortcuts.compose": "фокусуватися на полі введення",
   "keyboard_shortcuts.description": "Опис",
-  "keyboard_shortcuts.direct": "to open direct messages column",
+  "keyboard_shortcuts.direct": "відкрити колонку прямих повідомлень",
   "keyboard_shortcuts.down": "рухатися вниз стрічкою",
   "keyboard_shortcuts.enter": "відкрити статус",
   "keyboard_shortcuts.favourite": "вподобати",
-  "keyboard_shortcuts.favourites": "to open favourites list",
-  "keyboard_shortcuts.federated": "to open federated timeline",
+  "keyboard_shortcuts.favourites": "відкрити список улюбленого",
+  "keyboard_shortcuts.federated": "відкрити глобальну стрічку",
   "keyboard_shortcuts.heading": "Гарячі клавіші",
-  "keyboard_shortcuts.home": "to open home timeline",
+  "keyboard_shortcuts.home": "відкрити домашню стрічку",
   "keyboard_shortcuts.hotkey": "Гаряча клавіша",
   "keyboard_shortcuts.legend": "показати підказку",
-  "keyboard_shortcuts.local": "to open local timeline",
+  "keyboard_shortcuts.local": "відкрити локальну стрічку",
   "keyboard_shortcuts.mention": "згадати автора",
-  "keyboard_shortcuts.muted": "to open muted users list",
-  "keyboard_shortcuts.my_profile": "to open your profile",
-  "keyboard_shortcuts.notifications": "to open notifications column",
-  "keyboard_shortcuts.pinned": "to open pinned toots list",
+  "keyboard_shortcuts.muted": "відкрити список заглушених користувачів",
+  "keyboard_shortcuts.my_profile": "відкрити ваш профіль",
+  "keyboard_shortcuts.notifications": "відкрити колонку сповіщень",
+  "keyboard_shortcuts.pinned": "відкрити список закріплених дмухів",
   "keyboard_shortcuts.profile": "відкрити профіль автора",
   "keyboard_shortcuts.reply": "відповісти",
-  "keyboard_shortcuts.requests": "to open follow requests list",
+  "keyboard_shortcuts.requests": "відкрити список бажаючих підписатися",
   "keyboard_shortcuts.search": "сфокусуватися на пошуку",
-  "keyboard_shortcuts.start": "to open \"get started\" column",
-  "keyboard_shortcuts.toggle_hidden": "показати/приховати прихований текст",
-  "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
+  "keyboard_shortcuts.start": "відкрити колонку \"Початок\"",
+  "keyboard_shortcuts.toggle_hidden": "показати/приховати текст під попередженням",
+  "keyboard_shortcuts.toggle_sensitivity": "показати/приховати медіа",
   "keyboard_shortcuts.toot": "почати писати новий дмух",
   "keyboard_shortcuts.unfocus": "розфокусуватися з нового допису чи пошуку",
   "keyboard_shortcuts.up": "рухатися вверх списком",
   "lightbox.close": "Закрити",
   "lightbox.next": "Далі",
   "lightbox.previous": "Назад",
-  "lightbox.view_context": "View context",
+  "lightbox.view_context": "Переглянути контекст",
   "lists.account.add": "Додати до списку",
   "lists.account.remove": "Видалити зі списку",
   "lists.delete": "Видалити список",
   "lists.edit": "Редагувати список",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Змінити назву",
   "lists.new.create": "Додати список",
   "lists.new.title_placeholder": "Нова назва списку",
   "lists.search": "Шукати серед людей, на яких ви підписані",
   "lists.subheading": "Ваші списки",
-  "load_pending": "{count, plural, one {# new item} other {# new items}}",
+  "load_pending": "{count, plural, one {# новий елемент} other {# нових елементів}}",
   "loading_indicator.label": "Завантаження...",
   "media_gallery.toggle_visible": "Показати/приховати",
   "missing_indicator.label": "Не знайдено",
   "missing_indicator.sublabel": "Ресурс не знайдений",
   "mute_modal.hide_notifications": "Приховати сповіщення від користувача?",
-  "navigation_bar.apps": "Mobile apps",
+  "navigation_bar.apps": "Мобільні додатки",
   "navigation_bar.blocks": "Заблоковані користувачі",
   "navigation_bar.community_timeline": "Локальна стрічка",
-  "navigation_bar.compose": "Compose new toot",
+  "navigation_bar.compose": "Написати новий дмух",
   "navigation_bar.direct": "Прямі повідомлення",
   "navigation_bar.discover": "Знайти",
   "navigation_bar.domain_blocks": "Приховані домени",
@@ -239,61 +241,62 @@
   "navigation_bar.favourites": "Вподобане",
   "navigation_bar.filters": "Приховані слова",
   "navigation_bar.follow_requests": "Запити на підписку",
-  "navigation_bar.follows_and_followers": "Follows and followers",
+  "navigation_bar.follows_and_followers": "Підписки і підписники",
   "navigation_bar.info": "Про сайт",
-  "navigation_bar.keyboard_shortcuts": "Гарячі клавіши",
+  "navigation_bar.keyboard_shortcuts": "Гарячі клавіші",
   "navigation_bar.lists": "Списки",
   "navigation_bar.logout": "Вийти",
   "navigation_bar.mutes": "Заглушені користувачі",
   "navigation_bar.personal": "Особисте",
   "navigation_bar.pins": "Закріплені дмухи",
   "navigation_bar.preferences": "Налаштування",
-  "navigation_bar.profile_directory": "Profile directory",
+  "navigation_bar.profile_directory": "Каталог профілів",
   "navigation_bar.public_timeline": "Глобальна стрічка",
   "navigation_bar.security": "Безпека",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} вподобав(-ла) ваш допис",
   "notification.follow": "{name} підписався(-лась) на Вас",
   "notification.mention": "{name} згадав(-ла) Вас",
-  "notification.poll": "A poll you have voted in has ended",
+  "notification.poll": "Опитування, у якому ви голосували, закінчилося",
   "notification.reblog": "{name} передмухнув(-ла) Ваш допис",
   "notifications.clear": "Очистити сповіщення",
   "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.poll": "Poll results:",
+  "notifications.column_settings.poll": "Результати опитування:",
   "notifications.column_settings.push": "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.polls": "Poll results",
+  "notifications.filter.all": "Усі",
+  "notifications.filter.boosts": "Передмухи",
+  "notifications.filter.favourites": "Улюблені",
+  "notifications.filter.follows": "Підписки",
+  "notifications.filter.mentions": "Згадки",
+  "notifications.filter.polls": "Результати опитування",
   "notifications.group": "{count} сповіщень",
-  "poll.closed": "Closed",
-  "poll.refresh": "Refresh",
-  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
-  "poll.vote": "Vote",
-  "poll_button.add_poll": "Add a poll",
-  "poll_button.remove_poll": "Remove poll",
+  "poll.closed": "Закрито",
+  "poll.refresh": "Оновити",
+  "poll.total_votes": "{count, plural, one {# голос} few {# голоси} many {# голосів} other {# голосів}}",
+  "poll.vote": "Проголосувати",
+  "poll_button.add_poll": "Додати опитування",
+  "poll_button.remove_poll": "Видалити опитування",
   "privacy.change": "Змінити видимість допису",
   "privacy.direct.long": "Показати тільки згаданим користувачам",
-  "privacy.direct.short": "Направлений",
+  "privacy.direct.short": "Особисто",
   "privacy.private.long": "Показати тільки підписникам",
   "privacy.private.short": "Тільки для підписників",
   "privacy.public.long": "Показувати у публічних стрічках",
-  "privacy.public.short": "Публічний",
+  "privacy.public.short": "Публічно",
   "privacy.unlisted.long": "Не показувати у публічних стрічках",
   "privacy.unlisted.short": "Прихований",
   "regeneration_indicator.label": "Завантаження…",
-  "regeneration_indicator.sublabel": "Ваша домашня стрічка готова!",
+  "regeneration_indicator.sublabel": "Ваша домашня стрічка готується!",
   "relative_time.days": "{number}д",
   "relative_time.hours": "{number}г",
   "relative_time.just_now": "щойно",
@@ -305,82 +308,90 @@
   "report.hint": "Скаргу буде відправлено модераторам Вашого сайту. Ви можете надати їм пояснення, чому ви скаржитесь на аккаунт нижче:",
   "report.placeholder": "Додаткові коментарі",
   "report.submit": "Відправити",
-  "report.target": "Скаржимося на",
+  "report.target": "Скаржимося на {target}",
   "search.placeholder": "Пошук",
-  "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.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_popout.search_format": "Розширений формат пошуку",
+  "search_popout.tips.full_text": "Пошук за текстом знаходить статуси, які ви написали, вподобали, передмухнули, або в яких вас згадували. Також він знаходить імена користувачів, реальні імена та хештеґи.",
+  "search_popout.tips.hashtag": "хештеґ",
+  "search_popout.tips.status": "статус",
+  "search_popout.tips.text": "Пошук за текстом знаходить імена користувачів, реальні імена та хештеґи",
+  "search_popout.tips.user": "користувач",
+  "search_results.accounts": "Люди",
+  "search_results.hashtags": "Хештеґи",
+  "search_results.statuses": "Дмухів",
+  "search_results.statuses_fts_disabled": "Пошук дмухів за вмістом недоступний на цьому сервері Mastodon.",
   "search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}",
-  "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.admin_account": "Відкрити інтерфейс модерації для @{name}",
+  "status.admin_status": "Відкрити цей статус в інтерфейсі модерації",
+  "status.block": "Заблокувати @{name}",
+  "status.cancel_reblog_private": "Відмінити передмухання",
   "status.cannot_reblog": "Цей допис не може бути передмухнутий",
-  "status.copy": "Copy link to status",
+  "status.copy": "Копіювати посилання до статусу",
   "status.delete": "Видалити",
-  "status.detailed_status": "Detailed conversation view",
-  "status.direct": "Direct message @{name}",
-  "status.embed": "Embed",
+  "status.detailed_status": "Детальний вигляд бесіди",
+  "status.direct": "Пряме повідомлення до @{name}",
+  "status.embed": "Вбудувати",
   "status.favourite": "Подобається",
-  "status.filtered": "Filtered",
+  "status.filtered": "Відфільтровано",
   "status.load_more": "Завантажити більше",
   "status.media_hidden": "Медіаконтент приховано",
-  "status.mention": "Згадати",
-  "status.more": "More",
-  "status.mute": "Mute @{name}",
+  "status.mention": "Згадати @{name}",
+  "status.more": "Більше",
+  "status.mute": "Заглушити @{name}",
   "status.mute_conversation": "Заглушити діалог",
   "status.open": "Розгорнути допис",
-  "status.pin": "Pin on profile",
-  "status.pinned": "Pinned toot",
-  "status.read_more": "Read more",
+  "status.pin": "Закріпити у профілі",
+  "status.pinned": "Закріплений дмух",
+  "status.read_more": "Дізнатися більше",
   "status.reblog": "Передмухнути",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Передмухнути для початкової аудиторії",
   "status.reblogged_by": "{name} передмухнув(-ла)",
-  "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
-  "status.redraft": "Delete & re-draft",
+  "status.reblogs.empty": "Ніхто ще не передмухнув цього дмуху. Коли якісь користувачі це зроблять, вони будуть відображені тут.",
+  "status.redraft": "Видалити та перестворити",
   "status.reply": "Відповісти",
-  "status.replyAll": "Відповісти на тред",
-  "status.report": "Поскаржитися",
-  "status.sensitive_warning": "Непристойний зміст",
-  "status.share": "Share",
+  "status.replyAll": "Відповісти на ланцюжок",
+  "status.report": "Поскаржитися на @{name}",
+  "status.sensitive_warning": "Дражливий зміст",
+  "status.share": "Поділитися",
   "status.show_less": "Згорнути",
   "status.show_less_all": "Show less for all",
   "status.show_more": "Розгорнути",
   "status.show_more_all": "Show more for all",
-  "status.show_thread": "Show thread",
+  "status.show_thread": "Показати ланцюжок",
   "status.unmute_conversation": "Зняти глушення з діалогу",
-  "status.unpin": "Unpin from profile",
-  "suggestions.dismiss": "Dismiss suggestion",
-  "suggestions.header": "You might be interested in…",
+  "status.unpin": "Відкріпити від профілю",
+  "suggestions.dismiss": "Відхилити пропозицію",
+  "suggestions.header": "Вас може зацікавити…",
   "tabs_bar.federated_timeline": "Глобальна",
   "tabs_bar.home": "Головна",
   "tabs_bar.local_timeline": "Локальна",
   "tabs_bar.notifications": "Сповіщення",
   "tabs_bar.search": "Пошук",
-  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
-  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
-  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
+  "time_remaining.days": "{number, plural, one {# день} few {# дні} other {# днів}}",
+  "time_remaining.hours": "{number, plural, one {# година} few {# години} other {# годин}}",
+  "time_remaining.minutes": "{number, plural, one {# хвилина} few {# хвилини} other {# хвилин}}",
   "time_remaining.moments": "Moments remaining",
-  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
-  "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
+  "time_remaining.seconds": "{number, plural, one {# секунда} few {# секунди} other {# секунд}}",
+  "trends.count_by_accounts": "{count} {rawCount, plural, one {людина} few {людини} many {людей} other {людей}} talking",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "Вашу чернетку буде втрачено, якщо ви покинете Mastodon.",
   "upload_area.title": "Перетягніть сюди, щоб завантажити",
-  "upload_button.label": "Додати медіаконтент",
-  "upload_error.limit": "File upload limit exceeded.",
-  "upload_error.poll": "File upload not allowed with polls.",
+  "upload_button.label": "Додати медіаконтент ({formats})",
+  "upload_error.limit": "Ліміт завантаження файлів перевищено.",
+  "upload_error.poll": "Не можна завантажувати файли до опитувань.",
   "upload_form.description": "Опишіть для людей з вадами зору",
-  "upload_form.focus": "Обрізати",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "Видалити",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Завантаження...",
   "video.close": "Закрити відео",
-  "video.exit_fullscreen": "Вийти з повного екрану",
+  "video.exit_fullscreen": "Вийти з повноекранного режиму",
   "video.expand": "Розширити відео",
   "video.fullscreen": "На весь екран",
   "video.hide": "Приховати відео",
diff --git a/app/javascript/mastodon/locales/whitelist_et.json b/app/javascript/mastodon/locales/whitelist_et.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_et.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index bb774f1aa..16848831c 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -4,6 +4,7 @@
   "account.block": "屏蔽 @{name}",
   "account.block_domain": "隐藏来自 {domain} 的内容",
   "account.blocked": "已屏蔽",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "发送私信给 @{name}",
   "account.domain_blocked": "网站已屏蔽",
   "account.edit_profile": "修改个人资料",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
   "alert.unexpected.message": "发生了意外错误。",
   "alert.unexpected.title": "哎呀!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
   "bundle_column_error.body": "载入这个组件时发生了错误。",
   "bundle_column_error.retry": "重试",
@@ -156,7 +158,7 @@
   "home.column_settings.basic": "基本设置",
   "home.column_settings.show_reblogs": "显示转嘟",
   "home.column_settings.show_replies": "显示回复",
-  "home.column_settings.update_live": "Update in real-time",
+  "home.column_settings.update_live": "实时更新",
   "intervals.full.days": "{number} 天",
   "intervals.full.hours": "{number} 小时",
   "intervals.full.minutes": "{number} 分钟",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "用户目录",
   "navigation_bar.public_timeline": "跨站公共时间轴",
   "navigation_bar.security": "安全",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} 收藏了你的嘟文",
   "notification.follow": "{name} 开始关注你",
   "notification.mention": "{name} 提及了你",
@@ -316,7 +319,7 @@
   "search_results.accounts": "用户",
   "search_results.hashtags": "话题标签",
   "search_results.statuses": "嘟文",
-  "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
+  "search_results.statuses_fts_disabled": "此Mastodon服务器未启用嘟文内容搜索。",
   "search_results.total": "共 {count, number} 个结果",
   "status.admin_account": "打开 @{name} 的管理界面",
   "status.admin_status": "打开这条嘟文的管理界面",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "即将结束",
   "time_remaining.seconds": "剩余 {number, plural, one {# 秒} other {# 秒}}",
   "trends.count_by_accounts": "{count} 人正在讨论",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会丢失。",
   "upload_area.title": "将文件拖放到此处开始上传",
   "upload_button.label": "上传媒体文件 (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "文件大小超过限制。",
   "upload_error.poll": "投票中不允许上传文件。",
   "upload_form.description": "为视觉障碍人士添加文字说明",
-  "upload_form.focus": "设置缩略图",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "删除",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "上传中……",
   "video.close": "关闭视频",
   "video.exit_fullscreen": "退出全屏",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index b4c8b874a..f09ceffb3 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -4,6 +4,7 @@
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的一切文章",
   "account.blocked": "封鎖",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "私訊 @{name}",
   "account.domain_blocked": "服務站被隱藏",
   "account.edit_profile": "修改個人資料",
@@ -37,6 +38,7 @@
   "account.unmute_notifications": "取消來自 @{name} 通知的靜音",
   "alert.unexpected.message": "發生不可預期的錯誤。",
   "alert.unexpected.title": "噢!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
   "bundle_column_error.body": "加載本組件出錯。",
   "bundle_column_error.retry": "重試",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "跨站時間軸",
   "navigation_bar.security": "安全",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} 收藏了你的文章",
   "notification.follow": "{name} 開始關注你",
   "notification.mention": "{name} 提及你",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
   "trends.count_by_accounts": "{count} 位用戶在討論",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。",
   "upload_area.title": "將檔案拖放至此上載",
   "upload_button.label": "上載媒體檔案",
   "upload_error.limit": "File upload limit exceeded.",
   "upload_error.poll": "File upload not allowed with polls.",
   "upload_form.description": "為視覺障礙人士添加文字說明",
-  "upload_form.focus": "裁切",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "刪除",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "上載中……",
   "video.close": "關閉影片",
   "video.exit_fullscreen": "退出全熒幕",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 5f75b38d6..af988b320 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -1,22 +1,23 @@
 {
-  "account.add_or_remove_from_list": "從名單中新增或移除",
+  "account.add_or_remove_from_list": "從列表新增或移除",
   "account.badges.bot": "機器人",
   "account.block": "封鎖 @{name}",
-  "account.block_domain": "隱藏來自 {domain} 的所有嘟文",
+  "account.block_domain": "隱藏來自 {domain} 的所有內容",
   "account.blocked": "已封鎖",
+  "account.cancel_follow_request": "Cancel follow request",
   "account.direct": "傳私訊給 @{name}",
   "account.domain_blocked": "已隱藏網域",
   "account.edit_profile": "編輯個人資料",
   "account.endorse": "在個人資料推薦對方",
   "account.follow": "關注",
   "account.followers": "關注者",
-  "account.followers.empty": "還沒有人關注這位使用者。",
+  "account.followers.empty": "尚沒有人關注這位使用者。",
   "account.follows": "正在關注",
-  "account.follows.empty": "這個使用者尚未關注任何使用者。",
+  "account.follows.empty": "這位使用者尚未關注任何使用者。",
   "account.follows_you": "關注了你",
   "account.hide_reblogs": "隱藏來自 @{name} 的轉推",
-  "account.link_verified_on": "此連結的所有權已在 {date} 檢查",
-  "account.locked_info": "此帳號的隱私狀態被設為鎖定,擁有者將手動審核可關注此帳號的人。",
+  "account.link_verified_on": "已在 {date} 檢查此連結的擁有者權限",
+  "account.locked_info": "這隻帳戶的隱私狀態被設成鎖定。該擁有者會手動審核能關注這隻帳號的人。",
   "account.media": "媒體",
   "account.mention": "提及 @{name}",
   "account.moved_to": "{name} 已遷移至:",
@@ -33,22 +34,23 @@
   "account.unblock_domain": "取消隱藏 {domain}",
   "account.unendorse": "不再於個人資料頁面推薦對方",
   "account.unfollow": "取消關注",
-  "account.unmute": "不再靜音 @{name}",
-  "account.unmute_notifications": "不再靜音來自 @{name} 的通知",
+  "account.unmute": "取消靜音 @{name}",
+  "account.unmute_notifications": "重新接收來自 @{name} 的通知",
   "alert.unexpected.message": "發生了非預期的錯誤。",
   "alert.unexpected.title": "哎呀!",
+  "autosuggest_hashtag.per_week": "{count} per week",
   "boost_modal.combo": "下次您可以按 {combo} 跳過",
-  "bundle_column_error.body": "載入此組件時發生錯誤。",
+  "bundle_column_error.body": "載入此元件時發生錯誤。",
   "bundle_column_error.retry": "重試",
   "bundle_column_error.title": "網路錯誤",
   "bundle_modal_error.close": "關閉",
-  "bundle_modal_error.message": "載入此組件時發生錯誤。",
+  "bundle_modal_error.message": "載入此元件時發生錯誤。",
   "bundle_modal_error.retry": "重試",
   "column.blocks": "封鎖的使用者",
-  "column.community": "本地時間軸",
+  "column.community": "本機時間軸",
   "column.direct": "私訊",
   "column.domain_blocks": "隱藏的網域",
-  "column.favourites": "最愛",
+  "column.favourites": "收藏",
   "column.follow_requests": "關注請求",
   "column.home": "主頁",
   "column.lists": "名單",
@@ -64,44 +66,44 @@
   "column_header.show_settings": "顯示設定",
   "column_header.unpin": "取消釘選",
   "column_subheading.settings": "設定",
-  "community.column_settings.media_only": "僅媒體",
-  "compose_form.direct_message_warning": "這條嘟文只有被提及的使用者才能看到。",
+  "community.column_settings.media_only": "只有媒體",
+  "compose_form.direct_message_warning": "這條嘟文只有被提及的使用者才看得到。",
   "compose_form.direct_message_warning_learn_more": "了解更多",
-  "compose_form.hashtag_warning": "因這則嘟文設成「不公開」,因此它不會列在任何「#」標籤下。只有公開嘟文才能用「#」標籤找到。",
-  "compose_form.lock_disclaimer": "您的帳戶尚未{locked}。任何人都能關注您並看到您設定成僅關注者能看的嘟文。",
+  "compose_form.hashtag_warning": "由於這則嘟文被設定成「不公開」,所以它將不會被列在任何主題標籤下。只有公開的嘟文才能藉主題標籤找到。",
+  "compose_form.lock_disclaimer": "您的帳戶尚未{locked}。任何人都能關注您並看到您設定成只有關注者能看的嘟文。",
   "compose_form.lock_disclaimer.lock": "上鎖",
   "compose_form.placeholder": "您正在想些什麼?",
   "compose_form.poll.add_option": "新增選擇",
   "compose_form.poll.duration": "投票期限",
   "compose_form.poll.option_placeholder": "第 {number} 個選擇",
   "compose_form.poll.remove_option": "移除此選擇",
-  "compose_form.publish": "嘟掉",
+  "compose_form.publish": "嘟出去",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.hide": "Mark media as sensitive",
   "compose_form.sensitive.marked": "此媒體被標記為敏感內容",
-  "compose_form.sensitive.unmarked": "此媒體未被標記為敏感內容",
-  "compose_form.spoiler.marked": "正文已隱藏在警告之後",
+  "compose_form.sensitive.unmarked": "此媒體未標記為敏感內容",
+  "compose_form.spoiler.marked": "正文已隱藏到警告之後",
   "compose_form.spoiler.unmarked": "正文未被隱藏",
   "compose_form.spoiler_placeholder": "請在此處寫入警告訊息",
   "confirmation_modal.cancel": "取消",
   "confirmations.block.block_and_report": "Block & Report",
   "confirmations.block.confirm": "封鎖",
-  "confirmations.block.message": "你確定要封鎖 {name} ?",
+  "confirmations.block.message": "確定封鎖 {name} ?",
   "confirmations.delete.confirm": "刪除",
   "confirmations.delete.message": "你確定要刪除這條嘟文?",
   "confirmations.delete_list.confirm": "刪除",
-  "confirmations.delete_list.message": "確定要永久刪除此名單?",
+  "confirmations.delete_list.message": "確定永久刪除此名單?",
   "confirmations.domain_block.confirm": "隱藏整個網域",
-  "confirmations.domain_block.message": "確定封鎖整個 {domain} 嗎?多數情況下,封鎖或靜音幾個特定使用者應該就能滿足你的需求了。您將不能在任何公開時間軸或通知中看到來自該網域的內容。來自該網域的關注者將被移除。",
+  "confirmations.domain_block.message": "真的非常確定封鎖整個 {domain} 嗎?大部分情況下,你只需要封鎖或靜音少數特定的人就能滿足需求了。你將不能在任何公開的時間軸及通知中看到那個網域的內容。你來自該網域的關注者也會被移除。",
   "confirmations.mute.confirm": "靜音",
   "confirmations.mute.message": "確定靜音 {name} ?",
   "confirmations.redraft.confirm": "刪除並重新編輯",
-  "confirmations.redraft.message": "你確定要刪除這條嘟文並重新編輯它嗎?這麼做將失去轉嘟和最愛,而對原始嘟文的回覆將被孤立。",
+  "confirmations.redraft.message": "確定刪掉這則嘟文並重新編輯嗎?將會失去這則嘟文的轉嘟及收藏,且回覆這則的嘟文將會變成獨立的嘟文。",
   "confirmations.reply.confirm": "回覆",
   "confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?",
   "confirmations.unfollow.confirm": "取消關注",
   "confirmations.unfollow.message": "真的要取消關注 {name} 嗎?",
-  "embed.instructions": "要嵌入此嘟文,請將以下代碼貼進你的網站。",
+  "embed.instructions": "要嵌入此嘟文,請將以下程式碼貼進你的網站。",
   "embed.preview": "他會顯示成這樣:",
   "emoji_button.activity": "活動",
   "emoji_button.custom": "自訂",
@@ -109,7 +111,7 @@
   "emoji_button.food": "飲食",
   "emoji_button.label": "插入表情符號",
   "emoji_button.nature": "大自然",
-  "emoji_button.not_found": "就沒這表情符號吼!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "啊就沒這表情符號吼!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "物件",
   "emoji_button.people": "使用者",
   "emoji_button.recent": "最常使用",
@@ -123,15 +125,15 @@
   "empty_column.community": "本地時間軸是空的。快公開嘟些文搶頭香啊!",
   "empty_column.direct": "您還沒有任何私訊。當您私訊別人或收到私訊時,它將於此顯示。",
   "empty_column.domain_blocks": "尚未隱藏任何網域。",
-  "empty_column.favourited_statuses": "你還沒有將任何嘟文標為最愛。最愛的嘟文將顯示於此。",
-  "empty_column.favourites": "還沒有人將此嘟文標為最愛。如果有人標成最愛,則會顯示在這裡。",
-  "empty_column.follow_requests": "您尚未收到任何關注請求。收到時會顯示於此。",
-  "empty_column.hashtag": "這個「#」標籤下什麼都沒有。",
+  "empty_column.favourited_statuses": "你還沒收藏任何嘟文。這裡將會顯示你收藏的嘟文。",
+  "empty_column.favourites": "還沒有人收藏這則嘟文。這裡將會顯示被收藏的嘟文。",
+  "empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。",
+  "empty_column.hashtag": "這個主題標籤下什麼也沒有。",
   "empty_column.home": "您的首頁時間軸是空的!前往 {public} 或使用搜尋功能來認識其他人。",
   "empty_column.home.public_timeline": "公開時間軸",
-  "empty_column.list": "此份名單還沒有東西。當此名單的成員嘟出了新的嘟文時,它們就會出現在這裡。",
-  "empty_column.lists": "你還沒有建立任何名單。你建立的名單將會顯示在這裡。",
-  "empty_column.mutes": "你還沒有靜音任何使用者。",
+  "empty_column.list": "這份名單還沒有東西。當此名單的成員嘟出了新的嘟文時,它們就會顯示於此。",
+  "empty_column.lists": "你還沒有建立任何名單。這裡將會顯示你所建立的名單。",
+  "empty_column.mutes": "你尚未靜音任何使用者。",
   "empty_column.notifications": "您尚未收到任何通知,和別人互動開啟對話吧。",
   "empty_column.public": "這裡什麼都沒有!嘗試寫些公開的嘟文,或著自己關注其他伺服器的使用者後就會有嘟文出現了",
   "follow_request.authorize": "授權",
@@ -146,61 +148,61 @@
   "getting_started.terms": "服務條款",
   "hashtag.column_header.tag_mode.all": "以及{additional}",
   "hashtag.column_header.tag_mode.any": "或是{additional}",
-  "hashtag.column_header.tag_mode.none": "而不用{additional}",
+  "hashtag.column_header.tag_mode.none": "而無需{additional}",
   "hashtag.column_settings.select.no_options_message": "找不到建議",
-  "hashtag.column_settings.select.placeholder": "輸入「#」標籤…",
+  "hashtag.column_settings.select.placeholder": "輸入主題標籤…",
   "hashtag.column_settings.tag_mode.all": "全部",
   "hashtag.column_settings.tag_mode.any": "任一",
-  "hashtag.column_settings.tag_mode.none": "全都不要",
-  "hashtag.column_settings.tag_toggle": "對此欄位加入額外標籤",
+  "hashtag.column_settings.tag_mode.none": "全不",
+  "hashtag.column_settings.tag_toggle": "將額外標籤加入到這個欄位",
   "home.column_settings.basic": "基本",
-  "home.column_settings.show_reblogs": "顯示轉推",
+  "home.column_settings.show_reblogs": "顯示轉嘟",
   "home.column_settings.show_replies": "顯示回覆",
   "home.column_settings.update_live": "Update in real-time",
   "intervals.full.days": "{number, plural, one {# 天} other {# 天}}",
   "intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}",
   "intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}",
   "introduction.federation.action": "下一步",
-  "introduction.federation.federated.headline": "聯邦",
-  "introduction.federation.federated.text": "來自聯邦網路中其他伺服器的公開嘟文將會在聯邦網路時間軸中顯示。",
+  "introduction.federation.federated.headline": "站台聯盟",
+  "introduction.federation.federated.text": "來自聯盟宇宙中其他站台的公開嘟文將會在站點聯盟時間軸中顯示。",
   "introduction.federation.home.headline": "首頁",
-  "introduction.federation.home.text": "您所關注使用者所發的嘟文將顯示在首頁的訊息來源。您能關注任何伺服器上的任何人!",
-  "introduction.federation.local.headline": "本地",
-  "introduction.federation.local.text": "跟您同伺服器之使用者所發的公開嘟文將會顯示在本地時間軸中。",
+  "introduction.federation.home.text": "你關注使用者的嘟文將會在首頁動態中顯示。你可以關注任何伺服器上的任何人!",
+  "introduction.federation.local.headline": "本機",
+  "introduction.federation.local.text": "跟您同伺服器之使用者所發的公開嘟文將會顯示在本機時間軸中。",
   "introduction.interactions.action": "完成教學!",
-  "introduction.interactions.favourite.headline": "最愛",
-  "introduction.interactions.favourite.text": "您能稍候儲存嘟文,或者將嘟文加到最愛,讓作者知道您喜歡這嘟文。",
+  "introduction.interactions.favourite.headline": "關注",
+  "introduction.interactions.favourite.text": "您能儲存嘟文供稍候觀看,或者收藏嘟文,讓作者知道您喜歡這則嘟文。",
   "introduction.interactions.reblog.headline": "轉嘟",
-  "introduction.interactions.reblog.text": "您能透過轉嘟他人嘟文來分享給您的關注者。",
+  "introduction.interactions.reblog.text": "您能藉由轉嘟他人嘟文來分享給您的關注者。",
   "introduction.interactions.reply.headline": "回覆",
-  "introduction.interactions.reply.text": "您能回覆其他人或自己的嘟文。將會把這些回覆串成一串對話。",
-  "introduction.welcome.action": "開始!",
+  "introduction.interactions.reply.text": "您能回覆其他人或自己的嘟文,這麼做會把這些回覆串成一串對話。",
+  "introduction.welcome.action": "開始旅程吧!",
   "introduction.welcome.headline": "第一步",
-  "introduction.welcome.text": "歡迎來到聯邦!稍候您將可以廣播訊息並跨各種各式各樣的伺服器與朋友聊天。但這台伺服器,{domain},十分特殊 -- 它寄管了您的個人資料,所以請記住這台伺服器的名稱。",
+  "introduction.welcome.text": "歡迎來到聯盟宇宙!等等你就可以廣播訊息及跨越各種各式各樣的伺服器與朋友聊天。但這台伺服器,{domain},非常特別 - 它寄管了你的個人資料,所以請記住它的名字。",
   "keyboard_shortcuts.back": "返回上一頁",
-  "keyboard_shortcuts.blocked": "開啟「封鎖的使用者」名單",
+  "keyboard_shortcuts.blocked": "開啟「封鎖使用者」名單",
   "keyboard_shortcuts.boost": "轉嘟",
   "keyboard_shortcuts.column": "將焦點放在其中一欄的嘟文",
   "keyboard_shortcuts.compose": "將焦點移至撰寫文字區塊",
   "keyboard_shortcuts.description": "描述",
   "keyboard_shortcuts.direct": "開啟私訊欄",
-  "keyboard_shortcuts.down": "在名單中往下移動",
+  "keyboard_shortcuts.down": "往下移動名單項目",
   "keyboard_shortcuts.enter": "檢視嘟文",
-  "keyboard_shortcuts.favourite": "加入最愛",
-  "keyboard_shortcuts.favourites": "開啟最愛名單",
-  "keyboard_shortcuts.federated": "開啟聯邦時間軸",
+  "keyboard_shortcuts.favourite": "收藏",
+  "keyboard_shortcuts.favourites": "開啟收藏名單",
+  "keyboard_shortcuts.federated": "開啟站點聯盟時間軸",
   "keyboard_shortcuts.heading": "鍵盤快速鍵",
   "keyboard_shortcuts.home": "開啟首頁時間軸",
   "keyboard_shortcuts.hotkey": "快速鍵",
-  "keyboard_shortcuts.legend": "顯示此說明",
-  "keyboard_shortcuts.local": "開啟本地時間軸",
+  "keyboard_shortcuts.legend": "顯示此列表",
+  "keyboard_shortcuts.local": "開啟本機時間軸",
   "keyboard_shortcuts.mention": "提及作者",
   "keyboard_shortcuts.muted": "開啟靜音使用者名單",
   "keyboard_shortcuts.my_profile": "開啟個人資料頁面",
   "keyboard_shortcuts.notifications": "開啟通知欄",
   "keyboard_shortcuts.pinned": "開啟釘選的嘟文名單",
-  "keyboard_shortcuts.profile": "開啟作者的個人資料頁",
-  "keyboard_shortcuts.reply": "回應嘟文",
+  "keyboard_shortcuts.profile": "開啟作者的個人資料頁面",
+  "keyboard_shortcuts.reply": "回覆",
   "keyboard_shortcuts.requests": "開啟關注請求名單",
   "keyboard_shortcuts.search": "將焦點移至搜尋框",
   "keyboard_shortcuts.start": "開啟「開始使用」欄位",
@@ -208,7 +210,7 @@
   "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
   "keyboard_shortcuts.toot": "開始發出新嘟文",
   "keyboard_shortcuts.unfocus": "取消輸入文字區塊 / 搜尋的焦點",
-  "keyboard_shortcuts.up": "在名單中往上移動",
+  "keyboard_shortcuts.up": "往上移動名單項目",
   "lightbox.close": "關閉",
   "lightbox.next": "下一步",
   "lightbox.previous": "上一步",
@@ -227,16 +229,16 @@
   "media_gallery.toggle_visible": "切換可見性",
   "missing_indicator.label": "找不到",
   "missing_indicator.sublabel": "找不到此資源",
-  "mute_modal.hide_notifications": "隱藏來自這個使用者的通知?",
-  "navigation_bar.apps": "行動應用程式",
-  "navigation_bar.blocks": "封鎖的使用者",
-  "navigation_bar.community_timeline": "本地時間軸",
+  "mute_modal.hide_notifications": "隱藏來自這位使用者的通知?",
+  "navigation_bar.apps": "封鎖的使用者",
+  "navigation_bar.blocks": "封鎖使用者",
+  "navigation_bar.community_timeline": "本機時間軸",
   "navigation_bar.compose": "撰寫新嘟文",
   "navigation_bar.direct": "私訊",
   "navigation_bar.discover": "探索",
   "navigation_bar.domain_blocks": "隱藏的網域",
   "navigation_bar.edit_profile": "編輯個人資料",
-  "navigation_bar.favourites": "最愛內容",
+  "navigation_bar.favourites": "收藏",
   "navigation_bar.filters": "靜音詞彙",
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.follows_and_followers": "Follows and followers",
@@ -251,6 +253,7 @@
   "navigation_bar.profile_directory": "Profile directory",
   "navigation_bar.public_timeline": "聯邦時間軸",
   "navigation_bar.security": "安全性",
+  "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
   "notification.favourite": "{name} 把你的嘟文加入了最愛",
   "notification.follow": "{name} 關注了你",
   "notification.mention": "{name} 提到了你",
@@ -370,14 +373,22 @@
   "time_remaining.moments": "剩餘時間",
   "time_remaining.seconds": "剩餘 {number, plural, one {# 秒} other {# 秒}}",
   "trends.count_by_accounts": "{count} 位使用者在討論",
+  "trends.refresh": "Refresh",
   "ui.beforeunload": "如果離開 Mastodon,你的草稿將會不見。",
   "upload_area.title": "拖放來上傳",
   "upload_button.label": "上傳媒體檔案 (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "已達到檔案上傳限制。",
   "upload_error.poll": "不允許在投票上傳檔案。",
   "upload_form.description": "為視障人士增加文字說明",
-  "upload_form.focus": "變更預覽",
+  "upload_form.edit": "Edit",
   "upload_form.undo": "刪除",
+  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.apply": "Apply",
+  "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
+  "upload_modal.detect_text": "Detect text from picture",
+  "upload_modal.edit_media": "Edit media",
+  "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
+  "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "上傳中...",
   "video.close": "關閉影片",
   "video.exit_fullscreen": "退出全螢幕",
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 3b60878eb..0f4b209d4 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -32,6 +32,7 @@ import suggestions from './suggestions';
 import polls from './polls';
 import identity_proofs from './identity_proofs';
 import trends from './trends';
+import missed_updates from './missed_updates';
 
 const reducers = {
   dropdown_menu,
@@ -67,6 +68,7 @@ const reducers = {
   suggestions,
   polls,
   trends,
+  missed_updates,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/missed_updates.js b/app/javascript/mastodon/reducers/missed_updates.js
new file mode 100644
index 000000000..b71d62d82
--- /dev/null
+++ b/app/javascript/mastodon/reducers/missed_updates.js
@@ -0,0 +1,21 @@
+import { Map as ImmutableMap } from 'immutable';
+import { NOTIFICATIONS_UPDATE } from 'mastodon/actions/notifications';
+import { APP_FOCUS, APP_UNFOCUS } from 'mastodon/actions/app';
+
+const initialState = ImmutableMap({
+  focused: true,
+  unread: 0,
+});
+
+export default function missed_updates(state = initialState, action) {
+  switch(action.type) {
+  case APP_FOCUS:
+    return state.set('focused', true).set('unread', 0);
+  case APP_UNFOCUS:
+    return state.set('focused', false);
+  case NOTIFICATIONS_UPDATE:
+    return state.get('focused') ? state : state.update('unread', x => x + 1);
+  default:
+    return state;
+  }
+};
diff --git a/app/javascript/mastodon/utils/numbers.js b/app/javascript/mastodon/utils/numbers.js
index fdd8269ae..f7e4ceb93 100644
--- a/app/javascript/mastodon/utils/numbers.js
+++ b/app/javascript/mastodon/utils/numbers.js
@@ -4,7 +4,9 @@ import { FormattedNumber } from 'react-intl';
 export const shortNumberFormat = number => {
   if (number < 1000) {
     return <FormattedNumber value={number} />;
-  } else {
+  } else if (number < 1000000) {
     return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
+  } else {
+    return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>;
   }
 };
diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js
index a8ec5f3fa..7196dc96b 100644
--- a/app/javascript/mastodon/utils/resize_image.js
+++ b/app/javascript/mastodon/utils/resize_image.js
@@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => {
 });
 
 const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
-  if (!['image/jpeg', 'image/webp'].includes(type)) {
+  if (type !== 'image/jpeg') {
     resolve(1);
     return;
   }
@@ -71,7 +71,7 @@ const processImage = (img, { width, height, orientation, type = 'image/png' }) =
   // and return an all-white image instead. Assume reading failed if the resized
   // image is perfectly white.
   const imageData = context.getImageData(0, 0, width, height);
-  if (imageData.every(value => value === 255)) {
+  if (imageData.data.every(value => value === 255)) {
     throw 'Failed to read from canvas';
   }
 
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 6aea119e3..e49dcaadb 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -100,6 +100,15 @@ function main() {
 
     delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
     delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
+
+    delegate(document, '.blocks-table button.icon-button', 'click', function(e) {
+      e.preventDefault();
+
+      const classList = this.firstElementChild.classList;
+      classList.toggle('fa-chevron-down');
+      classList.toggle('fa-chevron-up');
+      this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden');
+    });
   });
 }
 
diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss
index 7b983efab..f9332caa3 100644
--- a/app/javascript/styles/mastodon/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -150,7 +150,7 @@ button {
 .layout-single-column .app-holder {
   &,
   & > div {
-    min-height: 100%;
+    min-height: 100vh;
   }
 }
 
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 2d04aeca7..5c30c1295 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3,6 +3,27 @@
   -ms-overflow-style: -ms-autohiding-scrollbar;
 }
 
+.link-button {
+  display: block;
+  font-size: 15px;
+  line-height: 20px;
+  color: $ui-highlight-color;
+  border: 0;
+  background: transparent;
+  padding: 0;
+  cursor: pointer;
+
+  &:hover,
+  &:active {
+    text-decoration: underline;
+  }
+
+  &:disabled {
+    color: $ui-primary-color;
+    cursor: default;
+  }
+}
+
 .button {
   background-color: $ui-highlight-color;
   border: 10px none;
@@ -482,9 +503,21 @@
   .autosuggest-hashtag {
     justify-content: space-between;
 
+    &__name {
+      flex: 1 1 auto;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+
     strong {
       font-weight: 500;
     }
+
+    &__uses {
+      flex: 0 0 auto;
+      width: 80px;
+      text-align: right;
+    }
   }
 
   .autosuggest-account-icon,
@@ -637,18 +670,6 @@
     .character-counter__wrapper {
       align-self: center;
       margin-right: 4px;
-
-      .character-counter {
-        cursor: default;
-        font-family: $font-sans-serif, sans-serif;
-        font-size: 14px;
-        font-weight: 600;
-        color: $lighter-text-color;
-
-        &.character-counter--over {
-          color: $warning-red;
-        }
-      }
     }
   }
 
@@ -665,6 +686,18 @@
   }
 }
 
+.character-counter {
+  cursor: default;
+  font-family: $font-sans-serif, sans-serif;
+  font-size: 14px;
+  font-weight: 600;
+  color: $lighter-text-color;
+
+  &.character-counter--over {
+    color: $warning-red;
+  }
+}
+
 .no-reduce-motion .spoiler-input {
   transition: height 0.4s ease, opacity 0.4s ease;
 }
@@ -4513,7 +4546,8 @@ a.status-card.compact:hover {
   }
 }
 
-.report-modal__statuses {
+.report-modal__statuses,
+.focal-point-modal__content {
   flex: 1 1 auto;
   min-height: 20vh;
   max-height: 80vh;
@@ -4534,6 +4568,12 @@ a.status-card.compact:hover {
   }
 }
 
+.focal-point-modal__content {
+  @media screen and (max-width: 480px) {
+    max-height: 40vh;
+  }
+}
+
 .report-modal__comment {
   padding: 20px;
   border-right: 1px solid $ui-secondary-color;
@@ -4555,16 +4595,56 @@ a.status-card.compact:hover {
     padding: 10px;
     font-family: inherit;
     font-size: 14px;
-    resize: vertical;
+    resize: none;
     border: 0;
     outline: 0;
     border-radius: 4px;
     border: 1px solid $ui-secondary-color;
-    margin-bottom: 20px;
+    min-height: 100px;
+    max-height: 50vh;
+    margin-bottom: 10px;
 
     &:focus {
       border: 1px solid darken($ui-secondary-color, 8%);
     }
+
+    &__wrapper {
+      background: $white;
+      border: 1px solid $ui-secondary-color;
+      margin-bottom: 10px;
+      border-radius: 4px;
+
+      .setting-text {
+        border: 0;
+        margin-bottom: 0;
+        border-radius: 0;
+
+        &:focus {
+          border: 0;
+        }
+      }
+
+      &__modifiers {
+        color: $inverted-text-color;
+        font-family: inherit;
+        font-size: 14px;
+        background: $white;
+      }
+    }
+
+    &__toolbar {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 20px;
+    }
+  }
+
+  .setting-text-label {
+    display: block;
+    color: $inverted-text-color;
+    font-size: 14px;
+    font-weight: 500;
+    margin-bottom: 10px;
   }
 
   .setting-toggle {
@@ -4688,10 +4768,10 @@ a.status-card.compact:hover {
 }
 
 .report-modal__target {
-  padding: 20px;
+  padding: 15px;
 
   .media-modal__close {
-    top: 19px;
+    top: 14px;
     right: 15px;
   }
 }
@@ -4702,6 +4782,7 @@ a.status-card.compact:hover {
   position: absolute;
   top: 0;
   left: 0;
+  z-index: 9999;
 }
 
 .media-gallery__gifv__label {
@@ -4960,6 +5041,10 @@ a.status-card.compact:hover {
   max-width: 100%;
   border-radius: 4px;
 
+  &.editable {
+    border-radius: 0;
+  }
+
   &:focus {
     outline: 0;
   }
@@ -5688,27 +5773,25 @@ noscript {
   }
 }
 
-.focal-point-modal {
-  max-width: 80vw;
-  max-height: 80vh;
-  position: relative;
-}
-
 .focal-point {
   position: relative;
-  cursor: pointer;
+  cursor: move;
   overflow: hidden;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: $base-shadow-color;
 
-  &.dragging {
-    cursor: move;
-  }
-
-  img {
-    max-width: 80vw;
+  img,
+  video {
+    display: block;
     max-height: 80vh;
-    width: auto;
+    width: 100%;
     height: auto;
-    margin: auto;
+    margin: 0;
+    object-fit: contain;
+    background: $base-shadow-color;
   }
 
   &__reticle {
@@ -5728,6 +5811,43 @@ noscript {
     top: 0;
     left: 0;
   }
+
+  &__preview {
+    position: absolute;
+    bottom: 10px;
+    right: 10px;
+    z-index: 2;
+    cursor: move;
+    transition: opacity 0.1s ease;
+
+    &:hover {
+      opacity: 0.5;
+    }
+
+    strong {
+      color: $primary-text-color;
+      font-size: 14px;
+      font-weight: 500;
+      display: block;
+      margin-bottom: 5px;
+    }
+
+    div {
+      border-radius: 4px;
+      box-shadow: 0 0 14px rgba($base-shadow-color, 0.2);
+    }
+  }
+
+  @media screen and (max-width: 480px) {
+    img,
+    video {
+      max-height: 100%;
+    }
+
+    &__preview {
+      display: none;
+    }
+  }
 }
 
 .account__header__content {
@@ -5980,12 +6100,12 @@ noscript {
 
     &__current {
       flex: 0 0 auto;
-      width: 100px;
       font-size: 24px;
       line-height: 36px;
       font-weight: 500;
       text-align: right;
       padding-right: 15px;
+      margin-left: 5px;
       color: $secondary-text-color;
     }
 
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 11ac6dfeb..fe6beba5d 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -241,3 +241,70 @@ a.table-action-link {
     }
   }
 }
+
+.blocks-table {
+  width: 100%;
+  max-width: 100%;
+  border-spacing: 0;
+  border-collapse: collapse;
+  table-layout: fixed;
+  border: 1px solid darken($ui-base-color, 8%);
+
+  thead {
+    border: 1px solid darken($ui-base-color, 8%);
+    background: darken($ui-base-color, 4%);
+    font-weight: 500;
+
+    th.severity-column {
+      width: 120px;
+    }
+
+    th.button-column {
+      width: 23px;
+    }
+  }
+
+  tbody > tr {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-bottom: 0;
+    background: darken($ui-base-color, 4%);
+
+    &:hover {
+      background: darken($ui-base-color, 2%);
+    }
+
+    &.even {
+      background: $ui-base-color;
+
+      &:hover {
+        background: lighten($ui-base-color, 2%);
+      }
+    }
+
+    &.rationale {
+      background: lighten($ui-base-color, 4%);
+      border-top: 0;
+
+      &:hover {
+        background: lighten($ui-base-color, 6%);
+      }
+
+      &.hidden {
+        display: none;
+      }
+    }
+
+    td:first-child {
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+
+  th,
+  td {
+    padding: 8px;
+    line-height: 18px;
+    vertical-align: top;
+    text-align: left;
+  }
+}
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index 8c30bc57c..04beb869c 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -100,6 +100,16 @@
       background-size: 44px 44px;
     }
   }
+
+  .trends__item {
+    padding: 10px;
+  }
+}
+
+.trends-widget {
+  h4 {
+    color: $darker-text-color;
+  }
 }
 
 .box-widget {
@@ -109,6 +119,15 @@
   box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
 }
 
+.placeholder-widget {
+  padding: 16px;
+  border-radius: 4px;
+  border: 2px dashed $dark-text-color;
+  text-align: center;
+  color: $darker-text-color;
+  margin-bottom: 10px;
+}
+
 .contact-widget,
 .landing-page__information.contact-widget {
   box-sizing: border-box;
@@ -526,6 +545,12 @@ $fluid-breakpoint: $maximum-width + 20px;
   a {
     font-size: 14px;
     line-height: 20px;
+  }
+}
+
+.notice-widget,
+.placeholder-widget {
+  a {
     text-decoration: none;
     font-weight: 500;
     color: $ui-highlight-color;
diff --git a/app/lib/search_query_parser.rb b/app/lib/search_query_parser.rb
index 405ad15b8..15956d4cf 100644
--- a/app/lib/search_query_parser.rb
+++ b/app/lib/search_query_parser.rb
@@ -1,14 +1,15 @@
 # frozen_string_literal: true
 
 class SearchQueryParser < Parslet::Parser
-  rule(:term)     { match('[^\s":]').repeat(1).as(:term) }
-  rule(:quote)    { str('"') }
-  rule(:colon)    { str(':') }
-  rule(:space)    { match('\s').repeat(1) }
-  rule(:operator) { (str('+') | str('-')).as(:operator) }
-  rule(:prefix)   { (term >> colon).as(:prefix) }
-  rule(:phrase)   { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) }
-  rule(:clause)   { (prefix.maybe >> operator.maybe >> (phrase | term)).as(:clause) }
-  rule(:query)    { (clause >> space.maybe).repeat.as(:query) }
+  rule(:term)      { match('[^\s":]').repeat(1).as(:term) }
+  rule(:quote)     { str('"') }
+  rule(:colon)     { str(':') }
+  rule(:space)     { match('\s').repeat(1) }
+  rule(:operator)  { (str('+') | str('-')).as(:operator) }
+  rule(:prefix)    { (term >> colon).as(:prefix) }
+  rule(:shortcode) { (colon >> term >> colon.maybe).as(:shortcode) }
+  rule(:phrase)    { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) }
+  rule(:clause)    { (prefix.maybe >> operator.maybe >> (phrase | term | shortcode)).as(:clause) }
+  rule(:query)     { (clause >> space.maybe).repeat.as(:query) }
   root(:query)
 end
diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index 2c4144790..6a299f59d 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -75,6 +75,8 @@ class SearchQueryTransformer < Parslet::Transform
 
     if clause[:term]
       TermClause.new(prefix, operator, clause[:term].to_s)
+    elsif clause[:shortcode]
+      TermClause.new(prefix, operator, ":#{clause[:term]}:")
     elsif clause[:phrase]
       PhraseClause.new(prefix, operator, clause[:phrase].map { |p| p[:term].to_s }.join(' '))
     else
diff --git a/app/models/account.rb b/app/models/account.rb
index 38379d20e..9d938c55d 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -131,6 +131,8 @@ class Account < ApplicationRecord
 
   delegate :chosen_languages, to: :user, prefix: false, allow_nil: true
 
+  update_index('accounts#account', :self) if Chewy.enabled?
+
   def local?
     domain.nil?
   end
@@ -173,6 +175,10 @@ class Account < ApplicationRecord
     subscription_expires_at.present?
   end
 
+  def searchable?
+    !(suspended? || moved?)
+  end
+
   def possibly_stale?
     last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago
   end
diff --git a/app/models/account_stat.rb b/app/models/account_stat.rb
index 9813aa84f..6d1097cec 100644
--- a/app/models/account_stat.rb
+++ b/app/models/account_stat.rb
@@ -16,6 +16,8 @@
 class AccountStat < ApplicationRecord
   belongs_to :account, inverse_of: :account_stat
 
+  update_index('accounts#account', :account) if Chewy.enabled?
+
   def increment_count!(key)
     update(attributes_for_increment(key))
   end
diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb
index 5fff3ef5d..2d5ebfca3 100644
--- a/app/models/concerns/account_avatar.rb
+++ b/app/models/concerns/account_avatar.rb
@@ -3,7 +3,7 @@
 module AccountAvatar
   extend ActiveSupport::Concern
 
-  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
+  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
   LIMIT = 2.megabytes
 
   class_methods do
diff --git a/app/models/concerns/account_counters.rb b/app/models/concerns/account_counters.rb
index 3581df8dd..6e25e1905 100644
--- a/app/models/concerns/account_counters.rb
+++ b/app/models/concerns/account_counters.rb
@@ -26,7 +26,8 @@ module AccountCounters
   private
 
   def save_account_stat
-    return unless account_stat&.changed?
+    return unless association(:account_stat).loaded? && account_stat&.changed?
+
     account_stat.save
   end
 end
diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb
index a748fdff7..067e166eb 100644
--- a/app/models/concerns/account_header.rb
+++ b/app/models/concerns/account_header.rb
@@ -3,7 +3,7 @@
 module AccountHeader
   extend ActiveSupport::Concern
 
-  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
+  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
   LIMIT = 2.megabytes
   MAX_PIXELS = 750_000 # 1500x500px
 
diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb
index 7c78bb456..246c2c27c 100644
--- a/app/models/concerns/attachmentable.rb
+++ b/app/models/concerns/attachmentable.rb
@@ -43,7 +43,7 @@ module Attachmentable
 
       width, height = FastImage.size(attachment.queued_for_write[:original].path)
 
-      raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height >= MAX_MATRIX_LIMIT)
+      raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported, must be below #{MAX_MATRIX_LIMIT} sqpx" if width.present? && height.present? && (width * height >= MAX_MATRIX_LIMIT)
     end
   end
 
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 643a7e46a..b21ad9042 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -28,7 +28,7 @@ class CustomEmoji < ApplicationRecord
     :(#{SHORTCODE_RE_FRAGMENT}):
     (?=[^[:alnum:]:]|$)/x
 
-  IMAGE_MIME_TYPES = %w(image/png image/gif image/webp).freeze
+  IMAGE_MIME_TYPES = %w(image/png image/gif).freeze
 
   belongs_to :category, class_name: 'CustomEmojiCategory', optional: true
   has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 37b8d98c6..4383cbd05 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -25,6 +25,7 @@ class DomainBlock < ApplicationRecord
   delegate :count, to: :accounts, prefix: true
 
   scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
+  scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
 
   class << self
     def suspend?(domain)
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 2c3a7f13b..57dd3edd9 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -36,6 +36,8 @@ class Form::AdminSettings
     show_replies_in_public_timelines
     spam_check_enabled
     trends
+    show_domain_blocks
+    show_domain_blocks_rationale
   ).freeze
 
   BOOLEAN_KEYS = %i(
@@ -74,6 +76,8 @@ class Form::AdminSettings
   validates :site_contact_email, :site_contact_username, presence: true
   validates :site_contact_username, existing_username: true
   validates :bootstrap_timeline_accounts, existing_username: { multiple: true }
+  validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }
+  validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }
 
   def initialize(_attributes = {})
     super
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 02ab8e0b2..29d25eae8 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -12,6 +12,7 @@
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
 #  autofollow :boolean          default(FALSE), not null
+#  comment    :text
 #
 
 class Invite < ApplicationRecord
@@ -22,6 +23,8 @@ class Invite < ApplicationRecord
 
   scope :available, -> { where(expires_at: nil).or(where('expires_at >= ?', Time.now.utc)) }
 
+  validates :comment, length: { maximum: 420 }
+
   before_validation :set_code
 
   def valid_for_use?
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index be762889c..d03751fd3 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -26,11 +26,11 @@ class MediaAttachment < ApplicationRecord
 
   enum type: [:image, :gifv, :video, :unknown, :audio]
 
-  IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif .webp).freeze
+  IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif).freeze
   VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze
   AUDIO_FILE_EXTENSIONS = %w(.ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp).freeze
 
-  IMAGE_MIME_TYPES             = %w(image/jpeg image/png image/gif image/webp).freeze
+  IMAGE_MIME_TYPES             = %w(image/jpeg image/png image/gif).freeze
   VIDEO_MIME_TYPES             = %w(video/webm video/mp4 video/quicktime video/ogg).freeze
   VIDEO_CONVERTIBLE_MIME_TYPES = %w(video/webm video/quicktime).freeze
   AUDIO_MIME_TYPES             = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/ogg audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/3gpp).freeze
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index f26ea0c74..a792b352b 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -25,7 +25,7 @@
 #
 
 class PreviewCard < ApplicationRecord
-  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
+  IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
   LIMIT = 1.megabytes
 
   self.inheritance_column = false
diff --git a/app/models/status.rb b/app/models/status.rb
index 642d3cf5e..de790027d 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -455,13 +455,16 @@ class Status < ApplicationRecord
     '👁'
   end
 
+  def status_stat
+    super || build_status_stat
+  end
+
   private
 
   def update_status_stat!(attrs)
     return if marked_for_destruction? || destroyed?
 
-    record = status_stat || build_status_stat
-    record.update(attrs)
+    status_stat.update(attrs)
   end
 
   def store_uri
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 1364d1dba..945e3a3c6 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -13,6 +13,8 @@
 #  listable            :boolean
 #  reviewed_at         :datetime
 #  requested_review_at :datetime
+#  last_status_at      :datetime
+#  last_trend_at       :datetime
 #
 
 class Tag < ApplicationRecord
@@ -33,7 +35,8 @@ class Tag < ApplicationRecord
   scope :unreviewed, -> { where(reviewed_at: nil) }
   scope :pending_review, -> { unreviewed.where.not(requested_review_at: nil) }
   scope :usable, -> { where(usable: [true, nil]) }
-  scope :discoverable, -> { where(listable: [true, nil]).joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
+  scope :listable, -> { where(listable: [true, nil]) }
+  scope :discoverable, -> { listable.joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
   scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
 
   delegate :accounts_count,
@@ -44,6 +47,8 @@ class Tag < ApplicationRecord
 
   after_save :save_account_tag_stat
 
+  update_index('tags#tag', :self) if Chewy.enabled?
+
   def account_tag_stat
     super || build_account_tag_stat
   end
@@ -109,7 +114,7 @@ class Tag < ApplicationRecord
   class << self
     def find_or_create_by_names(name_or_names)
       Array(name_or_names).map(&method(:normalize)).uniq { |str| str.mb_chars.downcase.to_s }.map do |normalized_name|
-        tag = matching_name(normalized_name).first || create(name: normalized_name)
+        tag = matching_name(normalized_name).first || create!(name: normalized_name)
 
         yield tag if block_given?
 
@@ -121,9 +126,10 @@ class Tag < ApplicationRecord
       normalized_term = normalize(term.strip).mb_chars.downcase.to_s
       pattern         = sanitize_sql_like(normalized_term) + '%'
 
-      Tag.where(arel_table[:name].lower.matches(pattern))
-         .where(arel_table[:score].gt(0).or(arel_table[:name].lower.eq(normalized_term)))
-         .order(Arel.sql('length(name) ASC, score DESC, name ASC'))
+      Tag.listable
+         .where(arel_table[:name].lower.matches(pattern))
+         .where(arel_table[:name].lower.eq(normalized_term).or(arel_table[:reviewed_at].not_eq(nil)))
+         .order(Arel.sql('length(name) ASC, name ASC'))
          .limit(limit)
          .offset(offset)
     end
diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb
index 3d60a7fea..e4ce988c1 100644
--- a/app/models/trending_tags.rb
+++ b/app/models/trending_tags.rb
@@ -17,6 +17,9 @@ class TrendingTags
       increment_historical_use!(tag.id, at_time)
       increment_unique_use!(tag.id, account.id, at_time)
       increment_vote!(tag, at_time)
+
+      tag.update(last_status_at: Time.now.utc) if tag.last_status_at.nil? || tag.last_status_at < 12.hours.ago
+      tag.update(last_trend_at: Time.now.utc)  if trending?(tag) && (tag.last_trend_at.nil? || tag.last_trend_at < 12.hours.ago)
     end
 
     def get(limit, filtered: true)
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index 61fa77852..067ba5c32 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -56,7 +56,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
         type: :unordered,
         part_of: ActivityPub::TagManager.instance.replies_uri_for(object),
         items: replies.map(&:second),
-        next: last_id ? ActivityPub::TagManager.instance.replies_uri_for(object, page: true, min_id: last_id) : nil
+        next: last_id ? ActivityPub::TagManager.instance.replies_uri_for(object, page: true, min_id: last_id) : ActivityPub::TagManager.instance.replies_uri_for(object, page: true, only_other_accounts: true)
       )
     )
   end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index c8f6bec7a..d40fe3380 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -26,6 +26,7 @@ class InitialStateSerializer < ActiveModel::Serializer
       access_token: object.token,
       locale: I18n.locale,
       domain: Rails.configuration.x.local_domain,
+      title: instance_presenter.site_title,
       admin: object.admin&.id&.to_s,
       search_enabled: Chewy.enabled?,
       repository: Mastodon::Version.repository,
@@ -54,6 +55,11 @@ class InitialStateSerializer < ActiveModel::Serializer
       store[:trends]            = Setting.trends && object.current_account.user.setting_trends
       store[:default_content_type] = object.current_account.user.setting_default_content_type
       store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font
+    else
+      store[:auto_play_gif] = Setting.auto_play_gif
+      store[:display_media] = Setting.display_media
+      store[:reduce_motion] = Setting.reduce_motion
+      store[:use_blurhash]  = Setting.use_blurhash
     end
 
     store
diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb
index 278affe13..680d9de6f 100644
--- a/app/serializers/rss/account_serializer.rb
+++ b/app/serializers/rss/account_serializer.rb
@@ -5,12 +5,12 @@ class RSS::AccountSerializer
   include StatusesHelper
   include RoutingHelper
 
-  def render(account, statuses)
+  def render(account, statuses, tag)
     builder = RSSBuilder.new
 
     builder.title("#{display_name(account)} (@#{account.local_username_and_domain})")
            .description(account_description(account))
-           .link(ActivityPub::TagManager.instance.url_for(account))
+           .link(tag.present? ? short_account_tag_url(account, tag) : short_account_url(account))
            .logo(full_pack_url('media/images/logo.svg'))
            .accent_color('2b90d9')
 
@@ -33,7 +33,7 @@ class RSS::AccountSerializer
     builder.to_xml
   end
 
-  def self.render(account, statuses)
-    new.render(account, statuses)
+  def self.render(account, statuses, tag)
+    new.render(account, statuses, tag)
   end
 end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index e1874d045..01caaefa9 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -4,105 +4,134 @@ class AccountSearchService < BaseService
   attr_reader :query, :limit, :offset, :options, :account
 
   def call(query, account = nil, options = {})
-    @query   = query.strip
-    @limit   = options[:limit].to_i
-    @offset  = options[:offset].to_i
-    @options = options
-    @account = account
+    @acct_hint = query.start_with?('@')
+    @query     = query.strip.gsub(/\A@/, '')
+    @limit     = options[:limit].to_i
+    @offset    = options[:offset].to_i
+    @options   = options
+    @account   = account
 
-    search_service_results
+    search_service_results.compact.uniq
   end
 
   private
 
   def search_service_results
-    return [] if query_blank_or_hashtag? || limit < 1
+    return [] if query.blank? || limit < 1
 
-    if resolving_non_matching_remote_account?
-      [ResolveAccountService.new.call("#{query_username}@#{query_domain}")].compact
-    else
-      search_results_and_exact_match.compact.uniq
-    end
+    [exact_match] + search_results
   end
 
-  def resolving_non_matching_remote_account?
-    offset.zero? && options[:resolve] && !exact_match? && !domain_is_local?
-  end
+  def exact_match
+    return unless offset.zero? && username_complete?
 
-  def search_results_and_exact_match
-    return search_results.to_a unless offset.zero?
+    return @exact_match if defined?(@exact_match)
 
-    results = [exact_match]
+    @exact_match = begin
+      if options[:resolve]
+        ResolveAccountService.new.call(query)
+      elsif domain_is_local?
+        Account.find_local(query_username)
+      else
+        Account.find_remote(query_username, query_domain)
+      end
+    end
+  end
 
-    return results if exact_match? && limit == 1
+  def search_results
+    return [] if limit_for_non_exact_results.zero?
 
-    results + search_results.to_a
+    @search_results ||= begin
+      if Chewy.enabled?
+        from_elasticsearch
+      else
+        from_database
+      end
+    end
   end
 
-  def query_blank_or_hashtag?
-    query.blank? || query.start_with?('#')
+  def from_database
+    if account
+      advanced_search_results
+    else
+      simple_search_results
+    end
   end
 
-  def split_query_string
-    @split_query_string ||= query.gsub(/\A@/, '').split('@')
+  def advanced_search_results
+    Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset)
   end
 
-  def query_username
-    @query_username ||= split_query_string.first || ''
+  def simple_search_results
+    Account.search_for(terms_for_query, limit_for_non_exact_results, offset)
   end
 
-  def query_domain
-    @query_domain ||= query_without_split? ? nil : split_query_string.last
-  end
+  def from_elasticsearch
+    must_clauses   = [{ multi_match: { query: terms_for_query, fields: likely_acct? ? %w(acct.edge_ngram acct) : %w(acct.edge_ngram acct display_name.edge_ngram display_name), type: 'most_fields', operator: 'and' } }]
+    should_clauses = []
 
-  def query_without_split?
-    split_query_string.size == 1
-  end
+    if account
+      return [] if options[:following] && following_ids.empty?
 
-  def domain_is_local?
-    @domain_is_local ||= TagManager.instance.local_domain?(query_domain)
-  end
+      if options[:following]
+        must_clauses << { terms: { id: following_ids } }
+      elsif following_ids.any?
+        should_clauses << { terms: { id: following_ids, boost: 100 } }
+      end
+    end
 
-  def search_from
-    options[:following] && account ? account.following : Account
-  end
+    query     = { bool: { must: must_clauses, should: should_clauses } }
+    functions = [reputation_score_function, followers_score_function, time_distance_function]
 
-  def exact_match?
-    exact_match.present?
-  end
+    records = AccountsIndex.query(function_score: { query: query, functions: functions, boost_mode: 'multiply', score_mode: 'avg' })
+                           .limit(limit_for_non_exact_results)
+                           .offset(offset)
+                           .objects
+                           .compact
 
-  def exact_match
-    return @exact_match if defined?(@exact_match)
+    ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
 
-    @exact_match = begin
-      if domain_is_local?
-        search_from.without_suspended.find_local(query_username)
-      else
-        search_from.without_suspended.find_remote(query_username, query_domain)
-      end
-    end
+    records
   end
 
-  def search_results
-    @search_results ||= begin
-      if account
-        advanced_search_results
-      else
-        simple_search_results
-      end
-    end
+  def reputation_score_function
+    {
+      script_score: {
+        script: {
+          source: "(doc['followers_count'].value + 0.0) / (doc['followers_count'].value + doc['following_count'].value + 1)",
+        },
+      },
+    }
   end
 
-  def advanced_search_results
-    Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset)
+  def followers_score_function
+    {
+      field_value_factor: {
+        field: 'followers_count',
+        modifier: 'log2p',
+        missing: 0,
+      },
+    }
   end
 
-  def simple_search_results
-    Account.search_for(terms_for_query, limit_for_non_exact_results, offset)
+  def time_distance_function
+    {
+      gauss: {
+        last_status_at: {
+          scale: '30d',
+          offset: '30d',
+          decay: 0.3,
+        },
+      },
+    }
+  end
+
+  def following_ids
+    @following_ids ||= account.active_relationships.pluck(:target_account_id)
   end
 
   def limit_for_non_exact_results
-    if offset.zero? && exact_match?
+    if exact_match?
       limit - 1
     else
       limit
@@ -113,7 +142,39 @@ class AccountSearchService < BaseService
     if domain_is_local?
       query_username
     else
-      "#{query_username} #{query_domain}"
+      query
     end
   end
+
+  def split_query_string
+    @split_query_string ||= query.split('@')
+  end
+
+  def query_username
+    @query_username ||= split_query_string.first || ''
+  end
+
+  def query_domain
+    @query_domain ||= query_without_split? ? nil : split_query_string.last
+  end
+
+  def query_without_split?
+    split_query_string.size == 1
+  end
+
+  def domain_is_local?
+    @domain_is_local ||= TagManager.instance.local_domain?(query_domain)
+  end
+
+  def exact_match?
+    exact_match.present?
+  end
+
+  def username_complete?
+    query.include?('@') && "@#{query}" =~ Account::MENTION_RE
+  end
+
+  def likely_acct?
+    @acct_hint || username_complete?
+  end
 end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 769d1ac7a..fe601bbf4 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -52,15 +52,15 @@ class SearchService < BaseService
     preloaded_relations = relations_map_for_account(@account, account_ids, account_domains)
 
     results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? }
-  rescue Faraday::ConnectionFailed
+  rescue Faraday::ConnectionFailed, Parslet::ParseFailed
     []
   end
 
   def perform_hashtags_search!
-    Tag.search_for(
-      @query.gsub(/\A#/, ''),
-      @limit,
-      @offset
+    TagSearchService.new.call(
+      @query,
+      limit: @limit,
+      offset: @offset
     )
   end
 
diff --git a/app/services/tag_search_service.rb b/app/services/tag_search_service.rb
new file mode 100644
index 000000000..64dd76bb7
--- /dev/null
+++ b/app/services/tag_search_service.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+class TagSearchService < BaseService
+  def call(query, options = {})
+    @query  = query.strip.gsub(/\A#/, '')
+    @offset = options[:offset].to_i
+    @limit  = options[:limit].to_i
+
+    if Chewy.enabled?
+      from_elasticsearch
+    else
+      from_database
+    end
+  end
+
+  private
+
+  def from_elasticsearch
+    query = {
+      function_score: {
+        query: {
+          multi_match: {
+            query: @query,
+            fields: %w(name.edge_ngram name),
+            type: 'most_fields',
+            operator: 'and',
+          },
+        },
+
+        functions: [
+          {
+            field_value_factor: {
+              field: 'usage',
+              modifier: 'log2p',
+              missing: 0,
+            },
+          },
+
+          {
+            gauss: {
+              last_status_at: {
+                scale: '7d',
+                offset: '14d',
+                decay: 0.5,
+              },
+            },
+          },
+        ],
+
+        boost_mode: 'multiply',
+      },
+    }
+
+    filter = {
+      bool: {
+        should: [
+          {
+            term: {
+              reviewed: {
+                value: true,
+              },
+            },
+          },
+
+          {
+            term: {
+              name: {
+                value: @query,
+              },
+            },
+          },
+        ],
+      },
+    }
+
+    TagsIndex.query(query).filter(filter).limit(@limit).offset(@offset).objects.compact
+  end
+
+  def from_database
+    Tag.search_for(@query, @limit, @offset)
+  end
+end
diff --git a/app/views/about/blocks.html.haml b/app/views/about/blocks.html.haml
new file mode 100644
index 000000000..a81a4d1eb
--- /dev/null
+++ b/app/views/about/blocks.html.haml
@@ -0,0 +1,48 @@
+- content_for :page_title do
+  = t('domain_blocks.title', instance: site_hostname)
+
+.grid
+  .column-0
+    .box-widget.rich-formatting
+      %h2= t('domain_blocks.blocked_domains')
+      %p= t('domain_blocks.description', instance: site_hostname)
+      .table-wrapper
+        %table.blocks-table
+          %thead
+            %tr
+              %th= t('domain_blocks.domain')
+              %th.severity-column= t('domain_blocks.severity')
+              - if @show_rationale
+                %th.button-column
+          %tbody
+            - if @blocks.empty?
+              %tr
+                %td{ colspan: @show_rationale ? 3 : 2 }= t('domain_blocks.no_domain_blocks')
+            - else
+              - @blocks.each_with_index do |block, i|
+                %tr{ class: i % 2 == 0 ? 'even': nil }
+                  %td{ title: block.domain }= block.domain
+                  %td= block_severity_text(block)
+                  - if @show_rationale
+                    %td
+                      - if block.public_comment.present?
+                        %button.icon-button{ title: t('domain_blocks.show_rationale'), 'aria-label' => t('domain_blocks.show_rationale') }
+                          = fa_icon 'chevron-down fw', 'aria-hidden' => true
+                - if @show_rationale
+                  - if block.public_comment.present?
+                    %tr.rationale.hidden
+                      %td{ colspan: 3 }= block.public_comment.presence
+      %h2= t('domain_blocks.severity_legend.title')
+      - if @blocks.any? { |block| block.reject_media? }
+        %h3= t('domain_blocks.media_block')
+        %p= t('domain_blocks.severity_legend.media_block')
+      - if @blocks.any? { |block| block.severity == 'silence' }
+        %h3= t('domain_blocks.silence')
+        %p= t('domain_blocks.severity_legend.silence')
+      - if @blocks.any? { |block| block.severity == 'suspend' }
+        %h3= t('domain_blocks.suspension')
+        %p= t('domain_blocks.severity_legend.suspension')
+        - if public_fetch_mode?
+          %p= t('domain_blocks.severity_legend.suspension_disclaimer')
+  .column-1
+    = render 'application/sidebar'
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 034304936..9c26dbabc 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -7,7 +7,7 @@
   - if @account.user&.setting_noindex
     %meta{ name: 'robots', content: 'noindex, noarchive' }/
 
-  %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
+  %link{ rel: 'alternate', type: 'application/rss+xml', href: @rss_url }/
   %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
 
   - if @older_url
@@ -56,24 +56,33 @@
 
     = render 'bio', account: @account
 
-    - unless @endorsed_accounts.empty?
+    - if @endorsed_accounts.empty? && @account.id == current_account&.id
+      .placeholder-widget= t('accounts.endorsements_hint')
+    - elsif !@endorsed_accounts.empty?
       .endorsements-widget
         %h4= t 'accounts.choices_html', name: content_tag(:bdi, display_name(@account, custom_emojify: true))
 
         - @endorsed_accounts.each do |account|
           = account_link_to account
 
-    - @account.featured_tags.order(statuses_count: :desc).each do |featured_tag|
-      .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
-        = link_to short_account_tag_path(@account, featured_tag.tag) do
-          %h4
-            = fa_icon 'hashtag'
-            = featured_tag.name
-            %small
-              - if featured_tag.last_status_at.nil?
-                = t('accounts.nothing_here')
-              - else
-                %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
-          .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
+    - if @featured_hashtags.empty? && @account.id == current_account&.id
+      .placeholder-widget
+        = t('accounts.featured_tags_hint')
+        = link_to settings_featured_tags_path do
+          = t('featured_tags.add_new')
+          = fa_icon 'chevron-right fw'
+    - else
+      - @featured_hashtags.each do |featured_tag|
+        .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
+          = link_to short_account_tag_path(@account, featured_tag.tag) do
+            %h4
+              = fa_icon 'hashtag'
+              = featured_tag.name
+              %small
+                - if featured_tag.last_status_at.nil?
+                  = t('accounts.nothing_here')
+                - else
+                  %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
+            .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
 
     = render 'application/sidebar'
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index b0ab394d6..5a9b33f04 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -91,6 +91,12 @@
   .fields-group
     = f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+
   .fields-group
     = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
     = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode?
diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml
index b5ce5845e..90c8f9dd1 100644
--- a/app/views/application/_sidebar.html.haml
+++ b/app/views/application/_sidebar.html.haml
@@ -4,3 +4,13 @@
 
   .hero-widget__text
     %p= @instance_presenter.site_short_description.html_safe.presence || @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
+
+- if Setting.trends
+  - trends = TrendingTags.get(3)
+
+  - unless trends.empty?
+    .endorsements-widget.trends-widget
+      %h4.emojify= t('footer.trending_now')
+
+      - trends.each do |tag|
+        = react_component :hashtag, hashtag: ActiveModelSerializers::SerializableResource.new(tag, serializer: REST::TagSerializer).as_json
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 6c5268b61..9530e612a 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -5,7 +5,7 @@
   = preload_link_tag asset_pack_path('features/notifications.js'), crossorigin: 'anonymous'
 
   %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
-  %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
+  = render_initial_state
 
 .app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
   %noscript
diff --git a/app/views/invites/_form.html.haml b/app/views/invites/_form.html.haml
index 3a2a5ef0e..b19f70539 100644
--- a/app/views/invites/_form.html.haml
+++ b/app/views/invites/_form.html.haml
@@ -10,5 +10,8 @@
   .fields-group
     = f.input :autofollow, wrapper: :with_label
 
+  .fields-group
+    = f.input :comment, wrapper: :with_label, input_html: { maxlength: 420 }
+
   .actions
     = f.button :button, t('invites.generate'), type: :submit
diff --git a/app/views/invites/_invite.html.haml b/app/views/invites/_invite.html.haml
index 62799ca5b..03050c868 100644
--- a/app/views/invites/_invite.html.haml
+++ b/app/views/invites/_invite.html.haml
@@ -21,5 +21,8 @@
       = t('invites.expired')
 
   %td
+    = invite.comment
+
+  %td
     - if invite.valid_for_use? && policy(invite).destroy?
       = table_link_to 'times', t('invites.delete'), invite_path(invite), method: :delete
diff --git a/app/views/invites/index.html.haml b/app/views/invites/index.html.haml
index 61420ab1e..62065d6ae 100644
--- a/app/views/invites/index.html.haml
+++ b/app/views/invites/index.html.haml
@@ -15,6 +15,7 @@
         %th
         %th= t('invites.table.uses')
         %th= t('invites.table.expires_at')
+        %th= t('invites.table.comment')
         %th
     %tbody
       = render @invites
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index da510fa7a..fb9ac5cec 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -1,3 +1,6 @@
+- content_for :header_tags do
+  = render_initial_state
+
 - content_for :content do
   .public-layout
     - unless @hide_navbar
diff --git a/app/views/public_timelines/show.html.haml b/app/views/public_timelines/show.html.haml
index f80157c67..591620f64 100644
--- a/app/views/public_timelines/show.html.haml
+++ b/app/views/public_timelines/show.html.haml
@@ -3,7 +3,6 @@
 
 - content_for :header_tags do
   %meta{ name: 'robots', content: 'noindex' }/
-  %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
 
 .page-header
   %h1= t('about.see_whats_happening')
diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml
index 5f69517f3..6734d027c 100644
--- a/app/views/settings/featured_tags/index.html.haml
+++ b/app/views/settings/featured_tags/index.html.haml
@@ -1,6 +1,10 @@
 - content_for :page_title do
   = t('settings.featured_tags')
 
+%p= t('featured_tags.hint_html')
+
+%hr.spacer/
+
 = simple_form_for @featured_tag, url: settings_featured_tags_path do |f|
   = render 'shared/error_messages', object: @featured_tag
 
diff --git a/app/views/shares/show.html.haml b/app/views/shares/show.html.haml
index 4c0390c42..28910d3ab 100644
--- a/app/views/shares/show.html.haml
+++ b/app/views/shares/show.html.haml
@@ -1,4 +1,4 @@
 - content_for :header_tags do
-  %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
+  = render_initial_state
 
 #mastodon-compose{ data: { props: Oj.dump(default_props) } }
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 1a9c58983..74d44bd7b 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -5,7 +5,6 @@
   %meta{ name: 'robots', content: 'noindex' }/
   %link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/
 
-  %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
   = render 'og'
 
 .page-header