about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.env.production.sample8
-rw-r--r--.ruby-version2
-rw-r--r--.travis.yml4
-rw-r--r--Aptfile1
-rw-r--r--Dockerfile2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock174
-rw-r--r--app/controllers/admin/custom_emojis_controller.rb34
-rw-r--r--app/controllers/admin/instances_controller.rb12
-rw-r--r--app/controllers/admin/settings_controller.rb14
-rw-r--r--app/controllers/home_controller.rb35
-rw-r--r--app/controllers/media_proxy_controller.rb40
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/settings_helper.rb2
-rw-r--r--app/javascript/images/logo.svg2
-rw-r--r--app/javascript/images/logo_alt.svg2
-rw-r--r--app/javascript/images/logo_full.svg2
-rw-r--r--app/javascript/images/mastodon_small.jpgbin25199 -> 0 bytes
-rw-r--r--app/javascript/images/preview.jpgbin0 -> 292252 bytes
-rw-r--r--app/javascript/mastodon/actions/height_cache.js17
-rw-r--r--app/javascript/mastodon/actions/statuses.js17
-rw-r--r--app/javascript/mastodon/components/intersection_observer_article.js33
-rw-r--r--app/javascript/mastodon/components/load_more.js2
-rw-r--r--app/javascript/mastodon/components/media_gallery.js4
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js19
-rw-r--r--app/javascript/mastodon/components/status.js19
-rw-r--r--app/javascript/mastodon/containers/card_container.js18
-rw-r--r--app/javascript/mastodon/containers/intersection_observer_article_container.js17
-rw-r--r--app/javascript/mastodon/containers/media_gallery_container.js34
-rw-r--r--app/javascript/mastodon/containers/status_container.js6
-rw-r--r--app/javascript/mastodon/containers/video_container.js26
-rw-r--r--app/javascript/mastodon/emoji.js60
-rw-r--r--app/javascript/mastodon/features/compose/util/counter.js4
-rw-r--r--app/javascript/mastodon/features/compose/util/url_regex.js196
-rw-r--r--app/javascript/mastodon/features/standalone/compose/index.js2
-rw-r--r--app/javascript/mastodon/features/status/components/card.js10
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js5
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/video_modal.js22
-rw-r--r--app/javascript/mastodon/features/ui/index.js4
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js4
-rw-r--r--app/javascript/mastodon/features/video/index.js304
-rw-r--r--app/javascript/mastodon/locales/ar.json11
-rw-r--r--app/javascript/mastodon/locales/bg.json11
-rw-r--r--app/javascript/mastodon/locales/ca.json11
-rw-r--r--app/javascript/mastodon/locales/de.json11
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json62
-rw-r--r--app/javascript/mastodon/locales/en.json13
-rw-r--r--app/javascript/mastodon/locales/eo.json11
-rw-r--r--app/javascript/mastodon/locales/es.json261
-rw-r--r--app/javascript/mastodon/locales/fa.json11
-rw-r--r--app/javascript/mastodon/locales/fi.json11
-rw-r--r--app/javascript/mastodon/locales/fr.json15
-rw-r--r--app/javascript/mastodon/locales/he.json11
-rw-r--r--app/javascript/mastodon/locales/hr.json12
-rw-r--r--app/javascript/mastodon/locales/hu.json11
-rw-r--r--app/javascript/mastodon/locales/id.json11
-rw-r--r--app/javascript/mastodon/locales/io.json11
-rw-r--r--app/javascript/mastodon/locales/it.json11
-rw-r--r--app/javascript/mastodon/locales/ja.json17
-rw-r--r--app/javascript/mastodon/locales/ko.json13
-rw-r--r--app/javascript/mastodon/locales/nl.json28
-rw-r--r--app/javascript/mastodon/locales/no.json11
-rw-r--r--app/javascript/mastodon/locales/oc.json15
-rw-r--r--app/javascript/mastodon/locales/pl.json11
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json189
-rw-r--r--app/javascript/mastodon/locales/pt.json11
-rw-r--r--app/javascript/mastodon/locales/ru.json11
-rw-r--r--app/javascript/mastodon/locales/th.json11
-rw-r--r--app/javascript/mastodon/locales/tr.json11
-rw-r--r--app/javascript/mastodon/locales/uk.json11
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json95
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json101
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json87
-rw-r--r--app/javascript/mastodon/reducers/height_cache.js23
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/statuses.js27
-rw-r--r--app/javascript/packs/public.js34
-rw-r--r--app/javascript/styles/about.scss9
-rw-r--r--app/javascript/styles/admin.scss8
-rw-r--r--app/javascript/styles/components.scss195
-rw-r--r--app/javascript/styles/forms.scss37
-rw-r--r--app/javascript/styles/stream_entries.scss147
-rw-r--r--app/lib/activitypub/activity/announce.rb7
-rw-r--r--app/lib/activitypub/activity/create.rb46
-rw-r--r--app/lib/activitypub/adapter.rb6
-rw-r--r--app/lib/activitypub/tag_manager.rb2
-rw-r--r--app/lib/formatter.rb58
-rw-r--r--app/lib/language_detector.rb39
-rw-r--r--app/lib/ostatus/activity/creation.rb20
-rw-r--r--app/lib/ostatus/atom_serializer.rb4
-rw-r--r--app/lib/request.rb2
-rw-r--r--app/lib/tag_manager.rb2
-rw-r--r--app/models/account.rb5
-rw-r--r--app/models/concerns/remotable.rb4
-rw-r--r--app/models/custom_emoji.rb38
-rw-r--r--app/models/instance_filter.rb28
-rw-r--r--app/models/media_attachment.rb10
-rw-r--r--app/models/site_upload.rb44
-rw-r--r--app/models/status.rb9
-rw-r--r--app/presenters/instance_presenter.rb6
-rw-r--r--app/serializers/activitypub/activity_serializer.rb6
-rw-r--r--app/serializers/activitypub/note_serializer.rb26
-rw-r--r--app/serializers/oembed_serializer.rb2
-rw-r--r--app/serializers/rest/instance_serializer.rb8
-rw-r--r--app/serializers/rest/media_attachment_serializer.rb12
-rw-r--r--app/serializers/rest/status_serializer.rb11
-rw-r--r--app/services/activitypub/fetch_remote_status_service.rb2
-rw-r--r--app/services/activitypub/process_account_service.rb78
-rw-r--r--app/services/activitypub/process_collection_service.rb4
-rw-r--r--app/services/block_domain_service.rb9
-rw-r--r--app/services/fetch_link_card_service.rb43
-rw-r--r--app/services/post_status_service.rb6
-rw-r--r--app/services/resolve_remote_account_service.rb1
-rw-r--r--app/validators/status_pin_validator.rb1
-rw-r--r--app/views/about/_og.html.haml10
-rw-r--r--app/views/about/_registration.html.haml21
-rw-r--r--app/views/about/more.html.haml11
-rw-r--r--app/views/about/show.html.haml11
-rw-r--r--app/views/accounts/_header.html.haml6
-rw-r--r--app/views/accounts/_og.html.haml17
-rw-r--r--app/views/accounts/show.html.haml2
-rw-r--r--app/views/admin/accounts/show.html.haml109
-rw-r--r--app/views/admin/custom_emojis/_custom_emoji.html.haml7
-rw-r--r--app/views/admin/custom_emojis/index.html.haml14
-rw-r--r--app/views/admin/custom_emojis/new.html.haml12
-rw-r--r--app/views/admin/instances/_instance.html.haml2
-rw-r--r--app/views/admin/instances/index.html.haml10
-rw-r--r--app/views/admin/settings/edit.html.haml5
-rw-r--r--app/views/auth/registrations/new.html.haml4
-rw-r--r--app/views/auth/sessions/two_factor.html.haml4
-rw-r--r--app/views/oauth/authorizations/show.html.haml3
-rw-r--r--app/views/settings/profiles/show.html.haml4
-rw-r--r--app/views/stream_entries/_og_description.html.haml4
-rw-r--r--app/views/stream_entries/_og_image.html.haml25
-rw-r--r--app/views/stream_entries/show.html.haml10
-rw-r--r--app/views/user_mailer/confirmation_instructions.es.html.erb12
-rw-r--r--app/views/user_mailer/confirmation_instructions.es.text.erb12
-rw-r--r--app/views/user_mailer/confirmation_instructions.pt-BR.html.erb12
-rw-r--r--app/views/user_mailer/confirmation_instructions.pt-BR.text.erb12
-rw-r--r--app/views/user_mailer/confirmation_instructions.zh-cn.html.erb2
-rw-r--r--app/views/user_mailer/confirmation_instructions.zh-cn.text.erb2
-rw-r--r--app/views/user_mailer/password_change.es.html.erb3
-rw-r--r--app/views/user_mailer/password_change.es.text.erb3
-rw-r--r--app/views/user_mailer/password_change.pt-BR.html.erb3
-rw-r--r--app/views/user_mailer/password_change.pt-BR.text.erb3
-rw-r--r--app/views/user_mailer/reset_password_instructions.es.html.erb8
-rw-r--r--app/views/user_mailer/reset_password_instructions.es.text.erb8
-rw-r--r--app/views/user_mailer/reset_password_instructions.pt-BR.html.erb8
-rw-r--r--app/views/user_mailer/reset_password_instructions.pt-BR.text.erb8
-rw-r--r--app/views/user_mailer/reset_password_instructions.zh-cn.html.erb2
-rw-r--r--app/views/user_mailer/reset_password_instructions.zh-cn.text.erb2
-rw-r--r--app/workers/activitypub/reply_distribution_worker.rb4
-rw-r--r--app/workers/pubsubhubbub/subscribe_worker.rb2
-rw-r--r--app/workers/refollow_worker.rb24
-rw-r--r--app/workers/scheduler/ip_cleanup_scheduler.rb12
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/paperclip.rb5
-rw-r--r--config/initializers/sidekiq.rb6
-rw-r--r--config/initializers/twitter_regex.rb42
-rw-r--r--config/locales/activerecord.ca.yml1
-rw-r--r--config/locales/activerecord.en.yml1
-rw-r--r--config/locales/activerecord.fa.yml1
-rw-r--r--config/locales/activerecord.fr.yml1
-rw-r--r--config/locales/activerecord.he.yml1
-rw-r--r--config/locales/activerecord.id.yml1
-rw-r--r--config/locales/activerecord.it.yml1
-rw-r--r--config/locales/activerecord.ja.yml1
-rw-r--r--config/locales/activerecord.nl.yml1
-rw-r--r--config/locales/activerecord.oc.yml1
-rw-r--r--config/locales/activerecord.pl.yml1
-rw-r--r--config/locales/activerecord.ru.yml1
-rw-r--r--config/locales/activerecord.uk.yml1
-rw-r--r--config/locales/ar.yml2
-rw-r--r--config/locales/devise.bg.yml14
-rw-r--r--config/locales/devise.de.yml84
-rw-r--r--config/locales/devise.eo.yml2
-rw-r--r--config/locales/devise.es.yml4
-rw-r--r--config/locales/devise.fa.yml4
-rw-r--r--config/locales/devise.fi.yml2
-rw-r--r--config/locales/devise.fr.yml4
-rw-r--r--config/locales/devise.he.yml52
-rw-r--r--config/locales/devise.hu.yml4
-rw-r--r--config/locales/devise.ja.yml12
-rw-r--r--config/locales/devise.nl.yml2
-rw-r--r--config/locales/devise.oc.yml8
-rw-r--r--config/locales/devise.pl.yml2
-rw-r--r--config/locales/devise.pt-BR.yml70
-rw-r--r--config/locales/devise.zh-TW.yml2
-rw-r--r--config/locales/doorkeeper.ar.yml2
-rw-r--r--config/locales/doorkeeper.bg.yml2
-rw-r--r--config/locales/doorkeeper.ca.yml2
-rw-r--r--config/locales/doorkeeper.de.yml2
-rw-r--r--config/locales/doorkeeper.en.yml2
-rw-r--r--config/locales/doorkeeper.eo.yml2
-rw-r--r--config/locales/doorkeeper.es.yml17
-rw-r--r--config/locales/doorkeeper.fa.yml2
-rw-r--r--config/locales/doorkeeper.fi.yml2
-rw-r--r--config/locales/doorkeeper.fr.yml2
-rw-r--r--config/locales/doorkeeper.he.yml6
-rw-r--r--config/locales/doorkeeper.hr.yml2
-rw-r--r--config/locales/doorkeeper.hu.yml4
-rw-r--r--config/locales/doorkeeper.id.yml2
-rw-r--r--config/locales/doorkeeper.io.yml2
-rw-r--r--config/locales/doorkeeper.it.yml2
-rw-r--r--config/locales/doorkeeper.ja.yml2
-rw-r--r--config/locales/doorkeeper.nl.yml53
-rw-r--r--config/locales/doorkeeper.no.yml2
-rw-r--r--config/locales/doorkeeper.oc.yml8
-rw-r--r--config/locales/doorkeeper.pl.yml2
-rw-r--r--config/locales/doorkeeper.pt-BR.yml117
-rw-r--r--config/locales/doorkeeper.pt.yml2
-rw-r--r--config/locales/doorkeeper.ru.yml2
-rw-r--r--config/locales/doorkeeper.th.yml2
-rw-r--r--config/locales/doorkeeper.uk.yml2
-rw-r--r--config/locales/doorkeeper.zh-CN.yml2
-rw-r--r--config/locales/doorkeeper.zh-HK.yml14
-rw-r--r--config/locales/doorkeeper.zh-TW.yml6
-rw-r--r--config/locales/en.yml20
-rw-r--r--config/locales/es.yml411
-rw-r--r--config/locales/fa.yml33
-rw-r--r--config/locales/ja.yml8
-rw-r--r--config/locales/nl.yml22
-rw-r--r--config/locales/oc.yml4
-rw-r--r--config/locales/pl.yml9
-rw-r--r--config/locales/pt-BR.yml579
-rw-r--r--config/locales/simple_form.ar.yml4
-rw-r--r--config/locales/simple_form.bg.yml4
-rw-r--r--config/locales/simple_form.ca.yml4
-rw-r--r--config/locales/simple_form.de.yml8
-rw-r--r--config/locales/simple_form.eo.yml4
-rw-r--r--config/locales/simple_form.es.yml15
-rw-r--r--config/locales/simple_form.fa.yml12
-rw-r--r--config/locales/simple_form.fi.yml4
-rw-r--r--config/locales/simple_form.he.yml36
-rw-r--r--config/locales/simple_form.hr.yml4
-rw-r--r--config/locales/simple_form.hu.yml4
-rw-r--r--config/locales/simple_form.id.yml4
-rw-r--r--config/locales/simple_form.ko.yml4
-rw-r--r--config/locales/simple_form.nl.yml24
-rw-r--r--config/locales/simple_form.no.yml15
-rw-r--r--config/locales/simple_form.oc.yml1
-rw-r--r--config/locales/simple_form.pt-BR.yml79
-rw-r--r--config/locales/simple_form.pt.yml4
-rw-r--r--config/locales/simple_form.th.yml12
-rw-r--r--config/locales/simple_form.tr.yml10
-rw-r--r--config/locales/simple_form.uk.yml4
-rw-r--r--config/locales/simple_form.zh-CN.yml4
-rw-r--r--config/locales/simple_form.zh-TW.yml4
-rw-r--r--config/locales/zh-CN.yml30
-rw-r--r--config/locales/zh-HK.yml4
-rw-r--r--config/locales/zh-TW.yml4
-rw-r--r--config/navigation.rb1
-rw-r--r--config/routes.rb4
-rw-r--r--config/sidekiq.yml15
-rw-r--r--db/migrate/20170913000752_create_site_uploads.rb10
-rw-r--r--db/migrate/20170917153509_create_custom_emojis.rb13
-rw-r--r--db/schema.rb26
-rw-r--r--lib/mastodon/unique_retry_job_middleware.rb20
-rw-r--r--lib/mastodon/version.rb2
-rw-r--r--lib/tasks/mastodon.rake17
-rw-r--r--spec/controllers/home_controller_spec.rb2
-rw-r--r--spec/fabricators/custom_emoji_fabricator.rb5
-rw-r--r--spec/fabricators/site_upload_fabricator.rb3
-rw-r--r--spec/fixtures/files/emojo.pngbin0 -> 29814 bytes
-rw-r--r--spec/fixtures/requests/activitypub-actor-noinbox.txt9
-rw-r--r--spec/fixtures/requests/activitypub-actor.txt9
-rw-r--r--spec/fixtures/requests/activitypub-feed.txt47
-rw-r--r--spec/fixtures/requests/activitypub-webfinger.txt7
-rw-r--r--spec/lib/activitypub/activity/create_spec.rb25
-rw-r--r--spec/lib/formatter_spec.rb126
-rw-r--r--spec/lib/language_detector_spec.rb34
-rw-r--r--spec/lib/ostatus/atom_serializer_spec.rb16
-rw-r--r--spec/lib/tag_manager_spec.rb10
-rw-r--r--spec/models/custom_emoji_spec.rb25
-rw-r--r--spec/models/site_upload_spec.rb5
-rw-r--r--spec/models/status_spec.rb21
-rw-r--r--spec/services/fetch_link_card_service_spec.rb11
-rw-r--r--spec/services/post_status_service_spec.rb9
-rw-r--r--spec/services/resolve_remote_account_service_spec.rb33
-rw-r--r--spec/views/about/show.html.haml_spec.rb1
-rw-r--r--spec/views/stream_entries/show.html.haml_spec.rb8
282 files changed, 4624 insertions, 1620 deletions
diff --git a/.env.production.sample b/.env.production.sample
index 3e054db84..e022a8405 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -101,11 +101,19 @@ SMTP_FROM_ADDRESS=notifications@example.com
 # Swift (optional)
 # SWIFT_ENABLED=true
 # SWIFT_USERNAME=
+# For Keystone V3, the value for SWIFT_TENANT should be the project name
 # SWIFT_TENANT=
 # SWIFT_PASSWORD=
+# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
+# issues with token rate-limiting during high load.
 # SWIFT_AUTH_URL=
 # SWIFT_CONTAINER=
 # SWIFT_OBJECT_URL=
+# SWIFT_REGION=
+# Defaults to 'default'
+# SWIFT_DOMAIN_NAME=
+# Defaults to 60 seconds. Set to 0 to disable
+# SWIFT_CACHE_TTL=
 
 # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
 # S3_CLOUDFRONT_HOST=
diff --git a/.ruby-version b/.ruby-version
index 005119baa..8e8299dcc 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.4.1
+2.4.2
diff --git a/.travis.yml b/.travis.yml
index d5b51fcb0..52ff15c01 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,18 +26,16 @@ addons:
   postgresql: 9.4
   apt:
     sources:
-    - ubuntu-toolchain-r-test
     - trusty-media
     packages:
     - ffmpeg
-    - g++-6
     - libprotobuf-dev
     - protobuf-compiler
     - libicu-dev
 
 rvm:
   - 2.3.4
-  - 2.4.1
+  - 2.4.2
 
 services:
   - redis-server
diff --git a/Aptfile b/Aptfile
index 48dff1a77..5dac83607 100644
--- a/Aptfile
+++ b/Aptfile
@@ -1,4 +1,5 @@
 ffmpeg
+libicu[0-9][0-9]
 libicu-dev
 libidn11
 libidn11-dev
diff --git a/Dockerfile b/Dockerfile
index 15138065b..3ad2ad7ef 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:2.4.1-alpine3.6
+FROM ruby:2.4.2-alpine3.6
 
 LABEL maintainer="https://github.com/tootsuite/mastodon" \
       description="A GNU Social-compatible microblogging server"
diff --git a/Gemfile b/Gemfile
index 637bf53ee..4f4861913 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,8 +5,8 @@ ruby '>= 2.3.0', '< 2.5.0'
 
 gem 'pkg-config', '~> 1.2'
 
-gem 'puma', '~> 3.8'
-gem 'rails', '~> 5.1.0'
+gem 'puma', '~> 3.10'
+gem 'rails', '~> 5.1.4'
 gem 'uglifier', '~> 3.2'
 
 gem 'hamlit-rails', '~> 0.2'
@@ -25,7 +25,7 @@ gem 'bootsnap'
 gem 'browser'
 gem 'charlock_holmes', '~> 0.7.5'
 gem 'iso-639'
-gem 'cld3', '~> 3.1'
+gem 'cld3', '~> 3.2.0'
 gem 'devise', '~> 4.2'
 gem 'devise-two-factor', '~> 3.0'
 gem 'doorkeeper', '~> 4.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index ddb97dd94..97db3aa9a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,25 +1,25 @@
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (5.1.3)
-      actionpack (= 5.1.3)
+    actioncable (5.1.4)
+      actionpack (= 5.1.4)
       nio4r (~> 2.0)
       websocket-driver (~> 0.6.1)
-    actionmailer (5.1.3)
-      actionpack (= 5.1.3)
-      actionview (= 5.1.3)
-      activejob (= 5.1.3)
+    actionmailer (5.1.4)
+      actionpack (= 5.1.4)
+      actionview (= 5.1.4)
+      activejob (= 5.1.4)
       mail (~> 2.5, >= 2.5.4)
       rails-dom-testing (~> 2.0)
-    actionpack (5.1.3)
-      actionview (= 5.1.3)
-      activesupport (= 5.1.3)
+    actionpack (5.1.4)
+      actionview (= 5.1.4)
+      activesupport (= 5.1.4)
       rack (~> 2.0)
-      rack-test (~> 0.6.3)
+      rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.0)
       rails-html-sanitizer (~> 1.0, >= 1.0.2)
-    actionview (5.1.3)
-      activesupport (= 5.1.3)
+    actionview (5.1.4)
+      activesupport (= 5.1.4)
       builder (~> 3.1)
       erubi (~> 1.4)
       rails-dom-testing (~> 2.0)
@@ -30,16 +30,16 @@ GEM
       case_transform (>= 0.2)
       jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
     active_record_query_trace (1.5.4)
-    activejob (5.1.3)
-      activesupport (= 5.1.3)
+    activejob (5.1.4)
+      activesupport (= 5.1.4)
       globalid (>= 0.3.6)
-    activemodel (5.1.3)
-      activesupport (= 5.1.3)
-    activerecord (5.1.3)
-      activemodel (= 5.1.3)
-      activesupport (= 5.1.3)
+    activemodel (5.1.4)
+      activesupport (= 5.1.4)
+    activerecord (5.1.4)
+      activemodel (= 5.1.4)
+      activesupport (= 5.1.4)
       arel (~> 8.0)
-    activesupport (5.1.3)
+    activesupport (5.1.4)
       concurrent-ruby (~> 1.0, >= 1.0.2)
       i18n (~> 0.7)
       minitest (~> 5.1)
@@ -57,33 +57,33 @@ GEM
       encryptor (~> 3.0.0)
     av (0.9.0)
       cocaine (~> 0.5.3)
-    aws-sdk (2.10.21)
-      aws-sdk-resources (= 2.10.21)
-    aws-sdk-core (2.10.21)
+    aws-sdk (2.10.46)
+      aws-sdk-resources (= 2.10.46)
+    aws-sdk-core (2.10.46)
       aws-sigv4 (~> 1.0)
       jmespath (~> 1.0)
-    aws-sdk-resources (2.10.21)
-      aws-sdk-core (= 2.10.21)
-    aws-sigv4 (1.0.1)
+    aws-sdk-resources (2.10.46)
+      aws-sdk-core (= 2.10.46)
+    aws-sigv4 (1.0.2)
     bcrypt (3.1.11)
-    better_errors (2.1.1)
+    better_errors (2.3.0)
       coderay (>= 1.0.0)
-      erubis (>= 2.6.6)
+      erubi (>= 1.0.0)
       rack (>= 0.9.0)
     binding_of_caller (0.7.2)
       debug_inspector (>= 0.0.1)
-    bootsnap (1.1.2)
+    bootsnap (1.1.3)
       msgpack (~> 1.0)
     brakeman (3.7.2)
-    browser (2.4.0)
+    browser (2.5.1)
     builder (3.2.3)
-    bullet (5.5.1)
+    bullet (5.6.1)
       activesupport (>= 3.0.0)
       uniform_notifier (~> 1.10.0)
     bundler-audit (0.6.0)
       bundler (~> 1.2)
       thor (~> 0.18)
-    capistrano (3.8.2)
+    capistrano (3.9.1)
       airbrussh (>= 1.0.0)
       i18n
       rake (>= 10.0.0)
@@ -99,9 +99,9 @@ GEM
       sshkit (~> 1.3)
     capistrano-yarn (2.0.2)
       capistrano (~> 3.0)
-    capybara (2.14.4)
+    capybara (2.15.1)
       addressable
-      mime-types (>= 1.16)
+      mini_mime (>= 0.1.3)
       nokogiri (>= 1.3.3)
       rack (>= 1.0.0)
       rack-test (>= 0.5.4)
@@ -110,12 +110,12 @@ GEM
       activesupport
     charlock_holmes (0.7.5)
     chunky_png (1.3.8)
-    cld3 (3.1.3)
+    cld3 (3.2.0)
       ffi (>= 1.1.0, < 1.10.0)
     climate_control (0.2.0)
     cocaine (0.5.8)
       climate_control (>= 0.0.3, < 1.0)
-    coderay (1.1.1)
+    coderay (1.1.2)
     colorize (0.8.1)
     concurrent-ruby (1.0.5)
     connection_pool (2.2.1)
@@ -151,13 +151,12 @@ GEM
       thread_safe
     encryptor (3.0.0)
     erubi (1.6.1)
-    erubis (2.7.0)
     et-orbi (1.0.5)
       tzinfo
-    excon (0.58.0)
+    excon (0.59.0)
     execjs (2.7.0)
-    fabrication (2.16.2)
-    faker (1.7.3)
+    fabrication (2.16.3)
+    faker (1.8.4)
       i18n (~> 0.5)
     fast_blank (1.0.0)
     ffi (1.9.18)
@@ -194,7 +193,7 @@ GEM
       railties (>= 4.0.1)
     hamster (3.0.0)
       concurrent-ruby (~> 1.0)
-    hashdiff (0.3.5)
+    hashdiff (0.3.6)
     highline (1.7.8)
     hiredis (0.6.1)
     hkdf (0.3.0)
@@ -213,11 +212,11 @@ GEM
       colorize
       rack
     i18n (0.8.6)
-    i18n-tasks (0.9.16)
+    i18n-tasks (0.9.18)
       activesupport (>= 4.0.2)
       ast (>= 2.1.0)
       easy_translate (>= 0.5.0)
-      erubis
+      erubi
       highline (>= 1.7.3)
       i18n
       parser (>= 2.2.3.0)
@@ -231,7 +230,7 @@ GEM
     json-ld (2.1.5)
       multi_json (~> 1.12)
       rdf (~> 2.2)
-    json-ld-preloaded (2.2.1)
+    json-ld-preloaded (2.2.2)
       json-ld (~> 2.1, >= 2.1.5)
       multi_json (~> 1.11)
       rdf (~> 2.2)
@@ -258,10 +257,11 @@ GEM
       letter_opener (~> 1.0)
       railties (>= 3.2)
     link_header (0.0.8)
-    lograge (0.5.1)
+    lograge (0.6.0)
       actionpack (>= 4, < 5.2)
       activesupport (>= 4, < 5.2)
       railties (>= 4, < 5.2)
+      request_store (~> 1.0)
     loofah (2.0.3)
       nokogiri (>= 1.5.9)
     mail (2.6.6)
@@ -276,27 +276,28 @@ GEM
       mime-types-data (~> 3.2015)
     mime-types-data (3.2016.0521)
     mimemagic (0.3.2)
+    mini_mime (0.1.4)
     mini_portile2 (2.2.0)
     minitest (5.10.3)
     msgpack (1.1.0)
-    multi_json (1.12.1)
+    multi_json (1.12.2)
     net-scp (1.2.1)
       net-ssh (>= 2.6.5)
-    net-ssh (4.1.0)
+    net-ssh (4.2.0)
     nio4r (2.1.0)
     nokogiri (1.8.0)
       mini_portile2 (~> 2.2.0)
     nokogumbo (1.4.13)
       nokogiri
-    oj (3.3.4)
-    openssl (2.0.4)
+    oj (3.3.5)
+    openssl (2.0.5)
     orm_adapter (0.5.0)
     ostatus2 (2.0.1)
       addressable (~> 2.4)
       http (~> 2.0)
       nokogiri (~> 1.6)
       openssl (~> 2.0)
-    ox (2.5.0)
+    ox (2.6.0)
     paperclip (5.1.0)
       activemodel (>= 4.2.0)
       activesupport (>= 4.2.0)
@@ -306,15 +307,15 @@ GEM
     paperclip-av-transcoder (0.6.4)
       av (~> 0.9.0)
       paperclip (>= 2.5.2)
-    parallel (1.11.2)
-    parallel_tests (2.14.2)
+    parallel (1.12.0)
+    parallel_tests (2.15.0)
       parallel
     parser (2.4.0.0)
       ast (~> 2.2)
     pg (0.21.0)
     pghero (1.7.0)
       activerecord
-    pkg-config (1.2.4)
+    pkg-config (1.2.7)
     powerpack (0.1.1)
     pry (0.10.4)
       coderay (~> 1.1.0)
@@ -323,7 +324,7 @@ GEM
     pry-rails (0.3.6)
       pry (>= 0.10.4)
     public_suffix (3.0.0)
-    puma (3.9.1)
+    puma (3.10.0)
     pundit (1.1.0)
       activesupport (>= 3.0.0)
     rabl (0.13.1)
@@ -334,20 +335,20 @@ GEM
     rack-cors (0.4.1)
     rack-protection (2.0.0)
       rack
-    rack-test (0.6.3)
-      rack (>= 1.0)
+    rack-test (0.7.0)
+      rack (>= 1.0, < 3)
     rack-timeout (0.4.2)
-    rails (5.1.3)
-      actioncable (= 5.1.3)
-      actionmailer (= 5.1.3)
-      actionpack (= 5.1.3)
-      actionview (= 5.1.3)
-      activejob (= 5.1.3)
-      activemodel (= 5.1.3)
-      activerecord (= 5.1.3)
-      activesupport (= 5.1.3)
+    rails (5.1.4)
+      actioncable (= 5.1.4)
+      actionmailer (= 5.1.4)
+      actionpack (= 5.1.4)
+      actionview (= 5.1.4)
+      activejob (= 5.1.4)
+      activemodel (= 5.1.4)
+      activerecord (= 5.1.4)
+      activesupport (= 5.1.4)
       bundler (>= 1.3.0)
-      railties (= 5.1.3)
+      railties (= 5.1.4)
       sprockets-rails (>= 2.0.0)
     rails-controller-testing (1.0.2)
       actionpack (~> 5.x, >= 5.0.1)
@@ -363,16 +364,16 @@ GEM
       railties (~> 5.0)
     rails-settings-cached (0.6.6)
       rails (>= 4.2.0)
-    railties (5.1.3)
-      actionpack (= 5.1.3)
-      activesupport (= 5.1.3)
+    railties (5.1.4)
+      actionpack (= 5.1.4)
+      activesupport (= 5.1.4)
       method_source
       rake (>= 0.8.7)
       thor (>= 0.18.1, < 2.0)
     rainbow (2.2.2)
       rake
-    rake (12.0.0)
-    rdf (2.2.8)
+    rake (12.1.0)
+    rdf (2.2.9)
       hamster (~> 3.0)
       link_header (~> 0.0, >= 0.0.8)
     rdf-normalize (0.3.2)
@@ -396,6 +397,7 @@ GEM
       redis-store (>= 1.2, < 2)
     redis-store (1.3.0)
       redis (>= 2.2)
+    request_store (1.3.2)
     responders (2.4.0)
       actionpack (>= 4.2.0, < 5.3)
       railties (>= 4.2.0, < 5.3)
@@ -410,7 +412,7 @@ GEM
     rspec-mocks (3.6.0)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.6.0)
-    rspec-rails (3.6.0)
+    rspec-rails (3.6.1)
       actionpack (>= 3.0)
       activesupport (>= 3.0)
       railties (>= 3.0)
@@ -422,15 +424,15 @@ GEM
       rspec-core (~> 3.0, >= 3.0.0)
       sidekiq (>= 2.4.0)
     rspec-support (3.6.0)
-    rubocop (0.49.1)
+    rubocop (0.50.0)
       parallel (~> 1.10)
       parser (>= 2.3.3.1, < 3.0)
       powerpack (~> 0.1)
-      rainbow (>= 1.99.1, < 3.0)
+      rainbow (>= 2.2.2, < 3.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (~> 1.0, >= 1.0.1)
     ruby-oembed (0.12.0)
-    ruby-progressbar (1.8.1)
+    ruby-progressbar (1.8.3)
     rufus-scheduler (3.4.2)
       et-orbi (~> 1.0)
     safe_yaml (1.0.4)
@@ -438,7 +440,7 @@ GEM
       crass (~> 1.0.2)
       nokogiri (>= 1.4.4)
       nokogumbo (~> 1.4.1)
-    sass (3.4.24)
+    sass (3.4.25)
     scss_lint (0.54.0)
       rake (>= 0.9, < 13)
       sass (~> 3.4.20)
@@ -450,12 +452,12 @@ GEM
     sidekiq-bulk (0.1.1)
       activesupport
       sidekiq
-    sidekiq-scheduler (2.1.8)
+    sidekiq-scheduler (2.1.9)
       redis (~> 3)
       rufus-scheduler (~> 3.2)
       sidekiq (>= 3)
       tilt (>= 1.4.0)
-    sidekiq-unique-jobs (5.0.9)
+    sidekiq-unique-jobs (5.0.10)
       sidekiq (>= 4.0, <= 6.0)
       thor (~> 0)
     simple-navigation (4.0.5)
@@ -463,20 +465,20 @@ GEM
     simple_form (3.5.0)
       actionpack (> 4, < 5.2)
       activemodel (> 4, < 5.2)
-    simplecov (0.14.1)
+    simplecov (0.15.1)
       docile (~> 1.1.0)
       json (>= 1.8, < 3)
       simplecov-html (~> 0.10.0)
-    simplecov-html (0.10.1)
+    simplecov-html (0.10.2)
     slop (3.6.0)
     sprockets (3.7.1)
       concurrent-ruby (~> 1.0)
       rack (> 1, < 3)
-    sprockets-rails (3.2.0)
+    sprockets-rails (3.2.1)
       actionpack (>= 4.0)
       activesupport (>= 4.0)
       sprockets (>= 3.0.0)
-    sshkit (1.13.1)
+    sshkit (1.14.0)
       net-scp (>= 1.1.2)
       net-ssh (>= 2.8.0)
     statsd-instrument (2.1.4)
@@ -541,7 +543,7 @@ DEPENDENCIES
   capistrano-yarn (~> 2.0)
   capybara (~> 2.14)
   charlock_holmes (~> 0.7.5)
-  cld3 (~> 3.1)
+  cld3 (~> 3.2.0)
   climate_control (~> 0.2)
   devise (~> 4.2)
   devise-two-factor (~> 3.0)
@@ -582,13 +584,13 @@ DEPENDENCIES
   pghero (~> 1.7)
   pkg-config (~> 1.2)
   pry-rails (~> 0.3)
-  puma (~> 3.8)
+  puma (~> 3.10)
   pundit (~> 1.1)
   rabl (~> 0.13)
   rack-attack (~> 5.0)
   rack-cors (~> 0.4)
   rack-timeout (~> 0.4)
-  rails (~> 5.1.0)
+  rails (~> 5.1.4)
   rails-controller-testing (~> 1.0)
   rails-i18n (~> 5.0)
   rails-settings-cached (~> 0.6)
@@ -620,7 +622,7 @@ DEPENDENCIES
   webpush
 
 RUBY VERSION
-   ruby 2.4.1p111
+   ruby 2.4.2p198
 
 BUNDLED WITH
    1.15.4
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
new file mode 100644
index 000000000..572ad1ac2
--- /dev/null
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Admin
+  class CustomEmojisController < BaseController
+    def index
+      @custom_emojis = CustomEmoji.where(domain: nil)
+    end
+
+    def new
+      @custom_emoji = CustomEmoji.new
+    end
+
+    def create
+      @custom_emoji = CustomEmoji.new(resource_params)
+
+      if @custom_emoji.save
+        redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
+      else
+        render :new
+      end
+    end
+
+    def destroy
+      CustomEmoji.find(params[:id]).destroy
+      redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
+    end
+
+    private
+
+    def resource_params
+      params.require(:custom_emoji).permit(:shortcode, :image)
+    end
+  end
+end
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb
index 3296e08db..22f02e5d0 100644
--- a/app/controllers/admin/instances_controller.rb
+++ b/app/controllers/admin/instances_controller.rb
@@ -14,8 +14,12 @@ module Admin
 
     private
 
+    def filtered_instances
+      InstanceFilter.new(filter_params).results
+    end
+
     def paginated_instances
-      Account.remote.by_domain_accounts.page(params[:page])
+      filtered_instances.page(params[:page])
     end
 
     helper_method :paginated_instances
@@ -27,5 +31,11 @@ module Admin
     def subscribeable_accounts
       Account.with_followers.remote.where(domain: params[:by_domain])
     end
+
+    def filter_params
+      params.permit(
+        :domain_name
+      )
+    end
   end
 end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index c5e6fe4e5..a2f86b8a9 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -14,6 +14,7 @@ module Admin
       open_deletion
       timeline_preview
       bootstrap_timeline_accounts
+      thumbnail
     ).freeze
 
     BOOLEAN_SETTINGS = %w(
@@ -22,14 +23,23 @@ module Admin
       timeline_preview
     ).freeze
 
+    UPLOAD_SETTINGS = %w(
+      thumbnail
+    ).freeze
+
     def edit
       @admin_settings = Form::AdminSettings.new
     end
 
     def update
       settings_params.each do |key, value|
-        setting = Setting.where(var: key).first_or_initialize(var: key)
-        setting.update(value: value_for_update(key, value))
+        if UPLOAD_SETTINGS.include?(key)
+          upload = SiteUpload.where(var: key).first_or_initialize(var: key)
+          upload.update(file: value)
+        else
+          setting = Setting.where(var: key).first_or_initialize(var: key)
+          setting.update(value: value_for_update(key, value))
+        end
       end
 
       flash[:notice] = I18n.t('generic.changes_saved_msg')
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index fbfb5473e..ad7f09f34 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -12,7 +12,30 @@ class HomeController < ApplicationController
   private
 
   def authenticate_user!
-    redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
+    return if user_signed_in?
+
+    matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
+
+    if matches
+      case matches[1]
+      when 'statuses'
+        status = Status.find_by(id: matches[2])
+
+        if status && (status.public_visibility? || status.unlisted_visibility?)
+          redirect_to(ActivityPub::TagManager.instance.url_for(status))
+          return
+        end
+      when 'accounts'
+        account = Account.find_by(id: matches[2])
+
+        if account
+          redirect_to(ActivityPub::TagManager.instance.url_for(account))
+          return
+        end
+      end
+    end
+
+    redirect_to(default_redirect_path)
   end
 
   def set_initial_state_json
@@ -29,4 +52,14 @@ class HomeController < ApplicationController
       admin: Account.find_local(Setting.site_contact_username),
     }
   end
+
+  def default_redirect_path
+    if request.path.start_with?('/web')
+      new_user_session_path
+    elsif single_user_mode?
+      short_account_path(Account.first)
+    else
+      about_path
+    end
+  end
 end
diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb
new file mode 100644
index 000000000..155670837
--- /dev/null
+++ b/app/controllers/media_proxy_controller.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class MediaProxyController < ApplicationController
+  include RoutingHelper
+
+  def show
+    RedisLock.acquire(lock_options) do |lock|
+      if lock.acquired?
+        @media_attachment = MediaAttachment.remote.find(params[:id])
+        redownload! if @media_attachment.needs_redownload? && !reject_media?
+      end
+    end
+
+    redirect_to full_asset_url(@media_attachment.file.url(version))
+  end
+
+  private
+
+  def redownload!
+    @media_attachment.file_remote_url = @media_attachment.remote_url
+    @media_attachment.created_at      = Time.now.utc
+    @media_attachment.save!
+  end
+
+  def version
+    if request.path.ends_with?('/small')
+      :small
+    else
+      :original
+    end
+  end
+
+  def lock_options
+    { redis: Redis.current, key: "media_download:#{params[:id]}" }
+  end
+
+  def reject_media?
+    DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media?
+  end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 61d4442c1..6d625e7db 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -42,4 +42,8 @@ module ApplicationHelper
 
     content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
   end
+
+  def opengraph(property, content)
+    tag(:meta, content: content, property: property)
+  end
 end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 369a45680..14776b354 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -41,7 +41,7 @@ module SettingsHelper
   end
 
   def filterable_languages
-    I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq
+    LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
   end
 
   def hash_to_object(hash)
diff --git a/app/javascript/images/logo.svg b/app/javascript/images/logo.svg
index 4b72b3ac8..034a9c221 100644
--- a/app/javascript/images/logo.svg
+++ b/app/javascript/images/logo.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="61.076954mm" height="65.47831mm" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>
diff --git a/app/javascript/images/logo_alt.svg b/app/javascript/images/logo_alt.svg
index e88ca7418..102d4c787 100644
--- a/app/javascript/images/logo_alt.svg
+++ b/app/javascript/images/logo_alt.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="61.077141mm" height="65.47831mm" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>
diff --git a/app/javascript/images/logo_full.svg b/app/javascript/images/logo_full.svg
index 8b1328e8c..c33883342 100644
--- a/app/javascript/images/logo_full.svg
+++ b/app/javascript/images/logo_full.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 713.35878 175.8678" height="49.633801mm" width="201.3257mm"><path d="M160.55476 105.43125c-2.4125 12.40625-21.5975 25.9825-43.63375 28.61375-11.49125 1.3725-22.80375 2.63125-34.8675 2.07875-19.73-.90375-35.2975-4.71-35.2975-4.71 0 1.92125.11875 3.75.355 5.46 2.565 19.47 19.3075 20.6375 35.16625 21.18125 16.00625.5475 30.2575-3.9475 30.2575-3.9475l.65875 14.4725s-11.19625 6.01125-31.14 7.11625c-10.99875.605-24.65375-.27625-40.56-4.485C6.99851 162.08 1.06601 125.31.15851 88-.11899 76.9225.05226 66.47625.05226 57.74125c0-38.1525 24.99625-49.335 24.99625-49.335C37.65226 2.6175 59.27976.18375 81.76351 0h.5525c22.48375.18375 44.125 2.6175 56.72875 8.40625 0 0 24.99625 11.1825 24.99625 49.335 0 0 .3125 28.1475-3.48625 47.69" fill="#3088d4"/><path d="M34.65751 48.494c0-5.55375 4.5025-10.055 10.055-10.055 5.55375 0 10.055 4.50125 10.055 10.055 0 5.5525-4.50125 10.055-10.055 10.055-5.5525 0-10.055-4.5025-10.055-10.055M178.86476 60.69975v46.195h-18.30125v-44.8375c0-9.4525-3.9775-14.24875-11.9325-14.24875-8.79375 0-13.2025 5.69125-13.2025 16.94375V89.2935h-18.19375V64.75225c0-11.2525-4.40875-16.94375-13.2025-16.94375-7.955 0-11.9325 4.79625-11.9325 14.24875v44.8375H73.79851v-46.195c0-9.44125 2.40375-16.94375 7.2325-22.495 4.98-5.55 11.50125-8.395 19.595-8.395 9.36625 0 16.45875 3.59875 21.14625 10.79875l4.56 7.6425 4.55875-7.6425c4.68875-7.2 11.78-10.79875 21.1475-10.79875 8.09375 0 14.61375 2.845 19.59375 8.395 4.82875 5.55125 7.2325 13.05375 7.2325 22.495M241.91276 83.663625c3.77625-3.99 5.595-9.015 5.595-15.075 0-6.06-1.81875-11.085-5.595-14.9275-3.63625-3.99125-8.25375-5.91125-13.84875-5.91125-5.59625 0-10.2125 1.92-13.84875 5.91125-3.6375 3.8425-5.45625 8.8675-5.45625 14.9275 0 6.06 1.81875 11.085 5.45625 15.075 3.63625 3.8425 8.2525 5.76375 13.84875 5.76375 5.595 0 10.2125-1.92125 13.84875-5.76375m5.595-52.025h18.04625v73.9h-18.04625v-8.72125c-5.455 7.2425-13.01 10.79-22.80125 10.79-9.3725 0-17.34625-3.695-24.06125-11.23375-6.57375-7.5375-9.93125-16.84875-9.93125-27.785 0-10.78875 3.3575-20.10125 9.93125-27.63875 6.715-7.5375 14.68875-11.38 24.06125-11.38 9.79125 0 17.34625 3.5475 22.80125 10.78875v-8.72zM326.26951 67.258625c5.315 3.99 7.97375 9.60625 7.83375 16.7 0 7.53875-2.65875 13.45-8.11375 17.58875-5.45625 3.99125-12.03 6.06-20.00375 6.06-14.40875 0-24.20125-5.9125-29.3775-17.58875l15.66875-9.31c2.0975 6.35375 6.71375 9.60625 13.70875 9.60625 6.43375 0 9.6525-2.07 9.6525-6.35625 0-3.10375-4.1975-5.91125-12.73-8.1275-3.21875-.8875-5.87625-1.77375-7.97375-2.51375-2.9375-1.18125-5.455-2.5125-7.55375-4.1375-5.17625-3.99-7.83375-9.3125-7.83375-16.11 0-7.2425 2.5175-13.00625 7.55375-17.145 5.17625-4.28625 11.47-6.355 19.025-6.355 12.03 0 20.84375 5.1725 26.5775 15.66625l-15.38625 8.8675c-2.23875-5.02375-6.015-7.53625-11.19125-7.53625-5.45625 0-8.11375 2.06875-8.11375 6.05875 0 3.10375 4.19625 5.91125 12.73 8.12875 6.575 1.4775 11.75 3.695 15.5275 6.50375M383.626635 49.966125h-15.8075v30.7425c0 3.695 1.4 5.91125 4.0575 6.945 1.95875.74 5.875.8875 11.75.59125v17.29375c-12.16875 1.4775-20.9825.295-26.15875-3.69625-5.175-3.8425-7.69375-10.93625-7.69375-21.13375v-30.7425h-12.17v-18.3275h12.17v-14.9275l18.045-5.76375v20.69125h15.8075v18.3275zM441.124885 83.2205c3.6375-3.84375 5.455-8.72125 5.455-14.6325 0-5.91125-1.8175-10.78875-5.455-14.63125-3.6375-3.84375-8.11375-5.76375-13.57-5.76375-5.455 0-9.93125 1.92-13.56875 5.76375-3.4975 3.99-5.31625 8.8675-5.31625 14.63125 0 5.765 1.81875 10.6425 5.31625 14.6325 3.6375 3.8425 8.11375 5.76375 13.56875 5.76375 5.45625 0 9.9325-1.92125 13.57-5.76375m-39.86875 13.15375c-7.13375-7.5375-10.63125-16.70125-10.63125-27.78625 0-10.9375 3.4975-20.1 10.63125-27.6375 7.13375-7.5375 15.9475-11.38 26.29875-11.38 10.3525 0 19.165 3.8425 26.3 11.38 7.135 7.5375 10.77125 16.84875 10.77125 27.6375 0 10.9375-3.63625 20.24875-10.77125 27.78625-7.135 7.53875-15.8075 11.2325-26.3 11.2325-10.49125 0-19.165-3.69375-26.29875-11.2325M524.92126 83.663625c3.6375-3.99 5.455-9.015 5.455-15.075 0-6.06-1.8175-11.085-5.455-14.9275-3.63625-3.99125-8.25375-5.91125-13.84875-5.91125-5.59625 0-10.2125 1.92-13.98875 5.91125-3.63625 3.8425-5.45625 8.8675-5.45625 14.9275 0 6.06 1.82 11.085 5.45625 15.075 3.77625 3.8425 8.5325 5.76375 13.98875 5.76375 5.595 0 10.2125-1.92125 13.84875-5.76375m5.455-81.585h18.04625v103.46h-18.04625v-8.72125c-5.315 7.2425-12.87 10.79-22.66125 10.79-9.3725 0-17.485-3.695-24.2-11.23375-6.575-7.5375-9.9325-16.84875-9.9325-27.785 0-10.78875 3.3575-20.10125 9.9325-27.63875 6.715-7.5375 14.8275-11.38 24.2-11.38 9.79125 0 17.34625 3.5475 22.66125 10.78875v-38.28zM611.79626 83.2205c3.63625-3.84375 5.455-8.72125 5.455-14.6325 0-5.91125-1.81875-10.78875-5.455-14.63125-3.6375-3.84375-8.11375-5.76375-13.57-5.76375-5.455 0-9.9325 1.92-13.56875 5.76375-3.49875 3.99-5.31625 8.8675-5.31625 14.63125 0 5.765 1.8175 10.6425 5.31625 14.6325 3.63625 3.8425 8.11375 5.76375 13.56875 5.76375 5.45625 0 9.9325-1.92125 13.57-5.76375m-39.86875 13.15375c-7.135-7.5375-10.63125-16.70125-10.63125-27.78625 0-10.9375 3.49625-20.1 10.63125-27.6375 7.135-7.5375 15.9475-11.38 26.29875-11.38 10.3525 0 19.165 3.8425 26.3 11.38 7.135 7.5375 10.77125 16.84875 10.77125 27.6375 0 10.9375-3.63625 20.24875-10.77125 27.78625-7.135 7.53875-15.8075 11.2325-26.3 11.2325-10.49125 0-19.16375-3.69375-26.29875-11.2325M713.35876 60.163875v45.37375h-18.04625v-43.00875c0-4.8775-1.25875-8.5725-3.77625-11.38-2.37875-2.5125-5.73625-3.84375-10.0725-3.84375-10.2125 0-15.3875 6.06-15.3875 18.3275v39.905h-18.04625v-73.89875h18.04625v8.27625c4.33625-6.94625 11.19-10.345 20.84375-10.345 7.69375 0 13.98875 2.66 18.885 8.12875 5.035 5.46875 7.55375 12.85875 7.55375 22.465" fill="#fff"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 713.35878 175.8678"><path d="M160.55476 105.43125c-2.4125 12.40625-21.5975 25.9825-43.63375 28.61375-11.49125 1.3725-22.80375 2.63125-34.8675 2.07875-19.73-.90375-35.2975-4.71-35.2975-4.71 0 1.92125.11875 3.75.355 5.46 2.565 19.47 19.3075 20.6375 35.16625 21.18125 16.00625.5475 30.2575-3.9475 30.2575-3.9475l.65875 14.4725s-11.19625 6.01125-31.14 7.11625c-10.99875.605-24.65375-.27625-40.56-4.485C6.99851 162.08 1.06601 125.31.15851 88-.11899 76.9225.05226 66.47625.05226 57.74125c0-38.1525 24.99625-49.335 24.99625-49.335C37.65226 2.6175 59.27976.18375 81.76351 0h.5525c22.48375.18375 44.125 2.6175 56.72875 8.40625 0 0 24.99625 11.1825 24.99625 49.335 0 0 .3125 28.1475-3.48625 47.69" fill="#3088d4"/><path d="M34.65751 48.494c0-5.55375 4.5025-10.055 10.055-10.055 5.55375 0 10.055 4.50125 10.055 10.055 0 5.5525-4.50125 10.055-10.055 10.055-5.5525 0-10.055-4.5025-10.055-10.055M178.86476 60.69975v46.195h-18.30125v-44.8375c0-9.4525-3.9775-14.24875-11.9325-14.24875-8.79375 0-13.2025 5.69125-13.2025 16.94375V89.2935h-18.19375V64.75225c0-11.2525-4.40875-16.94375-13.2025-16.94375-7.955 0-11.9325 4.79625-11.9325 14.24875v44.8375H73.79851v-46.195c0-9.44125 2.40375-16.94375 7.2325-22.495 4.98-5.55 11.50125-8.395 19.595-8.395 9.36625 0 16.45875 3.59875 21.14625 10.79875l4.56 7.6425 4.55875-7.6425c4.68875-7.2 11.78-10.79875 21.1475-10.79875 8.09375 0 14.61375 2.845 19.59375 8.395 4.82875 5.55125 7.2325 13.05375 7.2325 22.495M241.91276 83.663625c3.77625-3.99 5.595-9.015 5.595-15.075 0-6.06-1.81875-11.085-5.595-14.9275-3.63625-3.99125-8.25375-5.91125-13.84875-5.91125-5.59625 0-10.2125 1.92-13.84875 5.91125-3.6375 3.8425-5.45625 8.8675-5.45625 14.9275 0 6.06 1.81875 11.085 5.45625 15.075 3.63625 3.8425 8.2525 5.76375 13.84875 5.76375 5.595 0 10.2125-1.92125 13.84875-5.76375m5.595-52.025h18.04625v73.9h-18.04625v-8.72125c-5.455 7.2425-13.01 10.79-22.80125 10.79-9.3725 0-17.34625-3.695-24.06125-11.23375-6.57375-7.5375-9.93125-16.84875-9.93125-27.785 0-10.78875 3.3575-20.10125 9.93125-27.63875 6.715-7.5375 14.68875-11.38 24.06125-11.38 9.79125 0 17.34625 3.5475 22.80125 10.78875v-8.72zM326.26951 67.258625c5.315 3.99 7.97375 9.60625 7.83375 16.7 0 7.53875-2.65875 13.45-8.11375 17.58875-5.45625 3.99125-12.03 6.06-20.00375 6.06-14.40875 0-24.20125-5.9125-29.3775-17.58875l15.66875-9.31c2.0975 6.35375 6.71375 9.60625 13.70875 9.60625 6.43375 0 9.6525-2.07 9.6525-6.35625 0-3.10375-4.1975-5.91125-12.73-8.1275-3.21875-.8875-5.87625-1.77375-7.97375-2.51375-2.9375-1.18125-5.455-2.5125-7.55375-4.1375-5.17625-3.99-7.83375-9.3125-7.83375-16.11 0-7.2425 2.5175-13.00625 7.55375-17.145 5.17625-4.28625 11.47-6.355 19.025-6.355 12.03 0 20.84375 5.1725 26.5775 15.66625l-15.38625 8.8675c-2.23875-5.02375-6.015-7.53625-11.19125-7.53625-5.45625 0-8.11375 2.06875-8.11375 6.05875 0 3.10375 4.19625 5.91125 12.73 8.12875 6.575 1.4775 11.75 3.695 15.5275 6.50375M383.626635 49.966125h-15.8075v30.7425c0 3.695 1.4 5.91125 4.0575 6.945 1.95875.74 5.875.8875 11.75.59125v17.29375c-12.16875 1.4775-20.9825.295-26.15875-3.69625-5.175-3.8425-7.69375-10.93625-7.69375-21.13375v-30.7425h-12.17v-18.3275h12.17v-14.9275l18.045-5.76375v20.69125h15.8075v18.3275zM441.124885 83.2205c3.6375-3.84375 5.455-8.72125 5.455-14.6325 0-5.91125-1.8175-10.78875-5.455-14.63125-3.6375-3.84375-8.11375-5.76375-13.57-5.76375-5.455 0-9.93125 1.92-13.56875 5.76375-3.4975 3.99-5.31625 8.8675-5.31625 14.63125 0 5.765 1.81875 10.6425 5.31625 14.6325 3.6375 3.8425 8.11375 5.76375 13.56875 5.76375 5.45625 0 9.9325-1.92125 13.57-5.76375m-39.86875 13.15375c-7.13375-7.5375-10.63125-16.70125-10.63125-27.78625 0-10.9375 3.4975-20.1 10.63125-27.6375 7.13375-7.5375 15.9475-11.38 26.29875-11.38 10.3525 0 19.165 3.8425 26.3 11.38 7.135 7.5375 10.77125 16.84875 10.77125 27.6375 0 10.9375-3.63625 20.24875-10.77125 27.78625-7.135 7.53875-15.8075 11.2325-26.3 11.2325-10.49125 0-19.165-3.69375-26.29875-11.2325M524.92126 83.663625c3.6375-3.99 5.455-9.015 5.455-15.075 0-6.06-1.8175-11.085-5.455-14.9275-3.63625-3.99125-8.25375-5.91125-13.84875-5.91125-5.59625 0-10.2125 1.92-13.98875 5.91125-3.63625 3.8425-5.45625 8.8675-5.45625 14.9275 0 6.06 1.82 11.085 5.45625 15.075 3.77625 3.8425 8.5325 5.76375 13.98875 5.76375 5.595 0 10.2125-1.92125 13.84875-5.76375m5.455-81.585h18.04625v103.46h-18.04625v-8.72125c-5.315 7.2425-12.87 10.79-22.66125 10.79-9.3725 0-17.485-3.695-24.2-11.23375-6.575-7.5375-9.9325-16.84875-9.9325-27.785 0-10.78875 3.3575-20.10125 9.9325-27.63875 6.715-7.5375 14.8275-11.38 24.2-11.38 9.79125 0 17.34625 3.5475 22.66125 10.78875v-38.28zM611.79626 83.2205c3.63625-3.84375 5.455-8.72125 5.455-14.6325 0-5.91125-1.81875-10.78875-5.455-14.63125-3.6375-3.84375-8.11375-5.76375-13.57-5.76375-5.455 0-9.9325 1.92-13.56875 5.76375-3.49875 3.99-5.31625 8.8675-5.31625 14.63125 0 5.765 1.8175 10.6425 5.31625 14.6325 3.63625 3.8425 8.11375 5.76375 13.56875 5.76375 5.45625 0 9.9325-1.92125 13.57-5.76375m-39.86875 13.15375c-7.135-7.5375-10.63125-16.70125-10.63125-27.78625 0-10.9375 3.49625-20.1 10.63125-27.6375 7.135-7.5375 15.9475-11.38 26.29875-11.38 10.3525 0 19.165 3.8425 26.3 11.38 7.135 7.5375 10.77125 16.84875 10.77125 27.6375 0 10.9375-3.63625 20.24875-10.77125 27.78625-7.135 7.53875-15.8075 11.2325-26.3 11.2325-10.49125 0-19.16375-3.69375-26.29875-11.2325M713.35876 60.163875v45.37375h-18.04625v-43.00875c0-4.8775-1.25875-8.5725-3.77625-11.38-2.37875-2.5125-5.73625-3.84375-10.0725-3.84375-10.2125 0-15.3875 6.06-15.3875 18.3275v39.905h-18.04625v-73.89875h18.04625v8.27625c4.33625-6.94625 11.19-10.345 20.84375-10.345 7.69375 0 13.98875 2.66 18.885 8.12875 5.035 5.46875 7.55375 12.85875 7.55375 22.465" fill="#fff"/></svg>
diff --git a/app/javascript/images/mastodon_small.jpg b/app/javascript/images/mastodon_small.jpg
deleted file mode 100644
index 9c88ce3f7..000000000
--- a/app/javascript/images/mastodon_small.jpg
+++ /dev/null
Binary files differdiff --git a/app/javascript/images/preview.jpg b/app/javascript/images/preview.jpg
new file mode 100644
index 000000000..ec2856748
--- /dev/null
+++ b/app/javascript/images/preview.jpg
Binary files differdiff --git a/app/javascript/mastodon/actions/height_cache.js b/app/javascript/mastodon/actions/height_cache.js
new file mode 100644
index 000000000..4c752993f
--- /dev/null
+++ b/app/javascript/mastodon/actions/height_cache.js
@@ -0,0 +1,17 @@
+export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
+export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
+
+export function setHeight (key, id, height) {
+  return {
+    type: HEIGHT_CACHE_SET,
+    key,
+    id,
+    height,
+  };
+};
+
+export function clearHeight () {
+  return {
+    type: HEIGHT_CACHE_CLEAR,
+  };
+};
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 0b5e72c17..2204e0b14 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -23,9 +23,6 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
 export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
 export const STATUS_UNMUTE_FAIL    = 'STATUS_UNMUTE_FAIL';
 
-export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT';
-export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT';
-
 export function fetchStatusRequest(id, skipLoading) {
   return {
     type: STATUS_FETCH_REQUEST,
@@ -218,17 +215,3 @@ export function unmuteStatusFail(id, error) {
     error,
   };
 };
-
-export function setStatusHeight (id, height) {
-  return {
-    type: STATUS_SET_HEIGHT,
-    id,
-    height,
-  };
-};
-
-export function clearStatusesHeight () {
-  return {
-    type: STATUSES_CLEAR_HEIGHT,
-  };
-};
diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js
index 347767818..bb83a4da0 100644
--- a/app/javascript/mastodon/components/intersection_observer_article.js
+++ b/app/javascript/mastodon/components/intersection_observer_article.js
@@ -7,10 +7,13 @@ import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
 export default class IntersectionObserverArticle extends ImmutablePureComponent {
 
   static propTypes = {
-    intersectionObserverWrapper: PropTypes.object,
+    intersectionObserverWrapper: PropTypes.object.isRequired,
     id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+    saveHeightKey: PropTypes.string,
+    cachedHeight: PropTypes.number,
+    onHeightChange: PropTypes.func,
     children: PropTypes.node,
   };
 
@@ -34,13 +37,10 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
   }
 
   componentDidMount () {
-    if (!this.props.intersectionObserverWrapper) {
-      // TODO: enable IntersectionObserver optimization for notification statuses.
-      // These are managed in notifications/index.js rather than status_list.js
-      return;
-    }
-    this.props.intersectionObserverWrapper.observe(
-      this.props.id,
+    const { intersectionObserverWrapper, id } = this.props;
+
+    intersectionObserverWrapper.observe(
+      id,
       this.node,
       this.handleIntersection
     );
@@ -49,20 +49,21 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
   }
 
   componentWillUnmount () {
-    if (this.props.intersectionObserverWrapper) {
-      this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
-    }
+    const { intersectionObserverWrapper, id } = this.props;
+    intersectionObserverWrapper.unobserve(id, this.node);
 
     this.componentMounted = false;
   }
 
   handleIntersection = (entry) => {
+    const { onHeightChange, saveHeightKey, id } = this.props;
+
     if (this.node && this.node.children.length !== 0) {
       // save the height of the fully-rendered element
       this.height = getRectFromEntry(entry).height;
 
-      if (this.props.onHeightChange) {
-        this.props.onHeightChange(this.props.status, this.height);
+      if (onHeightChange && saveHeightKey) {
+        onHeightChange(saveHeightKey, id, this.height);
       }
     }
 
@@ -94,16 +95,16 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
   }
 
   render () {
-    const { children, id, index, listLength } = this.props;
+    const { children, id, index, listLength, cachedHeight } = this.props;
     const { isIntersecting, isHidden } = this.state;
 
-    if (!isIntersecting && isHidden) {
+    if (!isIntersecting && (isHidden || cachedHeight)) {
       return (
         <article
           ref={this.handleRef}
           aria-posinset={index}
           aria-setsize={listLength}
-          style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}
+          style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
           data-id={id}
           tabIndex='0'
         >
diff --git a/app/javascript/mastodon/components/load_more.js b/app/javascript/mastodon/components/load_more.js
index e2fe1fed7..c4c8c94a2 100644
--- a/app/javascript/mastodon/components/load_more.js
+++ b/app/javascript/mastodon/components/load_more.js
@@ -17,7 +17,7 @@ export default class LoadMore extends React.PureComponent {
     const { visible } = this.props;
 
     return (
-      <button className='load-more' disabled={!visible} style={{ opacity: visible ? 1 : 0 }} onClick={this.props.onClick}>
+      <button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
         <FormattedMessage id='status.load_more' defaultMessage='Load more' />
       </button>
     );
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index fa6ea72d5..a03b682b2 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -122,8 +122,8 @@ class Item extends React.PureComponent {
 
       const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
 
-      const srcSet = hasSize && `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
-      const sizes = hasSize && `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;
+      const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
+      const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
 
       thumbnail = (
         <a
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index 723dd322b..ff0540e5d 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import { ScrollContainer } from 'react-router-scroll';
 import PropTypes from 'prop-types';
-import IntersectionObserverArticle from './intersection_observer_article';
+import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
 import LoadMore from './load_more';
 import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
 import { throttle } from 'lodash';
@@ -9,6 +9,10 @@ import { List as ImmutableList } from 'immutable';
 
 export default class ScrollableList extends PureComponent {
 
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
   static propTypes = {
     scrollKey: PropTypes.string.isRequired,
     onScrollToBottom: PropTypes.func,
@@ -163,7 +167,7 @@ export default class ScrollableList extends PureComponent {
     const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
     const childrenCount = React.Children.count(children);
 
-    const loadMore     = <LoadMore visible={!isLoading && childrenCount > 0 && hasMore} onClick={this.handleLoadMore} />;
+    const loadMore     = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
     let scrollableArea = null;
 
     if (isLoading || childrenCount > 0 || !emptyMessage) {
@@ -173,9 +177,16 @@ export default class ScrollableList extends PureComponent {
             {prepend}
 
             {React.Children.map(this.props.children, (child, index) => (
-              <IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}>
+              <IntersectionObserverArticleContainer
+                key={child.key}
+                id={child.key}
+                index={index}
+                listLength={childrenCount}
+                intersectionObserverWrapper={this.intersectionObserverWrapper}
+                saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
+              >
                 {child}
-              </IntersectionObserverArticle>
+              </IntersectionObserverArticleContainer>
             ))}
 
             {loadMore}
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index b8617018d..78177c84d 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -12,7 +12,7 @@ import StatusContent from './status_content';
 import StatusActionBar from './status_action_bar';
 import { FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
+import { MediaGallery, Video } from '../features/ui/util/async-components';
 
 // We use the component (and not the container) since we do not want
 // to use the progress bar to show download progress
@@ -91,6 +91,10 @@ export default class Status extends ImmutablePureComponent {
     return <div className='media-spoiler-video' style={{ height: '110px' }} />;
   }
 
+  handleOpenVideo = startTime => {
+    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
+  }
+
   render () {
     let media = null;
     let statusAvatar;
@@ -130,9 +134,18 @@ export default class Status extends ImmutablePureComponent {
       if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
 
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+        const video = status.getIn(['media_attachments', 0]);
+
         media = (
-          <Bundle fetchComponent={VideoPlayer} loading={this.renderLoadingVideoPlayer} >
-            {Component => <Component media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />}
+          <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
+            {Component => <Component
+              preview={video.get('preview_url')}
+              src={video.get('url')}
+              width={239}
+              height={110}
+              sensitive={status.get('sensitive')}
+              onOpenVideo={this.handleOpenVideo}
+            />}
           </Bundle>
         );
       } else {
diff --git a/app/javascript/mastodon/containers/card_container.js b/app/javascript/mastodon/containers/card_container.js
new file mode 100644
index 000000000..11b9f88d4
--- /dev/null
+++ b/app/javascript/mastodon/containers/card_container.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Card from '../features/status/components/card';
+import { fromJS } from 'immutable';
+
+export default class CardContainer extends React.PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string,
+    card: PropTypes.array.isRequired,
+  };
+
+  render () {
+    const { card, ...props } = this.props;
+    return <Card card={fromJS(card)} {...props} />;
+  }
+
+}
diff --git a/app/javascript/mastodon/containers/intersection_observer_article_container.js b/app/javascript/mastodon/containers/intersection_observer_article_container.js
new file mode 100644
index 000000000..b6f162199
--- /dev/null
+++ b/app/javascript/mastodon/containers/intersection_observer_article_container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import IntersectionObserverArticle from '../components/intersection_observer_article';
+import { setHeight } from '../actions/height_cache';
+
+const makeMapStateToProps = (state, props) => ({
+  cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
+});
+
+const mapDispatchToProps = (dispatch) => ({
+
+  onHeightChange (key, id, height) {
+    dispatch(setHeight(key, id, height));
+  },
+
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle);
diff --git a/app/javascript/mastodon/containers/media_gallery_container.js b/app/javascript/mastodon/containers/media_gallery_container.js
new file mode 100644
index 000000000..812c3d4e5
--- /dev/null
+++ b/app/javascript/mastodon/containers/media_gallery_container.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from '../locales';
+import MediaGallery from '../components/media_gallery';
+import { fromJS } from 'immutable';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+export default class MediaGalleryContainer extends React.PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string.isRequired,
+    media: PropTypes.array.isRequired,
+  };
+
+  handleOpenMedia = () => {}
+
+  render () {
+    const { locale, media, ...props } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={messages}>
+        <MediaGallery
+          {...props}
+          media={fromJS(media)}
+          onOpenMedia={this.handleOpenMedia}
+        />
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index 9dff79b72..e8821223d 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -21,7 +21,7 @@ import {
   blockAccount,
   muteAccount,
 } from '../actions/accounts';
-import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses';
+import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
 import { initReport } from '../actions/reports';
 import { openModal } from '../actions/modal';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -141,10 +141,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
-  onHeightChange (status, height) {
-    dispatch(setStatusHeight(status.get('id'), height));
-  },
-
 });
 
 export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
diff --git a/app/javascript/mastodon/containers/video_container.js b/app/javascript/mastodon/containers/video_container.js
new file mode 100644
index 000000000..2fd353096
--- /dev/null
+++ b/app/javascript/mastodon/containers/video_container.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from '../locales';
+import Video from '../features/video';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+export default class VideoContainer extends React.PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string.isRequired,
+  };
+
+  render () {
+    const { locale, ...props } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={messages}>
+        <Video {...props} />
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js
index a41dfdd1d..865b85b61 100644
--- a/app/javascript/mastodon/emoji.js
+++ b/app/javascript/mastodon/emoji.js
@@ -3,28 +3,48 @@ import Trie from 'substring-trie';
 
 const trie = new Trie(Object.keys(unicodeMapping));
 
-const emojify = str => {
-  let rtn = '';
-  for (;;) {
-    let match, i = 0;
-    while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) {
-      i += str.codePointAt(i) < 65536 ? 1 : 2;
-    }
-    if (i === str.length)
-      break;
-    else if (str[i] === '<') {
-      let tagend = str.indexOf('>', i + 1) + 1;
-      if (!tagend)
-        break;
-      rtn += str.slice(0, tagend);
-      str = str.slice(tagend);
-    } else {
-      const [filename, shortCode] = unicodeMapping[match];
-      rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
-      str = str.slice(i + match.length);
+const emojify = (str, customEmojis = {}) => {
+  // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
+  // and replacing valid unicode strings
+  // that _aren't_ within tags with an <img> version.
+  // The goal is to be the same as an emojione.regUnicode replacement, but faster.
+  let i = -1;
+  let insideTag = false;
+  let insideShortname = false;
+  let shortnameStartIndex = -1;
+  let match;
+  while (++i < str.length) {
+    const char = str.charAt(i);
+    if (insideShortname && char === ':') {
+      const shortname = str.substring(shortnameStartIndex, i + 1);
+      if (shortname in customEmojis) {
+        const replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${customEmojis[shortname]}" />`;
+        str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
+        i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
+      } else {
+        i--;
+      }
+      insideShortname = false;
+    } else if (insideTag && char === '>') {
+      insideTag = false;
+    } else if (char === '<') {
+      insideTag = true;
+      insideShortname = false;
+    } else if (!insideTag && char === ':') {
+      insideShortname = true;
+      shortnameStartIndex = i;
+    } else if (!insideTag && (match = trie.search(str.substring(i)))) {
+      const unicodeStr = match;
+      if (unicodeStr in unicodeMapping) {
+        const [filename, shortCode] = unicodeMapping[unicodeStr];
+        const alt      = unicodeStr;
+        const replacement =  `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
+        str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
+        i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
+      }
     }
   }
-  return rtn + str;
+  return str;
 };
 
 export default emojify;
diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js
index f0fea1a0e..588a372c6 100644
--- a/app/javascript/mastodon/features/compose/util/counter.js
+++ b/app/javascript/mastodon/features/compose/util/counter.js
@@ -1,7 +1,9 @@
+import { urlRegex } from './url_regex';
+
 const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
 
 export function countableText(inputText) {
   return inputText
-    .replace(/https?:\/\/\S+/g, urlPlaceholder)
+    .replace(urlRegex, urlPlaceholder)
     .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2');
 };
diff --git a/app/javascript/mastodon/features/compose/util/url_regex.js b/app/javascript/mastodon/features/compose/util/url_regex.js
new file mode 100644
index 000000000..e676d1879
--- /dev/null
+++ b/app/javascript/mastodon/features/compose/util/url_regex.js
@@ -0,0 +1,196 @@
+const regexen = {};
+
+const regexSupplant = function(regex, flags) {
+  flags = flags || '';
+  if (typeof regex !== 'string') {
+    if (regex.global && flags.indexOf('g') < 0) {
+      flags += 'g';
+    }
+    if (regex.ignoreCase && flags.indexOf('i') < 0) {
+      flags += 'i';
+    }
+    if (regex.multiline && flags.indexOf('m') < 0) {
+      flags += 'm';
+    }
+
+    regex = regex.source;
+  }
+  return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
+    var newRegex = regexen[name] || '';
+    if (typeof newRegex !== 'string') {
+      newRegex = newRegex.source;
+    }
+    return newRegex;
+  }), flags);
+};
+
+const stringSupplant = function(str, values) {
+  return str.replace(/#\{(\w+)\}/g, function(match, name) {
+    return values[name] || '';
+  });
+};
+
+export const urlRegex = (function() {
+  regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/;
+  regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/;
+  regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/;
+  regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@@$###{invalid_chars_group}]|^)/);
+  regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen);
+  regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/);
+  regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
+  regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
+  regexen.validGTLD = regexSupplant(RegExp(
+  '(?:(?:' +
+    '삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
+    '政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
+    'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
+    'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
+    'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
+    'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
+    'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
+    'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
+    'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
+    'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
+    'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
+    'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
+    'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
+    'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
+    'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
+    'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
+    'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
+    'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
+    'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
+    'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
+    'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
+    'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
+    'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
+    'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
+    'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
+    'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
+    'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
+    'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
+    'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
+    'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
+    'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
+    'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
+    'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
+    'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
+    'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
+    'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
+    'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
+    'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
+    'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
+    'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
+    'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
+    'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
+    'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
+    'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
+    'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
+    'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
+    'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
+    'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
+    'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
+    'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
+    'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
+    'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
+    'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
+    'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
+    'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
+    'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
+    'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
+    'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
+    'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
+    'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
+    'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
+    'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
+    'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
+    'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
+    'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
+    'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
+    'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
+    'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
+    'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
+    'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
+    'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
+    'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
+    'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
+    'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
+    'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
+    'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
+    'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
+    'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
+    'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
+    'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
+    'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
+    'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
+    'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
+    'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
+    'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
+    'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
+    'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
+    'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
+  ')(?=[^0-9a-zA-Z@]|$))'));
+  regexen.validCCTLD = regexSupplant(RegExp(
+  '(?:(?:' +
+      '한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
+      'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
+      'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
+      'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' +
+      'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' +
+      're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' +
+      'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' +
+      'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' +
+      'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
+      'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
+      'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
+  ')(?=[^0-9a-zA-Z@]|$))'));
+  regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
+  regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
+  regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
+  regexen.validPortNumber = /[0-9]+/;
+  regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/;
+  regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i);
+  // Allow URL paths to contain up to two nested levels of balanced parens
+  //  1. Used in Wikipedia URLs like /Primer_(film)
+  //  2. Used in IIS sessions like /S(dfd346)/
+  //  3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/
+  regexen.validUrlBalancedParens = regexSupplant(
+    '\\('                                   +
+      '(?:'                                 +
+        '#{validGeneralUrlPathChars}+'      +
+        '|'                                 +
+        // allow one nested level of balanced parentheses
+        '(?:'                               +
+          '#{validGeneralUrlPathChars}*'    +
+          '\\('                             +
+            '#{validGeneralUrlPathChars}+'  +
+          '\\)'                             +
+          '#{validGeneralUrlPathChars}*'    +
+        ')'                                 +
+      ')'                                   +
+    '\\)'
+  , 'i');
+  // Valid end-of-path chracters (so /foo. does not gobble the period).
+  // 1. Allow =&# for empty URL parameters and other URL-join artifacts
+  regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
+  // Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/
+  regexen.validUrlPath = regexSupplant('(?:' +
+    '(?:' +
+      '#{validGeneralUrlPathChars}*' +
+        '(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
+        '#{validUrlPathEndingChars}'+
+      ')|(?:@#{validGeneralUrlPathChars}+\/)'+
+    ')', 'i');
+  regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
+  regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
+  regexen.validUrl = regexSupplant(
+    '('                                                          + // $1 URL
+      '(https?:\\/\\/)'                                          + // $2 Protocol
+      '(#{validDomain})'                                         + // $3 Domain(s)
+      '(?::(#{validPortNumber}))?'                               + // $4 Port number (optional)
+      '(\\/#{validUrlPath}*)?'                                   + // $5 URL Path
+      '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?'  + // $6 Query String
+    ')'
+  , 'gi');
+  return regexen.validUrl;
+}());
diff --git a/app/javascript/mastodon/features/standalone/compose/index.js b/app/javascript/mastodon/features/standalone/compose/index.js
index 96d07fefb..0d764575f 100644
--- a/app/javascript/mastodon/features/standalone/compose/index.js
+++ b/app/javascript/mastodon/features/standalone/compose/index.js
@@ -2,6 +2,7 @@ import React from 'react';
 import ComposeFormContainer from '../../compose/containers/compose_form_container';
 import NotificationsContainer from '../../ui/containers/notifications_container';
 import LoadingBarContainer from '../../ui/containers/loading_bar_container';
+import ModalContainer from '../../ui/containers/modal_container';
 
 export default class Compose extends React.PureComponent {
 
@@ -10,6 +11,7 @@ export default class Compose extends React.PureComponent {
       <div>
         <ComposeFormContainer />
         <NotificationsContainer />
+        <ModalContainer />
         <LoadingBarContainer className='loading-bar' />
       </div>
     );
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index 6b13e15cc..41c4300d3 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -1,4 +1,5 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import punycode from 'punycode';
 import classnames from 'classnames';
@@ -22,10 +23,15 @@ export default class Card extends React.PureComponent {
 
   static propTypes = {
     card: ImmutablePropTypes.map,
+    maxDescription: PropTypes.number,
+  };
+
+  static defaultProps = {
+    maxDescription: 50,
   };
 
   renderLink () {
-    const { card } = this.props;
+    const { card, maxDescription } = this.props;
 
     let image    = '';
     let provider = card.get('provider_name');
@@ -52,7 +58,7 @@ export default class Card extends React.PureComponent {
 
         <div className='status-card__content'>
           <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
-          <p className='status-card__description'>{(card.get('description') || '').substring(0, 50)}</p>
+          <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
           <span className='status-card__host'>{provider}</span>
         </div>
       </a>
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index b4979c603..8cd5abd3f 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -11,6 +11,7 @@ import Link from 'react-router-dom/Link';
 import { FormattedDate, FormattedNumber } from 'react-intl';
 import CardContainer from '../containers/card_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import Video from '../../video';
 import VisibilityIcon from '../../../../glitch/components/status/visibility_icon';
 
 export default class DetailedStatus extends ImmutablePureComponent {
@@ -36,6 +37,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
     e.stopPropagation();
   }
 
+  handleOpenVideo = startTime => {
+    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
+  }
+
   render () {
     const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
     const { settings } = this.props;
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 539af8ce3..5610095b9 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -78,7 +78,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
 
   handleChildrenContentChange() {
     if (!this.props.singleColumn) {
-      scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
+      this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
     }
   }
 
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js
index 9a9a49dfb..867c73ed5 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.js
@@ -1,35 +1,29 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import ExtendedVideoPlayer from '../../../components/extended_video_player';
-import { defineMessages, injectIntl } from 'react-intl';
-import IconButton from '../../../components/icon_button';
+import Video from '../../video';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
-const messages = defineMessages({
-  close: { id: 'lightbox.close', defaultMessage: 'Close' },
-});
-
-@injectIntl
 export default class VideoModal extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
     time: PropTypes.number,
     onClose: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
   };
 
   render () {
-    const { media, intl, time, onClose } = this.props;
-
-    const url = media.get('url');
+    const { media, time, onClose } = this.props;
 
     return (
       <div className='modal-root__modal media-modal'>
         <div>
-          <div className='media-modal__close'><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div>
-          <ExtendedVideoPlayer src={url} muted={false} controls time={time} />
+          <Video
+            preview={media.get('preview_url')}
+            src={media.get('url')}
+            startTime={time}
+            onCloseVideo={onClose}
+          />
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 7d12210bb..3732d301f 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -11,7 +11,7 @@ import { debounce } from 'lodash';
 import { uploadCompose } from '../../actions/compose';
 import { refreshHomeTimeline } from '../../actions/timelines';
 import { refreshNotifications } from '../../actions/notifications';
-import { clearStatusesHeight } from '../../actions/statuses';
+import { clearHeight } from '../../actions/height_cache';
 import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
 import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
@@ -77,7 +77,7 @@ export default class UI extends React.PureComponent {
 
   handleResize = debounce(() => {
     // The cached heights are no longer accurate, invalidate
-    this.props.dispatch(clearStatusesHeight());
+    this.props.dispatch(clearHeight());
 
     this.setState({ width: window.innerWidth });
   }, 500, {
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 2f5c52e9e..ddb7e32c9 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -109,6 +109,10 @@ export function VideoPlayer () {
   return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
 }
 
+export function Video () {
+  return import(/* webpackChunkName: "features/video" */'../../video');
+}
+
 export function EmbedModal () {
   return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
 }
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
new file mode 100644
index 000000000..f228e434b
--- /dev/null
+++ b/app/javascript/mastodon/features/video/index.js
@@ -0,0 +1,304 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { throttle } from 'lodash';
+import classNames from 'classnames';
+
+const messages = defineMessages({
+  play: { id: 'video.play', defaultMessage: 'Play' },
+  pause: { id: 'video.pause', defaultMessage: 'Pause' },
+  mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
+  unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
+  hide: { id: 'video.hide', defaultMessage: 'Hide video' },
+  expand: { id: 'video.expand', defaultMessage: 'Expand video' },
+  close: { id: 'video.close', defaultMessage: 'Close video' },
+  fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
+  exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
+});
+
+const findElementPosition = el => {
+  let box;
+
+  if (el.getBoundingClientRect && el.parentNode) {
+    box = el.getBoundingClientRect();
+  }
+
+  if (!box) {
+    return {
+      left: 0,
+      top: 0,
+    };
+  }
+
+  const docEl = document.documentElement;
+  const body  = document.body;
+
+  const clientLeft = docEl.clientLeft || body.clientLeft || 0;
+  const scrollLeft = window.pageXOffset || body.scrollLeft;
+  const left       = (box.left + scrollLeft) - clientLeft;
+
+  const clientTop = docEl.clientTop || body.clientTop || 0;
+  const scrollTop = window.pageYOffset || body.scrollTop;
+  const top       = (box.top + scrollTop) - clientTop;
+
+  return {
+    left: Math.round(left),
+    top: Math.round(top),
+  };
+};
+
+const getPointerPosition = (el, event) => {
+  const position = {};
+  const box = findElementPosition(el);
+  const boxW = el.offsetWidth;
+  const boxH = el.offsetHeight;
+  const boxY = box.top;
+  const boxX = box.left;
+
+  let pageY = event.pageY;
+  let pageX = event.pageX;
+
+  if (event.changedTouches) {
+    pageX = event.changedTouches[0].pageX;
+    pageY = event.changedTouches[0].pageY;
+  }
+
+  position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH));
+  position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
+
+  return position;
+};
+
+const isFullscreen = () => document.fullscreenElement ||
+  document.webkitFullscreenElement ||
+  document.mozFullScreenElement ||
+  document.msFullscreenElement;
+
+const exitFullscreen = () => {
+  if (document.exitFullscreen) {
+    document.exitFullscreen();
+  } else if (document.webkitExitFullscreen) {
+    document.webkitExitFullscreen();
+  } else if (document.mozCancelFullScreen) {
+    document.mozCancelFullScreen();
+  } else if (document.msExitFullscreen) {
+    document.msExitFullscreen();
+  }
+};
+
+const requestFullscreen = el => {
+  if (el.requestFullscreen) {
+    el.requestFullscreen();
+  } else if (el.webkitRequestFullscreen) {
+    el.webkitRequestFullscreen();
+  } else if (el.mozRequestFullScreen) {
+    el.mozRequestFullScreen();
+  } else if (el.msRequestFullscreen) {
+    el.msRequestFullscreen();
+  }
+};
+
+@injectIntl
+export default class Video extends React.PureComponent {
+
+  static propTypes = {
+    preview: PropTypes.string,
+    src: PropTypes.string.isRequired,
+    width: PropTypes.number,
+    height: PropTypes.number,
+    sensitive: PropTypes.bool,
+    startTime: PropTypes.number,
+    onOpenVideo: PropTypes.func,
+    onCloseVideo: PropTypes.func,
+    intl: PropTypes.object.isRequired,
+  };
+
+  state = {
+    progress: 0,
+    paused: true,
+    dragging: false,
+    fullscreen: false,
+    hovered: false,
+    muted: false,
+    revealed: !this.props.sensitive,
+  };
+
+  setPlayerRef = c => {
+    this.player = c;
+  }
+
+  setVideoRef = c => {
+    this.video = c;
+  }
+
+  setSeekRef = c => {
+    this.seek = c;
+  }
+
+  handlePlay = () => {
+    this.setState({ paused: false });
+  }
+
+  handlePause = () => {
+    this.setState({ paused: true });
+  }
+
+  handleTimeUpdate = () => {
+    this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) });
+  }
+
+  handleMouseDown = e => {
+    document.addEventListener('mousemove', this.handleMouseMove, true);
+    document.addEventListener('mouseup', this.handleMouseUp, true);
+    document.addEventListener('touchmove', this.handleMouseMove, true);
+    document.addEventListener('touchend', this.handleMouseUp, true);
+
+    this.setState({ dragging: true });
+    this.video.pause();
+    this.handleMouseMove(e);
+  }
+
+  handleMouseUp = () => {
+    document.removeEventListener('mousemove', this.handleMouseMove, true);
+    document.removeEventListener('mouseup', this.handleMouseUp, true);
+    document.removeEventListener('touchmove', this.handleMouseMove, true);
+    document.removeEventListener('touchend', this.handleMouseUp, true);
+
+    this.setState({ dragging: false });
+    this.video.play();
+  }
+
+  handleMouseMove = throttle(e => {
+    const { x } = getPointerPosition(this.seek, e);
+    this.video.currentTime = this.video.duration * x;
+    this.setState({ progress: x * 100 });
+  }, 60);
+
+  togglePlay = () => {
+    if (this.state.paused) {
+      this.video.play();
+    } else {
+      this.video.pause();
+    }
+  }
+
+  toggleFullscreen = () => {
+    if (isFullscreen()) {
+      exitFullscreen();
+    } else {
+      requestFullscreen(this.player);
+    }
+  }
+
+  componentDidMount () {
+    document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
+    document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
+    document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
+    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
+  }
+
+  componentWillUnmount () {
+    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
+    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
+    document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
+    document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
+  }
+
+  handleFullscreenChange = () => {
+    this.setState({ fullscreen: isFullscreen() });
+  }
+
+  handleMouseEnter = () => {
+    this.setState({ hovered: true });
+  }
+
+  handleMouseLeave = () => {
+    this.setState({ hovered: false });
+  }
+
+  toggleMute = () => {
+    this.video.muted = !this.video.muted;
+    this.setState({ muted: this.video.muted });
+  }
+
+  toggleReveal = () => {
+    if (this.state.revealed) {
+      this.video.pause();
+    }
+
+    this.setState({ revealed: !this.state.revealed });
+  }
+
+  handleLoadedData = () => {
+    if (this.props.startTime) {
+      this.video.currentTime = this.props.startTime;
+      this.video.play();
+    }
+  }
+
+  handleOpenVideo = () => {
+    this.video.pause();
+    this.props.onOpenVideo(this.video.currentTime);
+  }
+
+  handleCloseVideo = () => {
+    this.video.pause();
+    this.props.onCloseVideo();
+  }
+
+  render () {
+    const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl } = this.props;
+    const { progress, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
+
+    return (
+      <div className={classNames('video-player', { inactive: !revealed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
+        <video
+          ref={this.setVideoRef}
+          src={src}
+          poster={preview}
+          preload={!!startTime}
+          loop
+          role='button'
+          tabIndex='0'
+          width={width}
+          height={height}
+          onClick={this.togglePlay}
+          onPlay={this.handlePlay}
+          onPause={this.handlePause}
+          onTimeUpdate={this.handleTimeUpdate}
+          onLoadedData={this.handleLoadedData}
+        />
+
+        <button className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}>
+          <span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
+          <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
+        </button>
+
+        <div className={classNames('video-player__controls', { active: paused || hovered })}>
+          <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
+            <div className='video-player__seek__progress' style={{ width: `${progress}%` }} />
+
+            <span
+              className={classNames('video-player__seek__handle', { active: dragging })}
+              tabIndex='0'
+              style={{ left: `${progress}%` }}
+            />
+          </div>
+
+          <div className='video-player__buttons left'>
+            <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
+            <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
+            {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
+          </div>
+
+          <div className='video-player__buttons right'>
+            {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
+            {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>}
+            <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>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 2ceb6eb9a..3a6fa2874 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -33,6 +33,7 @@
   "column.home": "الرئيسية",
   "column.mutes": "الحسابات المكتومة",
   "column.notifications": "الإشعارات",
+  "column.pins": "Pinned toot",
   "column.public": "الخيط العام الموحد",
   "column_back_button.label": "العودة",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "معلومات إضافية",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "الحسابات المكتومة",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "التفضيلات",
   "navigation_bar.public_timeline": "الخيط العام الموحد",
   "notification.favourite": "{name} أعجب بمنشورك",
@@ -193,6 +195,15 @@
   "upload_button.label": "إضافة وسائط",
   "upload_form.undo": "إلغاء",
   "upload_progress.label": "يرفع...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "وسّع الفيديو",
   "video_player.toggle_sound": "تبديل الصوت",
   "video_player.toggle_visible": "إظهار / إخفاء الفيديو",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 183ba2673..9afe2d038 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -33,6 +33,7 @@
   "column.home": "Начало",
   "column.mutes": "Muted users",
   "column.notifications": "Известия",
+  "column.pins": "Pinned toot",
   "column.public": "Публичен канал",
   "column_back_button.label": "Назад",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Extended information",
   "navigation_bar.logout": "Излизане",
   "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Предпочитания",
   "navigation_bar.public_timeline": "Публичен канал",
   "notification.favourite": "{name} хареса твоята публикация",
@@ -193,6 +195,15 @@
   "upload_button.label": "Добави медия",
   "upload_form.undo": "Отмяна",
   "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Expand video",
   "video_player.toggle_sound": "Звук",
   "video_player.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 0e3d2bc18..7d45b4d6b 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -33,6 +33,7 @@
   "column.home": "Inici",
   "column.mutes": "Usuaris silenciats",
   "column.notifications": "Notificacions",
+  "column.pins": "Pinned toot",
   "column.public": "Línia de temps federada",
   "column_back_button.label": "Enrere",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Informació addicional",
   "navigation_bar.logout": "Tancar sessió",
   "navigation_bar.mutes": "Usuaris silenciats",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Preferències",
   "navigation_bar.public_timeline": "Línia de temps federada",
   "notification.favourite": "{name} ha afavorit el teu estat",
@@ -193,6 +195,15 @@
   "upload_button.label": "Afegir multimèdia",
   "upload_form.undo": "Desfer",
   "upload_progress.label": "Pujant...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Ampliar el vídeo",
   "video_player.toggle_sound": "Alternar so",
   "video_player.toggle_visible": "Alternar visibilitat",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 3133238cd..712c635c8 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -33,6 +33,7 @@
   "column.home": "Startseite",
   "column.mutes": "Stummgeschaltete Profile",
   "column.notifications": "Mitteilungen",
+  "column.pins": "Pinned toot",
   "column.public": "Gesamtes bekanntes Netz",
   "column_back_button.label": "Zurück",
   "column_header.hide_settings": "Einstellungen verbergen",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Erweiterte Informationen",
   "navigation_bar.logout": "Abmelden",
   "navigation_bar.mutes": "Stummgeschaltete Profile",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Einstellungen",
   "navigation_bar.public_timeline": "Föderierte Zeitleiste",
   "notification.favourite": "{name} favorisierte deinen Status",
@@ -193,6 +195,15 @@
   "upload_button.label": "Mediendatei hinzufügen",
   "upload_form.undo": "Entfernen",
   "upload_progress.label": "Lade hoch…",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Videoanzeige vergrößern",
   "video_player.toggle_sound": "Ton umschalten",
   "video_player.toggle_visible": "Sichtbarkeit umschalten",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 89f74a56b..3c19ad7dc 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -813,6 +813,10 @@
         "id": "navigation_bar.info"
       },
       {
+        "defaultMessage": "Pinned toots",
+        "id": "navigation_bar.pins"
+      },
+      {
         "defaultMessage": "FAQ",
         "id": "getting_started.faq"
       },
@@ -995,6 +999,15 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Pinned toot",
+        "id": "column.pins"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/pinned_statuses/index.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Federated timeline",
         "id": "column.public"
       },
@@ -1326,5 +1339,54 @@
       }
     ],
     "path": "app/javascript/mastodon/features/ui/components/video_modal.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Play",
+        "id": "video.play"
+      },
+      {
+        "defaultMessage": "Pause",
+        "id": "video.pause"
+      },
+      {
+        "defaultMessage": "Mute sound",
+        "id": "video.mute"
+      },
+      {
+        "defaultMessage": "Unmute sound",
+        "id": "video.unmute"
+      },
+      {
+        "defaultMessage": "Hide video",
+        "id": "video.hide"
+      },
+      {
+        "defaultMessage": "Expand video",
+        "id": "video.expand"
+      },
+      {
+        "defaultMessage": "Close video",
+        "id": "video.close"
+      },
+      {
+        "defaultMessage": "Full screen",
+        "id": "video.fullscreen"
+      },
+      {
+        "defaultMessage": "Exit full screen",
+        "id": "video.exit_fullscreen"
+      },
+      {
+        "defaultMessage": "Sensitive content",
+        "id": "status.sensitive_warning"
+      },
+      {
+        "defaultMessage": "Click to view",
+        "id": "status.sensitive_toggle"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/video/index.json"
   }
 ]
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index f42851f45..436079aeb 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -33,8 +33,8 @@
   "column.home": "Home",
   "column.mutes": "Muted users",
   "column.notifications": "Notifications",
-  "column.public": "Federated timeline",
   "column.pins": "Pinned toots",
+  "column.public": "Federated timeline",
   "column_back_button.label": "Back",
   "column_header.hide_settings": "Hide settings",
   "column_header.moveLeft_settings": "Move column to the left",
@@ -110,9 +110,9 @@
   "navigation_bar.info": "About this instance",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Preferences",
   "navigation_bar.public_timeline": "Federated timeline",
-  "navigation_bar.pins": "Pinned toots",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} followed you",
   "notification.mention": "{name} mentioned you",
@@ -195,6 +195,15 @@
   "upload_button.label": "Add media",
   "upload_form.undo": "Undo",
   "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Expand video",
   "video_player.toggle_sound": "Toggle sound",
   "video_player.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index d828d0858..945fcd8e0 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -33,6 +33,7 @@
   "column.home": "Hejmo",
   "column.mutes": "Muted users",
   "column.notifications": "Sciigoj",
+  "column.pins": "Pinned toot",
   "column.public": "Fratara tempolinio",
   "column_back_button.label": "Reveni",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Extended information",
   "navigation_bar.logout": "Elsaluti",
   "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Preferoj",
   "navigation_bar.public_timeline": "Fratara tempolinio",
   "notification.favourite": "{name} favoris vian mesaĝon",
@@ -193,6 +195,15 @@
   "upload_button.label": "Aldoni enhavaĵon",
   "upload_form.undo": "Malfari",
   "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Expand video",
   "video_player.toggle_sound": "Aktivigi sonojn",
   "video_player.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index d35eb84e7..5182b5094 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -1,106 +1,107 @@
 {
   "account.block": "Bloquear",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.block_domain": "Ocultar todo de {domain}",
+  "account.disclaimer_full": "La siguiente información del usuario puede estar incompleta.",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
-  "account.follows": "Seguir",
+  "account.follows": "Sigue",
   "account.follows_you": "Te sigue",
   "account.media": "Media",
-  "account.mention": "Mencionar",
-  "account.mute": "Silenciar",
+  "account.mention": "Mencionar a @{name}",
+  "account.mute": "Silenciar a @{name}",
   "account.posts": "Publicaciones",
-  "account.report": "Report @{name}",
+  "account.report": "Reportar a @{name}",
   "account.requested": "Esperando aprobación",
-  "account.share": "Share @{name}'s profile",
-  "account.unblock": "Desbloquear",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.share": "Compartir el perfil de @{name}",
+  "account.unblock": "Desbloquear a @{name}",
+  "account.unblock_domain": "Mostrar a {domain}",
   "account.unfollow": "Dejar de seguir",
-  "account.unmute": "Unmute @{name}",
-  "account.view_full_profile": "View full profile",
-  "boost_modal.combo": "You can press {combo} to skip this next time",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "account.unmute": "Dejar de silenciar a @{name}",
+  "account.view_full_profile": "Ver perfil completo",
+  "boost_modal.combo": "Puedes presionar {combo} para saltear 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",
+  "bundle_modal_error.close": "Cerrar",
+  "bundle_modal_error.message": "Algo salió mal al cargar este componente.",
+  "bundle_modal_error.retry": "Inténtalo de nuevo",
   "column.blocks": "Usuarios bloqueados",
-  "column.community": "Historia local",
+  "column.community": "Línea de tiempo local",
   "column.favourites": "Favoritos",
-  "column.follow_requests": "Solicitudes para seguirte",
+  "column.follow_requests": "Solicitudes de seguimiento",
   "column.home": "Inicio",
   "column.mutes": "Usuarios silenciados",
   "column.notifications": "Notificaciones",
+  "column.pins": "Toot fijado",
   "column.public": "Historia federada",
   "column_back_button.label": "Atrás",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
-  "column_subheading.navigation": "Navigation",
-  "column_subheading.settings": "Settings",
-  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
-  "compose_form.lock_disclaimer.lock": "locked",
+  "column_header.hide_settings": "Ocultar ajustes",
+  "column_header.moveLeft_settings": "Mover columna a la izquierda",
+  "column_header.moveRight_settings": "Mover columna a la derecha",
+  "column_header.pin": "Fijar",
+  "column_header.show_settings": "Mostrar ajustes",
+  "column_header.unpin": "Dejar de fijar",
+  "column_subheading.navigation": "Navegación",
+  "column_subheading.settings": "Ajustes",
+  "compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.",
+  "compose_form.lock_disclaimer.lock": "bloqueado",
   "compose_form.placeholder": "¿En qué estás pensando?",
-  "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
+  "compose_form.privacy_disclaimer": "Tu toot privado será enviado a usuario/s mencionados de {domains}. ¿Confías en {domainsCount, plural, one {ese servidor} other {esos servidores}}? La privacidad del toot funcionará solamente en instancias de Mastodon. Si {domains} {domainsCount, plural, one {no es una instancia de Mastodon} other {no son instancias de Mastodon}}, no habrá indicación de que tu toot es privado, y puede hacerse visible a remitentes inesperados.",
   "compose_form.publish": "Tootear",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Marcar contenido como sensible",
-  "compose_form.spoiler": "Ocultar texto tras advertencia",
+  "compose_form.spoiler": "Ocultar texto tras una advertencia",
   "compose_form.spoiler_placeholder": "Advertencia de contenido",
-  "confirmation_modal.cancel": "Cancel",
-  "confirmations.block.confirm": "Block",
-  "confirmations.block.message": "Are you sure you want to block {name}?",
-  "confirmations.delete.confirm": "Delete",
-  "confirmations.delete.message": "Are you sure you want to delete this status?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
-  "confirmations.mute.confirm": "Mute",
-  "confirmations.mute.message": "Are you sure you want to mute {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
-  "emoji_button.activity": "Activity",
-  "emoji_button.flags": "Flags",
-  "emoji_button.food": "Food & Drink",
+  "confirmation_modal.cancel": "Cancelar",
+  "confirmations.block.confirm": "Bloquear",
+  "confirmations.block.message": "¿Estás seguro de que quieres bloquear a {name}?",
+  "confirmations.delete.confirm": "Eliminar",
+  "confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?",
+  "confirmations.domain_block.confirm": "Ocultar dominio entero",
+  "confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio entero? En algunos casos es preferible bloquear o silenciar objetivos determinados.",
+  "confirmations.mute.confirm": "Silenciar",
+  "confirmations.mute.message": "¿Estás seguro de que quieres silenciar a {name}?",
+  "confirmations.unfollow.confirm": "Dejar de seguir",
+  "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?",
+  "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
+  "embed.preview": "Así es como se verá:",
+  "emoji_button.activity": "Actividad",
+  "emoji_button.flags": "Marcas",
+  "emoji_button.food": "Comida y bebida",
   "emoji_button.label": "Insertar emoji",
-  "emoji_button.nature": "Nature",
-  "emoji_button.objects": "Objects",
-  "emoji_button.people": "People",
-  "emoji_button.search": "Search...",
-  "emoji_button.symbols": "Symbols",
-  "emoji_button.travel": "Travel & Places",
-  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
-  "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
-  "empty_column.home.public_timeline": "the public timeline",
-  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
-  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
-  "follow_request.authorize": "Authorize",
-  "follow_request.reject": "Reject",
-  "getting_started.appsshort": "Apps",
+  "emoji_button.nature": "Naturaleza",
+  "emoji_button.objects": "Objetos",
+  "emoji_button.people": "Gente",
+  "emoji_button.search": "Buscar…",
+  "emoji_button.symbols": "Símbolos",
+  "emoji_button.travel": "Viajes y lugares",
+  "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
+  "empty_column.hashtag": "No hay nada en este hashtag aún.",
+  "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
+  "empty_column.home.inactivity": "Tus notificaciones están vacías. Si has estado inactivo por un tiempo, se regenerará para ti pronto.",
+  "empty_column.home.public_timeline": "la línea de tiempo pública",
+  "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
+  "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
+  "follow_request.authorize": "Autorizar",
+  "follow_request.reject": "Rechazar",
+  "getting_started.appsshort": "Aplicaciones",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Primeros pasos",
   "getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}.",
-  "getting_started.userguide": "User Guide",
-  "home.column_settings.advanced": "Advanced",
-  "home.column_settings.basic": "Basic",
-  "home.column_settings.filter_regex": "Filter out by regular expressions",
-  "home.column_settings.show_reblogs": "Show boosts",
-  "home.column_settings.show_replies": "Show replies",
-  "home.settings": "Column settings",
+  "getting_started.userguide": "Guía de usuario",
+  "home.column_settings.advanced": "Avanzado",
+  "home.column_settings.basic": "Básico",
+  "home.column_settings.filter_regex": "Filtrar con expresiones regulares",
+  "home.column_settings.show_reblogs": "Mostrar retoots",
+  "home.column_settings.show_replies": "Mostrar respuestas",
+  "home.settings": "Ajustes de columna",
   "lightbox.close": "Cerrar",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
-  "loading_indicator.label": "Cargando...",
-  "media_gallery.toggle_visible": "Toggle visibility",
-  "missing_indicator.label": "Not found",
+  "lightbox.next": "Siguiente",
+  "lightbox.previous": "Anterior",
+  "loading_indicator.label": "Cargando…",
+  "media_gallery.toggle_visible": "Cambiar visibilidad",
+  "missing_indicator.label": "No encontrado",
   "navigation_bar.blocks": "Usuarios bloqueados",
   "navigation_bar.community_timeline": "Historia local",
   "navigation_bar.edit_profile": "Editar perfil",
@@ -109,43 +110,44 @@
   "navigation_bar.info": "Información adicional",
   "navigation_bar.logout": "Cerrar sesión",
   "navigation_bar.mutes": "Usuarios silenciados",
+  "navigation_bar.pins": "Toots fijados",
   "navigation_bar.preferences": "Preferencias",
   "navigation_bar.public_timeline": "Historia federada",
   "notification.favourite": "{name} marcó tu estado como favorito",
   "notification.follow": "{name} te empezó a seguir",
   "notification.mention": "{name} te ha mencionado",
   "notification.reblog": "{name} ha retooteado tu estado",
-  "notifications.clear": "Clear notifications",
-  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+  "notifications.clear": "Limpiar notificaciones",
+  "notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?",
   "notifications.column_settings.alert": "Notificaciones de escritorio",
   "notifications.column_settings.favourite": "Favoritos:",
   "notifications.column_settings.follow": "Nuevos seguidores:",
   "notifications.column_settings.mention": "Menciones:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "Notificaciones push:",
+  "notifications.column_settings.push_meta": "Este dispositivo:",
   "notifications.column_settings.reblog": "Retoots:",
   "notifications.column_settings.show": "Mostrar en columna",
-  "notifications.column_settings.sound": "Play sound",
-  "onboarding.done": "Done",
-  "onboarding.next": "Next",
-  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
-  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
-  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
-  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
-  "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
-  "onboarding.page_one.welcome": "Welcome to Mastodon!",
-  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
-  "onboarding.page_six.almost_done": "Almost done...",
-  "onboarding.page_six.appetoot": "Bon Appetoot!",
-  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
-  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
-  "onboarding.page_six.guidelines": "community guidelines",
-  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
-  "onboarding.page_six.various_app": "mobile apps",
-  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
-  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
-  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
-  "onboarding.skip": "Skip",
+  "notifications.column_settings.sound": "Reproducir sonido",
+  "onboarding.done": "Listo",
+  "onboarding.next": "Siguiente",
+  "onboarding.page_five.public_timelines": "La línea de tiempo local muestra toots públicos de todos en {domain}. La línea de tiempo federada muestra toots públicos de cualquiera a quien la gente de {domain} siga. Estas son las líneas de tiempo públicas, una buena forma de conocer gente nueva.",
+  "onboarding.page_four.home": "La línea de tiempo principal muestra toots de gente que sigues.",
+  "onboarding.page_four.notifications": "Las notificaciones se muestran cuando alguien interactúa contigo.",
+  "onboarding.page_one.federation": "Mastodon es una red de servidores federados que conforman una red social aún más grande. Llamamos a estos servidores instancias.",
+  "onboarding.page_one.handle": "Estás en {domain}, así que tu nombre de usuario completo es {handle}",
+  "onboarding.page_one.welcome": "¡Bienvenido a Mastodon!",
+  "onboarding.page_six.admin": "El administrador de tu instancia es {admin}.",
+  "onboarding.page_six.almost_done": "Ya casi…",
+  "onboarding.page_six.appetoot": "¡Bon Appetoot!",
+  "onboarding.page_six.apps_available": "Hay {apps} disponibles para iOS, Android y otras plataformas.",
+  "onboarding.page_six.github": "Mastodon es software libre. Puedes reportar errores, pedir funciones nuevas, o contribuir al código en {github}.",
+  "onboarding.page_six.guidelines": "guías de la comunidad",
+  "onboarding.page_six.read_guidelines": "¡Por favor lee las {guidelines} de {domain}!",
+  "onboarding.page_six.various_app": "aplicaciones móviles",
+  "onboarding.page_three.profile": "Edita tu perfil para cambiar tu avatar, biografía y nombre de cabecera. Ahí, también encontrarás otros ajustes.",
+  "onboarding.page_three.search": "Usa la barra de búsqueda y revisa hashtags, como {illustration} y {introductions}. Para ver a alguien que no es de tu propia instancia, usa su nombre de usuario completo.",
+  "onboarding.page_two.compose": "Escribe toots en la columna de redacción. Puedes subir imágenes, cambiar ajustes de privacidad, y añadir advertencias de contenido con los siguientes íconos.",
+  "onboarding.skip": "Saltar",
   "privacy.change": "Ajustar privacidad",
   "privacy.direct.long": "Sólo mostrar a los usuarios mencionados",
   "privacy.direct.short": "Directo",
@@ -156,45 +158,54 @@
   "privacy.unlisted.long": "No mostrar en la historia federada",
   "privacy.unlisted.short": "Sin federar",
   "reply_indicator.cancel": "Cancelar",
-  "report.placeholder": "Additional comments",
-  "report.submit": "Submit",
-  "report.target": "Reporting",
+  "report.placeholder": "Comentarios adicionales",
+  "report.submit": "Publicar",
+  "report.target": "Reportando",
   "search.placeholder": "Buscar",
-  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
-  "standalone.public_title": "A look inside...",
-  "status.cannot_reblog": "This post cannot be boosted",
+  "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
+  "standalone.public_title": "Un pequeño vistazo...",
+  "status.cannot_reblog": "Este toot no puede retootearse",
   "status.delete": "Borrar",
-  "status.embed": "Embed",
+  "status.embed": "Incrustado",
   "status.favourite": "Favorito",
-  "status.load_more": "Load more",
-  "status.media_hidden": "Media hidden",
+  "status.load_more": "Cargar más",
+  "status.media_hidden": "Contenido multimedia oculto",
   "status.mention": "Mencionar",
-  "status.mute_conversation": "Mute conversation",
+  "status.mute_conversation": "Silenciar conversación",
   "status.open": "Expandir estado",
-  "status.pin": "Pin on profile",
-  "status.reblog": "Retoot",
+  "status.pin": "Fijar",
+  "status.reblog": "Retootear",
   "status.reblogged_by": "Retooteado por {name}",
   "status.reply": "Responder",
-  "status.replyAll": "Reply to thread",
+  "status.replyAll": "Responder al hilo",
   "status.report": "Reportar",
-  "status.sensitive_toggle": "Click para ver",
+  "status.sensitive_toggle": "Haz clic para ver",
   "status.sensitive_warning": "Contenido sensible",
-  "status.share": "Share",
+  "status.share": "Compartir",
   "status.show_less": "Mostrar menos",
   "status.show_more": "Mostrar más",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
+  "status.unmute_conversation": "Dejar de silenciar conversación",
+  "status.unpin": "Dejar de fijar",
   "tabs_bar.compose": "Redactar",
-  "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.federated_timeline": "Federado",
   "tabs_bar.home": "Inicio",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificaciones",
-  "upload_area.title": "Drag & drop to upload",
+  "upload_area.title": "Arrastra y suelta para subir",
   "upload_button.label": "Subir multimedia",
   "upload_form.undo": "Deshacer",
-  "upload_progress.label": "Uploading...",
-  "video_player.expand": "Expand video",
-  "video_player.toggle_sound": "Act/Desac. sonido",
-  "video_player.toggle_visible": "Toggle visibility",
-  "video_player.video_error": "Video could not be played"
+  "upload_progress.label": "Subiendo…",
+  "video.close": "Cerrar video",
+  "video.exit_fullscreen": "Salir de pantalla completa",
+  "video.expand": "Expandir vídeo",
+  "video.fullscreen": "Pantalla completa",
+  "video.hide": "Ocultar vídeo",
+  "video.mute": "Silenciar sonido",
+  "video.pause": "Pausar",
+  "video.play": "Reproducir",
+  "video.unmute": "Dejar de silenciar sonido",
+  "video_player.expand": "Expandir vídeo",
+  "video_player.toggle_sound": "Activar/Desactivar sonido",
+  "video_player.toggle_visible": "Cambiar visibilidad",
+  "video_player.video_error": "No se pudo reproducir el vídeo"
 }
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index d05b26eb9..23f4a41d6 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -33,6 +33,7 @@
   "column.home": "خانه",
   "column.mutes": "کاربران بی‌صداشده",
   "column.notifications": "اعلان‌ها",
+  "column.pins": "نوشته‌های ثابت",
   "column.public": "نوشته‌های همه‌جا",
   "column_back_button.label": "بازگشت",
   "column_header.hide_settings": "نهفتن تنظیمات",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "اطلاعات تکمیلی",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "کاربران بی‌صداشده",
+  "navigation_bar.pins": "نوشته‌های ثابت",
   "navigation_bar.preferences": "ترجیحات",
   "navigation_bar.public_timeline": "نوشته‌های همه‌جا",
   "notification.favourite": "‫{name}‬ نوشتهٔ شما را پسندید",
@@ -193,6 +195,15 @@
   "upload_button.label": "افزودن تصویر",
   "upload_form.undo": "واگردانی",
   "upload_progress.label": "بارگذاری...",
+  "video.close": "بستن ویدیو",
+  "video.exit_fullscreen": "خروج از حالت تمام صفحه",
+  "video.expand": "بزرگ‌کردن ویدیو",
+  "video.fullscreen": "تمام صفحه",
+  "video.hide": "نهفتن ویدیو",
+  "video.mute": "قطع صدا",
+  "video.pause": "توقف",
+  "video.play": "پخش",
+  "video.unmute": "پخش صدا",
   "video_player.expand": "بازکردن ویدیو",
   "video_player.toggle_sound": "تغییر صداداری",
   "video_player.toggle_visible": "تغییر پیدایی",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 926a57ff1..fc409a932 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -33,6 +33,7 @@
   "column.home": "Koti",
   "column.mutes": "Muted users",
   "column.notifications": "Ilmoitukset",
+  "column.pins": "Pinned toot",
   "column.public": "Yleinen aikajana",
   "column_back_button.label": "Takaisin",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Extended information",
   "navigation_bar.logout": "Kirjaudu ulos",
   "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Ominaisuudet",
   "navigation_bar.public_timeline": "Yleinen aikajana",
   "notification.favourite": "{name} tykkäsi statuksestasi",
@@ -193,6 +195,15 @@
   "upload_button.label": "Lisää mediaa",
   "upload_form.undo": "Peru",
   "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Expand video",
   "video_player.toggle_sound": "Äänet päälle/pois",
   "video_player.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 8ca632acc..5a436891b 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -33,8 +33,8 @@
   "column.home": "Accueil",
   "column.mutes": "Comptes masqués",
   "column.notifications": "Notifications",
-  "column.public": "Fil public global",
   "column.pins": "Pouets épinglés",
+  "column.public": "Fil public global",
   "column_back_button.label": "Retour",
   "column_header.hide_settings": "Masquer les paramètres",
   "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
@@ -110,9 +110,9 @@
   "navigation_bar.info": "Plus d’informations",
   "navigation_bar.logout": "Déconnexion",
   "navigation_bar.mutes": "Comptes masqués",
+  "navigation_bar.pins": "Pouets épinglés",
   "navigation_bar.preferences": "Préférences",
   "navigation_bar.public_timeline": "Fil public global",
-  "navigation_bar.pins": "Pouets épinglés",
   "notification.favourite": "{name} a ajouté à ses favoris :",
   "notification.follow": "{name} vous suit.",
   "notification.mention": "{name} vous a mentionné⋅e :",
@@ -166,7 +166,7 @@
   "standalone.public_title": "Jeter un coup d’œil…",
   "status.cannot_reblog": "Cette publication ne peut être boostée",
   "status.delete": "Effacer",
-  "status.embed": "Embed",
+  "status.embed": "Intégrer",
   "status.favourite": "Ajouter aux favoris",
   "status.load_more": "Charger plus",
   "status.media_hidden": "Média caché",
@@ -195,6 +195,15 @@
   "upload_button.label": "Joindre un média",
   "upload_form.undo": "Annuler",
   "upload_progress.label": "Envoi en cours…",
+  "video.close": "Fermer la vidéo",
+  "video.exit_fullscreen": "Quitter plein écran",
+  "video.expand": "Agrandir la vidéo",
+  "video.fullscreen": "Plein écran",
+  "video.hide": "Masquer la vidéo",
+  "video.mute": "Couper le son",
+  "video.pause": "Pause",
+  "video.play": "Lecture",
+  "video.unmute": "Rétablir le son",
   "video_player.expand": "Agrandir la vidéo",
   "video_player.toggle_sound": "Activer/Désactiver le son",
   "video_player.toggle_visible": "Afficher/Cacher la vidéo",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 9ef933108..06b401d39 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -33,6 +33,7 @@
   "column.home": "בבית",
   "column.mutes": "השתקות",
   "column.notifications": "התראות",
+  "column.pins": "Pinned toot",
   "column.public": "בפרהסיה",
   "column_back_button.label": "חזרה",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "מידע נוסף",
   "navigation_bar.logout": "יציאה",
   "navigation_bar.mutes": "השתקות",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "העדפות",
   "navigation_bar.public_timeline": "ציר זמן בין-קהילתי",
   "notification.favourite": "חצרוצך חובב על ידי {name}",
@@ -193,6 +195,15 @@
   "upload_button.label": "הוספת מדיה",
   "upload_form.undo": "ביטול",
   "upload_progress.label": "עולה...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "הרחבת וידאו",
   "video_player.toggle_sound": "הפעלת\\ביטול שמע",
   "video_player.toggle_visible": "הפעלת\\ביטול תצוגה",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index f301723cf..cb28ce9c1 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -33,6 +33,7 @@
   "column.home": "Dom",
   "column.mutes": "Utišani korisnici",
   "column.notifications": "Notifikacije",
+  "column.pins": "Pinned toot",
   "column.public": "Federalni timeline",
   "column_back_button.label": "Natrag",
   "column_header.hide_settings": "Hide settings",
@@ -61,7 +62,6 @@
   "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Utišaj",
   "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
-  "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
   "confirmations.unfollow.confirm": "Unfollow",
   "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
   "embed.instructions": "Embed this status on your website by copying the code below.",
@@ -110,6 +110,7 @@
   "navigation_bar.info": "Više informacija",
   "navigation_bar.logout": "Odjavi se",
   "navigation_bar.mutes": "Utišani korisnici",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Postavke",
   "navigation_bar.public_timeline": "Federalni timeline",
   "notification.favourite": "{name} je lajkao tvoj status",
@@ -194,6 +195,15 @@
   "upload_button.label": "Dodaj media",
   "upload_form.undo": "Poništi",
   "upload_progress.label": "Uploadam...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Proširi video",
   "video_player.toggle_sound": "Toggle zvuk",
   "video_player.toggle_visible": "Preklopi vidljivost",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index a708ec638..a13e4fee2 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -33,6 +33,7 @@
   "column.home": "Kezdőlap",
   "column.mutes": "Muted users",
   "column.notifications": "Értesítések",
+  "column.pins": "Pinned toot",
   "column.public": "Nyilvános",
   "column_back_button.label": "Vissza",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Extended information",
   "navigation_bar.logout": "Kijelentkezés",
   "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Beállítások",
   "navigation_bar.public_timeline": "Nyilvános időfolyam",
   "notification.favourite": "{name} kedvencnek jelölte az állapotod",
@@ -193,6 +195,15 @@
   "upload_button.label": "Média hozzáadása",
   "upload_form.undo": "Mégsem",
   "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Expand video",
   "video_player.toggle_sound": "Hang kapcsolása",
   "video_player.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index d71e293e8..349423cce 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -33,6 +33,7 @@
   "column.home": "Beranda",
   "column.mutes": "Pengguna dibisukan",
   "column.notifications": "Notifikasi",
+  "column.pins": "Pinned toot",
   "column.public": "Linimasa gabunggan",
   "column_back_button.label": "Kembali",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Informasi selengkapnya",
   "navigation_bar.logout": "Keluar",
   "navigation_bar.mutes": "Pengguna dibisukan",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Pengaturan",
   "navigation_bar.public_timeline": "Linimasa gabungan",
   "notification.favourite": "{name} menyukai status anda",
@@ -193,6 +195,15 @@
   "upload_button.label": "Tambahkan media",
   "upload_form.undo": "Undo",
   "upload_progress.label": "Mengunggah...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Tampilkan video",
   "video_player.toggle_sound": "Suara",
   "video_player.toggle_visible": "Tampilan",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 5df5c59a1..5f19509e2 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -33,6 +33,7 @@
   "column.home": "Hemo",
   "column.mutes": "Celita uzeri",
   "column.notifications": "Savigi",
+  "column.pins": "Pinned toot",
   "column.public": "Federata tempolineo",
   "column_back_button.label": "Retro",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Detaloza informi",
   "navigation_bar.logout": "Ekirar",
   "navigation_bar.mutes": "Celita uzeri",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Preferi",
   "navigation_bar.public_timeline": "Federata tempolineo",
   "notification.favourite": "{name} favorizis tua mesajo",
@@ -193,6 +195,15 @@
   "upload_button.label": "Adjuntar kontenajo",
   "upload_form.undo": "Desfacar",
   "upload_progress.label": "Kargante...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Extensar video",
   "video_player.toggle_sound": "Acendar sono",
   "video_player.toggle_visible": "Chanjar videbleso",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index eec35a70c..cedbb947c 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -33,6 +33,7 @@
   "column.home": "Home",
   "column.mutes": "Utenti silenziati",
   "column.notifications": "Notifiche",
+  "column.pins": "Pinned toot",
   "column.public": "Timeline federata",
   "column_back_button.label": "Indietro",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Informazioni estese",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Utenti silenziati",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Impostazioni",
   "navigation_bar.public_timeline": "Timeline federata",
   "notification.favourite": "{name} ha apprezzato il tuo post",
@@ -193,6 +195,15 @@
   "upload_button.label": "Aggiungi file multimediale",
   "upload_form.undo": "Annulla",
   "upload_progress.label": "Sto caricando...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Espandi video",
   "video_player.toggle_sound": "Attiva suono",
   "video_player.toggle_visible": "Attiva visibilità",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 65838a3f8..e78ac4c26 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -33,8 +33,8 @@
   "column.home": "ホーム",
   "column.mutes": "ミュートしたユーザー",
   "column.notifications": "通知",
-  "column.public": "連合タイムライン",
   "column.pins": "固定されたトゥート",
+  "column.public": "連合タイムライン",
   "column_back_button.label": "戻る",
   "column_header.hide_settings": "設定を隠す",
   "column_header.moveLeft_settings": "カラムを左に移動する",
@@ -97,8 +97,8 @@
   "home.column_settings.show_replies": "返信表示",
   "home.settings": "カラム設定",
   "lightbox.close": "閉じる",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "次",
+  "lightbox.previous": "前",
   "loading_indicator.label": "読み込み中...",
   "media_gallery.toggle_visible": "表示切り替え",
   "missing_indicator.label": "見つかりません",
@@ -110,9 +110,9 @@
   "navigation_bar.info": "このインスタンスについて",
   "navigation_bar.logout": "ログアウト",
   "navigation_bar.mutes": "ミュートしたユーザー",
+  "navigation_bar.pins": "固定されたトゥート",
   "navigation_bar.preferences": "ユーザー設定",
   "navigation_bar.public_timeline": "連合タイムライン",
-  "navigation_bar.pins": "固定されたトゥート",
   "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
   "notification.follow": "{name}さんにフォローされました",
   "notification.mention": "{name}さんがあなたに返信しました",
@@ -195,6 +195,15 @@
   "upload_button.label": "メディアを追加",
   "upload_form.undo": "やり直す",
   "upload_progress.label": "アップロード中...",
+  "video.close": "動画を閉じる",
+  "video.exit_fullscreen": "全画面を終了する",
+  "video.expand": "動画を拡大する",
+  "video.fullscreen": "全画面",
+  "video.hide": "動画を閉じる",
+  "video.mute": "ミュート",
+  "video.pause": "一時停止",
+  "video.play": "再生",
+  "video.unmute": "ミュートを解除する",
   "video_player.expand": "動画の詳細",
   "video_player.toggle_sound": "音の切り替え",
   "video_player.toggle_visible": "表示切り替え",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 8393e82e5..46ed772cf 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -33,8 +33,8 @@
   "column.home": "홈",
   "column.mutes": "뮤트 중인 사용자",
   "column.notifications": "알림",
-  "column.public": "연합 타임라인",
   "column.pins": "고정된 Toot",
+  "column.public": "연합 타임라인",
   "column_back_button.label": "돌아가기",
   "column_header.hide_settings": "Hide settings",
   "column_header.moveLeft_settings": "Move column to the left",
@@ -110,9 +110,9 @@
   "navigation_bar.info": "이 인스턴스에 대해서",
   "navigation_bar.logout": "로그아웃",
   "navigation_bar.mutes": "뮤트 중인 사용자",
+  "navigation_bar.pins": "고정된 Toot",
   "navigation_bar.preferences": "사용자 설정",
   "navigation_bar.public_timeline": "연합 타임라인",
-  "navigation_bar.pins": "고정된 Toot",
   "notification.favourite": "{name}님이 즐겨찾기 했습니다",
   "notification.follow": "{name}님이 나를 팔로우 했습니다",
   "notification.mention": "{name}님이 답글을 보냈습니다",
@@ -195,6 +195,15 @@
   "upload_button.label": "미디어 추가",
   "upload_form.undo": "재시도",
   "upload_progress.label": "업로드 중...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "동영상 자세히 보기",
   "video_player.toggle_sound": "소리 토글하기",
   "video_player.toggle_visible": "표시 전환",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index d6775e1e4..b696bccfd 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -12,7 +12,7 @@
   "account.mute": "Negeer @{name}",
   "account.posts": "Toots",
   "account.report": "Rapporteer @{name}",
-  "account.requested": "Wacht op goedkeuring",
+  "account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.",
   "account.share": "Profiel van @{name} delen",
   "account.unblock": "Deblokkeer @{name}",
   "account.unblock_domain": "{domain} niet meer negeren",
@@ -33,11 +33,13 @@
   "column.home": "Start",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
+  "column.pins": "Pinned toot",
   "column.public": "Globale tijdlijn",
+  "column.pins": "Vastgezette toots",
   "column_back_button.label": "terug",
   "column_header.hide_settings": "Instellingen verbergen",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.moveLeft_settings": "Kolom naar links verplaatsen",
+  "column_header.moveRight_settings": "Kolom naar rechts verplaatsen",
   "column_header.pin": "Vastmaken",
   "column_header.show_settings": "Instellingen tonen",
   "column_header.unpin": "Losmaken",
@@ -63,8 +65,8 @@
   "confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?",
   "confirmations.unfollow.confirm": "Ontvolgen",
   "confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.",
+  "embed.preview": "Zo komt het eruit te zien:",
   "emoji_button.activity": "Activiteiten",
   "emoji_button.flags": "Vlaggen",
   "emoji_button.food": "Eten en drinken",
@@ -85,6 +87,7 @@
   "follow_request.authorize": "Goedkeuren",
   "follow_request.reject": "Afkeuren",
   "getting_started.appsshort": "Apps",
+  "getting_started.donate": "Doneren",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Beginnen",
   "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
@@ -109,8 +112,10 @@
   "navigation_bar.info": "Uitgebreide informatie",
   "navigation_bar.logout": "Afmelden",
   "navigation_bar.mutes": "Genegeerde gebruikers",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Instellingen",
   "navigation_bar.public_timeline": "Globale tijdlijn",
+  "navigation_bar.pins": "Vastgezette toots",
   "notification.favourite": "{name} markeerde jouw toot als favoriet",
   "notification.follow": "{name} volgt jou nu",
   "notification.mention": "{name} vermeldde jou",
@@ -171,7 +176,7 @@
   "status.mention": "Vermeld @{name}",
   "status.mute_conversation": "Negeer conversatie",
   "status.open": "Toot volledig tonen",
-  "status.pin": "Pin on profile",
+  "status.pin": "Aan profielpagina vastmaken",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} boostte",
   "status.reply": "Reageren",
@@ -183,7 +188,7 @@
   "status.show_less": "Minder tonen",
   "status.show_more": "Meer tonen",
   "status.unmute_conversation": "Conversatie niet meer negeren",
-  "status.unpin": "Unpin from profile",
+  "status.unpin": "Van profielpagina losmaken",
   "tabs_bar.compose": "Schrijven",
   "tabs_bar.federated_timeline": "Globaal",
   "tabs_bar.home": "Start",
@@ -193,6 +198,15 @@
   "upload_button.label": "Media toevoegen",
   "upload_form.undo": "Ongedaan maken",
   "upload_progress.label": "Uploaden...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Video groter maken",
+  "video.fullscreen": "Volledig scherm",
+  "video.hide": "Video verbergen",
+  "video.mute": "Geluid uitschakelen",
+  "video.pause": "Pauze",
+  "video.play": "Afspelen",
+  "video.unmute": "Geluid inschakelen",
   "video_player.expand": "Video groter maken",
   "video_player.toggle_sound": "Geluid in-/uitschakelen",
   "video_player.toggle_visible": "Video wel/niet tonen",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index f3c24a807..742017c66 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -33,6 +33,7 @@
   "column.home": "Hjem",
   "column.mutes": "Dempede brukere",
   "column.notifications": "Varsler",
+  "column.pins": "Pinned toot",
   "column.public": "Felles tidslinje",
   "column_back_button.label": "Tilbake",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Utvidet informasjon",
   "navigation_bar.logout": "Logg ut",
   "navigation_bar.mutes": "Dempede brukere",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Preferanser",
   "navigation_bar.public_timeline": "Felles tidslinje",
   "notification.favourite": "{name} likte din status",
@@ -193,6 +195,15 @@
   "upload_button.label": "Legg til media",
   "upload_form.undo": "Angre",
   "upload_progress.label": "Laster opp...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Utvid video",
   "video_player.toggle_sound": "Veksle lyd",
   "video_player.toggle_visible": "Veksle synlighet",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index d2b2dd48f..512e4120d 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -33,8 +33,8 @@
   "column.home": "Acuèlh",
   "column.mutes": "Personas en silenci",
   "column.notifications": "Notificacions",
-  "column.public": "Flux public global",
   "column.pins": "Tuts penjats",
+  "column.public": "Flux public global",
   "column_back_button.label": "Tornar",
   "column_header.hide_settings": "Amagar los paramètres",
   "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
@@ -64,7 +64,7 @@
   "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
   "confirmations.unfollow.confirm": "Quitar de sègre",
   "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
-  "embed.instructions": "Embarcar aqueste estatut per o far veire sus un site Internet en copiar lo còdi çai-jos.",
+  "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
   "embed.preview": "Semblarà aquò : ",
   "emoji_button.activity": "Activitats",
   "emoji_button.flags": "Drapèus",
@@ -110,9 +110,9 @@
   "navigation_bar.info": "Mai informacions",
   "navigation_bar.logout": "Desconnexion",
   "navigation_bar.mutes": "Personas rescondudas",
+  "navigation_bar.pins": "Tuts penjats",
   "navigation_bar.preferences": "Preferéncias",
   "navigation_bar.public_timeline": "Flux public global",
-  "navigation_bar.pins": "Tuts penjats",
   "notification.favourite": "{name} a ajustat a sos favorits :",
   "notification.follow": "{name} vos sèc",
   "notification.mention": "{name} vos a mencionat :",
@@ -195,6 +195,15 @@
   "upload_button.label": "Ajustar un mèdia",
   "upload_form.undo": "Anullar",
   "upload_progress.label": "Mandadís…",
+  "video.close": "Tampar la vidèo",
+  "video.exit_fullscreen": "Sortir plen ecran",
+  "video.expand": "Agrandir la vidèo",
+  "video.fullscreen": "Ecran complet",
+  "video.hide": "Amagar la vidèo",
+  "video.mute": "Copar lo son",
+  "video.pause": "Pausa",
+  "video.play": "Lectura",
+  "video.unmute": "Restablir lo son",
   "video_player.expand": "Mostrar la vidèo",
   "video_player.toggle_sound": "Activar/Desactivar lo son",
   "video_player.toggle_visible": "Mostrar/Rescondre la vidèo",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index daa60128d..1d2443690 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -195,7 +195,16 @@
   "upload_button.label": "Dodaj zawartość multimedialną",
   "upload_form.undo": "Cofnij",
   "upload_progress.label": "Wysyłanie",
-  "video_player.expand": "Przełącz wideo",
+  "video.close": "Zamknij film",
+  "video.exit_fullscreen": "Opuść tryb pełnoekranowy",
+  "video.expand": "Rozszerz film",
+  "video.fullscreen": "Pełny ekran",
+  "video.hide": "Ukryj film",
+  "video.mute": "Wycisz",
+  "video.pause": "Pauzuj",
+  "video.play": "Odtwórz",
+  "video.unmute": "Cofnij wyciszenie",
+  "video_player.expand": "Rozszerz film",
   "video_player.toggle_sound": "Przełącz dźwięk",
   "video_player.toggle_visible": "Przełącz widoczność",
   "video_player.video_error": "Nie można odtworzyć pliku wideo"
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index e861bf73f..a5def0ad0 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -6,25 +6,25 @@
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
   "account.follows": "Segue",
-  "account.follows_you": "É seu seguidor",
+  "account.follows_you": "Segue você",
   "account.media": "Mídia",
   "account.mention": "Mencionar @{name}",
   "account.mute": "Silenciar @{name}",
   "account.posts": "Posts",
   "account.report": "Denunciar @{name}",
-  "account.requested": "Aguardando aprovação",
+  "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.",
   "account.share": "Compartilhar perfil de @{name}",
-  "account.unblock": "Não bloquear @{name}",
+  "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "account.view_full_profile": "Ver perfil completo",
-  "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "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",
-  "bundle_column_error.title": "Network error",
+  "bundle_column_error.title": "Erro de rede",
   "bundle_modal_error.close": "Fechar",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.",
   "bundle_modal_error.retry": "Tente novamente",
   "column.blocks": "Usuários bloqueados",
   "column.community": "Local",
@@ -33,7 +33,9 @@
   "column.home": "Página inicial",
   "column.mutes": "Usuários silenciados",
   "column.notifications": "Notificações",
+  "column.pins": "Postagens fixadas",
   "column.public": "Global",
+  "column.pins": "Postagens fixadas",
   "column_back_button.label": "Voltar",
   "column_header.hide_settings": "Esconder configurações",
   "column_header.moveLeft_settings": "Mover coluna para a esquerda",
@@ -43,156 +45,169 @@
   "column_header.unpin": "Desafixar",
   "column_subheading.navigation": "Navegação",
   "column_subheading.settings": "Configurações",
-  "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar as suas postagens só para seguidores.",
-  "compose_form.lock_disclaimer.lock": "locked",
+  "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.",
+  "compose_form.lock_disclaimer.lock": "trancado",
   "compose_form.placeholder": "No que você está pensando?",
-  "compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários do {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com outros.",
+  "compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários de {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com destinatários indesejados.",
   "compose_form.publish": "Publicar",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Marcar mídia como conteúdo sensível",
-  "compose_form.spoiler": "Esconder texto com aviso",
+  "compose_form.spoiler": "Esconder texto com aviso de conteúdo",
   "compose_form.spoiler_placeholder": "Aviso de conteúdo",
   "confirmation_modal.cancel": "Cancelar",
   "confirmations.block.confirm": "Bloquear",
   "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
   "confirmations.delete.confirm": "Excluir",
-  "confirmations.delete.message": "Você tem certeza de que quer excluir este status?",
+  "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
   "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
   "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
   "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
   "confirmations.unfollow.confirm": "Deixar de seguir",
   "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
-  "emoji_button.activity": "Activity",
-  "emoji_button.flags": "Flags",
-  "emoji_button.food": "Food & Drink",
+  "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
+  "embed.preview": "Aqui está uma previsão de como ficará:",
+  "emoji_button.activity": "Atividades",
+  "emoji_button.flags": "Bandeiras",
+  "emoji_button.food": "Comidas & Bebidas",
   "emoji_button.label": "Inserir Emoji",
-  "emoji_button.nature": "Nature",
-  "emoji_button.objects": "Objects",
-  "emoji_button.people": "People",
-  "emoji_button.search": "Search...",
-  "emoji_button.symbols": "Symbols",
-  "emoji_button.travel": "Travel & Places",
-  "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
-  "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
-  "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
+  "emoji_button.nature": "Natureza",
+  "emoji_button.objects": "Objetos",
+  "emoji_button.people": "Pessoas",
+  "emoji_button.search": "Buscar...",
+  "emoji_button.symbols": "Símbolos",
+  "emoji_button.travel": "Viagens & Lugares",
+  "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
+  "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag",
+  "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
+  "empty_column.home.inactivity": "A sua página inicial está vazia. Se você esteve inativo por um tempo, ela irá se regenerar em alguns intantes.",
   "empty_column.home.public_timeline": "global",
-  "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
-  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
+  "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!",
+  "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
   "getting_started.appsshort": "Apps",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Primeiros passos",
-  "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.",
-  "getting_started.userguide": "User Guide",
+  "getting_started.open_source_notice": "Mastodon é um software de código aberto. Você pode contribuir ou reportar problemas na página do GitHub do projeto: {github}.",
+  "getting_started.userguide": "Guia de usuário",
   "home.column_settings.advanced": "Avançado",
   "home.column_settings.basic": "Básico",
   "home.column_settings.filter_regex": "Filtrar com uma expressão regular",
-  "home.column_settings.show_reblogs": "Mostrar as partilhas",
+  "home.column_settings.show_reblogs": "Mostrar compartilhamentos",
   "home.column_settings.show_replies": "Mostrar as respostas",
-  "home.settings": "Parâmetros da listagem",
+  "home.settings": "Configurações de colunas",
   "lightbox.close": "Fechar",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "Próximo",
+  "lightbox.previous": "Anterior",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
-  "navigation_bar.blocks": "Utilizadores bloqueados",
+  "navigation_bar.blocks": "Usuários bloqueados",
   "navigation_bar.community_timeline": "Local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.logout": "Sair",
-  "navigation_bar.mutes": "Utilizadores silenciados",
+  "navigation_bar.mutes": "Usuários silenciados",
+  "navigation_bar.pins": "Postagens fixadas",
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.public_timeline": "Global",
-  "notification.favourite": "{name} adicionou o teu post aos favoritos",
-  "notification.follow": "{name} seguiu-te",
-  "notification.mention": "{name} mencionou-te",
-  "notification.reblog": "{name} partilhou o teu post",
+  "navigation_bar.preferences": "Preferências",
+  "navigation_bar.public_timeline": "Global",
+  "navigation_bar.pins": "Postagens fixadas",
+  "notification.favourite": "{name} adicionou a sua postagem aos favoritos",
+  "notification.follow": "{name} te seguiu",
+  "notification.mention": "{name} te mencionou",
+  "notification.reblog": "{name} compartilhou a sua postagem",
   "notifications.clear": "Limpar notificações",
-  "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
+  "notifications.clear_confirmation": "Você tem certeza de que quer limpar todas as suas notificações permanentemente?",
   "notifications.column_settings.alert": "Notificações no computador",
   "notifications.column_settings.favourite": "Favoritos:",
   "notifications.column_settings.follow": "Novos seguidores:",
   "notifications.column_settings.mention": "Menções:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
-  "notifications.column_settings.reblog": "Partilhas:",
+  "notifications.column_settings.push": "Enviar notificações",
+  "notifications.column_settings.push_meta": "Este aparelho",
+  "notifications.column_settings.reblog": "Compartilhamento:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
-  "onboarding.done": "Done",
-  "onboarding.next": "Next",
-  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
-  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
-  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
-  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
-  "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
-  "onboarding.page_one.welcome": "Welcome to Mastodon!",
-  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
-  "onboarding.page_six.almost_done": "Almost done...",
+  "onboarding.done": "Pronto",
+  "onboarding.next": "Próximo",
+  "onboarding.page_five.public_timelines": "A timeline local mostra postagens públicas de todos os usuários no {domain}. A timeline federada mostra todas as postagens de todas as pessoas que pessoas no {domain} seguem. Estas são as timelines públicas, uma ótima maneira de conhecer novas pessoas.",
+  "onboarding.page_four.home": "A página inicial mostra postagens de pessoas que você segue.",
+  "onboarding.page_four.notifications": "A coluna de notificações te mostra quando alguém interage com você.",
+  "onboarding.page_one.federation": "Mastodon é uma rede d servidores independentes se juntando para fazer uma grande rede social. Nós chamamos estes servidores de instâncias.",
+  "onboarding.page_one.handle": "Você está no {domain}, então o seu nome de usuário completo é {handle}",
+  "onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
+  "onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
+  "onboarding.page_six.almost_done": "Quase acabando...",
   "onboarding.page_six.appetoot": "Bon Appetoot!",
-  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
-  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
-  "onboarding.page_six.guidelines": "community guidelines",
-  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
-  "onboarding.page_six.various_app": "mobile apps",
-  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
-  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
-  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
-  "onboarding.skip": "Skip",
+  "onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.",
+  "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.",
+  "onboarding.page_six.guidelines": "diretrizes da comunidade",
+  "onboarding.page_six.read_guidelines": "Por favor, leia as {guidelines} do {domain}!",
+  "onboarding.page_six.various_app": "aplicativos móveis",
+  "onboarding.page_three.profile": "Edite o seu perfil para mudar o seu o seu avatar, bio e nome de exibição. No menu de configurações, você também encontrará outras preferências.",
+  "onboarding.page_three.search": "Use a barra de buscas para encontrar pessoas e consultar hashtahs, como #illustrations e #introductions. Para procurar por uma pessoa que não estiver nesta instância, use o nome de usuário completo dela.",
+  "onboarding.page_two.compose": "Escreva postagens na coluna de escrita. Você pode hospedar imagens, mudar as configurações de privacidade e adicionar alertas de conteúdo através dos ícones abaixo.",
+  "onboarding.skip": "Pular",
   "privacy.change": "Ajustar a privacidade da mensagem",
-  "privacy.direct.long": "Apenas para utilizadores mencionados",
-  "privacy.direct.short": "Directo",
-  "privacy.private.long": "Apenas para os seguidores",
-  "privacy.private.short": "Privado",
+  "privacy.direct.long": "Apenas para usuários mencionados",
+  "privacy.direct.short": "Direta",
+  "privacy.private.long": "Apenas para seus seguidores",
+  "privacy.private.short": "Privada",
   "privacy.public.long": "Publicar em todos os feeds",
-  "privacy.public.short": "Público",
-  "privacy.unlisted.long": "Não publicar nos feeds públicos",
-  "privacy.unlisted.short": "Não listar",
+  "privacy.public.short": "Pública",
+  "privacy.unlisted.long": "Não publicar em feeds públicos",
+  "privacy.unlisted.short": "Não listada",
   "reply_indicator.cancel": "Cancelar",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
   "report.target": "Denunciar",
   "search.placeholder": "Pesquisar",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
-  "standalone.public_title": "A look inside...",
-  "status.cannot_reblog": "This post cannot be boosted",
+  "standalone.public_title": "Dê uma espiada...",
+  "status.cannot_reblog": "Esta postagem não pode ser compartilhada",
   "status.delete": "Eliminar",
-  "status.embed": "Embed",
+  "status.embed": "Incorporar",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
-  "status.media_hidden": "Media escondida",
+  "status.media_hidden": "Mídia escondida",
   "status.mention": "Mencionar @{name}",
-  "status.mute_conversation": "Mute conversation",
+  "status.mute_conversation": "Silenciar conversa",
   "status.open": "Expandir",
-  "status.pin": "Pin on profile",
-  "status.reblog": "Partilhar",
-  "status.reblogged_by": "{name} partilhou",
+  "status.pin": "Fixar no perfil",
+  "status.reblog": "Compartilhar",
+  "status.reblogged_by": "{name} compartilhou",
   "status.reply": "Responder",
-  "status.replyAll": "Reply to thread",
-  "status.report": "Denúnciar @{name}",
+  "status.replyAll": "Responder à sequência",
+  "status.report": "Denunciar @{name}",
   "status.sensitive_toggle": "Clique para ver",
   "status.sensitive_warning": "Conteúdo sensível",
-  "status.share": "Share",
+  "status.share": "Compartilhar",
   "status.show_less": "Mostrar menos",
   "status.show_more": "Mostrar mais",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
+  "status.unmute_conversation": "Desativar silêncio desta conversa",
+  "status.unpin": "Desafixar do perfil",
   "tabs_bar.compose": "Criar",
   "tabs_bar.federated_timeline": "Global",
-  "tabs_bar.home": "Home",
+  "tabs_bar.home": "Página inicial",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
   "upload_area.title": "Arraste e solte para enviar",
-  "upload_button.label": "Adicionar media",
+  "upload_button.label": "Adicionar mídia",
   "upload_form.undo": "Anular",
-  "upload_progress.label": "A gravar...",
+  "upload_progress.label": "Salvando...",
+  "video.close": "Fechar vídeo",
+  "video.exit_fullscreen": "Sair da tela cheia",
+  "video.expand": "Expandir vídeo",
+  "video.fullscreen": "Tela cheia",
+  "video.hide": "Esconder vídeo",
+  "video.mute": "Silenciar vídeo",
+  "video.pause": "Parar",
+  "video.play": "Reproduzir",
+  "video.unmute": "Retirar silêncio",
   "video_player.expand": "Expandir vídeo",
   "video_player.toggle_sound": "Ligar/Desligar som",
   "video_player.toggle_visible": "Ligar/Desligar vídeo",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index f9e686411..cff528f83 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -33,6 +33,7 @@
   "column.home": "Home",
   "column.mutes": "Utilizadores silenciados",
   "column.notifications": "Notificações",
+  "column.pins": "Pinned toot",
   "column.public": "Global",
   "column_back_button.label": "Voltar",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Mais informações",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Utilizadores silenciados",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.public_timeline": "Global",
   "notification.favourite": "{name} adicionou o teu post aos favoritos",
@@ -193,6 +195,15 @@
   "upload_button.label": "Adicionar media",
   "upload_form.undo": "Anular",
   "upload_progress.label": "A gravar...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Expandir vídeo",
   "video_player.toggle_sound": "Ligar/Desligar som",
   "video_player.toggle_visible": "Ligar/Desligar vídeo",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 0f78f4b17..fcc147c87 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -33,6 +33,7 @@
   "column.home": "Главная",
   "column.mutes": "Список глушения",
   "column.notifications": "Уведомления",
+  "column.pins": "Pinned toot",
   "column.public": "Глобальная лента",
   "column_back_button.label": "Назад",
   "column_header.hide_settings": "Скрыть настройки",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Об узле",
   "navigation_bar.logout": "Выйти",
   "navigation_bar.mutes": "Список глушения",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Опции",
   "navigation_bar.public_timeline": "Глобальная лента",
   "notification.favourite": "{name} понравился Ваш статус",
@@ -193,6 +195,15 @@
   "upload_button.label": "Добавить медиаконтент",
   "upload_form.undo": "Отменить",
   "upload_progress.label": "Загрузка...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Развернуть видео",
   "video_player.toggle_sound": "Вкл./выкл. звук",
   "video_player.toggle_visible": "Показать/скрыть",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 069fdf7c3..f2752f5e0 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -33,6 +33,7 @@
   "column.home": "Home",
   "column.mutes": "Muted users",
   "column.notifications": "Notifications",
+  "column.pins": "Pinned toot",
   "column.public": "Federated timeline",
   "column_back_button.label": "Back",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "About this instance",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Preferences",
   "navigation_bar.public_timeline": "Federated timeline",
   "notification.favourite": "{name} favourited your status",
@@ -193,6 +195,15 @@
   "upload_button.label": "Add media",
   "upload_form.undo": "Undo",
   "upload_progress.label": "Uploading...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Expand video",
   "video_player.toggle_sound": "Toggle sound",
   "video_player.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 8a36bd207..2676b851c 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -33,6 +33,7 @@
   "column.home": "Anasayfa",
   "column.mutes": "Susturulmuş kullanıcılar",
   "column.notifications": "Bildirimler",
+  "column.pins": "Pinned toot",
   "column.public": "Federe zaman tüneli",
   "column_back_button.label": "Geri",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Genişletilmiş bilgi",
   "navigation_bar.logout": "Çıkış",
   "navigation_bar.mutes": "Sessize alınmış kullanıcılar",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Tercihler",
   "navigation_bar.public_timeline": "Federe zaman tüneli",
   "notification.favourite": "{name} senin durumunu favorilere ekledi",
@@ -193,6 +195,15 @@
   "upload_button.label": "Görsel ekle",
   "upload_form.undo": "Geri al",
   "upload_progress.label": "Yükleniyor...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Videoyu genişlet",
   "video_player.toggle_sound": "Sesi aç/kapa",
   "video_player.toggle_visible": "Göster/gizle",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 1d06218e6..6b5ab64ef 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -33,6 +33,7 @@
   "column.home": "Головна",
   "column.mutes": "Заглушені користувачі",
   "column.notifications": "Сповіщення",
+  "column.pins": "Pinned toot",
   "column.public": "Глобальна стрічка",
   "column_back_button.label": "Назад",
   "column_header.hide_settings": "Hide settings",
@@ -109,6 +110,7 @@
   "navigation_bar.info": "Про інстанцію",
   "navigation_bar.logout": "Вийти",
   "navigation_bar.mutes": "Заглушені користувачі",
+  "navigation_bar.pins": "Pinned toots",
   "navigation_bar.preferences": "Налаштування",
   "navigation_bar.public_timeline": "Глобальна стрічка",
   "notification.favourite": "{name} сподобався ваш допис",
@@ -193,6 +195,15 @@
   "upload_button.label": "Додати медіаконтент",
   "upload_form.undo": "Відмінити",
   "upload_progress.label": "Завантаження...",
+  "video.close": "Close video",
+  "video.exit_fullscreen": "Exit full screen",
+  "video.expand": "Expand video",
+  "video.fullscreen": "Full screen",
+  "video.hide": "Hide video",
+  "video.mute": "Mute sound",
+  "video.pause": "Pause",
+  "video.play": "Play",
+  "video.unmute": "Unmute sound",
   "video_player.expand": "Розгорнути ",
   "video_player.toggle_sound": "Увімкнути/вимкнути звук",
   "video_player.toggle_visible": "Показати/приховати",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 93faf8876..6037e7581 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -1,13 +1,13 @@
 {
   "account.block": "屏蔽 @{name}",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.block_domain": "隐藏一切来自 {domain} 的嘟文",
+  "account.disclaimer_full": "下列资料不一定完整。",
   "account.edit_profile": "修改个人资料",
   "account.follow": "关注",
   "account.followers": "关注者",
-  "account.follows": "正在关注",
+  "account.follows": "正关注",
   "account.follows_you": "关注你",
-  "account.media": "Media",
+  "account.media": "媒体",
   "account.mention": "提及 @{name}",
   "account.mute": "将 @{name} 静音",
   "account.posts": "嘟文",
@@ -15,40 +15,41 @@
   "account.requested": "等待审批",
   "account.share": "分享 @{name}的个人资料",
   "account.unblock": "解除对 @{name} 的屏蔽",
-  "account.unblock_domain": "解除封锁 {domain}",
+  "account.unblock_domain": "不再隐藏 {domain}",
   "account.unfollow": "取消关注",
   "account.unmute": "取消 @{name} 的静音",
   "account.view_full_profile": "查看完整资料",
   "boost_modal.combo": "如你想在下次路过时显示,请按{combo},",
   "bundle_column_error.body": "载入组件出错。",
-  "bundle_column_error.retry": "再次尝试",
+  "bundle_column_error.retry": "重试",
   "bundle_column_error.title": "网络错误",
   "bundle_modal_error.close": "关闭",
   "bundle_modal_error.message": "载入组件出错。",
-  "bundle_modal_error.retry": "再次尝试",
+  "bundle_modal_error.retry": "重试",
   "column.blocks": "屏蔽用户",
   "column.community": "本站时间轴",
-  "column.favourites": "赞过的嘟文",
+  "column.favourites": "收藏过的嘟文",
   "column.follow_requests": "关注请求",
   "column.home": "主页",
   "column.mutes": "被静音的用户",
   "column.notifications": "通知",
+  "column.pins": "Pinned toot",
   "column.public": "跨站公共时间轴",
   "column_back_button.label": "返回",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "隐藏设置",
+  "column_header.moveLeft_settings": "将栏左移",
+  "column_header.moveRight_settings": "将栏右移",
+  "column_header.pin": "置顶",
+  "column_header.show_settings": "显示设置",
+  "column_header.unpin": "撤顶",
   "column_subheading.navigation": "导航",
   "column_subheading.settings": "设置",
-  "compose_form.lock_disclaimer": "你的账户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
+  "compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
   "compose_form.lock_disclaimer.lock": "被保护",
   "compose_form.placeholder": "在想啥?",
   "compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务器实例,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务器实例} other {之中有些不是 Mastodon 服务器实例}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。",
   "compose_form.publish": "嘟嘟",
-  "compose_form.publish_loud": "{publish}!",
+  "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "将媒体文件标示为“敏感内容”",
   "compose_form.spoiler": "将部分文本藏于警告消息之后",
   "compose_form.spoiler_placeholder": "敏感内容的警告消息",
@@ -57,14 +58,14 @@
   "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
   "confirmations.delete.confirm": "删除",
   "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+  "confirmations.domain_block.confirm": "隐藏整个网站",
+  "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。",
   "confirmations.mute.confirm": "静音",
   "confirmations.mute.message": "想好了,真的要静音 {name}?",
   "confirmations.unfollow.confirm": "取消关注",
   "confirmations.unfollow.message": "确定要取消关注 {name}吗?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。",
+  "embed.preview": "到时大概长这样:",
   "emoji_button.activity": "活动",
   "emoji_button.flags": "旗帜",
   "emoji_button.food": "食物和饮料",
@@ -72,13 +73,13 @@
   "emoji_button.nature": "自然",
   "emoji_button.objects": "物体",
   "emoji_button.people": "人物",
-  "emoji_button.search": "搜索...",
+  "emoji_button.search": "搜索…",
   "emoji_button.symbols": "符号",
   "emoji_button.travel": "旅途和地点",
-  "empty_column.community": "本站时间轴暂时未有内容,快贴文来抢头香啊!",
+  "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
   "empty_column.hashtag": "这个标签暂时未有内容。",
   "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
-  "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
+  "empty_column.home.inactivity": "你的主页暂时没有内容。也许你太久没有来了?如果是这样,文章会慢慢出来,请稍后再看。",
   "empty_column.home.public_timeline": "公共时间轴",
   "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
   "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
@@ -96,33 +97,34 @@
   "home.column_settings.show_replies": "显示回应嘟文",
   "home.settings": "字段设置",
   "lightbox.close": "关闭",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "下一步",
+  "lightbox.previous": "上一步",
   "loading_indicator.label": "加载中……",
   "media_gallery.toggle_visible": "打开或关上",
   "missing_indicator.label": "找不到内容",
   "navigation_bar.blocks": "被屏蔽的用户",
   "navigation_bar.community_timeline": "本站时间轴",
   "navigation_bar.edit_profile": "修改个人资料",
-  "navigation_bar.favourites": "赞的内容",
+  "navigation_bar.favourites": "收藏的内容",
   "navigation_bar.follow_requests": "关注请求",
   "navigation_bar.info": "关于本站",
   "navigation_bar.logout": "注销",
   "navigation_bar.mutes": "被静音的用户",
+  "navigation_bar.pins": "置顶嘟文",
   "navigation_bar.preferences": "首选项",
   "navigation_bar.public_timeline": "跨站公共时间轴",
-  "notification.favourite": "{name} 赞了你的嘟文",
+  "notification.favourite": "{name} 收藏了你的嘟文",
   "notification.follow": "{name} 开始关注你",
   "notification.mention": "{name} 提及你",
   "notification.reblog": "{name} 转嘟了你的嘟文",
   "notifications.clear": "清空通知纪录",
   "notifications.clear_confirmation": "你确定要清空通知纪录吗?",
   "notifications.column_settings.alert": "显示桌面通知",
-  "notifications.column_settings.favourite": "你的嘟文被赞:",
+  "notifications.column_settings.favourite": "你的嘟文被收藏:",
   "notifications.column_settings.follow": "关注你:",
   "notifications.column_settings.mention": "提及你:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "推送通知",
+  "notifications.column_settings.push_meta": "此设备",
   "notifications.column_settings.reblog": "你的嘟文被转嘟:",
   "notifications.column_settings.show": "在通知栏显示",
   "notifications.column_settings.sound": "播放音效",
@@ -132,18 +134,18 @@
   "onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.",
   "onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~",
   "onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络,我们将这些独立但又相互连接的服务器叫做服务器实例。",
-  "onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整账户名称。",
+  "onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整帐户名称。",
   "onboarding.page_one.welcome": "欢迎来到 Mastodon!",
   "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
-  "onboarding.page_six.almost_done": "快完成了...",
+  "onboarding.page_six.almost_done": "差不多了…",
   "onboarding.page_six.appetoot": "嗷呜~",
   "onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~",
   "onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)",
   "onboarding.page_six.guidelines": "社区指南",
-  "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!",
+  "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!",
   "onboarding.page_six.various_app": "移动应用程序",
   "onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。",
-  "onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整账户名称(用户名@域名)啦。",
+  "onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整帐户名称(用户名@域名)啦。",
   "onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
   "onboarding.skip": "好啦好啦我知道啦",
   "privacy.change": "调整隐私设置",
@@ -161,29 +163,29 @@
   "report.target": "Reporting",
   "search.placeholder": "搜索",
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
-  "standalone.public_title": "A look inside...",
+  "standalone.public_title": "大家都在干啥?",
   "status.cannot_reblog": "没法转嘟这条嘟文啦……",
   "status.delete": "删除",
-  "status.embed": "Embed",
-  "status.favourite": "赞",
+  "status.embed": "嵌入",
+  "status.favourite": "收藏",
   "status.load_more": "加载更多",
   "status.media_hidden": "隐藏媒体内容",
   "status.mention": "提及 @{name}",
-  "status.mute_conversation": "Mute conversation",
+  "status.mute_conversation": "静音对话",
   "status.open": "展开嘟文",
-  "status.pin": "Pin on profile",
+  "status.pin": "置顶到资料",
   "status.reblog": "转嘟",
   "status.reblogged_by": "{name} 转嘟",
   "status.reply": "回应",
-  "status.replyAll": "Reply to thread",
+  "status.replyAll": "回应整串",
   "status.report": "举报 @{name}",
   "status.sensitive_toggle": "点击显示",
   "status.sensitive_warning": "敏感内容",
   "status.share": "Share",
   "status.show_less": "减少显示",
   "status.show_more": "显示更多",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
+  "status.unmute_conversation": "解禁对话",
+  "status.unpin": "解除置顶",
   "tabs_bar.compose": "撰写",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主页",
@@ -193,6 +195,15 @@
   "upload_button.label": "上传媒体文件",
   "upload_form.undo": "还原",
   "upload_progress.label": "上传中……",
+  "video.close": "关闭影片",
+  "video.exit_fullscreen": "退出全荧幕",
+  "video.expand": "展开影片",
+  "video.fullscreen": "全荧幕",
+  "video.hide": "隐藏影片",
+  "video.mute": "静音",
+  "video.pause": "暂停",
+  "video.play": "播放",
+  "video.unmute": "解除静音",
   "video_player.expand": "展开影片",
   "video_player.toggle_sound": "开关音效",
   "video_player.toggle_visible": "打开或关上",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index d689cd5ae..66d32fb7e 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -1,46 +1,47 @@
 {
   "account.block": "封鎖 @{name}",
-  "account.block_domain": "Hide everything from {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.block_domain": "隱藏來自 {domain} 的一切文章",
+  "account.disclaimer_full": "下列資料不一定完整。",
   "account.edit_profile": "修改個人資料",
   "account.follow": "關注",
   "account.followers": "關注的人",
-  "account.follows": "正在關注",
+  "account.follows": "正關注",
   "account.follows_you": "關注你",
-  "account.media": "Media",
+  "account.media": "媒體",
   "account.mention": "提及 @{name}",
   "account.mute": "將 @{name} 靜音",
   "account.posts": "文章",
   "account.report": "舉報 @{name}",
   "account.requested": "等候審批",
-  "account.share": "Share @{name}'s profile",
+  "account.share": "分享 @{name} 的個人資料",
   "account.unblock": "解除對 @{name} 的封鎖",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "不再隱藏 {domain}",
   "account.unfollow": "取消關注",
   "account.unmute": "取消 @{name} 的靜音",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "查看完整資料",
   "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_column_error.body": "加載本組件出錯。",
+  "bundle_column_error.retry": "重試",
+  "bundle_column_error.title": "網絡錯誤",
+  "bundle_modal_error.close": "關閉",
+  "bundle_modal_error.message": "加載本組件出錯。",
+  "bundle_modal_error.retry": "重試",
   "column.blocks": "封鎖用戶",
   "column.community": "本站時間軸",
-  "column.favourites": "喜歡的文章",
+  "column.favourites": "最愛的文章",
   "column.follow_requests": "關注請求",
   "column.home": "主頁",
   "column.mutes": "靜音名單",
   "column.notifications": "通知",
+  "column.pins": "Pinned toot",
   "column.public": "跨站時間軸",
   "column_back_button.label": "返回",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "隱藏設定",
+  "column_header.moveLeft_settings": "將欄左移",
+  "column_header.moveRight_settings": "將欄右移",
+  "column_header.pin": "置頂",
+  "column_header.show_settings": "顯示設定",
+  "column_header.unpin": "撤頂",
   "column_subheading.navigation": "瀏覽",
   "column_subheading.settings": "設定",
   "compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。",
@@ -48,7 +49,7 @@
   "compose_form.placeholder": "你在想甚麼?",
   "compose_form.privacy_disclaimer": "你的私人文章,將被遞送至 {domains}。你是否信任{domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將可無視文章的私隱設定,轉推文章給其他用戶閱讀。",
   "compose_form.publish": "發文",
-  "compose_form.publish_loud": "{publish}!",
+  "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
   "compose_form.spoiler": "將部份文字藏於警告訊息之後",
   "compose_form.spoiler_placeholder": "敏感警告訊息",
@@ -57,14 +58,14 @@
   "confirmations.block.message": "你確定要封鎖{name}嗎?",
   "confirmations.delete.confirm": "刪除",
   "confirmations.delete.message": "你確定要刪除{name}嗎?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+  "confirmations.domain_block.confirm": "隱藏整個網站",
+  "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。",
   "confirmations.mute.confirm": "靜音",
   "confirmations.mute.message": "你確定要將{name}靜音嗎?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "confirmations.unfollow.confirm": "取消關注",
+  "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?",
+  "embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。",
+  "embed.preview": "看上去會是這樣:",
   "emoji_button.activity": "活動",
   "emoji_button.flags": "旗幟",
   "emoji_button.food": "飲飲食食",
@@ -75,7 +76,7 @@
   "emoji_button.search": "搜尋…",
   "emoji_button.symbols": "符號",
   "emoji_button.travel": "旅遊景物",
-  "empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
+  "empty_column.community": "本站時間軸暫時未有內容,快文章來搶頭香啊!",
   "empty_column.hashtag": "這個標籤暫時未有內容。",
   "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
   "empty_column.home.inactivity": "你的主頁暫時沒有內容。也許你太久沒有來?如果是這樣,文章會慢慢出來,請稍後再看。",
@@ -96,34 +97,35 @@
   "home.column_settings.show_replies": "顯示回應文章",
   "home.settings": "欄位設定",
   "lightbox.close": "關閉",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "繼續",
+  "lightbox.previous": "回退",
   "loading_indicator.label": "載入中...",
   "media_gallery.toggle_visible": "打開或關上",
   "missing_indicator.label": "找不到內容",
   "navigation_bar.blocks": "被你封鎖的用戶",
   "navigation_bar.community_timeline": "本站時間軸",
   "navigation_bar.edit_profile": "修改個人資料",
-  "navigation_bar.favourites": "喜歡的內容",
+  "navigation_bar.favourites": "最愛的內容",
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本服務站",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "被你靜音的用戶",
+  "navigation_bar.pins": "置頂文章",
   "navigation_bar.preferences": "偏好設定",
   "navigation_bar.public_timeline": "跨站時間軸",
-  "notification.favourite": "{name} 喜歡你的文章",
+  "notification.favourite": "{name} 收藏了你的文章",
   "notification.follow": "{name} 開始關注你",
   "notification.mention": "{name} 提及你",
   "notification.reblog": "{name} 轉推你的文章",
   "notifications.clear": "清空通知紀錄",
   "notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
   "notifications.column_settings.alert": "顯示桌面通知",
-  "notifications.column_settings.favourite": "喜歡你的文章:",
-  "notifications.column_settings.follow": "關注你:",
-  "notifications.column_settings.mention": "提及你:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
-  "notifications.column_settings.reblog": "轉推你的文章:",
+  "notifications.column_settings.favourite": "收藏了你的文章:",
+  "notifications.column_settings.follow": "關注你:",
+  "notifications.column_settings.mention": "提及你:",
+  "notifications.column_settings.push": "推送通知",
+  "notifications.column_settings.push_meta": "這臺設備",
+  "notifications.column_settings.reblog": "轉推你的文章:",
   "notifications.column_settings.show": "在通知欄顯示",
   "notifications.column_settings.sound": "播放音效",
   "onboarding.done": "開始使用",
@@ -161,17 +163,17 @@
   "report.target": "舉報",
   "search.placeholder": "搜尋",
   "search_results.total": "{count, number} 項結果",
-  "standalone.public_title": "A look inside...",
+  "standalone.public_title": "站點一瞥…",
   "status.cannot_reblog": "這篇文章無法被轉推",
   "status.delete": "刪除",
-  "status.embed": "Embed",
-  "status.favourite": "喜歡",
+  "status.embed": "鑲嵌",
+  "status.favourite": "收藏",
   "status.load_more": "載入更多",
   "status.media_hidden": "隱藏媒體內容",
   "status.mention": "提及 @{name}",
-  "status.mute_conversation": "Mute conversation",
+  "status.mute_conversation": "靜音對話",
   "status.open": "展開文章",
-  "status.pin": "Pin on profile",
+  "status.pin": "置頂到資料頁",
   "status.reblog": "轉推",
   "status.reblogged_by": "{name} 轉推",
   "status.reply": "回應",
@@ -182,8 +184,8 @@
   "status.share": "Share",
   "status.show_less": "減少顯示",
   "status.show_more": "顯示更多",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
+  "status.unmute_conversation": "解禁對話",
+  "status.unpin": "解除置頂",
   "tabs_bar.compose": "撰寫",
   "tabs_bar.federated_timeline": "跨站",
   "tabs_bar.home": "主頁",
@@ -193,6 +195,15 @@
   "upload_button.label": "上載媒體檔案",
   "upload_form.undo": "還原",
   "upload_progress.label": "上載中……",
+  "video.close": "關閉影片",
+  "video.exit_fullscreen": "退出全熒幕",
+  "video.expand": "展開影片",
+  "video.fullscreen": "全熒幕",
+  "video.hide": "隱藏影片",
+  "video.mute": "靜音",
+  "video.pause": "暫停",
+  "video.play": "播放",
+  "video.unmute": "解除靜音",
   "video_player.expand": "展開影片",
   "video_player.toggle_sound": "開關音效",
   "video_player.toggle_visible": "打開或關上",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index dcb9d7f3c..b3cc6add7 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -1,11 +1,11 @@
 {
   "account.block": "封鎖 @{name}",
-  "account.block_domain": "隱藏來自 {domain} 的一切",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
-  "account.edit_profile": "編輯用戶資訊",
+  "account.block_domain": "隱藏來自 {domain} 的一切貼文",
+  "account.disclaimer_full": "下列資料不一定完整。",
+  "account.edit_profile": "編輯用者資訊",
   "account.follow": "關注",
   "account.followers": "專注者",
-  "account.follows": "正在關注",
+  "account.follows": "正關注",
   "account.follows_you": "關注你",
   "account.media": "媒體",
   "account.mention": "提到 @{name}",
@@ -13,19 +13,19 @@
   "account.posts": "貼文",
   "account.report": "檢舉 @{name}",
   "account.requested": "正在等待許可",
-  "account.share": "Share @{name}'s profile",
+  "account.share": "分享 @{name} 的用者資訊",
   "account.unblock": "取消封鎖 @{name}",
   "account.unblock_domain": "不再隱藏 {domain}",
   "account.unfollow": "取消關注",
   "account.unmute": "不再消音 @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "查看完整資訊",
   "boost_modal.combo": "下次你可以按 {combo} 來跳過",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_column_error.body": "加載本組件出錯。",
+  "bundle_column_error.retry": "重試",
+  "bundle_column_error.title": "網路錯誤",
+  "bundle_modal_error.close": "關閉",
+  "bundle_modal_error.message": "加載本組件出錯。",
+  "bundle_modal_error.retry": "重試",
   "column.blocks": "封鎖的使用者",
   "column.community": "本地時間軸",
   "column.favourites": "最愛",
@@ -33,21 +33,22 @@
   "column.home": "家",
   "column.mutes": "消音的使用者",
   "column.notifications": "通知",
+  "column.pins": "置頂貼文",
   "column.public": "聯盟時間軸",
   "column_back_button.label": "上一頁",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "隱藏設定",
+  "column_header.moveLeft_settings": "將欄左移",
+  "column_header.moveRight_settings": "將欄右移",
+  "column_header.pin": "置頂",
+  "column_header.show_settings": "顯示設定",
+  "column_header.unpin": "撤頂",
   "column_subheading.navigation": "瀏覽",
   "column_subheading.settings": "設定",
   "compose_form.lock_disclaimer": "你的帳號沒有{locked}。任何人都可以關注你,看到發給關注者的貼文。",
   "compose_form.lock_disclaimer.lock": "上鎖",
   "compose_form.placeholder": "在想些什麼?",
   "compose_form.privacy_disclaimer": "你的貼文會被傳到 {domains} 上被提到的使用者。你信任 {domainsCount, plural, one {這個伺服器} other {這些伺服器}}嗎?貼文的隱私設定只會在 Mastodon 副本上生效。如果 {domains} {domainsCount, plural, one {不是一個 Mastodon 副本} other {都不是 Mastodon 副本}},就不會被標記為非公開貼文,而且可能會被轉推或是讓不預期的人看見。",
-  "compose_form.publish": "推",
+  "compose_form.publish": "貼掉",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "將此媒體標為敏感",
   "compose_form.spoiler": "將訊息隱藏在警告訊息之後",
@@ -58,13 +59,13 @@
   "confirmations.delete.confirm": "刪除",
   "confirmations.delete.message": "你確定要刪除這個狀態?",
   "confirmations.domain_block.confirm": "隱藏整個網域",
-  "confirmations.domain_block.message": "你真的真的確定要封鎖整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
+  "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
   "confirmations.mute.confirm": "消音",
   "confirmations.mute.message": "你確定要消音 {name} ?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "confirmations.unfollow.confirm": "取消關注",
+  "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?",
+  "embed.instructions": "要內嵌此貼文,請將以下代碼貼進你的網站。",
+  "embed.preview": "看上去會變成這樣:",
   "emoji_button.activity": "活動",
   "emoji_button.flags": "旗幟",
   "emoji_button.food": "食物與飲料",
@@ -72,12 +73,12 @@
   "emoji_button.nature": "自然",
   "emoji_button.objects": "物件",
   "emoji_button.people": "人",
-  "emoji_button.search": "搜尋...",
+  "emoji_button.search": "搜尋…",
   "emoji_button.symbols": "符號",
   "emoji_button.travel": "旅遊與地點",
   "empty_column.community": "本地時間軸是空的。公開寫點什麼吧!",
   "empty_column.hashtag": "這個主題標籤下什麼都沒有。",
-  "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用戶。",
+  "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用者。",
   "empty_column.home.inactivity": "你家的訊息摘要是空的。如果你很久沒活動了,很快它就會重新產生。",
   "empty_column.home.public_timeline": "公開時間軸",
   "empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。",
@@ -96,22 +97,23 @@
   "home.column_settings.show_replies": "顯示回應",
   "home.settings": "欄位設定",
   "lightbox.close": "關閉",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "繼續",
+  "lightbox.previous": "回退",
   "loading_indicator.label": "讀取中...",
   "media_gallery.toggle_visible": "切換可見性",
   "missing_indicator.label": "找不到",
   "navigation_bar.blocks": "封鎖的使用者",
   "navigation_bar.community_timeline": "本地時間軸",
-  "navigation_bar.edit_profile": "編輯用戶資訊",
+  "navigation_bar.edit_profile": "編輯用者資訊",
   "navigation_bar.favourites": "最愛",
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本站",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "消音的使用者",
+  "navigation_bar.pins": "置頂貼文",
   "navigation_bar.preferences": "偏好設定",
   "navigation_bar.public_timeline": "聯盟時間軸",
-  "notification.favourite": "{name}喜歡你的狀態",
+  "notification.favourite": "{name}收藏了你的狀態",
   "notification.follow": "{name}關注了你",
   "notification.mention": "{name}提到了你",
   "notification.reblog": "{name}推了你的狀態",
@@ -121,8 +123,8 @@
   "notifications.column_settings.favourite": "最愛:",
   "notifications.column_settings.follow": "新的關注者:",
   "notifications.column_settings.mention": "提到:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "推送通知",
+  "notifications.column_settings.push_meta": "這臺設備",
   "notifications.column_settings.reblog": "轉推:",
   "notifications.column_settings.show": "顯示在欄位中",
   "notifications.column_settings.sound": "播放音效",
@@ -135,8 +137,8 @@
   "onboarding.page_one.handle": "你在 {domain} 上,所以你的帳號全名是 {handle}",
   "onboarding.page_one.welcome": "歡迎來到 Mastodon !",
   "onboarding.page_six.admin": "你的副本的管理員是 {admin} 。",
-  "onboarding.page_six.almost_done": "快好了...",
-  "onboarding.page_six.appetoot": "推口大開!",
+  "onboarding.page_six.almost_done": "快好了…",
+  "onboarding.page_six.appetoot": "貼口大開!",
   "onboarding.page_six.apps_available": "在 iOS 、 Android 和其他平台上有這些 {apps} 可以用。",
   "onboarding.page_six.github": "Mastodon 是自由的開源軟體。你可以在 {github} 上回報臭蟲、請求新功能或是做出貢獻。",
   "onboarding.page_six.guidelines": "社群指南",
@@ -161,17 +163,17 @@
   "report.target": "通報中",
   "search.placeholder": "搜尋",
   "search_results.total": "{count, number} 項結果",
-  "standalone.public_title": "A look inside...",
+  "standalone.public_title": "站點一瞥…",
   "status.cannot_reblog": "此貼文無法轉推",
   "status.delete": "刪除",
   "status.embed": "Embed",
-  "status.favourite": "喜愛",
+  "status.favourite": "收藏",
   "status.load_more": "載入更多",
   "status.media_hidden": "媒體已隱藏",
   "status.mention": "提到 @{name}",
   "status.mute_conversation": "消音對話",
   "status.open": "展開這個狀態",
-  "status.pin": "Pin on profile",
+  "status.pin": "置頂到個人資訊頁",
   "status.reblog": "轉推",
   "status.reblogged_by": "{name} 轉推了",
   "status.reply": "回應",
@@ -183,7 +185,7 @@
   "status.show_less": "看少點",
   "status.show_more": "看更多",
   "status.unmute_conversation": "不消音對話",
-  "status.unpin": "Unpin from profile",
+  "status.unpin": "解除置頂",
   "tabs_bar.compose": "編輯",
   "tabs_bar.federated_timeline": "聯盟",
   "tabs_bar.home": "家",
@@ -193,6 +195,15 @@
   "upload_button.label": "增加媒體",
   "upload_form.undo": "復原",
   "upload_progress.label": "上傳中...",
+  "video.close": "關閉影片",
+  "video.exit_fullscreen": "退出全熒幕",
+  "video.expand": "展開影片",
+  "video.fullscreen": "全熒幕",
+  "video.hide": "隱藏影片",
+  "video.mute": "消音",
+  "video.pause": "暫停",
+  "video.play": "播放",
+  "video.unmute": "解除消音",
   "video_player.expand": "展開影片",
   "video_player.toggle_sound": "切換音效",
   "video_player.toggle_visible": "切換可見性",
diff --git a/app/javascript/mastodon/reducers/height_cache.js b/app/javascript/mastodon/reducers/height_cache.js
new file mode 100644
index 000000000..2f5716fae
--- /dev/null
+++ b/app/javascript/mastodon/reducers/height_cache.js
@@ -0,0 +1,23 @@
+import { Map as ImmutableMap } from 'immutable';
+import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from '../actions/height_cache';
+
+const initialState = ImmutableMap();
+
+const setHeight = (state, key, id, height) => {
+  return state.update(key, ImmutableMap(), map => map.set(id, height));
+};
+
+const clearHeights = () => {
+  return ImmutableMap();
+};
+
+export default function statuses(state = initialState, action) {
+  switch(action.type) {
+  case HEIGHT_CACHE_SET:
+    return setHeight(state, action.key, action.id, action.height);
+  case HEIGHT_CACHE_CLEAR:
+    return clearHeights();
+  default:
+    return state;
+  }
+};
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index a54fca530..444a20845 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -21,6 +21,7 @@ import compose from './compose';
 import search from './search';
 import media_attachments from './media_attachments';
 import notifications from './notifications';
+import height_cache from './height_cache';
 
 const reducers = {
   timelines,
@@ -45,6 +46,7 @@ const reducers = {
   search,
   media_attachments,
   notifications,
+  height_cache,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index eec2a5f16..38b23504e 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -15,8 +15,6 @@ import {
   CONTEXT_FETCH_SUCCESS,
   STATUS_MUTE_SUCCESS,
   STATUS_UNMUTE_SUCCESS,
-  STATUS_SET_HEIGHT,
-  STATUSES_CLEAR_HEIGHT,
 } from '../actions/statuses';
 import {
   TIMELINE_REFRESH_SUCCESS,
@@ -60,9 +58,14 @@ const normalizeStatus = (state, status) => {
   }
 
   const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
+  const emojiMap = normalStatus.emojis.reduce((obj, emoji) => {
+    obj[`:${emoji.shortcode}:`] = emoji.url;
+    return obj;
+  }, {});
+
   normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
-  normalStatus.contentHtml = emojify(normalStatus.content);
-  normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''));
+  normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
+  normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap);
 
   return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
 };
@@ -95,18 +98,6 @@ const filterStatuses = (state, relationship) => {
   return state;
 };
 
-const setHeight = (state, id, height) => {
-  return state.update(id, ImmutableMap(), map => map.set('height', height));
-};
-
-const clearHeights = (state) => {
-  state.forEach(status => {
-    state = state.deleteIn([status.get('id'), 'height']);
-  });
-
-  return state;
-};
-
 const initialState = ImmutableMap();
 
 export default function statuses(state = initialState, action) {
@@ -148,10 +139,6 @@ export default function statuses(state = initialState, action) {
     return deleteStatus(state, action.id, action.references);
   case ACCOUNT_BLOCK_SUCCESS:
     return filterStatuses(state, action.relationship);
-  case STATUS_SET_HEIGHT:
-    return setHeight(state, action.id, action.height);
-  case STATUSES_CLEAR_HEIGHT:
-    return clearHeights(state);
   default:
     return state;
   }
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 8b201ecf8..8842d6dcb 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -25,6 +25,11 @@ function main() {
   const emojify = require('../mastodon/emoji').default;
   const { getLocale } = require('../mastodon/locales');
   const { localeData } = getLocale();
+  const VideoContainer = require('../mastodon/containers/video_container').default;
+  const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default;
+  const CardContainer = require('../mastodon/containers/card_container').default;
+  const React = require('react');
+  const ReactDOM = require('react-dom');
 
   localeData.forEach(IntlRelativeFormat.__addLocaleData);
 
@@ -66,22 +71,21 @@ function main() {
         window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
       });
     });
-  });
 
-  delegate(document, '.video-player video', 'click', ({ target }) => {
-    if (target.paused) {
-      target.play();
-    } else {
-      target.pause();
-    }
-  });
+    [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
+    });
 
-  delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() {
-    this.parentNode.classList.add('media-spoiler-wrapper__visible');
-  });
+    [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content);
+    });
 
-  delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() {
-    this.parentNode.classList.remove('media-spoiler-wrapper__visible');
+    [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
+    });
   });
 
   delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
@@ -126,7 +130,7 @@ function main() {
   delegate(document, '#account_avatar', 'change', ({ target }) => {
     const avatar = document.querySelector('.card.compact .avatar img');
     const [file] = target.files || [];
-    const url = URL.createObjectURL(file);
+    const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
 
     avatar.src = url;
   });
@@ -134,7 +138,7 @@ function main() {
   delegate(document, '#account_header', 'change', ({ target }) => {
     const header = document.querySelector('.card.compact');
     const [file] = target.files || [];
-    const url = URL.createObjectURL(file);
+    const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
 
     header.style.backgroundImage = `url(${url})`;
   });
diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss
index 28924738a..343de1590 100644
--- a/app/javascript/styles/about.scss
+++ b/app/javascript/styles/about.scss
@@ -137,7 +137,7 @@
       padding-bottom: 15px;
 
       .hero .heading {
-        padding-bottom: 30px;
+        padding-bottom: 20px;
         font-family: 'mastodon-font-sans-serif', sans-serif;
         font-size: 16px;
         font-weight: 400;
@@ -327,7 +327,7 @@
 
   .about-short {
     background: darken($ui-base-color, 4%);
-    padding: 50px 0;
+    padding: 50px 0 30px;
     font-family: 'mastodon-font-sans-serif', sans-serif;
     font-size: 16px;
     font-weight: 400;
@@ -640,8 +640,11 @@
     .header-wrapper {
       padding-top: 0;
 
+      &.compact {
+        padding-bottom: 0;
+      }
+
       &.compact .hero .heading {
-        padding-bottom: 20px;
         text-align: initial;
       }
     }
diff --git a/app/javascript/styles/admin.scss b/app/javascript/styles/admin.scss
index fa7859e38..87bc710af 100644
--- a/app/javascript/styles/admin.scss
+++ b/app/javascript/styles/admin.scss
@@ -97,6 +97,14 @@
       margin-bottom: 40px;
     }
 
+    h3 {
+      color: $ui-secondary-color;
+      font-size: 20px;
+      line-height: 28px;
+      font-weight: 400;
+      margin-bottom: 30px;
+    }
+
     h6 {
       font-size: 16px;
       color: $ui-secondary-color;
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 3cbfb7d3a..a6e5946a7 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -631,6 +631,10 @@
   opacity: 1;
   animation: fade 150ms linear;
 
+  .video-player {
+    margin-top: 8px;
+  }
+
   &.status-direct {
     background: lighten($ui-base-color, 8%);
 
@@ -867,6 +871,10 @@
       height: 22px;
     }
   }
+
+  .video-player {
+    margin-top: 8px;
+  }
 }
 
 .detailed-status__meta {
@@ -1610,9 +1618,8 @@
 
 .column,
 .drawer {
-  @supports(display: grid) { // hack to fix Chrome <57
-    contain: strict;
-  }
+  flex: 1 1 100%;
+  overflow: hidden;
 }
 
 @include limited-single-column('screen and (max-width: 360px)', $parent: null) {
@@ -1790,9 +1797,7 @@
   overflow-x: hidden;
   flex: 1 1 auto;
   -webkit-overflow-scrolling: touch;
-  @supports(display: grid) { // hack to fix Chrome <57
-    contain: strict;
-  }
+  will-change: transform; // improves perf in mobile Chrome
 
   &.optionally-scrollable {
     overflow-y: auto;
@@ -2642,7 +2647,7 @@ button.icon-button.active i.fa-retweet {
 
 .media-spoiler {
   background: $base-overlay-background;
-  color: $primary-text-color;
+  color: $ui-primary-color;
   border: 0;
   width: 100%;
   height: 100%;
@@ -4206,6 +4211,182 @@ button.icon-button.active i.fa-retweet {
   z-index: 5;
 }
 
+.video-player {
+  overflow: hidden;
+  position: relative;
+  background: $base-shadow-color;
+  max-width: 100%;
+
+  video {
+    height: 100%;
+    width: 100%;
+    z-index: 1;
+  }
+
+  &.fullscreen {
+    width: 100% !important;
+    height: 100% !important;
+    margin: 0;
+
+    video {
+      max-width: 100% !important;
+      max-height: 100% !important;
+    }
+  }
+
+  &.inline {
+    video {
+      object-fit: cover;
+      position: relative;
+      top: 50%;
+      transform: translateY(-50%);
+    }
+  }
+
+  &__controls {
+    position: absolute;
+    z-index: 2;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    box-sizing: border-box;
+    background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent);
+    padding: 0 10px;
+    opacity: 0;
+    transition: opacity .1s ease;
+
+    &.active {
+      opacity: 1;
+    }
+  }
+
+  &.inactive {
+    video,
+    .video-player__controls {
+      visibility: hidden;
+    }
+  }
+
+  &__spoiler {
+    display: none;
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 4;
+    border: 0;
+    background: $base-shadow-color;
+    color: $ui-primary-color;
+    transition: none;
+    pointer-events: none;
+
+    &.active {
+      display: block;
+      pointer-events: auto;
+
+      &:hover,
+      &:active,
+      &:focus {
+        color: lighten($ui-primary-color, 8%);
+      }
+    }
+
+    &__title {
+      display: block;
+      font-size: 14px;
+    }
+
+    &__subtitle {
+      display: block;
+      font-size: 11px;
+      font-weight: 500;
+    }
+  }
+
+  &__buttons {
+    padding-bottom: 10px;
+    font-size: 16px;
+
+    &.left {
+      float: left;
+
+      button {
+        padding-right: 10px;
+      }
+    }
+
+    &.right {
+      float: right;
+
+      button {
+        padding-left: 10px;
+      }
+    }
+
+    button {
+      background: transparent;
+      padding: 0;
+      border: 0;
+      color: $white;
+
+      &:active,
+      &:hover,
+      &:focus {
+        color: $ui-highlight-color;
+      }
+    }
+  }
+
+  &__seek {
+    cursor: pointer;
+    height: 24px;
+    position: relative;
+
+    &::before {
+      content: "";
+      width: 100%;
+      background: rgba($white, 0.35);
+      display: block;
+      position: absolute;
+      height: 4px;
+      top: 10px;
+    }
+
+    &__progress {
+      display: block;
+      position: absolute;
+      height: 4px;
+      top: 10px;
+      background: $ui-highlight-color;
+    }
+
+    &__handle {
+      position: absolute;
+      z-index: 3;
+      opacity: 0;
+      border-radius: 50%;
+      width: 12px;
+      height: 12px;
+      top: 6px;
+      margin-left: -6px;
+      transition: opacity .1s ease;
+      background: $ui-highlight-color;
+      pointer-events: none;
+
+      &.active {
+        opacity: 1;
+      }
+    }
+
+    &:hover {
+      .video-player__seek__handle {
+        opacity: 1;
+      }
+    }
+  }
+}
+
 .media-spoiler-video {
   background-size: cover;
   background-repeat: no-repeat;
diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss
index 747610237..0526f174c 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/forms.scss
@@ -349,9 +349,46 @@ code {
   box-shadow: 0 0 5px rgba($base-shadow-color, 0.2);
   text-align: center;
 
+  p {
+    margin-bottom: 15px;
+  }
+
+  .oauth-code {
+    color: $ui-secondary-color;
+    outline: 0;
+    box-sizing: border-box;
+    display: block;
+    width: 100%;
+    border: none;
+    padding: 10px;
+    font-family: 'mastodon-font-monospace', monospace;
+    background: $ui-base-color;
+    color: $ui-primary-color;
+    font-size: 14px;
+    margin: 0;
+
+    &::-moz-focus-inner {
+      border: 0;
+    }
+
+    &::-moz-focus-inner,
+    &:focus,
+    &:active {
+      outline: 0 !important;
+    }
+
+    &:focus {
+      background: lighten($ui-base-color, 4%);
+    }
+  }
+
   strong {
     font-weight: 500;
   }
+
+  @media screen and (max-width: 740px) and (min-width: 441px) {
+    margin-top: 40px;
+  }
 }
 
 .form-footer {
diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss
index 35225c045..453070b7c 100644
--- a/app/javascript/styles/stream_entries.scss
+++ b/app/javascript/styles/stream_entries.scss
@@ -140,19 +140,6 @@
         }
       }
     }
-
-    .status__attachments {
-      margin-top: 8px;
-      overflow: hidden;
-      width: 100%;
-      box-sizing: border-box;
-      position: relative;
-
-      .status__attachments__inner {
-        display: flex;
-        height: 214px;
-      }
-    }
   }
 
   .detailed-status.light {
@@ -233,139 +220,35 @@
       }
     }
 
-    .detailed-status__attachments {
-      margin-top: 8px;
-      overflow: hidden;
-      width: 100%;
-      box-sizing: border-box;
-      position: relative;
+    .status-card {
+      border-color: lighten($ui-secondary-color, 4%);
+      color: darken($ui-primary-color, 4%);
 
-      .status__attachments__inner {
-        display: flex;
-        height: 360px;
+      &:hover {
+        background: lighten($ui-secondary-color, 4%);
       }
     }
 
-    .video-player {
-      margin-top: 8px;
-      height: 300px;
-      overflow: hidden;
-      position: relative;
-
-      video {
-        position: relative;
-        z-index: 1;
-        width: 100%;
-        height: 100%;
-        object-fit: cover;
-        top: 50%;
-        transform: translateY(-50%);
-      }
-    }
-  }
-
-  .media-item,
-  .video-item {
-    box-sizing: border-box;
-    position: relative;
-    left: auto;
-    top: auto;
-    right: auto;
-    bottom: auto;
-    float: left;
-    border: medium none;
-    display: block;
-    flex: 1 1 auto;
-    width: 100%;
-    height: 100%;
-    overflow: hidden;
-    margin-right: 2px;
-
-    &:last-child {
-      margin-right: 0;
-    }
-
-    a {
-      display: block;
-      width: 100%;
-      height: 100%;
-      background: no-repeat scroll center center / cover;
-      text-decoration: none;
-      cursor: zoom-in;
-    }
-
-    video {
-      position: relative;
-      z-index: 1;
-      width: 100%;
-      height: 100%;
-      object-fit: cover;
-      top: 50%;
-      transform: translateY(-50%);
-    }
-  }
-
-  .video-item {
-    a {
-      cursor: pointer;
+    .status-card__title,
+    .status-card__description {
+      color: $ui-base-color;
     }
 
-    .video-item__play {
-      position: absolute;
-      top: 50%;
-      left: 50%;
-      font-size: 36px;
-      transform: translate(-50%, -50%);
-      padding: 5px;
-      border-radius: 100px;
-      color: rgba($primary-text-color, 0.8);
-      z-index: 1;
+    .status-card__image {
+      background: $ui-secondary-color;
     }
   }
 
   .media-spoiler {
     background: $ui-primary-color;
-    width: 100%;
-    height: 100%;
-    cursor: pointer;
-    position: absolute;
-    top: 0;
-    left: 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    flex-direction: column;
-    text-align: center;
+    color: $white;
     transition: all 100ms linear;
-    z-index: 2;
 
-    &:hover {
+    &:hover,
+    &:active,
+    &:focus {
       background: darken($ui-primary-color, 5%);
-    }
-
-    span {
-      display: block;
-
-      &:first-child {
-        font-size: 14px;
-      }
-
-      &:last-child {
-        font-size: 11px;
-        font-weight: 500;
-      }
-    }
-  }
-
-  .media-spoiler-wrapper {
-    &.media-spoiler-wrapper__visible {
-      .media-spoiler {
-        display: none;
-      }
-
-      .spoiler-button {
-        display: block;
-      }
+      color: unset;
     }
   }
 
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index c4da405c7..556f91235 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -11,7 +11,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
 
     return status unless status.nil?
 
-    status = Status.create!(account: @account, reblog: original_status, uri: @json['id'])
+    status = Status.create!(
+      account: @account,
+      reblog: original_status,
+      uri: @json['id'],
+      created_at: @json['published'] || Time.now.utc
+    )
     distribute(status)
     status
   end
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 9a34484f5..41f2b0bad 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -4,26 +4,31 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   def perform
     return if delete_arrived_first?(object_uri) || unsupported_object_type?
 
-    status = find_existing_status
+    RedisLock.acquire(lock_options) do |lock|
+      if lock.acquired?
+        @status = find_existing_status
+        process_status if @status.nil?
+      end
+    end
+
+    @status
+  end
 
-    return status unless status.nil?
+  private
 
+  def process_status
     ApplicationRecord.transaction do
-      status = Status.create!(status_params)
+      @status = Status.create!(status_params)
 
-      process_tags(status)
-      process_attachments(status)
+      process_tags(@status)
+      process_attachments(@status)
     end
 
-    resolve_thread(status)
-    distribute(status)
-    forward_for_reply if status.public_visibility? || status.unlisted_visibility?
-
-    status
+    resolve_thread(@status)
+    distribute(@status)
+    forward_for_reply if @status.public_visibility? || @status.unlisted_visibility?
   end
 
-  private
-
   def find_existing_status
     status   = status_from_uri(object_uri)
     status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
@@ -56,6 +61,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
         process_hashtag tag, status
       when 'Mention'
         process_mention tag, status
+      when 'Emoji'
+        process_emoji tag, status
       end
     end
   end
@@ -74,6 +81,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     account.mentions.create(status: status)
   end
 
+  def process_emoji(tag, _status)
+    shortcode = tag['name'].delete(':')
+    emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
+
+    return if !emoji.nil? || skip_download?
+
+    emoji = CustomEmoji.new(domain: @account.domain, shortcode: shortcode)
+    emoji.image_remote_url = tag['href']
+    emoji.save
+  end
+
   def process_attachments(status)
     return unless @object['attachment'].is_a?(Array)
 
@@ -182,4 +200,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     return unless @json['signature'].present? && reply_to_local?
     ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id)
   end
+
+  def lock_options
+    { redis: Redis.current, key: "create:#{@object['id']}" }
+  end
 end
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index 6ed66a239..790d2025c 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -14,6 +14,8 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
         'atomUri'                   => 'ostatus:atomUri',
         'inReplyToAtomUri'          => 'ostatus:inReplyToAtomUri',
         'conversation'              => 'ostatus:conversation',
+        'toot'                      => 'http://joinmastodon.org/ns#',
+        'Emoji'                     => 'toot:Emoji',
       },
     ],
   }.freeze
@@ -28,7 +30,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
 
   def serializable_hash(options = nil)
     options = serialization_options(options)
-    serialized_hash = CONTEXT.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options))
-    self.class.transform_key_casing!(serialized_hash, instance_options)
+    serialized_hash = ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)
+    CONTEXT.merge(self.class.transform_key_casing!(serialized_hash, instance_options))
   end
 end
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index 929e87852..1b4e271db 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -37,7 +37,7 @@ class ActivityPub::TagManager
   end
 
   def activity_uri_for(target)
-    return nil unless %i(note comment activity).include?(target.object_type) && target.local?
+    raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local?
 
     activity_account_status_url(target.account, target)
   end
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index cacc0364f..29fea27de 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -9,7 +9,7 @@ class Formatter
 
   include ActionView::Helpers::TextHelper
 
-  def format(status)
+  def format(status, options = {})
     if status.reblog?
       prepend_reblog = status.reblog.account.acct
       status         = status.proper
@@ -19,7 +19,11 @@ class Formatter
 
     raw_content = status.text
 
-    return reformat(raw_content) unless status.local?
+    unless status.local?
+      html = reformat(raw_content)
+      html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
+      return html
+    end
 
     linkable_accounts = status.mentions.map(&:account)
     linkable_accounts << status.account
@@ -27,6 +31,7 @@ class Formatter
     html = raw_content
     html = "RT @#{prepend_reblog} #{html}" if prepend_reblog
     html = encode_and_link_urls(html, linkable_accounts)
+    html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
     html = simple_format(html, {}, sanitize: false)
     html = html.delete("\n")
 
@@ -39,7 +44,9 @@ class Formatter
 
   def plaintext(status)
     return status.text if status.local?
-    strip_tags(status.text)
+
+    text = status.text.gsub(/(<br \/>|<br>|<\/p>)+/) { |match| "#{match}\n" }
+    strip_tags(text)
   end
 
   def simplified_format(account)
@@ -76,6 +83,47 @@ class Formatter
     end
   end
 
+  def encode_custom_emojis(html, emojis)
+    return html if emojis.empty?
+
+    emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h
+
+    i                     = -1
+    inside_tag            = false
+    inside_shortname      = false
+    shortname_start_index = -1
+
+    while i + 1 < html.size
+      i += 1
+
+      if inside_shortname && html[i] == ':'
+        shortcode = html[shortname_start_index + 1..i - 1]
+        emoji     = emoji_map[shortcode]
+
+        if emoji
+          replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{shortcode}:\" title=\":#{shortcode}:\" src=\"#{emoji}\" />"
+          before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''
+          html        = before_html + replacement + html[i + 1..-1]
+          i          += replacement.size - (shortcode.size + 2) - 1
+        else
+          i -= 1
+        end
+
+        inside_shortname = false
+      elsif inside_tag && html[i] == '>'
+        inside_tag = false
+      elsif html[i] == '<'
+        inside_tag       = true
+        inside_shortname = false
+      elsif !inside_tag && html[i] == ':'
+        inside_shortname      = true
+        shortname_start_index = i
+      end
+    end
+
+    html
+  end
+
   def rewrite(text, entities)
     chars = text.to_s.to_char_a
 
@@ -131,13 +179,13 @@ class Formatter
   end
 
   def link_html(url)
-    url    = Addressable::URI.parse(url).display_uri.to_s
+    url    = Addressable::URI.parse(url).to_s
     prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s
     text   = url[prefix.length, 30]
     suffix = url[prefix.length + 30..-1]
     cutoff = url[prefix.length..-1].length > 30
 
-    "<span class=\"invisible\">#{prefix}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{text}</span><span class=\"invisible\">#{suffix}</span>"
+    "<span class=\"invisible\">#{encode(prefix)}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{encode(text)}</span><span class=\"invisible\">#{encode(suffix)}</span>"
   end
 
   def hashtag_html(tag)
diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb
index 1d9932b52..a42460e10 100644
--- a/app/lib/language_detector.rb
+++ b/app/lib/language_detector.rb
@@ -1,26 +1,31 @@
 # frozen_string_literal: true
 
 class LanguageDetector
-  attr_reader :text, :account
+  include Singleton
 
-  def initialize(text, account = nil)
-    @text = text
-    @account = account
+  def initialize
     @identifier = CLD3::NNetLanguageIdentifier.new(1, 2048)
   end
 
-  def to_iso_s
-    detected_language_code || default_locale
+  def detect(text, account)
+    detect_language_code(text) || default_locale(account)
   end
 
-  def prepared_text
-    simplified_text.strip
+  def language_names
+    @language_names =
+      CLD3::TaskContextParams::LANGUAGE_NAMES.map { |name| iso6391(name.to_s).to_sym }
+                                             .uniq
   end
 
   private
 
-  def detected_language_code
-    iso6391(result.language).to_sym if detected_language_reliable?
+  def prepare_text(text)
+    simplify_text(text).strip
+  end
+
+  def detect_language_code(text)
+    result = @identifier.find_language(prepare_text(text))
+    iso6391(result.language.to_s).to_sym if result.reliable?
   end
 
   def iso6391(bcp47)
@@ -32,15 +37,7 @@ class LanguageDetector
     ISO_639.find(iso639).alpha2
   end
 
-  def result
-    @result ||= @identifier.find_language(prepared_text)
-  end
-
-  def detected_language_reliable?
-    result.reliable?
-  end
-
-  def simplified_text
+  def simplify_text(text)
     text.dup.tap do |new_text|
       new_text.gsub!(FetchLinkCardService::URL_PATTERN, '')
       new_text.gsub!(Account::MENTION_RE, '')
@@ -49,7 +46,7 @@ class LanguageDetector
     end
   end
 
-  def default_locale
-    account&.user_locale&.to_sym || nil
+  def default_locale(account)
+    account.user_locale&.to_sym
   end
 end
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index 1a23c9efa..d3f1629c4 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -42,6 +42,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
       save_mentions(status)
       save_hashtags(status)
       save_media(status)
+      save_emojis(status)
     end
 
     if thread? && status.thread.nil?
@@ -150,6 +151,25 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     end
   end
 
+  def save_emojis(parent)
+    do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
+
+    return if do_not_download
+
+    @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link|
+      next unless link['href'] && link['name']
+
+      shortcode = link['name'].delete(':')
+      emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain)
+
+      next unless emoji.nil?
+
+      emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain)
+      emoji.image_remote_url = link['href']
+      emoji.save
+    end
+  end
+
   def account_from_href(href)
     url = Addressable::URI.parse(href).normalize
 
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index b8e22a381..a6a5cb0c4 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -368,5 +368,9 @@ class OStatus::AtomSerializer
     end
 
     append_element(entry, 'mastodon:scope', status.visibility)
+
+    status.emojis.each do |emoji|
+      append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode)
+    end
   end
 end
diff --git a/app/lib/request.rb b/app/lib/request.rb
index c01e07925..b083edaf7 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -31,6 +31,8 @@ class Request
 
   def perform
     http_client.headers(headers).public_send(@verb, @url.to_s, @options)
+  rescue => e
+    raise e.class, "#{e.message} on #{@url}"
   end
 
   def headers
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index f33a20c6f..1d0a24e42 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -87,7 +87,7 @@ class TagManager
   def local_url?(url)
     uri    = Addressable::URI.parse(url).normalize
     domain = uri.host + (uri.port ? ":#{uri.port}" : '')
-    TagManager.instance.local_domain?(domain)
+    TagManager.instance.web_domain?(domain)
   end
 
   def uri_for(target)
diff --git a/app/models/account.rb b/app/models/account.rb
index ac27c7923..1b996e3cc 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -106,6 +106,7 @@ class Account < ApplicationRecord
   scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
   scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
   scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
+  scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
 
   delegate :email,
            :current_sign_in_ip,
@@ -174,6 +175,10 @@ class Account < ApplicationRecord
   end
 
   class << self
+    def readonly_attributes
+      super - %w(statuses_count following_count followers_count)
+    end
+
     def domains
       reorder(nil).pluck('distinct accounts.domain')
     end
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 270043a9e..990035b34 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -27,9 +27,11 @@ module Remotable
 
           matches  = response.headers['content-disposition']&.match(/filename="([^"]*)"/)
           filename = matches.nil? ? parsed_url.path.split('/').last : matches[1]
+          basename = SecureRandom.hex(8)
+          extname  = File.extname(filename)
 
           send("#{attachment_name}=", StringIO.new(response.to_s))
-          send("#{attachment_name}_file_name=", filename)
+          send("#{attachment_name}_file_name=", basename + extname)
 
           self[attribute_name] = url if has_attribute?(attribute_name)
         rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
new file mode 100644
index 000000000..f4d3b16a0
--- /dev/null
+++ b/app/models/custom_emoji.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: custom_emojis
+#
+#  id                 :integer          not null, primary key
+#  shortcode          :string           default(""), not null
+#  domain             :string
+#  image_file_name    :string
+#  image_content_type :string
+#  image_file_size    :integer
+#  image_updated_at   :datetime
+#  created_at         :datetime         not null
+#  updated_at         :datetime         not null
+#
+
+class CustomEmoji < ApplicationRecord
+  SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
+
+  SCAN_RE = /(?<=[^[:alnum:]:]|\n|^)
+    :(#{SHORTCODE_RE_FRAGMENT}):
+    (?=[^[:alnum:]:]|$)/x
+
+  has_attached_file :image
+
+  validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes }
+  validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 }
+
+  include Remotable
+
+  class << self
+    def from_text(text, domain)
+      return [] if text.blank?
+      shortcodes = text.scan(SCAN_RE).map(&:first)
+      where(shortcode: shortcodes, domain: domain)
+    end
+  end
+end
diff --git a/app/models/instance_filter.rb b/app/models/instance_filter.rb
new file mode 100644
index 000000000..5073cf1fa
--- /dev/null
+++ b/app/models/instance_filter.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class InstanceFilter
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def results
+    scope = Account.remote.by_domain_accounts
+    params.each do |key, value|
+      scope.merge!(scope_for(key, value)) if value.present?
+    end
+    scope
+  end
+
+  private
+
+  def scope_for(key, value)
+    case key.to_s
+    when 'domain_name'
+      Account.matches_domain(value)
+    else
+      raise "Unknown filter: #{key}"
+    end
+  end
+end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index d83ca44f1..d913e7372 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -56,15 +56,21 @@ class MediaAttachment < ApplicationRecord
 
   validates :account, presence: true
 
-  scope :attached, -> { where.not(status_id: nil) }
+  scope :attached,   -> { where.not(status_id: nil) }
   scope :unattached, -> { where(status_id: nil) }
-  scope :local, -> { where(remote_url: '') }
+  scope :local,      -> { where(remote_url: '') }
+  scope :remote,     -> { where.not(remote_url: '') }
+
   default_scope { order(id: :asc) }
 
   def local?
     remote_url.blank?
   end
 
+  def needs_redownload?
+    file.blank? && remote_url.present?
+  end
+
   def to_param
     shortcode
   end
diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb
new file mode 100644
index 000000000..8ffdc8313
--- /dev/null
+++ b/app/models/site_upload.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: site_uploads
+#
+#  id                :integer          not null, primary key
+#  var               :string           default(""), not null
+#  file_file_name    :string
+#  file_content_type :string
+#  file_file_size    :integer
+#  file_updated_at   :datetime
+#  meta              :json
+#  created_at        :datetime         not null
+#  updated_at        :datetime         not null
+#
+
+class SiteUpload < ApplicationRecord
+  has_attached_file :file
+
+  validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/
+  validates :var, presence: true, uniqueness: true
+
+  before_save :set_meta
+  after_commit :clear_cache
+
+  def cache_key
+    "site_uploads/#{var}"
+  end
+
+  private
+
+  def set_meta
+    tempfile = file.queued_for_write[:original]
+
+    return if tempfile.nil?
+
+    geometry  = Paperclip::Geometry.from_file(tempfile)
+    self.meta = { width: geometry.width.to_i, height: geometry.height.to_i }
+  end
+
+  def clear_cache
+    Rails.cache.delete(cache_key)
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 514cab2e4..326d128d6 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -55,7 +55,7 @@ class Status < ApplicationRecord
   has_one :notification, as: :activity, dependent: :destroy
   has_one :stream_entry, as: :activity, inverse_of: :status
 
-  validates :uri, uniqueness: true, unless: :local?
+  validates :uri, uniqueness: true, presence: true, unless: :local?
   validates :text, presence: true, unless: :reblog?
   validates_with StatusLengthValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
@@ -70,7 +70,6 @@ class Status < ApplicationRecord
   scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
   scope :with_public_visibility, -> { where(visibility: :public) }
   scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
-  scope :local_only, -> { left_outer_joins(:account).where(accounts: { domain: nil }) }
   scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: false }) }
   scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) }
   scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
@@ -132,6 +131,10 @@ class Status < ApplicationRecord
     !sensitive? && media_attachments.any?
   end
 
+  def emojis
+    CustomEmoji.from_text(text, account.domain)
+  end
+
   after_create :store_uri, if: :local?
 
   before_validation :prepare_contents, if: :local?
@@ -221,7 +224,7 @@ class Status < ApplicationRecord
     private
 
     def timeline_scope(local_only = false)
-      starting_scope = local_only ? Status.local_only : Status
+      starting_scope = local_only ? Status.local : Status
       starting_scope
         .with_public_visibility
         .without_reblogs
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index b1afb9e1f..1c08fb3bc 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -21,7 +21,7 @@ class InstancePresenter
   end
 
   def status_count
-    Rails.cache.fetch('local_status_count') { Status.local.count }
+    Rails.cache.fetch('local_status_count') { Account.local.sum(:statuses_count) }
   end
 
   def domain_count
@@ -44,4 +44,8 @@ class InstancePresenter
   def source_url
     Mastodon::Version.source_url
   end
+
+  def thumbnail
+    @thumbnail ||= Rails.cache.fetch('site_uploads/thumbnail') { SiteUpload.find_by(var: 'thumbnail') }
+  end
 end
diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb
index 349495e84..b252e008b 100644
--- a/app/serializers/activitypub/activity_serializer.rb
+++ b/app/serializers/activitypub/activity_serializer.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ActivityPub::ActivitySerializer < ActiveModel::Serializer
-  attributes :id, :type, :actor, :to, :cc
+  attributes :id, :type, :actor, :published, :to, :cc
 
   has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer
 
@@ -17,6 +17,10 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer
     ActivityPub::TagManager.instance.uri_for(object.account)
   end
 
+  def published
+    object.created_at.iso8601
+  end
+
   def to
     ActivityPub::TagManager.instance.to(object)
   end
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index d42f54263..e5d8e3f03 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -27,7 +27,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
   end
 
   def in_reply_to
-    return unless object.reply?
+    return unless object.reply? && !object.thread.nil?
 
     if object.thread.uri.nil? || object.thread.uri.start_with?('http')
       ActivityPub::TagManager.instance.uri_for(object.thread)
@@ -57,7 +57,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
   end
 
   def virtual_tags
-    object.mentions + object.tags
+    object.mentions + object.tags + object.emojis
   end
 
   def atom_uri
@@ -67,12 +67,14 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
   end
 
   def in_reply_to_atom_uri
-    return unless object.reply?
+    return unless object.reply? && !object.thread.nil?
 
     ::TagManager.instance.uri_for(object.thread)
   end
 
   def conversation
+    return if object.conversation.nil?
+
     if object.conversation.uri?
       object.conversation.uri
     else
@@ -135,4 +137,22 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
       "##{object.name}"
     end
   end
+
+  class CustomEmojiSerializer < ActiveModel::Serializer
+    include RoutingHelper
+
+    attributes :type, :href, :name
+
+    def type
+      'Emoji'
+    end
+
+    def href
+      full_asset_url(object.image.url)
+    end
+
+    def name
+      ":#{object.shortcode}:"
+    end
+  end
 end
diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb
index af03fd47a..0c8350e2d 100644
--- a/app/serializers/oembed_serializer.rb
+++ b/app/serializers/oembed_serializer.rb
@@ -45,7 +45,7 @@ class OEmbedSerializer < ActiveModel::Serializer
       height: height,
     }
 
-    content_tag(:iframe, nil, attributes) + content_tag(:script, nil, src: full_asset_url('embed.js'), async: true)
+    content_tag(:iframe, nil, attributes) + content_tag(:script, nil, src: full_asset_url('embed.js', skip_pipeline: true), async: true)
   end
 
   def width
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index a97137909..2898011fd 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -1,8 +1,10 @@
 # frozen_string_literal: true
 
 class REST::InstanceSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
   attributes :uri, :title, :description, :email,
-             :version, :urls, :stats
+             :version, :urls, :stats, :thumbnail
 
   def uri
     Rails.configuration.x.local_domain
@@ -24,6 +26,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
     Mastodon::Version.to_s
   end
 
+  def thumbnail
+    full_asset_url(instance_presenter.thumbnail.file.url) if instance_presenter.thumbnail
+  end
+
   def stats
     {
       user_count: instance_presenter.user_count,
diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb
index 9055b8db4..31189406a 100644
--- a/app/serializers/rest/media_attachment_serializer.rb
+++ b/app/serializers/rest/media_attachment_serializer.rb
@@ -7,11 +7,19 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
              :remote_url, :text_url, :meta
 
   def url
-    full_asset_url(object.file.url(:original))
+    if object.needs_redownload?
+      media_proxy_url(object.id, :original)
+    else
+      full_asset_url(object.file.url(:original))
+    end
   end
 
   def preview_url
-    full_asset_url(object.file.url(:small))
+    if object.needs_redownload?
+      media_proxy_url(object.id, :small)
+    else
+      full_asset_url(object.file.url(:small))
+    end
   end
 
   def text_url
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index 298a3bb40..d8efa8e60 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -17,6 +17,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
   has_many :media_attachments, serializer: REST::MediaAttachmentSerializer
   has_many :mentions
   has_many :tags
+  has_many :emojis
 
   def current_user?
     !current_user.nil?
@@ -106,4 +107,14 @@ class REST::StatusSerializer < ActiveModel::Serializer
       tag_url(object)
     end
   end
+
+  class CustomEmojiSerializer < ActiveModel::Serializer
+    include RoutingHelper
+
+    attributes :shortcode, :url
+
+    def url
+      full_asset_url(object.image.url)
+    end
+  end
 end
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
index 68ca58d62..a95931afe 100644
--- a/app/services/activitypub/fetch_remote_status_service.rb
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -17,6 +17,8 @@ class ActivityPub::FetchRemoteStatusService < BaseService
     actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account)
     actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id) if actor.nil?
 
+    return if actor.suspended?
+
     ActivityPub::Activity.factory(activity, actor).perform
   end
 
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index b54e447ad..811209537 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -12,12 +12,21 @@ class ActivityPub::ProcessAccountService < BaseService
     @uri         = @json['id']
     @username    = username
     @domain      = domain
-    @account     = Account.find_by(uri: @uri)
     @collections = {}
 
-    create_account  if @account.nil?
-    upgrade_account if @account.ostatus?
-    update_account
+    RedisLock.acquire(lock_options) do |lock|
+      if lock.acquired?
+        @account        = Account.find_by(uri: @uri)
+        @old_public_key = @account&.public_key
+        @old_protocol   = @account&.protocol
+
+        create_account if @account.nil?
+        update_account
+      end
+    end
+
+    after_protocol_change! if protocol_changed?
+    after_key_change! if key_changed?
 
     @account
   rescue Oj::ParseError
@@ -35,33 +44,46 @@ class ActivityPub::ProcessAccountService < BaseService
     @account.suspended   = true if auto_suspend?
     @account.silenced    = true if auto_silence?
     @account.private_key = nil
-    @account.save!
   end
 
   def update_account
     @account.last_webfingered_at = Time.now.utc
     @account.protocol            = :activitypub
-    @account.inbox_url           = @json['inbox'] || ''
-    @account.outbox_url          = @json['outbox'] || ''
-    @account.shared_inbox_url    = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
-    @account.followers_url       = @json['followers'] || ''
-    @account.url                 = url || @uri
-    @account.display_name        = @json['name'] || ''
-    @account.note                = @json['summary'] || ''
-    @account.avatar_remote_url   = image_url('icon')  unless skip_download?
-    @account.header_remote_url   = image_url('image') unless skip_download?
-    @account.public_key          = public_key || ''
-    @account.locked              = @json['manuallyApprovesFollowers'] || false
-    @account.statuses_count      = outbox_total_items    if outbox_total_items.present?
-    @account.following_count     = following_total_items if following_total_items.present?
-    @account.followers_count     = followers_total_items if followers_total_items.present?
+
+    set_immediate_attributes!
+    set_fetchable_attributes!
+
     @account.save_with_optional_media!
   end
 
-  def upgrade_account
+  def set_immediate_attributes!
+    @account.inbox_url        = @json['inbox'] || ''
+    @account.outbox_url       = @json['outbox'] || ''
+    @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
+    @account.followers_url    = @json['followers'] || ''
+    @account.url              = url || @uri
+    @account.display_name     = @json['name'] || ''
+    @account.note             = @json['summary'] || ''
+    @account.locked           = @json['manuallyApprovesFollowers'] || false
+  end
+
+  def set_fetchable_attributes!
+    @account.avatar_remote_url = image_url('icon')  unless skip_download?
+    @account.header_remote_url = image_url('image') unless skip_download?
+    @account.public_key        = public_key || ''
+    @account.statuses_count    = outbox_total_items    if outbox_total_items.present?
+    @account.following_count   = following_total_items if following_total_items.present?
+    @account.followers_count   = followers_total_items if followers_total_items.present?
+  end
+
+  def after_protocol_change!
     ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
   end
 
+  def after_key_change!
+    RefollowWorker.perform_async(@account.id)
+  end
+
   def image_url(key)
     value = first_of_value(@json[key])
 
@@ -120,15 +142,27 @@ class ActivityPub::ProcessAccountService < BaseService
   end
 
   def auto_suspend?
-    domain_block && domain_block.suspend?
+    domain_block&.suspend?
   end
 
   def auto_silence?
-    domain_block && domain_block.silence?
+    domain_block&.silence?
   end
 
   def domain_block
     return @domain_block if defined?(@domain_block)
     @domain_block = DomainBlock.find_by(domain: @domain)
   end
+
+  def key_changed?
+    !@old_public_key.nil? && @old_public_key != @account.public_key
+  end
+
+  def protocol_changed?
+    !@old_protocol.nil? && @old_protocol != @account.protocol
+  end
+
+  def lock_options
+    { redis: Redis.current, key: "process_account:#{@uri}" }
+  end
 end
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index bc04c50ba..0c6736a3f 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -7,9 +7,9 @@ class ActivityPub::ProcessCollectionService < BaseService
     @account = account
     @json    = Oj.load(body, mode: :strict)
 
-    return if @account.suspended? || !supported_context?
-
+    return unless supported_context?
     return if different_actor? && verify_account!.nil?
+    return if @account.suspended?
 
     case @json['type']
     when 'Collection', 'CollectionPage'
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index 1473bc841..eefdc0dbf 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -26,6 +26,7 @@ class BlockDomainService < BaseService
   def clear_media!
     clear_account_images
     clear_account_attachments
+    clear_emojos
   end
 
   def suspend_accounts!
@@ -51,6 +52,10 @@ class BlockDomainService < BaseService
     end
   end
 
+  def clear_emojos
+    emojis_from_blocked_domains.destroy_all
+  end
+
   def blocked_domain
     domain_block.domain
   end
@@ -62,4 +67,8 @@ class BlockDomainService < BaseService
   def media_from_blocked_domain
     MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil)
   end
+
+  def emojis_from_blocked_domains
+    CustomEmoji.where(domain: blocked_domain)
+  end
 end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index c38e9e7df..4acbfae7a 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -1,7 +1,15 @@
 # frozen_string_literal: true
 
 class FetchLinkCardService < BaseService
-  URL_PATTERN = %r{https?://\S+}
+  URL_PATTERN = %r{
+    (                                                                                                 #   $1 URL
+      (https?:\/\/)?                                                                                  #   $2 Protocol (optional)
+      (#{Twitter::Regex[:valid_domain]})                                                              #   $3 Domain(s)
+      (?::(#{Twitter::Regex[:valid_port_number]}))?                                                   #   $4 Port number (optional)
+      (/#{Twitter::Regex[:valid_url_path]}*)?                                                         #   $5 URL Path and anchor
+      (\?#{Twitter::Regex[:valid_url_query_chars]}*#{Twitter::Regex[:valid_url_query_ending_chars]})? #   $6 Query String
+    )
+  }iox
 
   def call(status)
     @status = status
@@ -14,11 +22,11 @@ class FetchLinkCardService < BaseService
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
         @card = PreviewCard.find_by(url: @url)
-        process_url if @card.nil?
+        process_url if @card.nil? || @card.updated_at <= 2.weeks.ago
       end
     end
 
-    attach_card unless @card.nil?
+    attach_card if @card&.persisted?
   rescue HTTP::ConnectionError, OpenSSL::SSL::SSLError
     nil
   end
@@ -26,8 +34,8 @@ class FetchLinkCardService < BaseService
   private
 
   def process_url
-    @card = PreviewCard.new(url: @url)
-    res   = Request.new(:head, @url).perform
+    @card ||= PreviewCard.new(url: @url)
+    res     = Request.new(:head, @url).perform
 
     return if res.code != 200 || res.mime_type != 'text/html'
 
@@ -40,7 +48,7 @@ class FetchLinkCardService < BaseService
 
   def parse_urls
     if @status.local?
-      urls = @status.text.match(URL_PATTERN).to_a.map { |uri| Addressable::URI.parse(uri).normalize }
+      urls = @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize }
     else
       html  = Nokogiri::HTML(@status.text)
       links = html.css('a')
@@ -106,12 +114,25 @@ class FetchLinkCardService < BaseService
     guess = detector.detect(html, response.charset)
     page  = Nokogiri::HTML(html, nil, guess&.fetch(:encoding))
 
-    @card.type             = :link
-    @card.title            = meta_property(page, 'og:title') || page.at_xpath('//title')&.content || ''
-    @card.description      = meta_property(page, 'og:description') || meta_property(page, 'description') || ''
-    @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image')
+    if meta_property(page, 'twitter:player')
+      @card.type   = :video
+      @card.width  = meta_property(page, 'twitter:player:width') || 0
+      @card.height = meta_property(page, 'twitter:player:height') || 0
+      @card.html   = content_tag(:iframe, nil, src: meta_property(page, 'twitter:player'),
+                                               width: @card.width,
+                                               height: @card.height,
+                                               allowtransparency: 'true',
+                                               scrolling: 'no',
+                                               frameborder: '0')
+    else
+      @card.type             = :link
+      @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image')
+    end
+
+    @card.title            = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
+    @card.description      = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
 
-    return if @card.title.blank?
+    return if @card.title.blank? && @card.html.blank?
 
     @card.save_with_optional_image!
   end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index e5b0fe438..d1b8f42c7 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -28,7 +28,7 @@ class PostStatusService < BaseService
                                         sensitive: options[:sensitive],
                                         spoiler_text: options[:spoiler_text] || '',
                                         visibility: options[:visibility] || account.user&.setting_default_privacy,
-                                        language: detect_language_for(text, account),
+                                        language: LanguageDetector.instance.detect(text, account),
                                         application: options[:application])
 
       attach_media(status, media)
@@ -73,10 +73,6 @@ class PostStatusService < BaseService
     media.update(status_id: status.id)
   end
 
-  def detect_language_for(text, account)
-    LanguageDetector.new(text, account).to_iso_s
-  end
-
   def process_mentions_service
     @process_mentions_service ||= ProcessMentionsService.new
   end
diff --git a/app/services/resolve_remote_account_service.rb b/app/services/resolve_remote_account_service.rb
index 7031c98f5..57c80fc82 100644
--- a/app/services/resolve_remote_account_service.rb
+++ b/app/services/resolve_remote_account_service.rb
@@ -80,6 +80,7 @@ class ResolveRemoteAccountService < BaseService
   def activitypub_ready?
     !@webfinger.link('self').nil? &&
       ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) &&
+      !actor_json.nil? &&
       actor_json['inbox'].present?
   end
 
diff --git a/app/validators/status_pin_validator.rb b/app/validators/status_pin_validator.rb
index f557df6af..9760e1138 100644
--- a/app/validators/status_pin_validator.rb
+++ b/app/validators/status_pin_validator.rb
@@ -5,5 +5,6 @@ class StatusPinValidator < ActiveModel::Validator
     pin.errors.add(:status, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog?
     pin.errors.add(:status, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id
     pin.errors.add(:status, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility)
+    pin.errors.add(:status, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count > 4
   end
 end
diff --git a/app/views/about/_og.html.haml b/app/views/about/_og.html.haml
new file mode 100644
index 000000000..dbd476915
--- /dev/null
+++ b/app/views/about/_og.html.haml
@@ -0,0 +1,10 @@
+- thumbnail = @instance_presenter.thumbnail
+= opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname)
+= opengraph 'og:url', about_url
+= opengraph 'og:type', 'website'
+= opengraph 'og:title', @instance_presenter.site_title
+= opengraph 'og:description', strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html'))
+= opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('preview.jpg', protocol: :request))
+= opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200'
+= opengraph 'og:image:height', thumbnail ? thumbnail.meta['height'] : '630'
+= opengraph 'twitter:card', 'summary_large_image'
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index f1c6e6b9d..7a28f9738 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -1,26 +1,13 @@
 = simple_form_for(new_user, url: user_registration_path) do |f|
   = f.simple_fields_for :account do |account_fields|
     .input-with-append
-      = account_fields.input :username,
-        autofocus: true,
-        placeholder: t('simple_form.labels.defaults.username'),
-        required: true,
-        input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+      = account_fields.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off' }
       .append
         = "@#{site_hostname}"
 
-  = f.input :email,
-    placeholder: t('simple_form.labels.defaults.email'),
-    required: true,
-    input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
-  = f.input :password,
-    placeholder: t('simple_form.labels.defaults.password'),
-    required: true,
-    input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
-  = f.input :password_confirmation,
-    placeholder: t('simple_form.labels.defaults.confirm_password'),
-    required: true,
-    input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
+  = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
+  = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
+  = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
 
   .actions
     = f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative'
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 99d7d2972..6e4d0cdd1 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -3,16 +3,7 @@
 
 - content_for :header_tags do
   = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
-  %meta{ property: 'og:site_name', content: site_title }/
-  %meta{ property: 'og:url', content: about_url }/
-  %meta{ property: 'og:type', content: 'website' }/
-  %meta{ property: 'og:title', content: site_hostname }/
-  %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html')) }/
-  %meta{ property: 'og:image', content: asset_pack_path('mastodon_small.jpg', protocol: :request) }/
-  %meta{ property: 'og:image:width', content: '400' }/
-  %meta{ property: 'og:image:height', content: '400' }/
-  %meta{ property: 'twitter:card', content: 'summary' }/
+  = render partial: 'og'
 
 .landing-page
   .header-wrapper.compact
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 5962436fc..737dbbcef 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -4,16 +4,7 @@
 - content_for :header_tags do
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
   = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
-
-  %meta{ property: 'og:site_name', content: site_title }/
-  %meta{ property: 'og:url', content: about_url }/
-  %meta{ property: 'og:type', content: 'website' }/
-  %meta{ property: 'og:title', content: site_hostname }/
-  %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html')) }/
-  %meta{ property: 'og:image', content: asset_pack_path('mastodon_small.jpg', protocol: :request) }/
-  %meta{ property: 'og:image:width', content: '400' }/
-  %meta{ property: 'og:image:height', content: '400' }/
-  %meta{ property: 'twitter:card', content: 'summary' }/
+  = render partial: 'og'
 
 .landing-page
   .header-wrapper
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index c16b7bf1f..dcc6661ba 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -43,15 +43,15 @@
     .details-counters
       .counter{ class: active_nav_class(short_account_url(account)) }
         = link_to short_account_url(account), class: 'u-url u-uid' do
-          %span.counter-number= number_to_human account.statuses_count
+          %span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
           %span.counter-label= t('accounts.posts')
 
       .counter{ class: active_nav_class(account_following_index_url(account)) }
         = link_to account_following_index_url(account) do
-          %span.counter-number= number_to_human account.following_count
+          %span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true
           %span.counter-label= t('accounts.following')
 
       .counter{ class: active_nav_class(account_followers_url(account)) }
         = link_to account_followers_url(account) do
-          %span.counter-number= number_to_human account.followers_count
+          %span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true
           %span.counter-label= t('accounts.followers')
diff --git a/app/views/accounts/_og.html.haml b/app/views/accounts/_og.html.haml
index 3ad39f391..1d16be590 100644
--- a/app/views/accounts/_og.html.haml
+++ b/app/views/accounts/_og.html.haml
@@ -1,8 +1,9 @@
-%meta{ property: 'og:url', content: url }/
-%meta{ property: 'og:site_name', content: site_title }/
-%meta{ property: 'og:title', content: [yield(:page_title).strip.presence, site_title].compact.join(' - ') }/
-%meta{ property: 'og:description', content: account.note }/
-%meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/
-%meta{ property: 'og:image:width', content: '120' }/
-%meta{ property: 'og:image:height', content: '120' }/
-%meta{ property: 'twitter:card', content: 'summary' }/
+= opengraph 'og:url', url
+= opengraph 'og:site_name', site_title
+= opengraph 'og:title', [yield(:page_title).strip.presence, site_title].compact.join(' - ')
+= opengraph 'og:description', account.note
+= opengraph 'og:image', full_asset_url(account.avatar.url(:original))
+= opengraph 'og:image:width', '120'
+= opengraph 'og:image:height', '120'
+= opengraph 'twitter:card', 'summary'
+= opengraph 'profile:username', account.local_username_and_domain
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index e0f9f869a..6c90b2c04 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -9,7 +9,7 @@
   %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
   %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
 
-  %meta{ property: 'og:type', content: 'profile' }/
+  = opengraph 'og:type', 'profile'
   = render 'og', account: @account, url: short_account_url(@account, only_path: false)
 
 - if show_landing_strip?
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 89355281a..3775b6721 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -37,29 +37,6 @@
           %th= t('admin.accounts.protocol')
           %td= @account.protocol.humanize
 
-        - if @account.ostatus?
-          %tr
-            %th= t('admin.accounts.feed_url')
-            %td= link_to @account.remote_url, @account.remote_url
-          %tr
-            %th= t('admin.accounts.push_subscription_expires')
-            %td
-              - if @account.subscribed?
-                %time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) }
-                  = l @account.subscription_expires_at
-              - else
-                = t('admin.accounts.not_subscribed')
-          %tr
-            %th= t('admin.accounts.salmon_url')
-            %td= link_to @account.salmon_url, @account.salmon_url
-        - elsif @account.activitypub?
-          %tr
-            %th= t('admin.accounts.inbox_url')
-            %td= link_to @account.inbox_url, @account.inbox_url
-          %tr
-            %th= t('admin.accounts.outbox_url')
-            %td= link_to @account.outbox_url, @account.outbox_url
-
       %tr
         %th= t('admin.accounts.follows')
         %td= @account.following_count
@@ -82,29 +59,73 @@
         %th= t('.targeted_reports')
         %td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id)
 
-%div{ style: 'float: right' }
-  - if @account.local?
-    = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
-    - if @account.user&.otp_required_for_login?
-      = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button'
-  - else
-    - if @account.ostatus?
+%div{ style: 'overflow: hidden' }
+  %div{ style: 'float: right' }
+    - if @account.local?
+      = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
+      - if @account.user&.otp_required_for_login?
+        = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button'
+    - else
+      = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button'
+
+  %div{ style: 'float: left' }
+    - if @account.silenced?
+      = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
+    - else
+      = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button'
+
+    - if @account.local?
+      - unless @account.user_confirmed?
+        = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button'
+
+    - if @account.suspended?
+      = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button'
+    - else
+      = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
+
+- unless @account.local?
+  %hr
+  %h3 OStatus
+
+  .table-wrapper
+    %table.table
+      %tbody
+        %tr
+          %th= t('admin.accounts.feed_url')
+          %td= link_to @account.remote_url, @account.remote_url
+        %tr
+          %th= t('admin.accounts.push_subscription_expires')
+          %td
+            - if @account.subscribed?
+              %time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) }
+                = l @account.subscription_expires_at
+            - else
+              = t('admin.accounts.not_subscribed')
+        %tr
+          %th= t('admin.accounts.salmon_url')
+          %td= link_to @account.salmon_url, @account.salmon_url
+
+  %div{ style: 'overflow: hidden' }
+    %div{ style: 'float: right' }
       = link_to @account.subscribed? ? t('admin.accounts.resubscribe') : t('admin.accounts.subscribe'), subscribe_admin_account_path(@account.id), method: :post, class: 'button'
       - if @account.subscribed?
         = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative'
-    = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button'
-
-%div{ style: 'float: left' }
-  - if @account.silenced?
-    = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
-  - else
-    = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button'
 
-  - if @account.local?
-    - unless @account.user_confirmed?
-      = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button'
+  %hr
+  %h3 ActivityPub
 
-  - if @account.suspended?
-    = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button'
-  - else
-    = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
+  .table-wrapper
+    %table.table
+      %tbody
+        %tr
+          %th= t('admin.accounts.inbox_url')
+          %td= link_to @account.inbox_url, @account.inbox_url
+        %tr
+          %th= t('admin.accounts.outbox_url')
+          %td= link_to @account.outbox_url, @account.outbox_url
+        %tr
+          %th= t('admin.accounts.shared_inbox_url')
+          %td= link_to @account.shared_inbox_url, @account.shared_inbox_url
+        %tr
+          %th= t('admin.accounts.followers_url')
+          %td= link_to @account.followers_url, @account.followers_url
diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml
new file mode 100644
index 000000000..ff1aa9925
--- /dev/null
+++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml
@@ -0,0 +1,7 @@
+%tr
+  %td
+    = image_tag custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:"
+  %td
+    %samp= ":#{custom_emoji.shortcode}:"
+  %td
+    = table_link_to 'times', t('admin.custom_emojis.delete'), admin_custom_emoji_path(custom_emoji), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml
new file mode 100644
index 000000000..d5f32e84b
--- /dev/null
+++ b/app/views/admin/custom_emojis/index.html.haml
@@ -0,0 +1,14 @@
+- content_for :page_title do
+  = t('admin.custom_emojis.title')
+
+.table-wrapper
+  %table.table
+    %thead
+      %tr
+        %th= t('admin.custom_emojis.emoji')
+        %th= t('admin.custom_emojis.shortcode')
+        %th
+    %tbody
+      = render @custom_emojis
+
+= link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button'
diff --git a/app/views/admin/custom_emojis/new.html.haml b/app/views/admin/custom_emojis/new.html.haml
new file mode 100644
index 000000000..672afe435
--- /dev/null
+++ b/app/views/admin/custom_emojis/new.html.haml
@@ -0,0 +1,12 @@
+- content_for :page_title do
+  = t('.title')
+
+= simple_form_for @custom_emoji, url: admin_custom_emojis_path do |f|
+  = render 'shared/error_messages', object: @custom_emoji
+
+  .fields-group
+    = f.input :shortcode, placeholder: t('admin.custom_emojis.shortcode'), hint: t('admin.custom_emojis.shortcode_hint')
+    = f.input :image, input_html: { accept: 'image/png' }, hint: t('admin.custom_emojis.image_hint')
+
+  .actions
+    = f.button :button, t('admin.custom_emojis.upload'), type: :submit
diff --git a/app/views/admin/instances/_instance.html.haml b/app/views/admin/instances/_instance.html.haml
index 435cd8f64..6efbbbe60 100644
--- a/app/views/admin/instances/_instance.html.haml
+++ b/app/views/admin/instances/_instance.html.haml
@@ -1,6 +1,6 @@
 %tr
   %td.domain
-    = instance.domain
+    = link_to instance.domain, admin_accounts_path(by_domain: instance.domain)
   %td.count
     = instance.accounts_count
   %td
diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml
index edbd3b217..3314ce077 100644
--- a/app/views/admin/instances/index.html.haml
+++ b/app/views/admin/instances/index.html.haml
@@ -1,6 +1,16 @@
 - content_for :page_title do
   = t('admin.instances.title')
 
+= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
+  .fields-group
+    - %i(domain_name).each do |key|
+      .input.string.optional
+        = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}")
+
+    .actions
+      %button= t('admin.instances.search')
+      = link_to t('admin.instances.reset'), admin_instances_path, class: 'button negative'
+
 .table-wrapper
   %table.table
     %thead
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 50d019ec4..468166035 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -11,6 +11,11 @@
   %hr/
 
   .fields-group
+    = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html')
+
+  %hr/
+
+  .fields-group
     = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
 
   .fields-group
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index d0529a20c..807020310 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -6,11 +6,11 @@
 
   = f.simple_fields_for :account do |ff|
     .input-with-append
-      = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+      = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off' }
       .append
         = "@#{site_hostname}"
 
-  = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
+  = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
   = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
   = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
 
diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml
index cb5e32f3e..2b07c923b 100644
--- a/app/views/auth/sessions/two_factor.html.haml
+++ b/app/views/auth/sessions/two_factor.html.haml
@@ -2,9 +2,7 @@
   = t('auth.login')
 
 = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
-  = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'),
-      input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true,
-      hint: t('simple_form.hints.sessions.otp')
+  = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true, hint: t('simple_form.hints.sessions.otp')
 
   .actions
     = f.button :button, t('auth.login'), type: :submit
diff --git a/app/views/oauth/authorizations/show.html.haml b/app/views/oauth/authorizations/show.html.haml
index b56667f35..ad5236007 100644
--- a/app/views/oauth/authorizations/show.html.haml
+++ b/app/views/oauth/authorizations/show.html.haml
@@ -1,3 +1,4 @@
 .form-container
   .flash-message
-    %code= params[:code]
+    %p= t('doorkeeper.authorizations.show.title')
+    %input{ type: 'text', class: 'oauth-code', readonly: true, value: params[:code], onClick: 'select()' }
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index 3fa540bba..551a7ca49 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -8,8 +8,8 @@
     = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name'), hint: t('simple_form.hints.defaults.display_name', count: 30 - @account.display_name.size).html_safe
     = f.input :note, placeholder: t('simple_form.labels.defaults.note'), hint: t('simple_form.hints.defaults.note', count: 500 - @account.note.size).html_safe
 
-  .card.compact{ style: "background-image: url(#{@account.header.url(:original)})" }
-    .avatar= image_tag @account.avatar.url(:original)
+  .card.compact{ style: "background-image: url(#{@account.header.url(:original)})", data: { original_src: @account.header.url(:original) } }
+    .avatar= image_tag @account.avatar.url(:original), data: { original_src: @account.avatar.url(:original) }
 
   .fields-group
     = f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar')
diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/stream_entries/_og_description.html.haml
index 5762aca04..d2fa99e63 100644
--- a/app/views/stream_entries/_og_description.html.haml
+++ b/app/views/stream_entries/_og_description.html.haml
@@ -1,4 +1,4 @@
 - if activity.is_a?(Status) && activity.spoiler_text?
-  %meta{ property: 'og:description', content: activity.spoiler_text }/
+  = opengraph 'og:description', activity.spoiler_text
 - else
-  %meta{ property: 'og:description', content: activity.content }/
+  = opengraph 'og:description', activity.content
diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml
index f725209d8..b5058583b 100644
--- a/app/views/stream_entries/_og_image.html.haml
+++ b/app/views/stream_entries/_og_image.html.haml
@@ -1,6 +1,23 @@
 - if activity.is_a?(Status) && activity.non_sensitive_with_media?
-  %meta{ property: 'og:image', content: full_asset_url(activity.media_attachments.first.file.url(:small)) }/
+  - activity.media_attachments.each do |media|
+    - if media.image?
+      = opengraph 'og:image', full_asset_url(media.file.url(:original))
+      = opengraph 'og:image:type', media.file_content_type
+      - unless media.file.meta.nil?
+        = opengraph 'og:image:width', media.file.meta['original']['width']
+        = opengraph 'og:image:height', media.file.meta['original']['height']
+    - elsif media.video?
+      = opengraph 'og:image', full_asset_url(media.file.url(:small))
+      = opengraph 'og:image:type', 'image/png'
+      - unless media.file.meta.nil?
+        = opengraph 'og:image:width', media.file.meta['small']['width']
+        = opengraph 'og:image:height', media.file.meta['small']['height']
+      = opengraph 'og:video', full_asset_url(media.file.url(:original))
+      = opengraph 'og:video:type', media.file_content_type
+      - unless media.file.meta.nil?
+        = opengraph 'og:video:width', media.file.meta['small']['width']
+        = opengraph 'og:video:height', media.file.meta['small']['height']
 - else
-  %meta{ property: 'og:image', content: full_asset_url(account.avatar.url(:original)) }/
-  %meta{ property: 'og:image:width', content: '120' }/
-  %meta{ property: 'og:image:height', content: '120' }/
+  = opengraph 'og:image', full_asset_url(account.avatar.url(:original))
+  = opengraph 'og:image:width', '120'
+  = opengraph 'og:image:height','120'
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index 5ef72f804..1bb8a32b2 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -6,15 +6,15 @@
   %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/
   %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@stream_entry.activity) }/
 
-  %meta{ property: 'og:site_name', content: site_title }/
-  %meta{ property: 'og:type', content: 'article' }/
-  %meta{ property: 'og:title', content: "#{@account.username} on #{site_hostname}" }/
-  %meta{ property: 'og:url', content: account_stream_entry_url(@account, @stream_entry) }/
+  = opengraph 'og:site_name', site_title
+  = opengraph 'og:type', 'article'
+  = opengraph 'og:title', "#{@account.username} on #{site_hostname}"
+  = opengraph 'og:url', account_stream_entry_url(@account, @stream_entry)
 
   = render 'stream_entries/og_description', activity: @stream_entry.activity
   = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account
 
-  %meta{ property: 'twitter:card', content: 'summary' }/
+  = opengraph 'twitter:card', 'summary_large_image'
 
 - if show_landing_strip?
   = render partial: 'shared/landing_strip', locals: { account: @stream_entry.account }
diff --git a/app/views/user_mailer/confirmation_instructions.es.html.erb b/app/views/user_mailer/confirmation_instructions.es.html.erb
new file mode 100644
index 000000000..1d46a12c0
--- /dev/null
+++ b/app/views/user_mailer/confirmation_instructions.es.html.erb
@@ -0,0 +1,12 @@
+<p>¡Bienvenido, <%= @resource.email %>!</p>
+
+<p>Acabas de crear una cuenta en <%= @instance %>.</p>
+
+<p>Para confirmar tu registro, por favor ingresa al siguiente enlace:<br>
+<%= link_to 'Confirmar mi cuenta', confirmation_url(@resource, confirmation_token: @token) %>
+
+<p>También revisa nuestros <%= link_to 'términos y condiciones', terms_url %>.</p>
+
+<p>Sinceramente,<p>
+
+<p>El equipo de <%= @instance %></p>
\ No newline at end of file
diff --git a/app/views/user_mailer/confirmation_instructions.es.text.erb b/app/views/user_mailer/confirmation_instructions.es.text.erb
new file mode 100644
index 000000000..e9d83b3f8
--- /dev/null
+++ b/app/views/user_mailer/confirmation_instructions.es.text.erb
@@ -0,0 +1,12 @@
+¡Bienvenido, <%= @resource.email %>!
+
+Acabas de crear una cuenta en <%= @instance %>.
+
+Para confirmar tu registro, por favor ingresa al siguiente enlace:
+<%= confirmation_url(@resource, confirmation_token: @token) %>
+
+Por favor, también revisa nuestros términos y condiciones <%= terms_url %>
+
+Sinceramente,
+
+El equipo de <%= @instance %>
\ No newline at end of file
diff --git a/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb b/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb
new file mode 100644
index 000000000..80edcfda7
--- /dev/null
+++ b/app/views/user_mailer/confirmation_instructions.pt-BR.html.erb
@@ -0,0 +1,12 @@
+<p>Boas vindas, <%= @resource.email %>!</p>
+
+<p>Você acabou de criar uma conta no <%= @instance %>.</p>
+
+<p>Para confirmar o seu cadastro, por favor clique no link a seguir: <br>
+<%= link_to 'Confirmar cadastro', confirmation_url(@resource, confirmation_token: @token) %>
+
+<p>Por favor, leia também os nossos <%= link_to 'termos de serviços', terms_url %>.</p>
+
+<p>Atenciosamente,<p>
+
+<p>A equipe do <%= @instance %></p>
diff --git a/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb b/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb
new file mode 100644
index 000000000..95efb3436
--- /dev/null
+++ b/app/views/user_mailer/confirmation_instructions.pt-BR.text.erb
@@ -0,0 +1,12 @@
+Boas vindas, <%= @resource.email %>!
+
+Você acabou de criar uma conta no <%= @instance %>.
+
+Para confirmar o seu cadastro, por favor clique no link a seguir:
+<%= confirmation_url(@resource, confirmation_token: @token) %>
+
+Por favor, leia também os nossos termos e condições de uso <%= terms_url %>
+
+Atenciosamente,
+
+A equipe do <%= @instance %>
diff --git a/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb b/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb
index 575b2ff9e..de2f8b6e0 100644
--- a/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb
+++ b/app/views/user_mailer/confirmation_instructions.zh-cn.html.erb
@@ -3,7 +3,7 @@
 <p>你刚刚在 <%= @instance %> 创建了帐号。</p>
 
 <p>点击下面的链接来完成注册啦 : <br>
-<%= link_to '确认账户', confirmation_url(@resource, confirmation_token: @token) %>
+<%= link_to '确认帐户', confirmation_url(@resource, confirmation_token: @token) %>
 
 <p>别忘了看看 <%= link_to '使用条款', terms_url %>。</p>
 
diff --git a/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb b/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb
index ce237a32d..d7d4b4b23 100644
--- a/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb
+++ b/app/views/user_mailer/confirmation_instructions.zh-cn.text.erb
@@ -3,7 +3,7 @@
 你刚刚在 <%= @instance %> 创建了帐号。
 
 点击下面的链接来完成注册啦 : <br>
-<%= link_to '确认账户', confirmation_url(@resource, confirmation_token: @token) %>
+<%= link_to '确认帐户', confirmation_url(@resource, confirmation_token: @token) %>
 
 别忘了看看 <%= link_to 'terms and conditions', terms_url %>。
 
diff --git a/app/views/user_mailer/password_change.es.html.erb b/app/views/user_mailer/password_change.es.html.erb
new file mode 100644
index 000000000..0a9eb4c4c
--- /dev/null
+++ b/app/views/user_mailer/password_change.es.html.erb
@@ -0,0 +1,3 @@
+<p>¡Hola, <%= @resource.email %>!</p>
+
+<p>Te contactamos para notificarte que tu contraseña en <%= @instance %> ha sido modificada.</p>
\ No newline at end of file
diff --git a/app/views/user_mailer/password_change.es.text.erb b/app/views/user_mailer/password_change.es.text.erb
new file mode 100644
index 000000000..192faf9ad
--- /dev/null
+++ b/app/views/user_mailer/password_change.es.text.erb
@@ -0,0 +1,3 @@
+¡Hola, <%= @resource.email %>!
+
+Te contactamos para notificarte que tu contraseña en <%= @instance %> ha sido modificada.
\ No newline at end of file
diff --git a/app/views/user_mailer/password_change.pt-BR.html.erb b/app/views/user_mailer/password_change.pt-BR.html.erb
new file mode 100644
index 000000000..5f707ba09
--- /dev/null
+++ b/app/views/user_mailer/password_change.pt-BR.html.erb
@@ -0,0 +1,3 @@
+<p>Olá, <%= @resource.email %>!</p>
+
+<p>Estamos te contatando para te notificar que a senha senha no <%= @instance %> foi modificada.</p>
diff --git a/app/views/user_mailer/password_change.pt-BR.text.erb b/app/views/user_mailer/password_change.pt-BR.text.erb
new file mode 100644
index 000000000..d8b76648c
--- /dev/null
+++ b/app/views/user_mailer/password_change.pt-BR.text.erb
@@ -0,0 +1,3 @@
+Olá, <%= @resource.email %>!
+
+Estamos te contatando para te notificar que a senha senha no <%= @instance %> foi modificada.
diff --git a/app/views/user_mailer/reset_password_instructions.es.html.erb b/app/views/user_mailer/reset_password_instructions.es.html.erb
new file mode 100644
index 000000000..4eeb6601d
--- /dev/null
+++ b/app/views/user_mailer/reset_password_instructions.es.html.erb
@@ -0,0 +1,8 @@
+<p>¡Hola, <%= @resource.email %>!</p>
+
+<p>Alguien pidió un enlace para cambiar tu contraseña en <%= @instance %>. Puedes hacer esto con el siguiente enlace.</p>
+
+<p><%= link_to 'Cambiar mi contraseña', edit_password_url(@resource, reset_password_token: @token) %></p>
+
+<p>Si no fuiste tú, por favor ignora este mensaje.</p>
+<p>Tu contraseña no cambiará hasta que ingreses al enlace y crees una nueva.</p>
diff --git a/app/views/user_mailer/reset_password_instructions.es.text.erb b/app/views/user_mailer/reset_password_instructions.es.text.erb
new file mode 100644
index 000000000..8abafcc99
--- /dev/null
+++ b/app/views/user_mailer/reset_password_instructions.es.text.erb
@@ -0,0 +1,8 @@
+¡Hola, <%= @resource.email %>!
+
+Alguien pidió un enlace para cambiar tu contraseña en <%= @instance %>. Puedes hacer esto con el siguiente enlace.
+
+<%= edit_password_url(@resource, reset_password_token: @token) %>
+
+Si no fuiste tú, por favor ignora este mensaje.
+Tu contraseña no cambiará hasta que ingreses al enlace y crees una nueva.
diff --git a/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb b/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb
new file mode 100644
index 000000000..940438b7c
--- /dev/null
+++ b/app/views/user_mailer/reset_password_instructions.pt-BR.html.erb
@@ -0,0 +1,8 @@
+<p>Olá, <%= @resource.email %>!</p>
+
+<p>Alguém solicitou um link para mudar a sua senha no <%= @instance %>. Você pode fazer isso através do link abaixo:</p>
+
+<p><%= link_to 'Mudar a minha senha', edit_password_url(@resource, reset_password_token: @token) %></p>
+
+<p>Se você não solicitou isso, por favor ignore este e-mail.</p>
+<p>A senha senha não será modificada até que você acesse o link acima e crie uma nova.</p>
diff --git a/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb b/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb
new file mode 100644
index 000000000..f574fe08f
--- /dev/null
+++ b/app/views/user_mailer/reset_password_instructions.pt-BR.text.erb
@@ -0,0 +1,8 @@
+Olá, <%= @resource.email %>!
+
+Alguém solicitou um link para mudar a sua senha no <%= @instance %>. Você pode fazer isso através do link abaixo:
+
+<%= edit_password_url(@resource, reset_password_token: @token) %>
+
+Se você não solicitou isso, por favor ignore este e-mail.
+A senha senha não será modificada até que você acesse o link acima e crie uma nova.
diff --git a/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb b/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb
index 245382b2c..51e3073f1 100644
--- a/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb
+++ b/app/views/user_mailer/reset_password_instructions.zh-cn.html.erb
@@ -1,6 +1,6 @@
 <p><%= @resource.email %> ,嗨呀!!</p>
 
-<p>有人(但愿是你)请求更改你Mastodon账户的密码。如果是你的话,请点击下面的链接:</p>
+<p>有人(但愿是你)请求更改你Mastodon帐户的密码。如果是你的话,请点击下面的链接:</p>
 
 <p><%= link_to '更改密码', edit_password_url(@resource, reset_password_token: @token) %></p>
 
diff --git a/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb b/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb
index 574a0bb2e..7df590f78 100644
--- a/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb
+++ b/app/views/user_mailer/reset_password_instructions.zh-cn.text.erb
@@ -1,6 +1,6 @@
 <%= @resource.email %> ,嗨呀!!
 
-有人(但愿是你)请求更改你Mastodon账户的密码。如果是你的话,请点击下面的链接:
+有人(但愿是你)请求更改你Mastodon帐户的密码。如果是你的话,请点击下面的链接:
 
 <%= link_to '更改密码', edit_password_url(@resource, reset_password_token: @token) %>
 
diff --git a/app/workers/activitypub/reply_distribution_worker.rb b/app/workers/activitypub/reply_distribution_worker.rb
index f9127340f..fe99fc05f 100644
--- a/app/workers/activitypub/reply_distribution_worker.rb
+++ b/app/workers/activitypub/reply_distribution_worker.rb
@@ -7,9 +7,9 @@ class ActivityPub::ReplyDistributionWorker
 
   def perform(status_id)
     @status  = Status.find(status_id)
-    @account = @status.thread.account
+    @account = @status.thread&.account
 
-    return if skip_distribution?
+    return if @account.nil? || skip_distribution?
 
     ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
       [signed_payload, @status.account_id, inbox_url]
diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb
index 130c967e0..7560c2671 100644
--- a/app/workers/pubsubhubbub/subscribe_worker.rb
+++ b/app/workers/pubsubhubbub/subscribe_worker.rb
@@ -3,7 +3,7 @@
 class Pubsubhubbub::SubscribeWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false, unique_retry: true
+  sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false
 
   sidekiq_retry_in do |count|
     case count
diff --git a/app/workers/refollow_worker.rb b/app/workers/refollow_worker.rb
new file mode 100644
index 000000000..66bcd27c3
--- /dev/null
+++ b/app/workers/refollow_worker.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class RefollowWorker
+  include Sidekiq::Worker
+
+  sidekiq_options queue: 'pull', retry: false
+
+  def perform(target_account_id)
+    target_account = Account.find(target_account_id)
+    return unless target_account.protocol == :activitypub
+
+    target_account.followers.where(domain: nil).find_each do |follower|
+      # Locally unfollow remote account
+      follower.unfollow!(target_account)
+
+      # Schedule re-follow
+      begin
+        FollowService.new.call(follower, target_account)
+      rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
+        next
+      end
+    end
+  end
+end
diff --git a/app/workers/scheduler/ip_cleanup_scheduler.rb b/app/workers/scheduler/ip_cleanup_scheduler.rb
new file mode 100644
index 000000000..9f1593c91
--- /dev/null
+++ b/app/workers/scheduler/ip_cleanup_scheduler.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+require 'sidekiq-scheduler'
+
+class Scheduler::IpCleanupScheduler
+  include Sidekiq::Worker
+
+  def perform
+    time_ago = 5.years.ago
+    SessionActivation.where('updated_at < ?', time_ago).destroy_all
+    User.where('last_sign_in_at < ?', time_ago).update_all(last_sign_in_ip: nil)
+  end
+end
diff --git a/config/application.rb b/config/application.rb
index 49382c2b9..eb89f0a10 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -10,7 +10,6 @@ require_relative '../app/lib/exceptions'
 require_relative '../lib/paperclip/gif_transcoder'
 require_relative '../lib/paperclip/video_transcoder'
 require_relative '../lib/mastodon/version'
-require_relative '../lib/mastodon/unique_retry_job_middleware'
 
 Dotenv::Railtie.load
 
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index e9f455251..f27aae7ec 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -47,9 +47,12 @@ elsif ENV['SWIFT_ENABLED'] == 'true'
     fog_credentials: {
       provider: 'OpenStack',
       openstack_username: ENV.fetch('SWIFT_USERNAME'),
-      openstack_tenant: ENV.fetch('SWIFT_TENANT'),
+      openstack_project_name: ENV.fetch('SWIFT_TENANT'),
       openstack_api_key: ENV.fetch('SWIFT_PASSWORD'),
       openstack_auth_url: ENV.fetch('SWIFT_AUTH_URL'),
+      openstack_domain_name: ENV['SWIFT_DOMAIN_NAME'] || 'default',
+      openstack_region: ENV['SWIFT_REGION'],
+      openstack_cache_ttl: ENV['SWIFT_CACHE_TTL'] || 60,
     },
     fog_directory: ENV.fetch('SWIFT_CONTAINER'),
     fog_host: ENV.fetch('SWIFT_OBJECT_URL'),
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 0ee77730e..b70784d79 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -9,14 +9,8 @@ end
 
 Sidekiq.configure_server do |config|
   config.redis = redis_params
-  config.client_middleware do |chain|
-    chain.add Mastodon::UniqueRetryJobMiddleware
-  end
 end
 
 Sidekiq.configure_client do |config|
   config.redis = redis_params
-  config.client_middleware do |chain|
-    chain.add Mastodon::UniqueRetryJobMiddleware
-  end
 end
diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb
new file mode 100644
index 000000000..e924fac22
--- /dev/null
+++ b/config/initializers/twitter_regex.rb
@@ -0,0 +1,42 @@
+module Twitter
+  class Regex
+
+    REGEXEN[:valid_general_url_path_chars] = /[^\p{White_Space}\(\)\?]/iou
+    REGEXEN[:valid_url_path_ending_chars] = /[^\p{White_Space}\(\)\?!\*';:=\,\.\$%\[\]\p{Pd}~&\|@]|(?:#{REGEXEN[:valid_url_balanced_parens]})/iou
+    REGEXEN[:valid_url_balanced_parens] = /
+      \(
+        (?:
+          #{REGEXEN[:valid_general_url_path_chars]}+
+          |
+          # allow one nested level of balanced parentheses
+          (?:
+            #{REGEXEN[:valid_general_url_path_chars]}*
+            \(
+              #{REGEXEN[:valid_general_url_path_chars]}+
+            \)
+            #{REGEXEN[:valid_general_url_path_chars]}*
+          )
+        )
+      \)
+    /iox
+    REGEXEN[:valid_url_path] = /(?:
+      (?:
+        #{REGEXEN[:valid_general_url_path_chars]}*
+        (?:#{REGEXEN[:valid_url_balanced_parens]} #{REGEXEN[:valid_general_url_path_chars]}*)*
+        #{REGEXEN[:valid_url_path_ending_chars]}
+      )|(?:#{REGEXEN[:valid_general_url_path_chars]}+\/)
+    )/iox
+    REGEXEN[:valid_url] = %r{
+      (                                                                                     #   $1 total match
+        (#{REGEXEN[:valid_url_preceding_chars]})                                            #   $2 Preceeding chracter
+        (                                                                                   #   $3 URL
+          (https?:\/\/)?                                                                    #   $4 Protocol (optional)
+          (#{REGEXEN[:valid_domain]})                                                       #   $5 Domain(s)
+          (?::(#{REGEXEN[:valid_port_number]}))?                                            #   $6 Port number (optional)
+          (/#{REGEXEN[:valid_url_path]}*)?                                                  #   $7 URL Path and anchor
+          (\?#{REGEXEN[:valid_url_query_chars]}*#{REGEXEN[:valid_url_query_ending_chars]})? #   $8 Query String
+        )
+      )
+    }iox
+  end
+end
diff --git a/config/locales/activerecord.ca.yml b/config/locales/activerecord.ca.yml
index d4d3d1e40..12e347ad9 100644
--- a/config/locales/activerecord.ca.yml
+++ b/config/locales/activerecord.ca.yml
@@ -1,3 +1,4 @@
+---
 ca:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml
index c3354fa25..428aaf727 100644
--- a/config/locales/activerecord.en.yml
+++ b/config/locales/activerecord.en.yml
@@ -1,3 +1,4 @@
+---
 en:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.fa.yml b/config/locales/activerecord.fa.yml
index eaff8f077..1cd50eec7 100644
--- a/config/locales/activerecord.fa.yml
+++ b/config/locales/activerecord.fa.yml
@@ -1,3 +1,4 @@
+---
 fa:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml
index 858777c0e..c4b04c73a 100644
--- a/config/locales/activerecord.fr.yml
+++ b/config/locales/activerecord.fr.yml
@@ -1,3 +1,4 @@
+---
 fr:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.he.yml b/config/locales/activerecord.he.yml
index e4b16d289..e3b363efc 100644
--- a/config/locales/activerecord.he.yml
+++ b/config/locales/activerecord.he.yml
@@ -1,3 +1,4 @@
+---
 he:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.id.yml b/config/locales/activerecord.id.yml
index fd606cd3a..9fa093e9d 100644
--- a/config/locales/activerecord.id.yml
+++ b/config/locales/activerecord.id.yml
@@ -1,3 +1,4 @@
+---
 id:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.it.yml b/config/locales/activerecord.it.yml
index be2f0b4c1..4cec9fb63 100644
--- a/config/locales/activerecord.it.yml
+++ b/config/locales/activerecord.it.yml
@@ -1,3 +1,4 @@
+---
 it:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.ja.yml b/config/locales/activerecord.ja.yml
index 975912f0f..7bc4fc308 100644
--- a/config/locales/activerecord.ja.yml
+++ b/config/locales/activerecord.ja.yml
@@ -1,3 +1,4 @@
+---
 ja:
   activerecord:
     attributes:
diff --git a/config/locales/activerecord.nl.yml b/config/locales/activerecord.nl.yml
index b39c8cd78..eeabab34a 100644
--- a/config/locales/activerecord.nl.yml
+++ b/config/locales/activerecord.nl.yml
@@ -1,3 +1,4 @@
+---
 nl:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.oc.yml b/config/locales/activerecord.oc.yml
index a26018f43..e52f79188 100644
--- a/config/locales/activerecord.oc.yml
+++ b/config/locales/activerecord.oc.yml
@@ -1,3 +1,4 @@
+---
 oc:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.pl.yml b/config/locales/activerecord.pl.yml
index f82e1b875..bd8e40a6a 100644
--- a/config/locales/activerecord.pl.yml
+++ b/config/locales/activerecord.pl.yml
@@ -1,3 +1,4 @@
+---
 pl:
   activerecord:
     attributes:
diff --git a/config/locales/activerecord.ru.yml b/config/locales/activerecord.ru.yml
index 316637888..1a7ac9978 100644
--- a/config/locales/activerecord.ru.yml
+++ b/config/locales/activerecord.ru.yml
@@ -1,3 +1,4 @@
+---
 ru:
   activerecord:
     errors:
diff --git a/config/locales/activerecord.uk.yml b/config/locales/activerecord.uk.yml
index 24febc1bc..00b2b8d8b 100644
--- a/config/locales/activerecord.uk.yml
+++ b/config/locales/activerecord.uk.yml
@@ -1,3 +1,4 @@
+---
 uk:
   activerecord:
     errors:
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 604b09600..666b8cb97 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -94,7 +94,7 @@ ar:
         one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418"
         other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418"
     favourite:
-      body: 'أُعجب %{name} بمنشورك'
+      body: أُعجب %{name} بمنشورك
       subject: "%{name} favourited your status"
     follow:
       body: "%{name} من متتبعيك الآن !"
diff --git a/config/locales/devise.bg.yml b/config/locales/devise.bg.yml
index 7485b8236..8e1ba6eb4 100644
--- a/config/locales/devise.bg.yml
+++ b/config/locales/devise.bg.yml
@@ -11,7 +11,7 @@ bg:
       invalid: Невалиден имейл адрес или парола.
       last_attempt: Разполагаш с още един опит преди профилът ти да бъде заключен.
       locked: Профилът ти е заключен.
-      not_found_in_database: "Невалидни стойности за %{authentication_keys} или парола."
+      not_found_in_database: Невалидни стойности за %{authentication_keys} или парола.
       timeout: Сесията ти изтече, моля влез отново, за да продължиш.
       unauthenticated: Преди да продължиш, трябва да влезеш в профила си или да се регистрираш.
       unconfirmed: Преди да продължиш, трябва да потвърдиш регистрацията си.
@@ -21,12 +21,12 @@ bg:
       password_change:
         subject: 'Mastodon: Паролата е променена'
       reset_password_instructions:
-        subject: 'Инструкции за смяна на паролата'
+        subject: Инструкции за смяна на паролата
       unlock_instructions:
-        subject: 'Инструкции за отключване'
+        subject: Инструкции за отключване
     omniauth_callbacks:
-      failure: "Не успяхме да те упълномощим чрез %{kind}, защото \"%{reason}\"."
-      success: "Успешно упълномощаване чрез %{kind} профил."
+      failure: Не успяхме да те упълномощим чрез %{kind}, защото "%{reason}".
+      success: Успешно упълномощаване чрез %{kind} профил.
     passwords:
       no_token: Може да достъпваш тази страница само от имейл за промяна на паролата. Ако тази страница е отворена от такъв имейл, увери се, че използваш целия URL-адрес, който сме ти изпратили.
       send_instructions: Ще получиш писмо с инструкции как да промениш паролата си до няколко минути.
@@ -52,10 +52,10 @@ bg:
   errors:
     messages:
       already_confirmed: е вече потвърден, моля опитай да влезеш в профила си с него
-      confirmation_period_expired: "трябва да се потвърди в рамките на %{period}, моля направи нова заявка за потвърждение"
+      confirmation_period_expired: трябва да се потвърди в рамките на %{period}, моля направи нова заявка за потвърждение
       expired: е изтекъл, моля заяви нов
       not_found: не е намерен
       not_locked: не бе заключен
       not_saved:
-        one: "Една грешка попречи този %{resource} да бъде записан:"
+        one: 'Една грешка попречи този %{resource} да бъде записан:'
         other: "%{count} грешки попречиха този %{resource} да бъде записан:"
diff --git a/config/locales/devise.de.yml b/config/locales/devise.de.yml
index 58bfaa3d6..035a4713c 100644
--- a/config/locales/devise.de.yml
+++ b/config/locales/devise.de.yml
@@ -2,60 +2,60 @@
 de:
   devise:
     confirmations:
-      confirmed: "Vielen Dank für deine Registrierung. Bitte melde dich jetzt an."
-      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail, mit der du deine Registrierung bestätigen kannst."
-      send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert, erhältst Du in wenigen Minuten eine E-Mail mit der du deine Registrierung bestätigen kannst."
+      confirmed: Vielen Dank für deine Registrierung. Bitte melde dich jetzt an.
+      send_instructions: Du erhältst in wenigen Minuten eine E-Mail, mit der du deine Registrierung bestätigen kannst.
+      send_paranoid_instructions: Falls Deine E-Mail-Adresse in unserer Datenbank existiert, erhältst Du in wenigen Minuten eine E-Mail mit der du deine Registrierung bestätigen kannst.
     failure:
-      already_authenticated: "Du bist bereits angemeldet."
-      inactive: "Dein Account ist nicht aktiv."
-      invalid: "Ungültige Anmeldedaten."
-      last_attempt: "Du hast noch einen Versuch bevor dein Account gesperrt wird."
-      locked: "Dein Account ist gesperrt."
-      not_found_in_database: "E-Mail-Adresse oder Passwort ungültig."
-      timeout: "Deine Sitzung ist abgelaufen, bitte melde dich erneut an."
-      unauthenticated: "Du musst Dich anmelden oder registrieren, bevor du fortfahren kannst."
-      unconfirmed: "Du musst deinen Account bestätigen, bevor du fortfahren kannst."
+      already_authenticated: Du bist bereits angemeldet.
+      inactive: Dein Account ist nicht aktiv.
+      invalid: Ungültige Anmeldedaten.
+      last_attempt: Du hast noch einen Versuch bevor dein Account gesperrt wird.
+      locked: Dein Account ist gesperrt.
+      not_found_in_database: E-Mail-Adresse oder Passwort ungültig.
+      timeout: Deine Sitzung ist abgelaufen, bitte melde dich erneut an.
+      unauthenticated: Du musst Dich anmelden oder registrieren, bevor du fortfahren kannst.
+      unconfirmed: Du musst deinen Account bestätigen, bevor du fortfahren kannst.
     mailer:
       confirmation_instructions:
-        subject: "Mastodon: Anleitung zur Bestätigung deines Accounts"
+        subject: 'Mastodon: Anleitung zur Bestätigung deines Accounts'
       password_change:
         subject: 'Mastodon: Passwort wurde geändert'
       reset_password_instructions:
-        subject: "Mastodon: Anleitung um dein Passwort zurückzusetzen"
+        subject: 'Mastodon: Anleitung um dein Passwort zurückzusetzen'
       unlock_instructions:
-        subject: "Mastodon: Anleitung um deinen Account freizuschalten"
+        subject: 'Mastodon: Anleitung um deinen Account freizuschalten'
     omniauth_callbacks:
-      failure: "Du konntest nicht mit deinem %{kind}-Account angemeldet werden, weil '%{reason}'."
-      success: "Du hast dich erfolgreich mit Deinem %{kind}-Account angemeldet."
+      failure: Du konntest nicht mit deinem %{kind}-Account angemeldet werden, weil '%{reason}'.
+      success: Du hast dich erfolgreich mit Deinem %{kind}-Account angemeldet.
     passwords:
-      no_token: "Du kannst diese Seite nur über den Link aus der E-Mail zum Passwort-Zurücksetzen aufrufen. Wenn du einen solchen Link aufgerufen hast, stelle bitte sicher, dass du die vollständige Adresse aufrufst."
-      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie du dein Passwort zurücksetzen kannst."
-      send_paranoid_instructions: "Falls deine E-Mail-Adresse in unserer Datenbank existiert erhältst du in wenigen Minuten eine E-Mail mit der Anleitung, wie du dein Passwort zurücksetzen kannst."
-      updated: "Dein Passwort wurde geändert. Du bist jetzt angemeldet."
-      updated_not_active: "Dein Passwort wurde geändert."
+      no_token: Du kannst diese Seite nur über den Link aus der E-Mail zum Passwort-Zurücksetzen aufrufen. Wenn du einen solchen Link aufgerufen hast, stelle bitte sicher, dass du die vollständige Adresse aufrufst.
+      send_instructions: Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie du dein Passwort zurücksetzen kannst.
+      send_paranoid_instructions: Falls deine E-Mail-Adresse in unserer Datenbank existiert erhältst du in wenigen Minuten eine E-Mail mit der Anleitung, wie du dein Passwort zurücksetzen kannst.
+      updated: Dein Passwort wurde geändert. Du bist jetzt angemeldet.
+      updated_not_active: Dein Passwort wurde geändert.
     registrations:
-      destroyed: "Dein Account wurde gelöscht."
-      signed_up: "Du hast dich erfolgreich registriert."
-      signed_up_but_inactive: "Du hast dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account inaktiv ist."
-      signed_up_but_locked: "Du hast dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account gesperrt ist."
-      signed_up_but_unconfirmed: "Du hast Dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account noch nicht bestätigt ist. Du erhältst in Kürze eine E-Mail mit der Anleitung, wie Du Deinen Account freischalten kannst."
-      update_needs_confirmation: "Deine Daten wurden aktualisiert, aber du musst deine neue E-Mail-Adresse bestätigen. Du erhälst in wenigen Minuten eine E-Mail, mit der du die Änderung deiner E-Mail-Adresse abschließen kannst."
-      updated: "Deine Daten wurden aktualisiert."
+      destroyed: Dein Account wurde gelöscht.
+      signed_up: Du hast dich erfolgreich registriert.
+      signed_up_but_inactive: Du hast dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account inaktiv ist.
+      signed_up_but_locked: Du hast dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account gesperrt ist.
+      signed_up_but_unconfirmed: Du hast Dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account noch nicht bestätigt ist. Du erhältst in Kürze eine E-Mail mit der Anleitung, wie Du Deinen Account freischalten kannst.
+      update_needs_confirmation: Deine Daten wurden aktualisiert, aber du musst deine neue E-Mail-Adresse bestätigen. Du erhälst in wenigen Minuten eine E-Mail, mit der du die Änderung deiner E-Mail-Adresse abschließen kannst.
+      updated: Deine Daten wurden aktualisiert.
     sessions:
-      already_signed_out: "Erfolgreich abgemeldet."
-      signed_in: "Erfolgreich angemeldet."
-      signed_out: "Erfolgreich abgemeldet."
+      already_signed_out: Erfolgreich abgemeldet.
+      signed_in: Erfolgreich angemeldet.
+      signed_out: Erfolgreich abgemeldet.
     unlocks:
-      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie du deinen Account entsperren können."
-      send_paranoid_instructions: "Falls deine E-Mail-Adresse in unserer Datenbank existiert erhältst du in wenigen Minuten eine E-Mail mit der Anleitung, wie du deinen Account entsperren kannst."
-      unlocked: "Dein Account wurde entsperrt. Du bist jetzt angemeldet."
+      send_instructions: Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie du deinen Account entsperren können.
+      send_paranoid_instructions: Falls deine E-Mail-Adresse in unserer Datenbank existiert erhältst du in wenigen Minuten eine E-Mail mit der Anleitung, wie du deinen Account entsperren kannst.
+      unlocked: Dein Account wurde entsperrt. Du bist jetzt angemeldet.
   errors:
     messages:
-      already_confirmed: "wurde bereits bestätigt."
-      confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an."
-      expired: "ist abgelaufen, bitte neu anfordern."
-      not_found: "wurde nicht gefunden."
-      not_locked: "ist nicht gesperrt"
+      already_confirmed: wurde bereits bestätigt.
+      confirmation_period_expired: muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an.
+      expired: ist abgelaufen, bitte neu anfordern.
+      not_found: wurde nicht gefunden.
+      not_locked: ist nicht gesperrt
       not_saved:
-        one: "Konnte %{resource} nicht speichern: ein Fehler."
-        other: "Konnte %{resource} nicht speichern: %{count} Fehler."
+        one: 'Konnte %{resource} nicht speichern: ein Fehler.'
+        other: 'Konnte %{resource} nicht speichern: %{count} Fehler.'
diff --git a/config/locales/devise.eo.yml b/config/locales/devise.eo.yml
index b786647dd..a2303ec1e 100644
--- a/config/locales/devise.eo.yml
+++ b/config/locales/devise.eo.yml
@@ -58,4 +58,4 @@ eo:
       not_locked: ne estis ŝlosita
       not_saved:
         one: '1 eraro malpermesis al tiu %{resource} esti konservita:'
-        other: '%{count} eraroj malpermesis al tiu %{resource} esti konservita:'
+        other: "%{count} eraroj malpermesis al tiu %{resource} esti konservita:"
diff --git a/config/locales/devise.es.yml b/config/locales/devise.es.yml
index 634a5e97f..5a689f347 100644
--- a/config/locales/devise.es.yml
+++ b/config/locales/devise.es.yml
@@ -34,8 +34,8 @@ es:
       updated: Su contraseña ha sido cambiada con éxito. Ahora ya está registrado.
       updated_not_active: Su contraseña ha sido cambiada con éxito.
     registrations:
-      destroyed: ¡Adios! Su cuenta ha sido cancelada con éxito. Esperamos verle pronto de nuevo.
-      signed_up: ¡Bienvenido! Se ha registrado con éxito.
+      destroyed: "¡Adios! Su cuenta ha sido cancelada con éxito. Esperamos verle pronto de nuevo."
+      signed_up: "¡Bienvenido! Se ha registrado con éxito."
       signed_up_but_inactive: Se ha registrado con éxito. Sin embargo, no podemos identificarle porque su cuenta no ha sido activada todavía.
       signed_up_but_locked: Se ha registrado con éxito. Sin embargo, no podemos identificarle porque su cuenta está bloqueada.
       signed_up_but_unconfirmed: Un mensaje con un enlace de confirmación ha sido enviado a su correo electrónico. Por favor siga el enlace para activar su cuenta.
diff --git a/config/locales/devise.fa.yml b/config/locales/devise.fa.yml
index 18f63b25d..f78412f91 100644
--- a/config/locales/devise.fa.yml
+++ b/config/locales/devise.fa.yml
@@ -57,5 +57,5 @@ fa:
       not_found: پیدا نشد
       not_locked: قفل نبود
       not_saved:
-        one: 'خطایی نگذاشت که این %{resource} ذخیره شود'
-        other: "به خاطر %{count} خطا، این %{resource} ذخیره نشد"
+        one: خطایی نگذاشت که این %{resource} ذخیره شود
+        other: به خاطر %{count} خطا، این %{resource} ذخیره نشد
diff --git a/config/locales/devise.fi.yml b/config/locales/devise.fi.yml
index bf4f530dc..91ab9559c 100644
--- a/config/locales/devise.fi.yml
+++ b/config/locales/devise.fi.yml
@@ -29,7 +29,7 @@ fi:
       success: Onnistuneesti varmennettu %{kind} tilillä.
     passwords:
       no_token: Et pääse tälle sivulle ilman salasanan vaihto sähköpostia. Jos tulet tämmöisestä postista, varmista että sinulla on täydellinen URL.
-      send_instructions:  Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen.
+      send_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen.
       send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen.
       updated: Salasanasi vaihdettu onnistuneesti. Olet nyt kirjautunut sisään.
       updated_not_active: Salasanasi vaihdettu onnistuneesti.
diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml
index 6805e4f38..237ae8f6f 100644
--- a/config/locales/devise.fr.yml
+++ b/config/locales/devise.fr.yml
@@ -17,7 +17,7 @@ fr:
       unconfirmed: Vous devez valider votre compte pour continuer.
     mailer:
       confirmation_instructions:
-        subject: "Merci de confirmer votre inscription sur %{instance}"
+        subject: Merci de confirmer votre inscription sur %{instance}
       password_change:
         subject: Votre mot de passe a été modifié avec succés.
       reset_password_instructions:
@@ -58,4 +58,4 @@ fr:
       not_locked: n’était pas verrouillé⋅e
       not_saved:
         one: 'Une erreur a empêché ce(tte) %{resource} d’être sauvegardé⋅e :'
-        other: '%{count} erreurs ont empêché %{resource} d’être sauvegardé⋅e :'
+        other: "%{count} erreurs ont empêché %{resource} d’être sauvegardé⋅e :"
diff --git a/config/locales/devise.he.yml b/config/locales/devise.he.yml
index 975c2a360..ddb688625 100644
--- a/config/locales/devise.he.yml
+++ b/config/locales/devise.he.yml
@@ -2,19 +2,19 @@
 he:
   devise:
     confirmations:
-      confirmed: 'כתובת הדוא"ל אומתה בהצלחה.'
-      send_instructions: 'נשלח אליך דוא"ל עם הוראות לאימות כתובת הדוא"ל שאמור להתקבל בדקות הקרובות. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.'
-      send_paranoid_instructions: 'אם כתובת הדוא"ל שלך קיימת במסד הנתונים, יתקבל בדקות הקרובות דוא"ל עם הוראות לאימות כתובתך. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.'
+      confirmed: כתובת הדוא"ל אומתה בהצלחה.
+      send_instructions: נשלח אליך דוא"ל עם הוראות לאימות כתובת הדוא"ל שאמור להתקבל בדקות הקרובות. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.
+      send_paranoid_instructions: אם כתובת הדוא"ל שלך קיימת במסד הנתונים, יתקבל בדקות הקרובות דוא"ל עם הוראות לאימות כתובתך. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.
     failure:
-      already_authenticated: 'חשבון זה כבר מחובר.'
-      inactive: 'חשבון זה טרם הופעל.'
-      invalid: '%{authentication_keys} או סיסמא לא נכונים.'
-      last_attempt: 'יש לך עוד ניסיון אחד לפני נעילת החשבון.'
-      locked: 'חשבון זה נעול.'
+      already_authenticated: חשבון זה כבר מחובר.
+      inactive: חשבון זה טרם הופעל.
+      invalid: "%{authentication_keys} או סיסמא לא נכונים."
+      last_attempt: יש לך עוד ניסיון אחד לפני נעילת החשבון.
+      locked: חשבון זה נעול.
       not_found_in_database: "%{authentication_keys} או סיסמא לא נכונים."
-      timeout: 'פג תוקף השהיה בחשבון. נא להכנס מחדש על מנת להמשיך.'
-      unauthenticated: 'יש להרשם או להכנס לחשבון על מנת להמשיך.'
-      unconfirmed: 'יש לאמת את כתובת הדוא"ל על מנת להמשיך.'
+      timeout: פג תוקף השהיה בחשבון. נא להכנס מחדש על מנת להמשיך.
+      unauthenticated: יש להרשם או להכנס לחשבון על מנת להמשיך.
+      unconfirmed: יש לאמת את כתובת הדוא"ל על מנת להמשיך.
     mailer:
       confirmation_instructions:
         subject: 'מסטודון: הוראות אימות'
@@ -25,29 +25,29 @@ he:
       unlock_instructions:
         subject: 'מסטודון: הוראות לביטול נעילה'
     omniauth_callbacks:
-      failure: "לא ניתן לאמת את חשבונך מ־%{kind} מהסיבה: \"%{reason}\"."
-      success: "נכשל אימות מחשבון %{kind}."
+      failure: 'לא ניתן לאמת את חשבונך מ־%{kind} מהסיבה: "%{reason}".'
+      success: נכשל אימות מחשבון %{kind}.
     passwords:
-      no_token: 'לא ניתן לגשת לעמוד זה, אלא מדוא"ל איפוס סיסמא. אם לא הגעת מדוא"ל איפוס סיסמא, יש לוודא שכתובת הקישורית הוקלדה בשלמותה.'
-      send_instructions: 'בדקות הקרובות יתקבל דוא"ל עם הוראות לאיפוס סיסמתך. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.'
-      send_paranoid_instructions: 'אם כתובת הדוא"ל שלך קיימת במסד הנתונים, יתקבל בדקות הקרובות דוא"ל עם הוראות לאחזור סיסמא. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.'
-      updated: 'סיסמתך שונתה בהצלחה. הינך כעת במצב מחובר.'
-      updated_not_active: 'סיסמתך שונתה בהצלחה.'
+      no_token: לא ניתן לגשת לעמוד זה, אלא מדוא"ל איפוס סיסמא. אם לא הגעת מדוא"ל איפוס סיסמא, יש לוודא שכתובת הקישורית הוקלדה בשלמותה.
+      send_instructions: בדקות הקרובות יתקבל דוא"ל עם הוראות לאיפוס סיסמתך. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.
+      send_paranoid_instructions: אם כתובת הדוא"ל שלך קיימת במסד הנתונים, יתקבל בדקות הקרובות דוא"ל עם הוראות לאחזור סיסמא. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.
+      updated: סיסמתך שונתה בהצלחה. הינך כעת במצב מחובר.
+      updated_not_active: סיסמתך שונתה בהצלחה.
     registrations:
-      destroyed: 'בייוש! חשבונך בוטל בהצלחה. אנחנו מקווים לראות אותך שוב בקרוב.'
-      signed_up: 'ברוכים הבאים! נרשמת בהצלחה.'
-      signed_up_but_inactive: 'נרשמת בהצלחה. למרות זאת לא הצליחה הכניסה לחשבון מאחר וחשבונך עוד לא הופעל.'
-      signed_up_but_locked: 'נרשמת בהצלחה. למרות זאת לא הצליחה הכניסה לחשבון מאחר וחשבונך נעול.'
-      signed_up_but_unconfirmed: 'דוא"ל עם קישורית לאימות נשלך לכתובתך. נא לעקוב אחר הקישורית על מנת להפעיל את החשבון. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.'
-      update_needs_confirmation: 'עדכת את חשבונך בהצלחה, אך יש צורך לאמת את כתובת הדוא"ל החדשה שלך. נא לבדוק בחשבון הדוא"ל לקבלת קישורית אימות על מנת לאמת את הכתובת החדשה. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.'
+      destroyed: בייוש! חשבונך בוטל בהצלחה. אנחנו מקווים לראות אותך שוב בקרוב.
+      signed_up: ברוכים הבאים! נרשמת בהצלחה.
+      signed_up_but_inactive: נרשמת בהצלחה. למרות זאת לא הצליחה הכניסה לחשבון מאחר וחשבונך עוד לא הופעל.
+      signed_up_but_locked: נרשמת בהצלחה. למרות זאת לא הצליחה הכניסה לחשבון מאחר וחשבונך נעול.
+      signed_up_but_unconfirmed: דוא"ל עם קישורית לאימות נשלך לכתובתך. נא לעקוב אחר הקישורית על מנת להפעיל את החשבון. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.
+      update_needs_confirmation: עדכת את חשבונך בהצלחה, אך יש צורך לאמת את כתובת הדוא"ל החדשה שלך. נא לבדוק בחשבון הדוא"ל לקבלת קישורית אימות על מנת לאמת את הכתובת החדשה. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.
       updated: חשבונך עודכן בהצלחה.
     sessions:
       already_signed_out: יצאת מהחשבון בהצלחה.
       signed_in: נכנסת לחשבון בהצלחה.
       signed_out: יצאת מהחשבון בהצלחה.
     unlocks:
-      send_instructions: 'בדקות הקרובות ישלח אליך דוא"ל עם הוראות לביטול נעילת החשבון. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.'
-      send_paranoid_instructions: 'אם חשבונך קיים, בדקות הקרובות ישלח אליך דוא"ל עם הוראות לביטול נעילת החשבון. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.'
+      send_instructions: בדקות הקרובות ישלח אליך דוא"ל עם הוראות לביטול נעילת החשבון. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.
+      send_paranoid_instructions: אם חשבונך קיים, בדקות הקרובות ישלח אליך דוא"ל עם הוראות לביטול נעילת החשבון. יש לבדוק את תיבת הספאם ליתר בטחון אם ההודעה לא הגיעה תוך דקות ספורות.
       unlocked: נעילת חשבונך בוטלה בהצלחה. נא להכנס לחשבון על מנת להמשיך.
   errors:
     messages:
diff --git a/config/locales/devise.hu.yml b/config/locales/devise.hu.yml
index 2eb7da45c..911ba7b94 100644
--- a/config/locales/devise.hu.yml
+++ b/config/locales/devise.hu.yml
@@ -28,7 +28,7 @@ hu:
       failure: "%{kind} nem hitelesíthető, mert %{reason}."
       success: Sikeres hitelesítés %{kind} fiókról.
     passwords:
-      no_token: Nem férhetsz hozzá az oldalhoz jelszó visszaállító e-mail nélkül. Ha egy jelszó visszaállító e-mail hozott ide, ellenőrizd, hogy a megadott teljes URL-t használd. 
+      no_token: Nem férhetsz hozzá az oldalhoz jelszó visszaállító e-mail nélkül. Ha egy jelszó visszaállító e-mail hozott ide, ellenőrizd, hogy a megadott teljes URL-t használd.
       send_instructions: Pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat.
       send_paranoid_instructions: Ha létezik az e-mail cím, pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat.
       updated: Jelszavad sikeresen frissült. Bejelentkeztél.
@@ -36,7 +36,7 @@ hu:
     registrations:
       destroyed: Viszlát! A fiókod sikeresen törölve. Reméljük hamarosan viszontláthatunk.
       signed_up: Üdvözlünk! Sikeresen regisztráltál.
-      signed_up_but_inactive: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod még nem lett aktiválva. 
+      signed_up_but_inactive: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod még nem lett aktiválva.
       signed_up_but_locked: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod le lett zárva.
       signed_up_but_unconfirmed: Egy üzenet a megerősítési linkkel kiküldésre került az e-mail címedre. Kérjük használd a linket a fiókod aktiválásához.
       update_needs_confirmation: Sikeresen frissítetted a fiókodat, de szükségünk van az e-mail címed megerősítésére. Kérlek ellenőrizd az e-mailedet és kövesd a levélben szereplő megerősítési linket az e-mail címed megerősítéséhez.
diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml
index 5d61ccdec..2001abe96 100644
--- a/config/locales/devise.ja.yml
+++ b/config/locales/devise.ja.yml
@@ -8,10 +8,10 @@ ja:
     failure:
       already_authenticated: 既にログイン済みです。
       inactive: あなたのアカウントはまだ有効化されていません。
-      invalid: '%{authentication_keys}かパスワードが誤っています。'
+      invalid: "%{authentication_keys}かパスワードが誤っています。"
       last_attempt: あと1回失敗するとアカウントがロックされます。
       locked: アカウントはロックされました。
-      not_found_in_database: '%{authentication_keys}かパスワードが誤っています'
+      not_found_in_database: "%{authentication_keys}かパスワードが誤っています"
       timeout: セッションの有効期限が切れました。続行するには再度ログインしてください。
       unauthenticated: 続行するにはログインするか、アカウントを作成してください。
       unconfirmed: 続行するにはメールアドレスを確認する必要があります。
@@ -25,8 +25,8 @@ ja:
       unlock_instructions:
         subject: 'Mastodon: アカウントのロックの解除'
     omniauth_callbacks:
-      failure: '%{reason}によって%{kind}からのアクセスを認証できませんでした。'
-      success: '%{kind}からのアクセスは正常に認証されました。'
+      failure: "%{reason}によって%{kind}からのアクセスを認証できませんでした。"
+      success: "%{kind}からのアクセスは正常に認証されました。"
     passwords:
       no_token: パスワード再発行のメール以外からこのページにアクセスすることはできません。 パスワード再発行のメールからアクセスしたのにもかかわらずこのメッセージが表示される場合は、アクセスしたURLが間違っていないか確認してください。
       send_instructions: パスワード再発行の方法が記載されたメールが間もなく送信されます。
@@ -52,10 +52,10 @@ ja:
   errors:
     messages:
       already_confirmed: は確認されました。ログインを試してください。
-      confirmation_period_expired: '%{period}以内に確認が必要です。再度試してください。'
+      confirmation_period_expired: "%{period}以内に確認が必要です。再度試してください。"
       expired: は期限切れです。再度試してください。
       not_found: 見つかりません。
       not_locked: ロックされていません。
       not_saved:
-        one: 'エラーが発生したため、%{resource}の保存に失敗しました。'
+        one: エラーが発生したため、%{resource}の保存に失敗しました。
         other: "%{count}個のエラーが発生したため、保存に失敗しました。 %{resource}"
diff --git a/config/locales/devise.nl.yml b/config/locales/devise.nl.yml
index f8f59e660..ff79d036e 100644
--- a/config/locales/devise.nl.yml
+++ b/config/locales/devise.nl.yml
@@ -24,7 +24,7 @@ nl:
       reset_password_instructions:
         subject: 'Mastodon: Wachtwoord opnieuw instellen'
       unlock_instructions:
-        subject: 'Mastodon: Instructies om opschorten account ongedaan te maken' 
+        subject: 'Mastodon: Instructies om opschorten account ongedaan te maken'
     omniauth_callbacks:
       failure: Kon je niet aanmelden met jouw %{kind} account, omdat "%{reason}".
       success: Successvol aangemeld met jouw %{kind} account.
diff --git a/config/locales/devise.oc.yml b/config/locales/devise.oc.yml
index 99e62a10e..5cccb48ff 100644
--- a/config/locales/devise.oc.yml
+++ b/config/locales/devise.oc.yml
@@ -17,13 +17,13 @@ oc:
       unconfirmed: Vos cal confirmar vòstra adreça de corrièl abans de contunhar.
     mailer:
       confirmation_instructions:
-        subject: "Mercés de confirmar vòstra inscripcion sus %{instance}"
+        subject: Mercés de confirmar vòstra inscripcion sus %{instance}
       password_change:
-        subject: 'Mastodon : senhal cambiat'
+        subject: Mastodon : senhal cambiat
       reset_password_instructions:
-        subject: 'Mastodon : instruccions per reïnicializar lo senhal'
+        subject: Mastodon : instruccions per reïnicializar lo senhal
       unlock_instructions:
-        subject: 'Mastodon : instuccions de desblocatge'
+        subject: Mastodon : instuccions de desblocatge
     omniauth_callbacks:
       failure: Fracàs al moment de vos autentificar de %{kind} perque "%{reason}".
       success: Sètz ben autentificat dempuèi lo compte %{kind}.
diff --git a/config/locales/devise.pl.yml b/config/locales/devise.pl.yml
index d537efc6e..4b1eb2c60 100644
--- a/config/locales/devise.pl.yml
+++ b/config/locales/devise.pl.yml
@@ -58,4 +58,4 @@ pl:
       not_locked: było zablokowane
       not_saved:
         one: '1 błąd uniemożliwił zapisanie zasobu %{resource}:'
-        other: "Błędy (%{count}) uniemożliwiły zapisanie zasobu %{resource}:"
+        other: 'Błędy (%{count}) uniemożliwiły zapisanie zasobu %{resource}:'
diff --git a/config/locales/devise.pt-BR.yml b/config/locales/devise.pt-BR.yml
index c647fabbd..13736b3a3 100644
--- a/config/locales/devise.pt-BR.yml
+++ b/config/locales/devise.pt-BR.yml
@@ -2,60 +2,60 @@
 pt-BR:
   devise:
     confirmations:
-      confirmed: O seu endereço de email foi confirmado.
-      send_instructions: Você irá receber um email com instruções em como confirmar o seu endereço de email dentro de alguns minutos.
-      send_paranoid_instructions: Se o seu endereço de email já existir na nossa base de dados, irá receber um email com instruções em como confirmá-lo dentro de alguns minutos.
+      confirmed: O seu endereço de e-mail foi confirmado.
+      send_instructions: Você receberá uma mensagem em sua caixa de entrada com instruções sobre como confirmar o seu endereço de e-mail dentro de alguns minutos. Por favor, cheque a sua pasta de spam caso não tenha recebido esta mensagem.
+      send_paranoid_instructions: Se o seu endereço de e-mail já existir em nossa base de dados, você receberá uma mensagem em sua caixa de entrada com instruções sobre confirmá-lo dentro de alguns minutos. Por favor, cheque a sua pasta de spam caso não tenha recebido esta mensagem.
     failure:
       already_authenticated: A sua sessão já está aberta.
       inactive: A sua contra ainda não está ativada.
-      invalid: "%{authentication_keys} ou password inválidos."
-      last_attempt: Tem mais uma tentativa antes de a sua conta ser protegida.
-      locked: A sua conta está protegida
-      not_found_in_database: "%{authentication_keys} ou password inválidos."
-      timeout: A sua sessão expirou. Por favore entre de novo para continuar.
-      unauthenticated: Você precsa de entrar ou registar-se antes de continuar.
-      unconfirmed: Você tem de confirmar o seu endereço de email antes de continuar.
+      invalid: "%{authentication_keys} ou senha inválida."
+      last_attempt: Você tem apenas mais uma tentativa sobrando antes que a sua conta seja bloqueada.
+      locked: A sua conta está bloqueada.
+      not_found_in_database: "%{authentication_keys} ou senha inválida."
+      timeout: A sua sessão expirou. Por favor, entre novamente para continuar.
+      unauthenticated: Você precisa entrar ou registar-se antes de continuar.
+      unconfirmed: Você precisa confirmar o seu endereço de e-mail antes de continuar.
     mailer:
       confirmation_instructions:
         subject: 'Mastodon: Instruções de confirmação'
       password_change:
-        subject: 'Mastodon: Password nova'
+        subject: 'Mastodon: Senha modificada'
       reset_password_instructions:
-        subject: 'Mastodon: Instruções para editar a password'
+        subject: 'Mastodon: Instruções para mudança de senha'
       unlock_instructions:
-        subject: 'Mastodon: Instruções para desproteger a sua conta'
+        subject: 'Mastodon: Instruções de desbloqueio'
     omniauth_callbacks:
-      failure: Could not authenticate you from %{kind} because "%{reason}".
-      success: Successfully authenticated from %{kind} account.
+      failure: Não foi possível autenticá-lo como %{kind} porque "%{reason}".
+      success: Autenticado com sucesso como %{kind}.
     passwords:
-      no_token: Você não pode aceder a esta página sem ter vindo de um email para mudar a password. Se este for o case, por favor faça questão de verificar que usou o URL no email.
-      send_instructions: Irá receber um email com instruções em como mudar a sua password dentro de algns minutos.
-      send_paranoid_instructions: Se  seu endereço de email existe na nossa base de dados, irá receber um link para recuperar a sua password dentro de alguns minutos.
-      updated: A sua password foi alterada. A sua sessão está aberta.
-      updated_not_active: A sua password foi alterada.
+      no_token: Você não pode acessar esta página se não tiver vindo de uma mensagem de mudança de senha. Se este for o caso, por favor verifique se a URL utilizada está completa.
+      send_instructions: Se o seu endereço de e-mail já estiver cadastrado em nossa base de dados, você receberá uma mensagem com um link para realizar a mudança de senha em alguns minutos. Por favor, cheque a sua pasta de spam caso não tenha recebido esta mensagem.
+      send_paranoid_instructions: Se o seu endereço de e-mail já estiver cadastrado em nossa base de dados, você receberá uma mensagem com um link para realizar a mudança de senha em alguns minutos. Por favor, cheque a sua pasta de spam caso não tenha recebido esta mensagem.
+      updated: A sua senha foi alterada. A sua sessão está aberta.
+      updated_not_active: A sua senha foi alterada.
     registrations:
       destroyed: Adeus! A sua conta foi cancelada. Esperamos vê-lo em breve.
-      signed_up: Bem vindo! A sua conta foi registada com sucesso.
-      signed_up_but_inactive: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta ainda não foi ativada.
-      signed_up_but_locked: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta está protegida.
-      signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o seu email. Por favor siga o link para ativar a sua conta.
-      update_needs_confirmation: Você mudou o seu endereço de email ou password, mas é necessário confirmar a mudança. Por favor siga o link que foi enviado para o seu novo endereço de email.
+      signed_up: Bem vindo! A sua conta foi registrada com sucesso.
+      signed_up_but_inactive: A sua conta foi registrada. No entanto, não abrimos a sua sessão porque a sua conta ainda não foi ativada.
+      signed_up_but_locked: A sua conta foi registrada. No entanto, não abrimos a sua sessão porque a sua conta está bloqueada.
+      signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o seu endereço de e-mail. Por favor, siga o link para ativar a sua conta e, caso não tenha recebido esta mensagem, cheque a sua pasta de spam.
+      update_needs_confirmation: Você mudou o seu endereço de e-mail ou a sua senha, mas é necessário confirmar a mudança. Por favor siga o link que foi enviado para o seu novo endereço de e-mail e, caso não tenha recebido esta mensagem, cheque a sua pasta de spam.
       updated: A sua conta foi alterada com sucesso.
     sessions:
-      already_signed_out: Sessão fechada.
+      already_signed_out: Sessão encerrada.
       signed_in: Sessão iniciada.
-      signed_out: Sessão fechada.
+      signed_out: Sessão encerrada.
     unlocks:
-      send_instructions: Irá receber um email com instruções para desproteger a sua conta dentro de alguns minutos.
-      send_paranoid_instructions: Se a sua conta existe, irá receber um email com instruções a detalhar como a desproteger dentro de alguns minutos.
-      unlocked: A sua conta foi desprotegida. Por favor inicie sessão para continuar.
+      send_instructions: Você receberá uma mensagem com instruções para desbloquear a sua conta em alguns instantes. Por favor, cheque a sua pasta de spam caso não tenha recebido esta mensagem.
+      send_paranoid_instructions: Se a sua conta já existe, você receberá uma mensagem com instruções para desbloquear a sua conta em alguns instantes. Por favor, cheque a sua pasta de spam caso não tenha recebido esta mensagem.
+      unlocked: A sua conta foi desbloqueada com sucesso. Por favor inicie sessão para continuar.
   errors:
     messages:
       already_confirmed: já foi confirmado, por favor tente iniciar sessão
-      confirmation_period_expired: tem de ser confirmado dentro de %{period}, por favor tente outra vez
-      expired: expirou, por favor tente outra vez
+      confirmation_period_expired: precisa ser confirmada em até %{period}, por favor, solicite novo link de confirmação
+      expired: expirou, por favor solicite uma nova
       not_found: não encontrado
-      not_locked: não está protegido
+      not_locked: não está bloqueada
       not_saved:
-        one: '1 erro impediu este %{resource} de ser guardado:'
-        other: "%{count} erros impediram este %{resource} de ser guardado:"
+        one: '1 erro impediu este %{resource} de ser salvo(a):'
+        other: "%{count} erros impediram este %{resource} de ser salvo(a):"
diff --git a/config/locales/devise.zh-TW.yml b/config/locales/devise.zh-TW.yml
index c38839b9a..e627653f1 100644
--- a/config/locales/devise.zh-TW.yml
+++ b/config/locales/devise.zh-TW.yml
@@ -57,5 +57,5 @@ zh-TW:
       not_found: 找不到
       not_locked: 並未被鎖定
       not_saved:
-        one: '1 個錯誤使 %{resource} 無法被儲存︰'
+        one: 1 個錯誤使 %{resource} 無法被儲存︰
         other: "%{count} 個錯誤使 %{resource} 無法被儲存︰"
diff --git a/config/locales/doorkeeper.ar.yml b/config/locales/doorkeeper.ar.yml
index cd4a9cb50..7980e107a 100644
--- a/config/locales/doorkeeper.ar.yml
+++ b/config/locales/doorkeeper.ar.yml
@@ -57,7 +57,7 @@ ar:
         prompt: طلبَ تطبيق %{client_name} تصريحا لاستعمال حسابك.
         title: إذن بالتصريح
       show:
-        title: رمز الترخيص
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: إبطال التصريح
diff --git a/config/locales/doorkeeper.bg.yml b/config/locales/doorkeeper.bg.yml
index 6fafdfc55..24de4aee0 100644
--- a/config/locales/doorkeeper.bg.yml
+++ b/config/locales/doorkeeper.bg.yml
@@ -57,7 +57,7 @@ bg:
         prompt: Приложението %{client_name} заявява достъп до твоя акаунт
         title: Изисква се упълномощаване
       show:
-        title: Код за упълномощаване
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Отмяна
diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml
index ef937c4e6..38dbbdde9 100644
--- a/config/locales/doorkeeper.ca.yml
+++ b/config/locales/doorkeeper.ca.yml
@@ -57,7 +57,7 @@ ca:
         prompt: La aplicació %{client_name} sol⋅licita tenir accés al teu compte
         title: Es requereix autorizació
       show:
-        title: Codi de autorització
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Revocar
diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml
index b0ba2fb98..1588e4f9e 100644
--- a/config/locales/doorkeeper.de.yml
+++ b/config/locales/doorkeeper.de.yml
@@ -57,7 +57,7 @@ de:
         prompt: Soll %{client_name} für die Benutzung dieses Accounts autorisiert werden?
         title: Autorisierung erforderlich
       show:
-        title: Autorisierungscode
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Ungültig machen
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index efbd81d43..33d544bed 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -63,7 +63,7 @@ en:
         prompt: Application %{client_name} requests access to your account
         title: Authorization required
       show:
-        title: Authorization code
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Revoke
diff --git a/config/locales/doorkeeper.eo.yml b/config/locales/doorkeeper.eo.yml
index 33cc7cc19..ce39f3bd1 100644
--- a/config/locales/doorkeeper.eo.yml
+++ b/config/locales/doorkeeper.eo.yml
@@ -57,7 +57,7 @@ eo:
         prompt: La aplikaĵo %{client_name} petas aliron al via konto
         title: Rajtigo bezonata
       show:
-        title: Rajtiga kodo
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Malrajtigi
diff --git a/config/locales/doorkeeper.es.yml b/config/locales/doorkeeper.es.yml
index f3efb2301..ca6e620d8 100644
--- a/config/locales/doorkeeper.es.yml
+++ b/config/locales/doorkeeper.es.yml
@@ -5,6 +5,8 @@ es:
       doorkeeper/application:
         name: Nombre
         redirect_uri: URI para redirección
+        scopes: Ámbitos
+        website: Sitio web
     errors:
       models:
         doorkeeper/application:
@@ -23,26 +25,30 @@ es:
         edit: Editar
         submit: Enviar
       confirmations:
-        destroy: ¿Está seguro?
+        destroy: "¿Está seguro?"
       edit:
         title: Editar aplicación
       form:
-        error: ¡Uuups! Compruebe su formulario
+        error: "¡Uuups! Compruebe su formulario"
       help:
         native_redirect_uri: Utilice %{native_redirect_uri} para pruebas locales
         redirect_uri: Utilice una línea por URI
         scopes: Separe los ámbitos con espacios. Déjelo en blanco para utilizar los ámbitos por defecto.
       index:
+        application: Aplicación
         callback_url: Callback URL
+        delete: Eliminar
         name: Nombre
         new: Nueva aplicación
+        scopes: Ámbitos
+        show: Mostrar
         title: Sus aplicaciones
       new:
         title: Nueva aplicación
       show:
         actions: Acciones
         application_id: Id de la aplicación
-        callback_urls: Callback urls
+        callback_urls: Callback URLs
         scopes: Ámbitos
         secret: Secreto
         title: 'Aplicación: %{name}'
@@ -57,16 +63,17 @@ es:
         prompt: La aplicación %{client_name} solicita tener acceso a su cuenta
         title: Se requiere autorización
       show:
-        title: Código de autorización
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Revocar
       confirmations:
-        revoke: ¿Está seguro?
+        revoke: "¿Está seguro?"
       index:
         application: Aplicación
         created_at: Creado el
         date_format: "%A-%m-%d %H:%M:%S"
+        scopes: Ámbitos
         title: Sus aplicaciones autorizadas
     errors:
       messages:
diff --git a/config/locales/doorkeeper.fa.yml b/config/locales/doorkeeper.fa.yml
index 343580530..6a4be575e 100644
--- a/config/locales/doorkeeper.fa.yml
+++ b/config/locales/doorkeeper.fa.yml
@@ -63,7 +63,7 @@ fa:
         prompt: Application %{client_name} requests access to your account
         title: Authorization required
       show:
-        title: Authorization code
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Revoke
diff --git a/config/locales/doorkeeper.fi.yml b/config/locales/doorkeeper.fi.yml
index cd1a9d058..44aceec4c 100644
--- a/config/locales/doorkeeper.fi.yml
+++ b/config/locales/doorkeeper.fi.yml
@@ -57,7 +57,7 @@ fi:
         prompt: Applikaatio %{client_name} pyytää lupaa tilillesi
         title: Valtuutus vaaditaan
       show:
-        title: Valtuutus koodi
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Evää
diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml
index 487018209..3398b248d 100644
--- a/config/locales/doorkeeper.fr.yml
+++ b/config/locales/doorkeeper.fr.yml
@@ -59,7 +59,7 @@ fr:
         prompt: Autoriser %{client_name} à utiliser votre compte ?
         title: Autorisation requise
       show:
-        title: Code d’autorisation
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Annuler
diff --git a/config/locales/doorkeeper.he.yml b/config/locales/doorkeeper.he.yml
index 778902788..78e72a56f 100644
--- a/config/locales/doorkeeper.he.yml
+++ b/config/locales/doorkeeper.he.yml
@@ -1,5 +1,5 @@
 ---
-he :
+he:
   activerecord:
     attributes:
       doorkeeper/application:
@@ -57,7 +57,7 @@ he :
         prompt: ישום %{client_name} מבקש גישה לחשבונך
         title: נדרשת הרשאה
       show:
-        title: קוד הרשאה
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: ביטול
@@ -72,7 +72,7 @@ he :
     errors:
       messages:
         access_denied: בעלי המשאב או שרת ההרשאה דחו את הבקשה.
-        credential_flow_not_configured: 'התהליך "Resource Owner Password Credentials" נכשל בשל חוסר בתצורת Doorkeeper.configure.resource_owner_from_credentials.'
+        credential_flow_not_configured: התהליך "Resource Owner Password Credentials" נכשל בשל חוסר בתצורת Doorkeeper.configure.resource_owner_from_credentials.
         invalid_client: הרשאת הלקוח נכשלה עקב לקוח שאינו ידוע, חוסר בהרשאת לקוח או שיטת הרשאה שאינה נתמכת.
         invalid_grant: חוזה ההרשאה המצורף אינו חוקי, אינו תקף, מבוטל, או שאינו מתאים לקישורית ההפניה שבשימוש על ידי בקשת ההרשאה, או שהופק על ידי לקוח אחר.
         invalid_redirect_uri: קישורית ההפניה המצורפת אינה חוקית.
diff --git a/config/locales/doorkeeper.hr.yml b/config/locales/doorkeeper.hr.yml
index 26d21c10d..e0240938e 100644
--- a/config/locales/doorkeeper.hr.yml
+++ b/config/locales/doorkeeper.hr.yml
@@ -57,7 +57,7 @@ hr:
         prompt: Aplikacija %{client_name} je zatražila pristup tvom računu
         title: Traži se autorizacija
       show:
-        title: Autorizacijski kod
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Odbij
diff --git a/config/locales/doorkeeper.hu.yml b/config/locales/doorkeeper.hu.yml
index b1c6dd6c9..54e732f0c 100644
--- a/config/locales/doorkeeper.hu.yml
+++ b/config/locales/doorkeeper.hu.yml
@@ -57,7 +57,7 @@ hu:
         prompt: "%{client_name} nevű alkalmazás engedélyt kér a fiókodhoz való hozzáféréshez."
         title: Engedély szükséges
       show:
-        title: Engedély kódja
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Visszavonás
@@ -84,7 +84,7 @@ hu:
           unknown: Hozzáférési kulcs érvénytelen
         resource_owner_authenticator_not_configured: Erőforrás tulajdonos keresés megszakadt, ugyanis a Doorkeeper.configure.resource_owner_authenticator beállítatlan.
         server_error: Hitelesítő szervert váratlan esemény érte, mely meggátolta a kérés teljesítését.
-        temporarily_unavailable: A hitelesítő szerver jelenleg nem tudja teljesíteni a kérést egy átmeneti túlterheltség vagy a kiszolgáló karbantartása miatt. 
+        temporarily_unavailable: A hitelesítő szerver jelenleg nem tudja teljesíteni a kérést egy átmeneti túlterheltség vagy a kiszolgáló karbantartása miatt.
         unauthorized_client: A kliens nincs feljogosítva a kérés teljesítésére.
         unsupported_grant_type: A hitelesítés módja nem támogatott a hitelesítő kiszolgálón.
         unsupported_response_type: A hitelesítő kiszolgáló nem támogatja ezt a választ.
diff --git a/config/locales/doorkeeper.id.yml b/config/locales/doorkeeper.id.yml
index 4ae97c5df..95560ecf9 100644
--- a/config/locales/doorkeeper.id.yml
+++ b/config/locales/doorkeeper.id.yml
@@ -57,7 +57,7 @@ id:
         prompt: Aplikasi %{client_name} meminta akses pada akun anda
         title: Izin diperlukan
       show:
-        title: Kode izin
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Cabut izin
diff --git a/config/locales/doorkeeper.io.yml b/config/locales/doorkeeper.io.yml
index 8c093a9d8..28466d3ae 100644
--- a/config/locales/doorkeeper.io.yml
+++ b/config/locales/doorkeeper.io.yml
@@ -57,7 +57,7 @@ io:
         prompt: Application %{client_name} requests access to your account
         title: Authorization required
       show:
-        title: Authorization code
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Revoke
diff --git a/config/locales/doorkeeper.it.yml b/config/locales/doorkeeper.it.yml
index 4b5e463b0..e5a2d3f6e 100644
--- a/config/locales/doorkeeper.it.yml
+++ b/config/locales/doorkeeper.it.yml
@@ -57,7 +57,7 @@ it:
         prompt: L'applicazione %{client_name} richiede l'accesso al tuo account
         title: Autorizzazione richiesta
       show:
-        title: Codice autorizzazione
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Disabilita
diff --git a/config/locales/doorkeeper.ja.yml b/config/locales/doorkeeper.ja.yml
index 9e3b72761..0b670fda4 100644
--- a/config/locales/doorkeeper.ja.yml
+++ b/config/locales/doorkeeper.ja.yml
@@ -63,7 +63,7 @@ ja:
         prompt: アプリ %{client_name} があなたのアカウントへのアクセスを要求しています。
         title: 認証が必要です。
       show:
-        title: 認証コード
+        title: 認証コードをコピーしてアプリに貼り付けて下さい。
     authorized_applications:
       buttons:
         revoke: 取消
diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml
index 77cf2503b..f97aa8111 100644
--- a/config/locales/doorkeeper.nl.yml
+++ b/config/locales/doorkeeper.nl.yml
@@ -3,9 +3,10 @@ nl:
   activerecord:
     attributes:
       doorkeeper/application:
-        name: Naam
+        name: Naam toepassing
         redirect_uri: Redirect-URI
-        scopes: Scopes
+        scopes: Toestemmingen
+        website: Website toepassing
     errors:
       models:
         doorkeeper/application:
@@ -26,27 +27,31 @@ nl:
       confirmations:
         destroy: Weet je het zeker?
       edit:
-        title: Applicatie bewerken
+        title: Toepassing bewerken
       form:
         error: Oops! Controleer het formulier op fouten
       help:
         native_redirect_uri: Gebruik %{native_redirect_uri} voor lokale tests
         redirect_uri: 'Gebruik één regel per URI. '
-        scopes: Scopes met spaties van elkaar scheiden. Laat leeg om de standaardscopes te gebruiken.
+        scopes: Toestemmingen met spaties van elkaar scheiden. Laat leeg om de standaardtoestemmingen te gebruiken.
       index:
+        application: Toepassing
         callback_url: Callback-URL
+        delete: Verwijderen
         name: Naam
-        new: Nieuwe applicatie
-        title: Jouw applicaties
+        new: Nieuwe toepassing
+        scopes: Toestemmingen
+        show: Tonen
+        title: Jouw toepassingen
       new:
-        title: Nieuwe applicatie
+        title: Nieuwe toepassing
       show:
         actions: Acties
-        application_id: Applicatie-ID
+        application_id: Client-key
         callback_urls: Callback-URL's
-        scopes: Scopes
-        secret: Secret
-        title: 'Applicatie: %{name}'
+        scopes: Toestemmingen
+        secret: Client-secret
+        title: 'Toepassing: %{name}'
     authorizations:
       buttons:
         authorize: Autoriseren
@@ -54,21 +59,21 @@ nl:
       error:
         title: Er is een fout opgetreden
       new:
-        able_to: Deze applicatie zal in staat zijn om
+        able_to: Deze toepassing zal in staat zijn om
         prompt: "%{client_name} autoriseren om uw account te gebruiken?"
         title: Autorisatie vereist
       show:
-        title: Autorisatie-code
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Intrekken
       confirmations:
         revoke: Weet je het zeker?
       index:
-        application: Applicatie
+        application: Toepassing
         created_at: Aangemaakt op
         date_format: "%d-%m-%Y %H:%M:%S"
-        title: Jouw geautoriseerde applicaties
+        title: Jouw geautoriseerde toepassingen
     errors:
       messages:
         access_denied: De resource-eigenaar of autorisatie-server weigerde het verzoek.
@@ -87,28 +92,28 @@ nl:
         server_error: De autorisatieserver is is een onverwachte situatie tegengekomen die het verzoek verhinderde.
         temporarily_unavailable: De autorisatieserver is momenteel niet in staat het verzoek te behandelen als gevolg van een tijdelijke overbelasting of onderhoud aan de server.
         unauthorized_client: De client is niet bevoegd om dit verzoek op deze manier uit te voeren.
-        unsupported_grant_type: Het type autorisatie wordt niet door de autorisatieserver ondersteund 
+        unsupported_grant_type: Het type autorisatie wordt niet door de autorisatieserver ondersteund
         unsupported_response_type: De autorisatieserver ondersteund dit antwoordtype niet
     flash:
       applications:
         create:
-          notice: Applicatie aangemaakt.
+          notice: Toepassing aangemaakt.
         destroy:
-          notice: Applicatie verwijderd.
+          notice: Toepassing verwijderd.
         update:
-          notice: Applicatie bewerkt.
+          notice: Toepassing bewerkt.
       authorized_applications:
         destroy:
-          notice: Applicatie ingetrokken.
+          notice: Toepassing ingetrokken.
     layouts:
       admin:
         nav:
-          applications: Applicaties
+          applications: Toepassingen
           home: Home
           oauth2_provider: OAuth2-provider
       application:
         title: OAuth-autorisatie vereist
     scopes:
-      follow: accounts te volgen, te negeren en te blokkeren.
-      read: jouw accountgegevens te lezen
-      write: namens jou berichten te plaatsen
+      follow: accounts volgen, negeren en blokkeren
+      read: berichten lezen
+      write: berichten plaatsen
diff --git a/config/locales/doorkeeper.no.yml b/config/locales/doorkeeper.no.yml
index 23ca61d4d..ba061e0ca 100644
--- a/config/locales/doorkeeper.no.yml
+++ b/config/locales/doorkeeper.no.yml
@@ -57,7 +57,7 @@
         prompt: Applikasjon %{client_name} spør om tilgang til din konto
         title: Autorisasjon påkrevd
       show:
-        title: Autoriserings kode
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Opphev
diff --git a/config/locales/doorkeeper.oc.yml b/config/locales/doorkeeper.oc.yml
index b6aebea48..6069b77ad 100644
--- a/config/locales/doorkeeper.oc.yml
+++ b/config/locales/doorkeeper.oc.yml
@@ -51,7 +51,7 @@ oc:
         callback_urls: urls de rapèls
         scopes: Encastres
         secret: Secret
-        title: 'Aplicacion : %{name}'
+        title: Aplicacion : %{name}
     authorizations:
       buttons:
         authorize: Autorizar
@@ -63,7 +63,7 @@ oc:
         prompt: L’aplicacion %{client_name} demanda l’accès al vòstre compte.
         title: Cal l’autorizacion
       show:
-        title: Còdi d’autorizacion
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Revocar
@@ -71,8 +71,8 @@ oc:
         revoke: Ne sètz segur ?
       index:
         application: Aplicacion
-        created_at: Creada lo
-        date_format: "%d-%m-%Y %Ho%M %S"
+        created_at: Creacion
+        date_format: "%-d %b. de %Y %Ho%M %S"
         scopes: Encastres
         title: Las vòstras aplicacions autorizadas
     errors:
diff --git a/config/locales/doorkeeper.pl.yml b/config/locales/doorkeeper.pl.yml
index ee3f4cad8..824a6be90 100644
--- a/config/locales/doorkeeper.pl.yml
+++ b/config/locales/doorkeeper.pl.yml
@@ -63,7 +63,7 @@ pl:
         prompt: Aplikacja %{client_name} prosi o dostęp do Twojego konta
         title: Wymagana jest autoryzacja
       show:
-        title: Kod autoryzacji
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Unieważnij
diff --git a/config/locales/doorkeeper.pt-BR.yml b/config/locales/doorkeeper.pt-BR.yml
index 85ea3bfcc..53cadec38 100644
--- a/config/locales/doorkeeper.pt-BR.yml
+++ b/config/locales/doorkeeper.pt-BR.yml
@@ -3,17 +3,19 @@ pt-BR:
   activerecord:
     attributes:
       doorkeeper/application:
-        name: Nome
-        redirect_uri: Redirect URI
+        name: Nome do aplicativo
+        redirect_uri: URI de redirecionamento
+        scopes: Autorizações
+        website: Website do aplicativo
     errors:
       models:
         doorkeeper/application:
           attributes:
             redirect_uri:
               fragment_present: não pode conter um fragmento.
-              invalid_uri: tem de ser um URI válido.
-              relative_uri: tem de ser um URI absoluto.
-              secured_uri: tem de ser um HTTPS/SSL URI.
+              invalid_uri: precisa ser uma URI válida.
+              relative_uri: precisa ser uma URI absoluta.
+              secured_uri: precisa ser uma URI HTTPS/SSL.
   doorkeeper:
     applications:
       buttons:
@@ -21,92 +23,97 @@ pt-BR:
         cancel: Cancelar
         destroy: Destruir
         edit: Editar
-        submit: Submeter
+        submit: Enviar
       confirmations:
-        destroy: Tem a certeza?
+        destroy: Você tem certeza?
       edit:
-        title: Editar aplicação
+        title: Editar aplicativo
       form:
-        error: Oops! Verifique que o formulário não tem erros
+        error: Oops! Verifique o seu formulário para saber de possíveis erros
       help:
         native_redirect_uri: Use %{native_redirect_uri} para testes locais
-        redirect_uri: Utilize uma linha por URI
-        scopes: Separate scopes with spaces. Leave blank to use the default scopes.
+        redirect_uri: Use uma linha para cada URI
+        scopes: Separe autorizações com espaços. Deixe em branco para usar autorizações padrões.
       index:
-        callback_url: Callback URL
+        application: Aplicativos
+        callback_url: URL de retorno
+        delete: Excluir
         name: Nome
-        new: Nova Aplicação
-        title: As suas aplicações
+        new: Novo aplicativo
+        scopes: Autorizações
+        show: Mostrar
+        title: Seus aplicativos
       new:
-        title: Nova aplicação
+        title: Novos aplicativos
       show:
         actions: Ações
-        application_id: Id de Aplicação
-        callback_urls: Callback urls
-        scopes: Scopes
-        secret: Segredo
-        title: 'Aplicação: %{name}'
+        application_id: Chave do cliente
+        callback_urls: URLs de retorno
+        scopes: Autorizações
+        secret: Segredo do cliente
+        title: 'Application: %{name}'
     authorizations:
       buttons:
-        authorize: Autorize
-        deny: Não autorize
+        authorize: Autorizar
+        deny: Negar
       error:
         title: Ocorreu um erro
       new:
-        able_to: Vai poder
-        prompt: Aplicação %{client_name} requisita acesso à sua conta
-        title: Autorização é necessária
+        able_to: Será capaz de
+        prompt: O aplicativo %{client_name} solicita acesso à sua conta
+        title: Autorização necessária
       show:
-        title: Código de autorização
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Revogar
       confirmations:
-        revoke: Tem a certeza?
+        revoke: Você tem certeza?
       index:
-        application: Aplicação
-        created_at: Criada em
-        date_format: "%Y-%m-%d %H:%M:%S"
-        title: As suas aplicações autorizadas
+        application: Aplicativos
+        created_at: Autorizados
+        date_format: "%d-%m-%Y %H:%M:%S"
+        scopes: Autorizações
+        title: Aplicativos autorizados
     errors:
       messages:
-        access_denied: The resource owner or authorization server denied the request.
-        credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.
-        invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
-        invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
-        invalid_redirect_uri: The redirect uri included is not valid.
-        invalid_request: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.
-        invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found
-        invalid_scope: The requested scope is invalid, unknown, or malformed.
+        access_denied: O proprietário ou servidor de autorização negou a solicitação.
+        credential_flow_not_configured: Cadeira de Credenciais de Senha do Proprietário falhou porque Doorkeeper.configure.resource_owner_from_credentials não foram configuradas.
+        invalid_client: Autenticação do cliente falhou por causa de um cliente desconhecido, nenhum cliente de autenticação incluído ou método de autenticação não suportado.
+        invalid_grant: A garantia de autorização é inválida, expirou, foi revogada, não é equivalente à URI de redirecionamento usada da solicitação de autorização ou foi emitida por outro cliente.
+        invalid_redirect_uri: A URI de redirecionamento incluída não é válida.
+        invalid_request: A solicitação não possui um parâmetro obrigatório, inclui um valor não suportado ou está mal formatada.
+        invalid_resource_owner: As credenciais do proprietário não são válidas ou o proprietário não pôde ser encontrado.
+        invalid_scope: A autorização requirida é inválida, desconhecida ou está mal formatada.
         invalid_token:
           expired: O token de acesso expirou
           revoked: O token de acesso foi revogado
           unknown: O token de acesso é inválido
-        resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.
-        server_error: The authorization server encountered an unexpected condition which prevented it from fulfilling the request.
-        temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.
-        unauthorized_client: The client is not authorized to perform this request using this method.
-        unsupported_grant_type: The authorization grant type is not supported by the authorization server.
-        unsupported_response_type: The authorization server does not support this response type.
+        resource_owner_authenticator_not_configured: Procura pelo proprietário falhou porque Doorkeeper.configure.resource_owner_authenticator não foi configurado.
+        server_error: O servidor de autorização encontrou uma condição inesperada que preveniu a solicitação de ser respondida.
+        temporarily_unavailable: O servidor de autorização é incapaz de lidar com a solicitação no momento por causa d múltiplas requisições ou manutenção programada.
+        unauthorized_client: O cliente não possui autorização para performar esta solicitação usando este método.
+        unsupported_grant_type: O tipo de garantia de autorização não é suportada pelo servidor de autorização.
+        unsupported_response_type: O servidor de autorização não suporta este tipo de resposta.
     flash:
       applications:
         create:
-          notice: Aplicação criada.
+          notice: Aplicativo criado.
         destroy:
-          notice: Aplicação eliminada.
+          notice: Aplicativo deletado.
         update:
-          notice: Aplicação alterada.
+          notice: Aplicativo atualizado.
       authorized_applications:
         destroy:
-          notice: Aplicação revogada.
+          notice: Aplicativo revogado.
     layouts:
       admin:
         nav:
-          applications: Aplicações
-          oauth2_provider: OAuth2 Provider
+          applications: Aplicativo
+          oauth2_provider: Provedor de OAuth2
       application:
-        title: Autorização OAuth necessária
+        title: Autorização OAuth obrigatória
     scopes:
-      follow: siga, bloqueie, desbloqueie, e deixe de seguir contas
-      read: tenha acesso aos dados da sua conta
-      write: públique por si
+      follow: seguir, bloquear, desbloquear e deixar de seguir outras contas
+      read: ler os dados da sua conta
+      write: postar em seu nome
diff --git a/config/locales/doorkeeper.pt.yml b/config/locales/doorkeeper.pt.yml
index 87e01ba94..30d9f7f5a 100644
--- a/config/locales/doorkeeper.pt.yml
+++ b/config/locales/doorkeeper.pt.yml
@@ -57,7 +57,7 @@ pt:
         prompt: Aplicação %{client_name} pede acesso à tua conta
         title: Autorização é necessária
       show:
-        title: Código de autorização
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Revogar
diff --git a/config/locales/doorkeeper.ru.yml b/config/locales/doorkeeper.ru.yml
index 8862936dc..2234a9bbe 100644
--- a/config/locales/doorkeeper.ru.yml
+++ b/config/locales/doorkeeper.ru.yml
@@ -57,7 +57,7 @@ ru:
         prompt: Приложение %{client_name} запрашивает доступ к Вашему аккаунту
         title: Требуется авторизация
       show:
-        title: Код авторизации
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Отозвать авторизацию
diff --git a/config/locales/doorkeeper.th.yml b/config/locales/doorkeeper.th.yml
index 29673971b..60edae1e4 100644
--- a/config/locales/doorkeeper.th.yml
+++ b/config/locales/doorkeeper.th.yml
@@ -57,7 +57,7 @@ th:
         prompt: Application %{client_name} requests access to your account
         title: Authorization required
       show:
-        title: Authorization code
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: ยกเลิกการอนุญาติ
diff --git a/config/locales/doorkeeper.uk.yml b/config/locales/doorkeeper.uk.yml
index a5289541b..d80abf01a 100644
--- a/config/locales/doorkeeper.uk.yml
+++ b/config/locales/doorkeeper.uk.yml
@@ -57,7 +57,7 @@ uk:
         prompt: Податок %{client_name} просить доступу до вашого акаунту
         title: Необхідна авторизація
       show:
-        title: Код авторизації
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: Відкликати авторизацію
diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml
index 6db8697b8..12b38b81f 100644
--- a/config/locales/doorkeeper.zh-CN.yml
+++ b/config/locales/doorkeeper.zh-CN.yml
@@ -58,7 +58,7 @@ zh-CN:
         prompt: 授权 %{client_name} 使用你的帐号?
         title: 需要你授权
       show:
-        title: 授权码
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: 注销
diff --git a/config/locales/doorkeeper.zh-HK.yml b/config/locales/doorkeeper.zh-HK.yml
index bb44c272b..c8edc2b72 100644
--- a/config/locales/doorkeeper.zh-HK.yml
+++ b/config/locales/doorkeeper.zh-HK.yml
@@ -57,7 +57,7 @@ zh-HK:
         prompt: 應用程式 %{client_name} 要求得到你用戶的部份權限
         title: 需要用戶授權
       show:
-        title: 授權代碼
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: 取消授權
@@ -72,12 +72,9 @@ zh-HK:
     errors:
       messages:
         access_denied: 資源擁有者或授權伺服器不接受請求。
-        credential_flow_not_configured: 資源擁有者密碼認證程序 (Resource Owner Password Credentials
-          flow) 失敗,原因是 Doorkeeper.configure.resource_owner_from_credentials 沒有設定。
-        invalid_client: 用戶程式認證 (Client authentication) 失敗,原因是用戶程式未有登記、沒有指定用戶程式 (client)、或者使用了不支援的認證方法
-          (method)。
-        invalid_grant: 授權申請 (authorization grant) 不正確、過期、已被取消,或者無法對應授權請求 (authorization
-          request) 內的轉接 URI,或者屬於別的用戶程式。
+        credential_flow_not_configured: 資源擁有者密碼認證程序 (Resource Owner Password Credentials flow) 失敗,原因是 Doorkeeper.configure.resource_owner_from_credentials 沒有設定。
+        invalid_client: 用戶程式認證 (Client authentication) 失敗,原因是用戶程式未有登記、沒有指定用戶程式 (client)、或者使用了不支援的認證方法 (method)。
+        invalid_grant: 授權申請 (authorization grant) 不正確、過期、已被取消,或者無法對應授權請求 (authorization request) 內的轉接 URI,或者屬於別的用戶程式。
         invalid_redirect_uri: 不正確的轉接網址。
         invalid_request: 請求缺少了必要的參數、包含了不支援的參數、或者其他輸入錯誤。
         invalid_resource_owner: 資源擁有者的登入資訊錯誤、或者無法找到該資源擁有者。
@@ -86,8 +83,7 @@ zh-HK:
           expired: access token 已經過期
           revoked: access token 已被取消
           unknown: access token 不正確
-        resource_owner_authenticator_not_configured: 無法找到資源擁有者,原因是 Doorkeeper.configure.resource_owner_authenticator
-          沒有設定。
+        resource_owner_authenticator_not_configured: 無法找到資源擁有者,原因是 Doorkeeper.configure.resource_owner_authenticator 沒有設定。
         server_error: 認證伺服器遇上未知狀況,令請求無法通過。
         temporarily_unavailable: 認證伺服器由於臨時負荷過重或者維護,目前未能處理請求。
         unauthorized_client: 用戶程式無權用此方法 (method) 請行這個請求。
diff --git a/config/locales/doorkeeper.zh-TW.yml b/config/locales/doorkeeper.zh-TW.yml
index 7e8c78cd9..01e62df73 100644
--- a/config/locales/doorkeeper.zh-TW.yml
+++ b/config/locales/doorkeeper.zh-TW.yml
@@ -10,7 +10,7 @@ zh-TW:
         doorkeeper/application:
           attributes:
             redirect_uri:
-              fragment_present: 'URI 不可包含 "#fragment" 部份'
+              fragment_present: URI 不可包含 "#fragment" 部份
               invalid_uri: 必需有正確的 URI.
               relative_uri: 必需為絕對 URI.
               secured_uri: 必需使用有 HTTPS/SSL 加密的 URI.
@@ -45,7 +45,7 @@ zh-TW:
         callback_urls: 回傳網址
         scopes: 權限範圍
         secret: 密碼
-        title: '應用程式︰ %{name}'
+        title: 應用程式︰ %{name}
     authorizations:
       buttons:
         authorize: 允許
@@ -57,7 +57,7 @@ zh-TW:
         prompt: 應用程式 %{client_name} 要求取得您帳號的部份權限
         title: 需要授權
       show:
-        title: 授權代碼
+        title: Copy this authorization code and paste it to the application.
     authorized_applications:
       buttons:
         revoke: 取消授權
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 64fc556f3..9013f0ac9 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -60,6 +60,7 @@ en:
       email: E-mail
       feed_url: Feed URL
       followers: Followers
+      followers_url: Followers URL
       follows: Follows
       inbox_url: Inbox URL
       ip: IP
@@ -93,6 +94,7 @@ en:
       resubscribe: Resubscribe
       salmon_url: Salmon URL
       search: Search
+      shared_inbox_url: Shared Inbox URL
       show:
         created_reports: Reports created by this account
         report: report
@@ -106,6 +108,18 @@ en:
       unsubscribe: Unsubscribe
       username: Username
       web: Web
+    custom_emojis:
+      created_msg: Emoji successfully created!
+      delete: Delete
+      destroyed_msg: Emojo successfully destroyed!
+      emoji: Emoji
+      image_hint: PNG up to 50KB
+      new:
+        title: Add new custom emoji
+      shortcode: Shortcode
+      shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
+      title: Custom emojis
+      upload: Upload
     domain_blocks:
       add_new: Add new
       created_msg: Domain block is now being processed
@@ -141,6 +155,8 @@ en:
     instances:
       account_count: Known accounts
       domain_name: Domain
+      reset: Reset
+      search: Search
       title: Known Instances
     reports:
       action_taken_by: Action taken by
@@ -193,6 +209,9 @@ en:
         desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags
         title: Custom terms of service
       site_title: Instance name
+      thumbnail:
+        desc_html: Used for previews via OpenGraph and API. 1200x630px recommended
+        title: Instance thumbnail
       timeline_preview:
         desc_html: Display public timeline on landing page
         title: Timeline preview
@@ -446,6 +465,7 @@ en:
     open_in_web: Open in web
     over_character_limit: character limit of %{max} exceeded
     pin_errors:
+      limit: Too many toots pinned
       ownership: Someone else's toot cannot be pinned
       private: Non-public toot cannot be pinned
       reblog: A boost cannot be pinned
diff --git a/config/locales/es.yml b/config/locales/es.yml
index a02330521..ca3bdd983 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -5,35 +5,251 @@ es:
     about_this: Acerca de esta instancia
     closed_registrations: Los registros están actualmente cerrados en esta instancia.
     contact: Contacto
+    contact_missing: No especificado
+    contact_unavailable: N/A
     description_headline: "¿Qué es %{domain}?"
     domain_count_after: otras instancias
     domain_count_before: Conectado a
+    extended_description_html: |
+      <h3>Un buen lugar para las reglas</h3>
+      <p>La descripción extendida no se ha colocado aún.</p>
+    features:
+      humane_approach_body: Aprendiendo de los errores de otras redes, Mastodon apunta a las decisiones de diseño ético para combatir el desuso de las redes sociales.
+      humane_approach_title: Una misión más humana
+      not_a_product_body: Mastodon no es una red comercial. Nada de publicidad, nada de minado de datos, nada de jardines murados. No hay ninguna autoridad central.
+      not_a_product_title: Eres una persona, no un producto
+      real_conversation_body: Con 500 caracteres a tu disposición y soporte para contenido granular y advertencias de contenido, puedes expresarte como quieras.
+      real_conversation_title: Hecho para verdaderas conversaciones
+      within_reach_body: Aplicaciones múltiples para iOS, Android, y otras plataformas gracias a un ecosistema de APIs amigable al desarrollador para permitirte estar con tus amigos donde sea.
+      within_reach_title: Always within reach
+    find_another_instance: Busca otra instancia
+    generic_description: "%{domain} es un servidor en la red"
+    hosted_on: Mastodon hosteado en %{domain}
+    learn_more: Aprende más
     other_instances: Otras instancias
     source_code: Código fuente
     status_count_after: estados
-    status_count_before: Que han escrito
+    status_count_before: Qué han escrito
     user_count_after: usuarios registrados
     user_count_before: Tenemos
+    what_is_mastodon: "¿Qué es Mastodon?"
   accounts:
     follow: Seguir
     followers: Seguidores
     following: Siguiendo
+    media: Media
     nothing_here: "¡No hay nada aquí!"
     people_followed_by: Usuarios a quien %{name} sigue
     people_who_follow: Usuarios que siguen a %{name}
     posts: Toots
+    posts_with_replies: Toots con respuestas
     remote_follow: Seguir
+    reserved_username: El nombre de usuario está reservado
+    roles:
+      admin: Administrador
     unfollow: Dejar de seguir
+  admin:
+    accounts:
+      are_you_sure: "¿Estás seguro?"
+      confirm: Confirmar
+      confirmed: Confirmado
+      disable_two_factor_authentication: Desactivar autenticación de dos factores
+      display_name: Nombre
+      domain: Dominio
+      edit: Editar
+      email: E-mail
+      feed_url: URL de notificaciones
+      followers: Seguidores
+      followers_url: URL de los seguidores
+      follows: Sigue
+      inbox_url: URL de la bandeja de entrada
+      ip: IP
+      location:
+        all: Todos
+        local: Local
+        remote: Remoto
+        title: Localización
+      media_attachments: Multimedia
+      moderation:
+        all: Todos
+        silenced: Silenciados
+        suspended: Suspendidos
+        title: Moderación
+      most_recent_activity: Actividad más reciente
+      most_recent_ip: IP más reciente
+      not_subscribed: No se está suscrito
+      order:
+        alphabetic: Alfabético
+        most_recent: Más reciente
+        title: Orden
+      outbox_url: URL de bandeja de salida
+      perform_full_suspension: Performar suspensión completa
+      profile_url: URL del perfil
+      protocol: Protocolo
+      public: Público
+      push_subscription_expires: Expiración de la suscripción PuSH
+      redownload: Refrescar avatar
+      reset: Reiniciar
+      reset_password: Reiniciar contraseña
+      resubscribe: Re-suscribir
+      salmon_url: URL de salmón
+      search: Buscar
+      shared_inbox_url: URL de bandeja compartida
+      show:
+        created_reports: Reportes hechos por esta cuenta
+        report: reportar
+        targeted_reports: Reportes hechos sobre esta cuenta
+      silence: Silenciar
+      statuses: Estados
+      subscribe: Suscribir
+      title: Cuentas
+      undo_silenced: Des-silenciar
+      undo_suspension: Des-suspender
+      unsubscribe: Desuscribir
+      username: Nombre de usuario
+      web: Web
+    domain_blocks:
+      add_new: Añadir nuevo
+      created_msg: El bloque de dominio está siendo procesado
+      destroyed_msg: El bloque de dominio se deshizo
+      domain: Dominio
+      new:
+        create: Crear bloque
+        hint: El bloque de dominio no prevendrá la creación de entradas de cuenta en la base de datos, pero aplicará retroactiva y automáticamente métodos de moderación específica en dichas cuentas.
+        severity:
+          desc_html: "<strong>Silenciar</strong> hará los posts de la cuenta invisibles a cualquiera que no lo esté siguiendo. <strong>Suspender</strong> eliminará todo el contenido, media, y datos del perfil. Usa <strong>Ninguno</strong> si solo quieres rechazar archivos multimedia."
+          noop: Ninguno
+          silence: Silenciar
+          suspend: Suspender
+        title: Nuevo bloque de dominio
+      reject_media: Rechazar archivos multimedia
+      reject_media_hint: Remueve localmente archivos multimedia almacenados para descargar cualquiera en el futuro. Irrelevante para suspensiones
+      severities:
+        noop: Ninguno
+        silence: Silenciar
+        suspend: Suspender
+      severity: Severidad
+      show:
+        affected_accounts:
+          one: Una cuenta en la base de datos afectada
+          other: "%{count} cuentas en la base de datos afectadas"
+        retroactive:
+          silence: Des-silenciar todas las cuentas existentes de este dominio
+          suspend: Des-suspender todas las cuentas existentes de este dominio
+        title: Deshacer bloque de dominio para %{domain}
+        undo: Deshacer
+      title: Bloques de Dominio
+      undo: Deshacer
+    instances:
+      account_count: Cuentas conocidas
+      domain_name: Dominio
+      reset: Reiniciar
+      search: Buscar
+      title: Instancias conocidas
+    reports:
+      action_taken_by: Acción tomada por
+      are_you_sure: "¿Estás seguro?"
+      comment:
+        label: Comentario
+        none: Ninguno
+      delete: Eliminar
+      id: ID
+      mark_as_resolved: Marcar como resuelto
+      nsfw:
+        'false': Mostrar multimedia
+        'true': Ocultar multimedia
+      report: 'Reportar #%{id}'
+      report_contents: Contenido
+      reported_account: Cuenta reportada
+      reported_by: Reportado por
+      resolved: Resuelto
+      silence_account: Silenciar cuenta
+      status: Estado
+      suspend_account: Suspender cuenta
+      target: Objetivo
+      title: Reportes
+      unresolved: No resuelto
+      view: Ver
+    settings:
+      bootstrap_timeline_accounts:
+        desc_html: Separa nombres de usuarios múltiples con coma. Solo funcionará con cuentas desbloqueadas. Si está vacío, el predeterminado son todos los administradores locales
+        title: Seguimientos predeterminados para usuarios nuevos
+      contact_information:
+        email: Correo de trabajo
+        username: Nombre de usuario
+      registrations:
+        closed_message:
+          desc_html: Se muestra en la portada cuando los registros están cerrados. Puedes usar tags HTML
+          title: Mensaje de registro cerrado
+        deletion:
+          desc_html: Permite a cualquiera a eliminar su cuenta
+          title: Eliminación de cuenta abierta
+        open:
+          desc_html: Permite a cualquiera a registrar una cuenta
+          title: Registro abierto
+      site_description:
+        desc_html: Párrafo introductorio en la portada y en meta tags. Puedes usar tags HTML, en particular <code>&lt;a&gt;</code> y <code>&lt;em&gt;</code>.
+        title: Descripción de instancia
+      site_description_extended:
+        desc_html: Un buen lugar para tu código de conducta, reglas, guías y otras cosas que estén impuestas aparte en tu instancia. Puedes usar tags HTML
+        title: Información extendida personalizada
+      site_terms:
+        desc_html: Puedes escribir tus propias políticas de privacidad, términos de servicio u otras legalidades. Puedes usar tags HTML
+        title: Términos de servicio personalizados
+      site_title: Nombre de instancia
+      thumbnail:
+        desc_html: Se usa para muestras con OpenGraph y APIs. Se recomienda 1200x630px
+        title: Portada de instancia
+      timeline_preview:
+        desc_html: Mostrar línea de tiempo pública en la portada
+        title: Previsualización
+      title: Ajustes del sitio
+    statuses:
+      back_to_account: Volver a la cuenta
+      batch:
+        delete: Eliminar
+        nsfw_off: NSFW OFF
+        nsfw_on: NSFW ON
+      execute: Ejecutar
+      failed_to_execute: Falló al ejecutar
+      media:
+        hide: Ocultar multimedia
+        show: Mostrar multimedia
+        title: Multimedia
+      no_media: No hay multimedia
+      title: Estado de las cuentas
+      with_media: Con multimedia
+    subscriptions:
+      callback_url: URL del callback
+      confirmed: Confirmado
+      expires_in: Expira en
+      last_delivery: Última entrega
+      title: WebSub
+      topic: Tópico
+    title: Administración
+  admin_mailer:
+    new_report:
+      body: "%{reporter} ha reportado a %{target}"
+      subject: Nuevo reporte para la %{instance} (#%{id})
   application_mailer:
     settings: 'Cambiar preferencias de correo: %{link}'
     signature: Notificaciones de Mastodon desde %{instance}
     view: 'Vista:'
   applications:
+    created: Aplicación creada exitosamente
+    destroyed: Apicación eliminada exitosamente
     invalid_url: La URL proporcionada es incorrecta
+    regenerate_token: Regenerar token de acceso
+    token_regenerated: Token de acceso regenerado exitosamente
+    warning: Ten mucho cuidado con estos datos. ¡No los compartas con nadie!
+    your_token: Tu token de acceso
   auth:
+    agreement_html: Al registrarte aceptas <a href="%{rules_path}">nuestros y términos y condiciones del servicio</a> y <a href="%{terms_path}">nuestras políticas de privacidda</a>.
     change_password: Cambiar contraseña
+    delete_account_html: Si desea eliminar su cuenta, puede <a href="%{path}">proceder aquí</a>. Será pedido de una confirmación.
     didnt_get_confirmation: "¿No recibió el correo de confirmación?"
     forgot_password: "¿Olvidaste tu contraseña?"
+    invalid_reset_password_token: El token de reinicio de contraseña es inválido o expiró. Por favor pide uno nuevo.
     login: Iniciar sesión
     logout: Cerrar sesión
     register: Registrarse
@@ -43,7 +259,13 @@ es:
   authorize_follow:
     error: Desafortunadamente, ha ocurrido un error buscando la cuenta remota
     follow: Seguir
-    title: Seguir %{acct}
+    follow_request: 'Tienes una solicitud de seguimiento de:'
+    following: "¡Éxito! Ahora estás siguiendo a:"
+    post_follow:
+      close: O, puedes simplemente cerrar esta ventana.
+      return: Regresar al perfil del usuario
+      web: Ir al sitio web
+    title: Seguir a %{acct}
   datetime:
     distance_in_words:
       about_x_hours: "%{count}h"
@@ -58,14 +280,43 @@ es:
       x_minutes: "%{count}m"
       x_months: "%{count}m"
       x_seconds: "%{count}s"
+  deletes:
+    bad_password_msg: "¡Buen intento, hackers! Contraseña incorrecta"
+    confirm_password: Ingresa tu contraseña actual para demostrar tu identidad
+    description_html: Esto removerá el contenido de tu cuenta y la desactivará <strong>permanente e irrevesiblemente</strong>. Tu nombre de usuario quedará reservado para prevenir futuros robos de identidad.
+    proceed: Eliminar cuenta
+    success_msg: Tu cuenta se eliminó con éxito
+    warning_html: Se garantiza únicamente la eliminación del contenido de esta instancia. El contenido que se haya compartido extensamente dejará sus huellas. Los servidores fuera de línea y los que se hayan desuscrito de tus actualizaciones ya no actualizarán sus bases de datos.
+    warning_title: Disponibilidad diseminada del contenido
+  errors:
+    '403': No tienes permiso para acceder a esta página.
+    '404': La página que estabas buscando no existe.
+    '410': La página que estabas buscando no existe más.
+    '422':
+      content: Verificación de seguridad fallida. ¿Estás bloqueando algunas cookies?
+      title: Verificación de seguridad fallida
+    '429': Asfixiado
+    noscript_html: Para usar la aplicación web de Mastodon, por favor activa Javascript. Alternativamente, prueba alguna de las <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">aplicaciones nativas</a> para Mastodon para tu plataforma.
   exports:
     blocks: Personas que has bloqueado
     csv: CSV
     follows: Personas que sigues
     storage: Almacenamiento
+  followers:
+    domain: Dominio
+    explanation_html: Si deseas asegurar la privacidad de tus estados, tienes que cuidarte de quién te sigue. <strong>Tus estados privados son enviados a todas las instancias de tus seguidores</strong>. Puede que desees revisarlas, y remover seguidores si no confías en tu privacidad para ser respetado por el staff o software de esas instancias.
+    followers_count: Número de seguidores
+    lock_link: Bloquear tu cuenta
+    purge: Remover de los seguidores
+    success:
+      one: En el proceso de bloquear suavemente usuarios de un solo dominio...
+      other: En el proceso de bloquear suavemente usuarios de %{count} dominios...
+    true_privacy_html: Por favor ten en cuenta que <strong>la verdadera privacidad se consigue con encriptación de punto a punto</strong>.
+    unlocked_warning_html: Todos pueden seguirte  para ver tus estados privados inmediatamente. %{lock_link} para poder chequear y rechazar seguidores.
+    unlocked_warning_title: Tu cuenta no está bloqueada
   generic:
     changes_saved_msg: "¡Cambios guardados con éxito!"
-    powered_by: powered by %{link}
+    powered_by: gracias a %{link}
     save_changes: Guardar cambios
     validation_errors:
       one: "¡Algo no está bien! Por favor, revisa el error"
@@ -76,9 +327,10 @@ es:
     types:
       blocking: Lista de bloqueados
       following: Lista de seguidos
+      muting: Lista de silenciados
     upload: Cargar
   landing_strip_html: "<strong>%{name}</strong> es un usuario en %{link_to_root_path}. Puedes seguirlo(a) o interactuar con el o ella si tienes una cuenta en cualquier parte del fediverse."
-  landing_strip_signup_html: Si no tienes una, puedes <a href="%{sign_up_path}">registrar aquí</a>.
+  landing_strip_signup_html: Si no tienes una, puedes <a href="%{sign_up_path}">registrarte aquí</a>.
   media_attachments:
     validations:
       images_and_video: No se puede adjuntar un video a un estado que ya contenga imágenes
@@ -88,8 +340,8 @@ es:
       body: 'Un resumen de lo que te perdiste en %{instance} desde tu última visita el %{since}:'
       mention: "%{name} te ha mencionado en:"
       new_followers_summary:
-        one: "¡Hurra!. Alguien más te ha comenzado a seguir"
-        other: "¡Genial!. Te han seguido %{count} nuevas personas"
+        one: "¡Hurra! Alguien más te ha comenzado a seguir"
+        other: "¡Genial! Te han seguido %{count} nuevas personas"
       subject:
         one: "1 nueva notificación desde tu última visita \U0001F418"
         other: "%{count} nuevas notificaciones desde tu última visita \U0001F418"
@@ -122,11 +374,65 @@ es:
   pagination:
     next: Próximo
     prev: Anterior
+    truncate: "&hellip;"
+  push_notifications:
+    favourite:
+      title: "%A {name} le gustó tu estado"
+    follow:
+      title: "%{name} te ha empezado a seguir"
+    group:
+      title: "%{count} notificaciones"
+    mention:
+      action_boost: Boost
+      action_expand: Mostrar más
+      action_favourite: Me Gusta
+      title: "%{name} te mencionó"
+    reblog:
+      title: "%{name} boosteó tu estado"
   remote_follow:
     acct: Ingesa el usuario@dominio de la persona que quieres seguir
     missing_resource: No se pudo encontrar la URL de redirección necesaria para su cuenta.
     proceed: Proceder a seguir
     prompt: 'Vas a seguir a:'
+  sessions:
+    activity: Última actividad
+    browser: Navegador
+    browsers:
+      alipay: Alipay
+      blackberry: Blackberry
+      chrome: Chrome
+      edge: Microsoft Edge
+      firefox: Firefox
+      generic: Desconocido
+      ie: Internet Explorer
+      micro_messenger: MicroMessenger
+      nokia: Nokia S40 Ovi Browser
+      opera: Opera
+      phantom_js: PhantomJS
+      qq: QQ Browser
+      safari: Safari
+      uc_browser: UCBrowser
+      weibo: Weibo
+    current_session: Sesión actual
+    description: "%{browser} en %{platform}"
+    explanation: Estos son los navegadores web conectados actualmente en tu cuenta de Mastodon.
+    ip: IP
+    platforms:
+      adobe_air: Adobe Air
+      android: Android
+      blackberry: Blackberry
+      chrome_os: ChromeOS
+      firefox_os: Firefox OS
+      ios: iOS
+      linux: Linux
+      mac: Mac
+      other: Desconocido
+      windows: Windows
+      windows_mobile: Windows Mobile
+      windows_phone: Windows Phone
+    revoke: Revoke
+    revoke_success: Sesión revocada exitosamente
+    title: Sesiones
   settings:
     authorized_apps: Aplicaciones autorizadas
     back: Volver al inicio
@@ -136,26 +442,115 @@ es:
     preferences: Preferencias
     settings: Ajustes
     two_factor_authentication: Autenticación de dos factores
+    your_apps: Tus aplicaciones
   statuses:
     open_in_web: Abrir en web
     over_character_limit: Límite de caracteres de %{max} superado
+    pin_errors:
+      limit: Demasiados toots fijados
+      ownership: El toot de alguien más no puede fijarse
+      private: Los toots no-públicos no pueden fijarse
+      reblog: Un boost no puede fijarse
     show_more: Mostrar más
     visibilities:
       private: Sólo mostrar a seguidores
+      private_long: Solo mostrar a tus seguidores
       public: Público
+      public_long: Todos pueden ver
       unlisted: Público, pero no mostrar en la historia federada
+      unlisted_long: Todos pueden ver, pero no está listado en las líneas de tiempo públicas
   stream_entries:
     click_to_show: Click para mostrar
+    pinned: Toot fijado
     reblogged: retooteado
     sensitive_content: Contenido sensible
+  terms:
+    body_html: |
+      <h2>Políticas de privacidad</h2>
+
+      <h3 id="collect">¿Qué información recolectamos?</h3>
+
+      <p>Recolectamos información tuya cuando te registras en nuestro sitio y adquirimos datos cuando participas en el foro leyendo, escribiendo, y evaluando el contenido compartido aquí.</p>
+
+      <p>Cuando te registras en nuestro sitio, puede que se te pida tu nombre y dirección de correo electrónico. De todas formas, puedes visitar nuestro sitio sin registrarte. Tu dirección de correo electrónico será verificada por un e-mail conteniendo un enlace único. Si ese enlace es visitado, sabemos que tú controlas esa dirección.</p>
+
+      <p>Cuando te registras y posteas, grabamos la IP de la que se origina esa acción. También puede que retengamos logs del servidor, que incluyen la dirección IP de todos los pedidos a nuestro servidor.</p>
+
+      <h3 id="use">¿Para qué usamos tu información?</h3>
+
+      <p>Toda la información que recolectamos de ti puede usarse en una de las siguientes maneras:</p>
+
+      <ul>
+        <li>Para personalizar tu experiencia &mdash; tu información nos ayuda a responder mejor tus necesidades individuales.</li>
+        <li>Para mejorar nuestro sitio &mdash; nos esforzamos continuamente en mejorar nuestras ofertas del sitio basándonos en la información y apoyo que recibimos de ti.</li>
+        <li>Para mejorar el servicio al cliente &mdash; tu información nos ayuda a responder más efectivamente al servicio al cliente y otras necesidades.</li>
+        <li>Para enviar e-mails periódicos &mdash; la dirección de e-mail que provees puede usarse para enviarte información, notificaciones que pides sobre cambios en tópicos o en respuesta a tu nombre de usuario, responder consultas, y/u otros pedidos o preguntas.</li>
+      </ul>
+
+      <h3 id="protect">¿Cómo protegemos tu información?</h3>
+
+      <p>Implementamos una variedad de medidas de seguridad para mantener a salvo tu información personal cuando entras, publicas, o accesas a ella.</p>
+
+      <h3 id="data-retention">¿Cuáles son sus políticas de retención de datos?</h3>
+
+      <p>Haremos un gran esfuerzo en:</p>
+
+      <ul>
+        <li>Retener logs del servidor conteniendo la dirección IP de todos los pedidos a este servidor en no más de 90 días.</li>
+        <li>Retener las direcciones IP asociadas con usuarios registrados y sus posts no más de 5 años.</li>
+      </ul>
+
+      <h3 id="cookies"¿Usamos cookies?</h3>
+
+      <p>Sí. Las cookies son pequeños archivos que un sitio web o su proveedor de servicio transfieren al disco duro de tu computadora a través de tu navegador web (si se le permite). Estas cookies permiten al sitio reconocer tu navegador y, si y tienes una cuenta registrada, asociarlo con ella.</p>
+
+      <p>Usamos cookies para entender y guardar tus preferencias para futuras visitas y agregar datos compilados sobre el tráfico del sitio e interacción para que podamos ofrecer una mejor experiencia y herramientas en el futuro. Puede que contratemos con proveedores de servicio de tercera mano para que nos asistan en el mejor entendimiento de nuestros visitantes del sitio. A estos proveedores de servicio no se les permite usar la información recolectada a nuestras espaldas excepto para ayudarnos a conducir y mejorar nuestro trabajo.</p>
+
+      <h3 id="disclose">¿Revelamos alguna información a terceras manos?</h3>
+
+      <p>No vendemos, intercambiamos, ni de ninguna otra manera transferimos tu información personal identificable a terceras partes. Esto no incluye las terceras manos que nos asisten en operar nuestro sitio, conducción o trabajo, o en servirte, tanto como que éstas acepten en mantener esta información confidencial. Puede que también liberemos tu información cuando creamos que es apropiado para cumplir con la ley, enforzar nuestras políticas del sitio, o proteger la nuestra u otros derechos, propiedad, o seguridad. De todas formas, la información del visitante autorizado no-personal puede proveerse a otras partes por marketing, publicidad, u otros usos.</p>
+
+      <h3 id="third-party">Enlaces de terceras partes</h3>
+
+      <p>Ocasionalmente, a nuestra discreción, puede que incluyamos u ofrezcamos productos de terceras partes o servicios en nuestro sitio. Estas terceras partes tienen políticas de privacidad separadas e independientes. Por lo tanto no tenemos responsabilidad u obligación por el contenido y actividades de estos sitios enlazados. Sin embargo, buscamos proteger la integridad de nuestro sitio y dar la bienvenida a cualquier ayuda sobre estos sitios.</p>
+
+      <h3 id="coppa">Children's Online Privacy Protection Act Compliance (Cumplimiento de la Ley de la Protección Privada en Línea del Niño)</h3>
+
+      <p>Nuestro sitio y todos nuestros productos y servicios están dirigidos a gente que tiene al menos 13 años de edad. Si el servidor está alojado en EE.UU, y tienes menos de 13 años, no uses este sitio por los requerimientos del COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>).</p>
+
+      <h3 id="online">Solo Políticas de Privacidad en Línea</h3>
+
+      <p>Estas políticas de privacidad aplican únicamente a la información recolectada a través de nuestro sitio y no a información recolectada <i>offline</i>.</p>
+
+      <h3 id="consent">Tu Consentimiento</h3>
+
+      <p>Al usar nuestro sitio, estás consentido a nuestras políticas de privacidad del sitio.</p>
+
+      <h3 id="changes">Cambios a nuestras Políticas de Privacidad</h3>
+
+      <p>Si decidimos cambiar nuestras políticas de privacidad, las publicaremos en esta página.</p>
+
+      <p>Este documento está publicado bajo la licencia CC-BY-SA. Última vez actualizado el 31 de Mayo del 2013.</p>
+
+      <p>Adaptado originalmente del <a href="https://github.com/discourse/discourse">discurso de las políticas de privacidad</a>.</p>
+    title: Términos del Servicio y Políticas de Privacidad de %{instance}
   time:
     formats:
-      default: "%b %d, %Y, %H:%M"
+      default: "%d de %b del %Y, %H:%M"
   two_factor_authentication:
-    description_html: Sí habilitas la <strong>autenticación de dos factores</strong>, se requerirá estar en posesión de su teléfono, lo que generará tokens para que usted pueda iniciar sesión.
+    code_hint: Ingresa el código generado por tu aplicación de autenticación para confirmar
+    description_html: Si habilitas la <strong>autenticación de dos factores</strong>, se requerirá estar en posesión de su teléfono, lo que generará tokens para que usted pueda iniciar sesión.
     disable: Deshabilitar
     enable: Habilitar
+    enabled_success: Verificación de dos factores activada exitosamente
+    generate_recovery_codes: generar códigos de recuperación
     instructions_html: "<strong>Escanea este código QR desde Google Authenticator o una aplicación similar en su teléfono</strong>. Desde ahora, esta aplicación va a generar tokens que tienes que ingresar cuando quieras iniciar sesión."
+    lost_recovery_codes: Los códigos de recuperación te permiten obtener acceso a tu cuenta si pierdes tu teléfono. Si has perdido tus códigos de recuperación, puedes regenerarlos aquí. Tus viejos códigos de recuperación se harán inválidos.
+    recovery_codes: Hacer copias de seguridad de tus códigos de recuperación
+    recovery_instructions_html: Si pierdes acceso a tu teléfono, puedes usar uno de los siguientes códigos de recuperación para obtener acceso a tu cuenta. <strong>Mantenlos a salvo</strong>. Por ejemplo, puedes imprimirlos y guardarlos con otros documentos importantes.
+    setup: Configurar
+    wrong_code: "¡El código ingresado es inválido! ¿El dispositivo y tiempo del servidor están correctos?"
   users:
     invalid_email: La dirección de correo es incorrecta
     invalid_otp_token: Código de dos factores incorrecto
+    signed_in_as: 'Sesión iniciada como:'
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index ba726fc75..f7921b1cf 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -37,12 +37,16 @@ fa:
     follow: پی بگیرید
     followers: پیگیران
     following: پی می‌گیرد
+    media: عکس و ویدیو
     nothing_here: این‌جا چیزی نیست!
     people_followed_by: کسانی که %{name} پی می‌گیرد
     people_who_follow: کسانی که %{name} را پی می‌گیرند
-    posts: نوشته
+    posts: نوشته‌ها
+    posts_with_replies: نوشته‌ها و پاسخ‌ها
     remote_follow: پیگیری غیرمستقیم
     reserved_username: این نام کاربری در دسترس نیست
+    roles:
+      admin: مدیر
     unfollow: پایان پیگیری
   admin:
     accounts:
@@ -56,7 +60,9 @@ fa:
       email: ایمیل
       feed_url: نشانی فید
       followers: پیگیران
+      followers_url: نشانی پیگیران
       follows: پی می‌گیرد
+      inbox_url: نشانی صندوق ورودی
       ip: IP
       location:
         all: همه
@@ -76,8 +82,10 @@ fa:
         alphabetic: الفبایی
         most_recent: تازه‌ترین‌ها
         title: ترتیب
+      outbox_url: نشانی صندوق خروجی
       perform_full_suspension: انجام تعلیق کامل
       profile_url: نشانی نمایه
+      protocol: پروتکل
       public: عمومی
       push_subscription_expires: عضویت از راه PuSH منقضی شد
       redownload: به‌روزرسانی تصویر نمایه
@@ -86,6 +94,7 @@ fa:
       resubscribe: اشتراک دوباره
       salmon_url: نشانی Salmon
       search: جستجو
+      shared_inbox_url: نشانی صندوق ورودی مشترک
       show:
         created_reports: گزارش‌ها از طرف این حساب
         report: گزارش
@@ -160,6 +169,9 @@ fa:
       unresolved: حل‌نشده
       view: نمایش
     settings:
+      bootstrap_timeline_accounts:
+        desc_html: نام‌های کاربری را با ویرگول از هم جدا کنید. تنها حساب‌های محلی و قفل‌نشده کار می‌کنند. اگر این‌جا را خالی بگذارید، به طور پیش‌فرض همهٔ مدیرهای این سرور پی‌گرفته خواهند شد.
+        title: پیگیری‌های پیش‌فرض برای کاربران تازه
       contact_information:
         email: ایمیل کاری
         username: نام کاربری
@@ -183,6 +195,9 @@ fa:
         desc_html: می‌توانید سیاست رازداری، شرایط استفاده، یا سایر مسائل قانونی را به دلخواه خود بنویسید. تگ‌های HTML هم مجاز است
         title: شرایط استفادهٔ سفارشی
       site_title: نام سرور
+      thumbnail:
+        desc_html: برای دیدن با OpenGraph و رابط برنامه‌نویسی. وضوح پیشنهادی ۱۲۰۰×۶۳۰ پیکسل
+        title: تصویر کوچک فوری
       timeline_preview:
         desc_html: نوشته‌های عمومی این سرور را در صفحهٔ آغازین نشان دهید
         title: پیش‌نمایش نوشته‌ها
@@ -220,7 +235,13 @@ fa:
     signature: اعلان‌های ماستدون از %{instance}
     view: 'نمایش:'
   applications:
+    created: برنامه با موفقیت ساخته شد
+    destroyed: برنامه با موفقیت پاک شد
     invalid_url: نشانی واردشده معتبر نیست
+    regenerate_token: دوباره‌سازی کد دسترسی
+    token_regenerated: کد دسترسی با موفقیت ساخته شد
+    warning: خیلی مواظب این اطلاعات باشید و آن را به هیچ کس ندهید!
+    your_token: کد دسترسی شما
   auth:
     agreement_html: پیش از عضو شدن باید <a href="%{rules_path}">شرایط استفاده</a> و <a href="%{terms_path}">سیاست رازداری</a> ما را بپذیرید.
     change_password: امنیت
@@ -228,6 +249,7 @@ fa:
     delete_account_html: اگر می‌خواهید حساب خود را پاک کنید، از <a href="%{path}">این‌جا</a> پیش بروید. از شما درخواست تأیید خواهد شد.
     didnt_get_confirmation: راهنمایی برای تأیید را دریافت نکردید؟
     forgot_password: رمزتان را گم کرده‌اید؟
+    invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
     login: ورود
     logout: خروج
     register: عضو شوید
@@ -416,6 +438,7 @@ fa:
     authorized_apps: برنامه‌های مجاز
     back: بازگشت به ماستدون
     delete: پاک‌کردن حساب
+    development: Development
     edit_profile: ویرایش نمایه
     export: برون‌سپاری داده‌ها
     followers: پیگیران مورد تأیید
@@ -423,9 +446,15 @@ fa:
     preferences: ترجیحات
     settings: تنظیمات
     two_factor_authentication: ورود دومرحله‌ای
+    your_apps: برنامهٔ شما
   statuses:
     open_in_web: بازکردن در وب
     over_character_limit: از حد مجاز %{max} حرف فراتر رفتید
+    pin_errors:
+      limit: نوشته‌های ثابت بیش از حد
+      ownership: نوشته‌های دیگران را نمی‌توان ثابت کرد
+      private: نوشته‌های غیرعمومی را نمی‌توان ثابت کرد
+      reblog: بازبوق‌ها را نمی‌توان ثابت کرد
     show_more: نمایش
     visibilities:
       private: خصوصی
@@ -436,6 +465,7 @@ fa:
       unlisted_long: عمومی، ولی در فهرست نوشته‌ها نمایش نمی‌یابد
   stream_entries:
     click_to_show: برای نمایش کلیک کنید
+    pinned: نوشته‌های ثابت
     reblogged: بازبوقید
     sensitive_content: محتوای حساس
   terms:
@@ -530,3 +560,4 @@ fa:
   users:
     invalid_email: نشانی ایمیل نامعتبر است
     invalid_otp_token: کد ورود دومرحله‌ای نامعتبر است
+    signed_in_as: 'واردشده به نام:'
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index fcaa64dab..f7170f6a3 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -60,6 +60,7 @@ ja:
       email: E-mail
       feed_url: フィードURL
       followers: フォロワー数
+      followers_url: Followers URL
       follows: フォロー数
       inbox_url: Inbox URL
       ip: IP
@@ -93,6 +94,7 @@ ja:
       resubscribe: 再講読
       salmon_url: Salmon URL
       search: 検索
+      shared_inbox_url: Shared Inbox URL
       show:
         created_reports: このアカウントで作られたレポート
         report: レポート
@@ -141,6 +143,8 @@ ja:
     instances:
       account_count: 既知のアカウント数
       domain_name: ドメイン名
+      reset: リセット
+      search: 検索
       title: 既知のインスタンス
     reports:
       action_taken_by: レポート処理者
@@ -193,6 +197,9 @@ ja:
         desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます
         title: カスタム利用規約
       site_title: インスタンスの名前
+      thumbnail:
+        desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です。
+        title: インスタンスのサムネイル
       timeline_preview:
         desc_html: ランディングページに公開タイムラインを表示します
         title: タイムラインプレビュー
@@ -445,6 +452,7 @@ ja:
     open_in_web: Webで開く
     over_character_limit: 上限は %{max}文字までです
     pin_errors:
+      limit: 固定されているトゥートが多すぎます
       ownership: 他人のトゥートを固定することはできません
       private: 非公開のトゥートを固定することはできません
       reblog: ブーストされたトゥートを固定することはできません
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 2b7a1a511..06035b6c5 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -41,8 +41,11 @@ nl:
     people_followed_by: Mensen die %{name} volgt
     people_who_follow: Mensen die %{name} volgen
     posts: Toots
+    posts_with_replies: Toots met reacties
     remote_follow: Extern volgen
     reserved_username: Deze gebruikersnaam is gereserveerd
+    roles:
+      admin: Beheerder
     unfollow: Ontvolgen
   admin:
     accounts:
@@ -56,7 +59,9 @@ nl:
       email: E-mail
       feed_url: Feed-URL
       followers: Volgers
+      followers_url: Volgers-URL
       follows: Volgt
+      inbox_url: Inbox-URL
       ip: IP
       location:
         all: Alles
@@ -86,6 +91,7 @@ nl:
       resubscribe: Opnieuw abonneren
       salmon_url: Salmon-URL
       search: Zoeken
+      shared_inbox_url: Gedeelde inbox-URL
       show:
         created_reports: Toots door dit account gerapporteerd
         report: gerapporteerd
@@ -160,6 +166,9 @@ nl:
       unresolved: Onopgelost
       view: Weergeven
     settings:
+      bootstrap_timeline_accounts:
+        desc_html: Meerdere gebruikersnamen met komma's scheiden. Alleen lokale en niet opgeschorte accounts werken. Laat leeg voor alle lokale beheerders.
+        title: Standaard te volgen accounts voor nieuwe gebruikers
       contact_information:
         email: Vul een openbaar gebruikt e-mailadres in
         username: Vul een gebruikersnaam in
@@ -183,6 +192,9 @@ nl:
         desc_html: Je kan hier jouw eigen privacybeleid, gebruikersvoorwaarden en ander juridisch jargon kwijt. Je kan HTML gebruiken.
         title: Aangepaste gebruikersvoorwaarden
       site_title: Naam Mastodon-server
+      thumbnail:
+        desc_html: Gebruikt als voorvertoning voor OpenGraph en de API. 1200x630px aanbevolen.
+        title: Thumbnail Mastodon-server
       timeline_preview:
         desc_html: Toon de openbare tijdlijn op de startpagina
         title: Voorbeeld tijdlijn
@@ -272,7 +284,7 @@ nl:
       content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies?
       title: Veiligheidsverificatie mislukt
     '429': Te veel verbindingsaanvragen.
-    noscript_html: Schakel JavaScript in om de webapplicatie van Mastodon te gebruiken. Als alternatief kan je een <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">Mastodon-app</a> zoeken voor jouw platform.
+    noscript_html: Schakel JavaScript in om de webapp van Mastodon te kunnen gebruiken. Als alternatief kan je een <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">Mastodon-app</a> zoeken voor jouw platform.
   exports:
     blocks: Jij blokkeert
     csv: CSV
@@ -412,6 +424,7 @@ nl:
     authorized_apps: Geautoriseerde apps
     back: Terug naar Mastodon
     delete: Account deletion
+    development: Ontwikkelaars
     edit_profile: Profiel bewerken
     export: Export
     followers: Geautoriseerde volgers
@@ -419,9 +432,15 @@ nl:
     preferences: Voorkeuren
     settings: Instellingen
     two_factor_authentication: Tweestapsverificatie
+    your_apps: Jouw toepassingen
   statuses:
     open_in_web: In de webapp openen
     over_character_limit: Limiet van %{max} tekens overschreden
+    pin_errors:
+      limit: Te veel toots vastgezet
+      ownership: Een toot van iemand anders kan niet worden vastgezet
+      private: Alleen openbare toots kunnen worden vastgezet
+      reblog: Een boost kan niet worden vastgezet
     show_more: Meer tonen
     visibilities:
       private: Alleen volgers
@@ -432,6 +451,7 @@ nl:
       unlisted_long: Aan iedereen tonen, maar niet op openbare tijdlijnen
   stream_entries:
     click_to_show: Klik om te tonen
+    pinned: Vastgemaakte toot
     reblogged: boostte
     sensitive_content: Gevoelige inhoud
   terms:
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index c3807428b..5d9506132 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -291,8 +291,8 @@ oc:
     - dissabte
     formats:
       default: "%e/%m/%Y"
-      long: Lo %e %B de %Y
-      short: "%e %b"
+      long: Lo %-d %b de %Y
+      short: "%e %B"
     month_names:
     - 
     - de genièr
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 88125f692..4dbbd7cb1 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -60,6 +60,7 @@ pl:
       email: Adres e-mail
       feed_url: Adres kanału
       followers: Śledzący
+      followers_url: Adres śledzących
       follows: Śledzeni
       ip: Adres IP
       location:
@@ -90,6 +91,7 @@ pl:
       resubscribe: Ponów subskrypcję
       salmon_url: Adres Salmon
       search: Szukaj
+      shared_inbox_url: Adres udostępnianej skrzynki
       show:
         created_reports: Zgłoszenia tego użytkownika
         report: zgłoszeń
@@ -165,6 +167,9 @@ pl:
       unresolved: Nierozwiązane
       view: Wyświetl
     settings:
+      bootstrap_timeline_accounts:
+        desc_html: Oddzielaj nazwy użytkowników przecinkami. Działa tylko dla niezablokowanych kont w obrębie instancji. Jeżeli puste, zostaną użyte konta administratorów instancji.
+        title: Domyślne obserwacje nowych użytkowników
       contact_information:
         email: Służbowy adres e-mail
         username: Nazwa użytkownika do kontaktu
@@ -188,6 +193,9 @@ pl:
         desc_html: Miejsce na własną politykę prywatności, zasady użytkowania i inne unormowania prawne. Możesz korzystać z tagów HTML
         title: Niestandardowe zasady użytkowania
       site_title: Nazwa instancji
+      thumbnail:
+        desc_html: 'Używana w podglądzie przez OpenGraph i API. Zalecany rozmiar: 1200x630 pikseli.'
+        title: Miniatura instancji
       timeline_preview:
         desc_html: Wyświetlaj publiczną oś czasu na stronie widocznej dla niezalogowanych
         title: Podgląd osi czasu
@@ -445,6 +453,7 @@ pl:
     open_in_web: Otwórz w przeglądarce
     over_character_limit: limit %{max} znaków przekroczony
     pin_errors:
+      limit: Nie możesz przypiąć więcej wpisów
       ownership: Nie możesz przypiąć cudzego wpisu
       private: Nie możesz przypiąć niepublicznego wpisu
       reblog: Nie możesz przypiąć podbicia wpisu
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 750120299..f2b46927b 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -1,52 +1,76 @@
 ---
 pt-BR:
   about:
-    about_mastodon_html: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Escolha um servidor que você confie &mdash; qualquer um que escolher, você poderá interagir com todo o resto. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas.
-    about_this: Sobre essa instância
-    closed_registrations: Registros estão fechadas para essa instância.
+    about_mastodon_html: Mastodon é uma rede social baseada em protocolos abertos e software gratuito e de código aberto. É descentralizada como e-mail.
+    about_this: Sobre
+    closed_registrations: Cadastros estão atualmente fechados nesta instância. No entanto! Você pode procurar uma instância diferente na qual possa criar uma conta e acessar a mesma rede por lá.
     contact: Contato
+    contact_missing: Não definido
+    contact_unavailable: N/A
     description_headline: O que é %{domain}?
     domain_count_after: outras instâncias
     domain_count_before: Conectado a
-    other_instances: Outras instâncias
-    source_code: Source code
-    status_count_after: status
-    status_count_before: Quem autorizou
+    extended_description_html: "<h3>Um bom lugar para as regras</h3> <p>A descrição extendida ainda não foi definida.</p>"
+    features:
+      humane_approach_body: Aprendendo com erros de outras redes, Mastodon tem como objetivo fazer decisões éticas de design para combater o desuso de redes sociais.
+      humane_approach_title: Uma abordagem mais humana
+      not_a_product_body: Mastodon não é uma rede comercial. Sem propagandas, coleta de dados, jardins fechados. Não há uma autoridade central.
+      not_a_product_title: Você é uma pessoa e não um produto
+      real_conversation_body: Com 500 caracteres à sua disposição e suporte para conteúdo granular e avisos de conteúdo, você pode se expressar da maneira que desejar.
+      real_conversation_title: Feito para conversas reais
+      within_reach_body: Vários apps para iOS, Android e outras plataformas graças a um ecossistema de API amigável para desenvolvedores proporcionam que você possa se manter atualizado sobre seus amigos de qualquer lugar.
+      within_reach_title: Sempre a seu alcance
+    find_another_instance: Encontre outra instância
+    generic_description: "%{domain} é um servidor na rede"
+    hosted_on: Mastodon hospedado em %{domain}
+    learn_more: Saiba mais
+    other_instances: Lista de instâncias
+    source_code: Código fonte
+    status_count_after: postagens
+    status_count_before: Autores de
     user_count_after: usuários
-    user_count_before: Lugar de
+    user_count_before: Casa de
+    what_is_mastodon: O que é Mastodon?
   accounts:
     follow: Seguir
     followers: Seguidores
     following: Seguindo
+    media: Mídia
     nothing_here: Não há nada aqui!
-    people_followed_by: Pessoas seguidas por %{name}
+    people_followed_by: Pessoas que %{name} segue
     people_who_follow: Pessoas que seguem %{name}
-    posts: Posts
-    remote_follow: Acesso remoto
-    unfollow: Unfollow
+    posts: Toots
+    posts_with_replies: Toots com respostas
+    remote_follow: Siga remotamente
+    reserved_username: Este usuário está reservado
+    roles:
+      admin: Administrador
+    unfollow: Deixar de seguir
   admin:
     accounts:
       are_you_sure: Você tem certeza?
-      confirm: Confirme
+      confirm: Confirmar
       confirmed: Confirmado
-      disable_two_factor_authentication: Desabilitar senha de 2 passos
-      display_name: Nome mostrado
-      domain: Domain
+      disable_two_factor_authentication: Desativar 2FA
+      display_name: Nome de exibição
+      domain: Domínio
       edit: Editar
       email: E-mail
-      feed_url: URL do Feed
+      feed_url: Feed URL
       followers: Seguidores
-      follows: Seguindo
+      follows: Segue
+      inbox_url: Inbox URL
+      ip: IP
       location:
         all: Todos
         local: Local
         remote: Remoto
-        title: Local
-      media_attachments: Mídia anexadas
+        title: Localização
+      media_attachments: Mídia(s) anexada(s)
       moderation:
         all: Todos
-        silenced: Silenciado
-        suspended: Supenso
+        silenced: Silenciados
+        suspended: Suspensos
         title: Moderação
       most_recent_activity: Atividade mais recente
       most_recent_ip: IP mais recente
@@ -55,206 +79,281 @@ pt-BR:
         alphabetic: Alfabética
         most_recent: Mais recente
         title: Ordem
-      perform_full_suspension: Fazer suspensão completa
+      outbox_url: Outbox URL
+      perform_full_suspension: Efetue suspensão total
       profile_url: URL do perfil
+      protocol: Protocolo
       public: Público
-      push_subscription_expires: PuSH subscription expires
-      reset_password: Resetar senha
+      push_subscription_expires: Inscrição PuSH expira
+      redownload: Atualizar avatar
+      reset: Anular
+      reset_password: Modificar senha
+      resubscribe: Reinscrever-se
       salmon_url: Salmon URL
+      search: Pesquisar
       show:
-        created_reports: Reports criados por esta conta
-        report: report
-        targeted_reports: Reports feitos sobre esta conta
+        created_reports: Relatórios criados por esta conta
+        report: relatórios
+        targeted_reports: Relatórios feitos sobre esta conta
       silence: Silêncio
-      statuses: Status
+      statuses: Postagens
+      subscribe: Inscrever-se
       title: Contas
-      undo_silenced: Desfazer silenciar
-      undo_suspension: Desfazer supensão
-      username: Usuário
+      undo_silenced: Retirar silêncio
+      undo_suspension: Retirar suspensão
+      unsubscribe: Desinscrever-se
+      username: Nome de usuário
       web: Web
     domain_blocks:
-      add_new: Adicionar nova
-      created_msg: Bloqueio do domínio está sendo processado
-      destroyed_msg: Bloqueio de domínio está sendo desfeito
+      add_new: Adicionar novo
+      created_msg: Bloqueio de domínio está sendo processado
+      destroyed_msg: Bloqueio de domínio desfeito
       domain: Domínio
       new:
         create: Criar bloqueio
-        hint: O bloqueio de dominio não vai previnir a criação de entradas no banco de dados, mas irá, retroativamente e automaticamente aplicar métodos de moderação específica nessas contas.
+        hint: O bloqueio de domínio não prevenirá a criação de entradas de contas na base de dados, mas vai reatroativa e automaticamente aplicar métodos específicos de moderação nestas contas.
         severity:
-          desc_html: "<strong>Silenciar</strong> irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. <strong>Supender</strong> irá remover todos o conteúdo das contas, mídia e dados do perfil."
-          silence: Silenciar
-          suspend: Suspender
+          desc_html: O <strong>Silêncio</strong> fará com que as postagens da conta fiquem invisíveis para qualquer um que não a esteja seguindo. A <strong>Suspensão</strong> removerá todo o conteúdo da conta, mídia e dados de perfil. Use <strong>Nenhum</strong> se você apenas deseja rejeitar arquivos de mídia.
+          noop: Nenhum
+          silence: Silêncio
+          suspend: Suspensão
         title: Novo bloqueio de domínio
       reject_media: Rejeitar arquivos de mídia
-      reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer o download de novos no futuro. Irrelevante em suspensões.
+      reject_media_hint: Remove arquivos de mídia armazenados localmente e recusa quaisquer outros no futuro. Irrelevante para suspensões.
       severities:
-        silence: Silenciar
-        suspend: Suspender
-      severity: Severidade
+        noop: Nenhum
+        silence: Silêncio
+        suspend: Suspensão
+      severity: Rigidez
       show:
         affected_accounts:
-          one: Uma conta no banco de dados afetada
-          other: "%{count} contas no banco de dados afetada"
+          one: Uma conta no banco de dados foi afetada
+          other: "%{count} contas no banco de dados foram afetadas"
         retroactive:
-          silence: Desilenciar todas as contas existentes nesse domínio
-          suspend: Desuspender todas as contas existentes nesse domínio
-        title: Desfazer bloqueio de domínio para %{domain}
-      title: Bloqueio de domínio
-      undo: Desfazer
+          silence: Retirar silêncio de todas as contas existentes neste domínio
+          suspend: Retirar suspensão de todas as contas neste domínio
+        title: Retirar bloqueio de domínio de %{domain}
+        undo: Retirar
+      title: Bloqueios de domínio
+      undo: Retirar
     instances:
       account_count: Contas conhecidas
       domain_name: Domínio
       title: Instâncias conhecidas
     reports:
+      action_taken_by: Ação realizada por
+      are_you_sure: Você tem certeza?
       comment:
-        label: Commentário
-        none: None
-      delete: Deletar
+        label: Comentário
+        none: Nenhum
+      delete: Excluir
       id: ID
-      mark_as_resolved: Marque como resolvido
-      report: 'Report #%{id}'
-      report_contents: Conteúdo
+      mark_as_resolved: Marcar como resolvido
+      nsfw:
+        'false': Mostrar mídias anexadas
+        'true': Esconder mídias anexadas
+      report: 'Reportar #%{id}'
+      report_contents: Conteúdos
       reported_account: Conta reportada
-      reported_by: Reportado por
+      reported_by: Reportada por
       resolved: Resolvido
-      silence_account: Conta silenciada
+      silence_account: Silenciar conta
       status: Status
-      suspend_account: Conta suspensa
-      target: Target
-      title: Reports
-      unresolved: Unresolved
-      view: View
+      suspend_account: Suspender conta
+      target: Alvo
+      title: Denúncias
+      unresolved: Não resolvido
+      view: Visualizar
     settings:
+      bootstrap_timeline_accounts:
+        desc_html: Separe nomes de usuário através de vírgulas. Funciona apenas com contas locais e destrancadas. O padrão quando vazio são todos os administradores locais.
+        title: Usuários a serem seguidos por padrão por novas contas
       contact_information:
-        email: Entre um endereço de email público
-        username: Entre com usuário
+        email: E-mail
+        username: Contate usuário
       registrations:
         closed_message:
-          desc_html: Mostrar na página inicial quando registros estão fecados<br/>Você pode usar tags HTML
-          title: Mensagem de registro fechados
+          desc_html: Exibido na página inicial quando cadastros estão fechados. Você pode usar tags HTML.
+          title: Mensagem de cadastros fechados
+        deletion:
+          desc_html: Permitir que qualquer um delete a sua conta
+          title: Exclusão aberta de contas
         open:
-          title: Aberto para registro
+          desc_html: Permitir que qualquer um crie uma conta
+          title: Cadastro aberto
       site_description:
-        desc_html: Mostrar como parágrafo e usado como meta tag.<br/>Vôce pode usar tags HTML, em particular <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
-        title: Descrição do site
+        desc_html: Parágrafo introdutório na página inicial e em meta tags. Você pode usar tags HTML, em especial <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
+        title: Descrição da instância
       site_description_extended:
-        desc_html: Mostrar na página de informação extendiada <br/>Você pode usar tags HTML
-        title: Descrição extendida do site
-      site_title: Título do site
-      title: Preferências do site
+        desc_html: Um ótimo lugar para seu código de conduta, regras, diretrizes e outras coisas para diferenciar a sua instância. Você pode usar tags HTML.
+        title: Informação estendida customizada
+      site_terms:
+        desc_html: Você pode escrever a sua própria política de privacidade, termos de serviço, entre outras coisas.Você pode usar tags HTML.
+        title: Termos de serviço customizados
+      site_title: Nome da instância
+      timeline_preview:
+        desc_html: Exibir a timeline pública na página inicial
+        title: Prévia da timeline
+      title: Configurações do site
+    statuses:
+      back_to_account: Voltar para página da conta
+      batch:
+        delete: Deletar
+        nsfw_off: NSFW OFF
+        nsfw_on: NSFW ON
+      execute: Executar
+      failed_to_execute: Falha em executar
+      media:
+        hide: Esconder mídia
+        show: Mostrar mídia
+        title: Mídia
+      no_media: Não há mídia
+      title: Postagens da conta
+      with_media: Com mídia
     subscriptions:
-      callback_url: URL de Callback
+      callback_url: Callback URL
       confirmed: Confirmado
       expires_in: Expira em
       last_delivery: Última entrega
       title: WebSub
       topic: Tópico
     title: Administração
+  admin_mailer:
+    new_report:
+      body: "%{reporter} reportou %{target}"
+      subject: Nova denúncia sobre %{instance} (#%{id})
   application_mailer:
-    settings: 'Mudar preferências de email: %{link}'
-    signature: notificações Mastodon de %{instance}
-    view: 'View:'
+    salutation: "%{name},"
+    settings: 'Change e-mail preferences: %{link}'
+    signature: Notificações do Mastodon de %{instance}
+    view: 'Visualizar:'
   applications:
-    invalid_url: URL dada é inválida
+    created: Aplicação criada com sucesso
+    destroyed: Aplicação excluída com sucesso
+    invalid_url: A URL provida é inválida
+    regenerate_token: Regenerar token de acesso
+    token_regenerated: Token de acesso renegerado com sucesso
+    warning: Tenha cuidado com estes dados. Nunca compartilhe com alguém!
+    your_token: Seu token de acesso
   auth:
-    change_password: Mudar senha
+    agreement_html: Cadastrando-se você concorda com <a href="%{rules_path}">nossos termos de serviço</a> e <a href="%{terms_path}">política de privacidade</a>.
+    change_password: Segurança
+    delete_account: Excluir conta
+    delete_account_html: Se você deseja excluir a sua conta, você pode <a href="%{path}">prosseguir para cá</a>. Uma confirmação será requisitada.
     didnt_get_confirmation: Não recebeu instruções de confirmação?
-    forgot_password: Esqueceu a senha?
+    forgot_password: Esqueceu a sua senha?
+    invalid_reset_password_token: Token de modificação de senha é inválido ou expirou. Por favor, requisite um novo.
     login: Entrar
     logout: Sair
-    register: Registar
+    register: Cadastrar-se
     resend_confirmation: Reenviar instruções de confirmação
-    reset_password: Resetar senha
-    set_new_password: Editar password
+    reset_password: Modificar senha
+    set_new_password: Definir uma nova senha
   authorize_follow:
-    error: Infelizmente houve um erro olhando uma conta remota
+    error: Infelizmente, ocorreu um erro quando visualizando a conta remota.
     follow: Seguir
+    follow_request: 'Você mandou uma solicitação de seguidor para:'
+    following: 'Sucesso! Você agora está seguindo:'
+    post_follow:
+      close: Ou você pode simplesmente fechar esta janela.
+      return: Retornar ao perfil do usuário
+      web: Voltar para a página inicial
     title: Seguir %{acct}
   datetime:
     distance_in_words:
       about_x_hours: "%{count}h"
-      about_x_months: "%{count}mo"
-      about_x_years: "%{count}y"
-      almost_x_years: "%{count}y"
+      about_x_months: "%{count} meses"
+      about_x_years: "%{count} anos"
+      almost_x_years: "%{count} anos"
       half_a_minute: Agora
-      less_than_x_minutes: "%{count}m"
+      less_than_x_minutes: "%{count} meses"
       less_than_x_seconds: Agora
-      over_x_years: "%{count}y"
-      x_days: "%{count}d"
-      x_minutes: "%{count}m"
-      x_months: "%{count}mo"
-      x_seconds: "%{count}s"
+      over_x_years: "%{count} anos"
+      x_days: "%{count} dias"
+      x_minutes: "%{count} minutos"
+      x_months: "%{count} meses"
+      x_seconds: "%{count} segundos"
+  deletes:
+    bad_password_msg: Boa tentativa, hackers! Senha incorreta.
+    confirm_password: Insira a sua senha atual para verificar a sua identidade
+    description_html: Isto vai <strong>permanente e irreversivelmente</strong> remover conteúdo de sua conta e desativá-la. O seu nome de usuário permanecerá reservado para previnir futuros roubos de identidade.
+    proceed: Excluir conta
+    success_msg: A sua conta foi excluída com sucesso
+    warning_html: Apenas a exclusão de conteúdo desta instância em particular é garantida. Conteúdo que tenha sido largamente compartilhado muito provavelmente deixará traços. Servidores offline e servidores que se desinscreveram de suas atualizações não irão atualizar as suas bases de dados.
+    warning_title: Disponibilidade de conteúdo disseminado
   errors:
-    '403': Você não tem permissão para ver essa página.
-    '404': A página que você procura não existe.
-    '410': A página que você procura não existe mais.
+    '403': Você não tem permissão para ver esta página.
+    '404': A página pela qual você está procurando não existe.
+    '410': A página pela qual você está procurando não existe mais.
     '422':
-      content: Verificação de segurança falhou. Você está bloqueando cookies?
-      title: Verificação de segurança falhou
+      content: A verificação de segurança falhou. Você desativou o uso de cookies?
+      title: Falha na verificação de segurança
+    '429': Muitas requisições
+    noscript_html: Para usar o aplicativo web do Mastodon, por favor ative o JavaScript. Alternativamente, experimente um dos <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md">apps nativos</a> para o Mastodon para a sua plataforma.
   exports:
-    blocks: Você bloqueia
+    blocks: Você bloqueou
     csv: CSV
     follows: Você segue
-    mutes: Você selencia
-    storage: Mídia de dados
+    mutes: Você silenciou
+    storage: Armazenamento de mídia
   followers:
     domain: Domínio
-    explanation_html: Se você quer garantir a privacidade doe seu status, você precisa saber quem te segue. <strong>Seu status privado é enviado a todas as instancias que você tem seguidores</strong>. Você pode querer reavaliar e remover os seguidores que você não confia que sua privacidade vai ser mantida pelos administradores ou softwares de outras instancias.
-    followers_count: Númbero de seguidores
-    lock_link: Bloquear sua conta
-    purge: Remove dos seguidores
+    explanation_html: Se você quer garantir a privacidade de suas postagens, você deve ficar atento a quem está te seguindo.<strong>Suas postagens privadas são enviadas para todas as instâncias em que você tem seguidores</strong>. Convém revisá-las e remover seguidores se você acredita que a sua privacidade não será respeitada pela equipe ou software destas instâncias.
+    followers_count: Número de seguidores
+    lock_link: Tranque a sua conta
+    purge: Remover de seus seguidores
     success:
-      one: Em processo de bloquear seguidores de um domínio...
-      other: Em processo de bloqueio-leve dos seguidores de %{count} domínios...
-    true_privacy_html: Saiba que <strong>privaicade de verdade só pode ser atingida com criptografia ponto-a-ponto</strong>.
-    unlocked_warning_html: Qualquer um que te seguir para ver seus status privado imediatamente. %{lock_link} para poder rever e rejeitar seguidores.
-    unlocked_warning_title: Sua conta não está bloqueada
+      one: No processo de bloqueio suave de seguidores de outro domínio...
+      other: No processo de bloqueio suave de seguidores de outros %{count} domínios...
+    true_privacy_html: Lembre-se de que <strong>a verdadeira privacidade só pode ser alcançada através de encriptação ponto-a-ponto</strong>.
+    unlocked_warning_html: Qualquer pessoa pode te seguir e ver as suas postagens privadas. %{lock_link} para ser capaz de revisar e rejeitar seguidores.
+    unlocked_warning_title: A sua conta não está trancada
   generic:
-    changes_saved_msg: Mudanças guardadas!
+    changes_saved_msg: Mudanças salvas com sucesso!
     powered_by: powered by %{link}
-    save_changes: Guardar alterações
+    save_changes: Salvar mudanças
     validation_errors:
-      one: Algo não está correto. Por favor reveja o erro abaixo
-      other: Algo não está correto. Por favor reveja os %{count} erros abaixo
+      one: Algo não está certo! Por favor, reveja o erro abaixo
+      other: Algo não está certo! Por favor, reveja os %{count} erros abaixo
   imports:
-    preface: Você pode importar certos dados, como as pessoas que você segue ou estão bloqueadas para sua conta nessa instancia, de arquivos com dados criados por outra instancia.
-    success: Seus dados foram carregados com sucesso and serão processados em algum tempo
+    preface: Você pode importar dados que você exportou de outra instância, como a lista de pessoas que você segue ou bloqueou.
+    success: Os seus dados foram enviados com sucesso e serão processados em instantes
     types:
       blocking: Lista de bloqueio
-      following: Lista de seguidos
-      muting: Lista de silenciados
-    upload: Carregar
-  landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse."
-  landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
+      following: Pessoas que você segue
+      muting: Lista de silêncio
+    upload: Enviar
+  landing_strip_html: "<strong>%{name}</strong> é um usuário no %{link_to_root_path}. Você pode segui-lo ou interagir com ele se você tiver uma conta em qualquer lugar no fediverso."
+  landing_strip_signup_html: Se não, você pode <a href="%{sign_up_path}">se cadastrar aqui</a>.
   media_attachments:
     validations:
-      images_and_video: Cannot attach a video to a status that already contains images
-      too_many: Cannot attach more than 4 files
+      images_and_video: Não é possível anexar um vídeo a uma postagem que já contém imagens.
+      too_many: Não é possível anexar mais de quatro imagens.
   notification_mailer:
     digest:
-      body: 'Isto é um resumo do que você perdeu em %{instance} desde sua última visita %{since}:'
-      mention: "%{name} mencionou você em:"
+      body: 'Aqui está um resumo do que você perdeu no %{instance} desde o seu último acesso em %{since}:'
+      mention: "%{name} te mencionou em:"
       new_followers_summary:
-        one: Você tem um novo seguidor!
-        other: Você conseguiu %{count} novos seguidores! Incrível
+        one: Você tem um novo seguidor! Yay!
+        other: Você tem %{count} novos seguidores! Maravilha!
       subject:
-        one: "1 nova notificação desde sua última visita \U0001F418"
-        other: "%{count} novas notificações desde a última visita \U0001F418"
+        one: "Uma nova notificação desde o seu último acesso \U0001F418"
+        other: "%{count} novas notificações desde o seu último acesso \U0001F418"
     favourite:
-      body: 'O seu post foi favoritado por %{name}:'
-      subject: "%{name} favouritou o seu post"
+      body: 'Sua postagem foi favoritada por %{name}:'
+      subject: "%{name} favoritou a sua postagem"
     follow:
-      body: "%{name} seguiu você!"
-      subject: "%{name} segue você"
+      body: "%{name} está te seguindo!"
+      subject: "%{name} está te seguindo"
     follow_request:
-      body: "%{name} pediu para te seguir"
-      subject: 'Seguidor pendente: %{name}'
+      body: "%{name} requisitou autorização para te seguir"
+      subject: 'Pending follower: %{name}'
     mention:
       body: 'Você foi mencionado por %{name} em:'
-      subject: Foi mencionado por %{name}
+      subject: Você foi mencionado por %{name}
     reblog:
-      body: 'O seu post foi reblogado por %{name}:'
-      subject: "%{name} reblogou o seu post"
+      body: 'Sua postagem foi compartilhada por %{name}:'
+      subject: "%{name} compartilhou a sua postagem"
   number:
     human:
       decimal_units:
@@ -267,56 +366,190 @@ pt-BR:
           trillion: T
           unit: ''
   pagination:
-    next: Next
-    prev: Prev
+    next: Próximo
+    prev: Anterior
     truncate: "&hellip;"
+  push_notifications:
+    favourite:
+      title: "%{name} favoritou a sua postagem"
+    follow:
+      title: "%{name} está te seguindo"
+    group:
+      title: "%{count} notificações"
+    mention:
+      action_boost: Compartilhar
+      action_expand: Mostrar mais
+      action_favourite: Favoritar
+      title: "%{name} mencionou você"
+    reblog:
+      title: "%{name} compartilhou a sua postagem"
   remote_follow:
-    acct: Entre seu usuário@domínio do qual quer seguir
-    missing_resource: Não foi possível achar a URL de redirecionamento para sua conta
-    proceed: Prossiga para seguir
-    prompt: 'Você vai seguir:'
+    acct: Insira o seu usuário@domínio do qual você quer seguir
+    missing_resource: Não foi possível encontrar a URL de direcionamento para a sua conta
+    proceed: Prosseguir para seguir
+    prompt: 'Você irá seguir:'
+  sessions:
+    activity: Última atividade
+    browser: Navegador
+    browsers:
+      alipay: Alipay
+      blackberry: Blackberry
+      chrome: Chrome
+      edge: Microsoft Edge
+      firefox: Firefox
+      generic: Unknown browser
+      ie: Internet Explorer
+      micro_messenger: MicroMessenger
+      nokia: Nokia S40 Ovi Browser
+      opera: Opera
+      phantom_js: PhantomJS
+      qq: QQ Browser
+      safari: Safari
+      uc_browser: UCBrowser
+      weibo: Weibo
+    current_session: Sessão atual
+    description: "%{browser} em %{platform}"
+    explanation: Estes são os navegadores que estão conectados com a sua conta do Mastodon.
+    ip: IP
+    platforms:
+      adobe_air: Adobe Air
+      android: Android
+      blackberry: Blackberry
+      chrome_os: ChromeOS
+      firefox_os: Firefox OS
+      ios: iOS
+      linux: Linux
+      mac: Mac
+      other: unknown platform
+      windows: Windows
+      windows_mobile: Windows Mobile
+      windows_phone: Windows Phone
+    revoke: Revogar
+    revoke_success: Sessão revogada com sucesso
+    title: Sessões
   settings:
-    authorized_apps: Aplicativos autorizados
-    back: Voltar ao Mastodon
+    authorized_apps: Apps autorizados
+    back: Voltar para o Mastodon
+    delete: Exclusão de conta
+    development: Desenvolvimento
     edit_profile: Editar perfil
-    export: Importar dados
+    export: Exportar dados
     followers: Seguidores autorizados
     import: Importar
     preferences: Preferências
-    settings: Settings
-    two_factor_authentication: Autenticação Two-factor
+    settings: Configurações
+    two_factor_authentication: Autenticação em dois passos
+    your_apps: Seus aplicativos
   statuses:
-    open_in_web: Abrir no browser
-    over_character_limit: limite de caracter excedeu %{max}
+    open_in_web: Abrir na web
+    over_character_limit: limite de caracteres de %{max} excedido
+    pin_errors:
+      ownership: Toots de outras pessoas não podem ser fixados
+      private: Toot não-público não pode ser fixado
+      reblog: Um compartilhamento não pode ser fixado
     show_more: Mostrar mais
     visibilities:
-      private: Seguidores-apenas
+      private: Apenas seguidores
       private_long: Mostrar apenas para seguidores
       public: Público
-      public_long: Qualquer um pode ver
-      unlisted: Público, mas não mostre no timeline público
-      unlisted_long: Todo mundo pode ver mas não será listado nas timeline públicas
+      public_long: Todos podem ver
+      unlisted: Não listado
+      unlisted_long: Todos podem ver, porém não será postado nas timelines públicas
   stream_entries:
-    click_to_show: Clique pra mostrar
-    reblogged: boosted
+    click_to_show: Clique para mostrar
+    pinned: Toot fixado
+    reblogged: compartilhado
     sensitive_content: Conteúdo sensível
+  terms:
+    body_html: |
+      <h2>Política de privacidade</h2>
+
+      <h3 id="collect">Que informações nós coletamos?</h3>
+
+      <p>Coletamos informações quando você se cadastra em nosso site e capturamos dados quando você participa do fórum lendo, escrevendo e analisando o conteúdo aqui compartilhado.</p>
+
+      <p>Quando você se registrar em nosso site, será requisitado que você ceda seu nome e endereço de e-mail. Você pode, porém, visitar nosso site sem se cadastrar. Seu endereço de e-mail será verificado por uma mensagem contendo um link único. Se este link for visitado, saberemos que você controla este endereço de e-mail.</p>
+
+      <p>Quando registrado e postando, nós gravamos o endereço de IP de onde a postagem se originou. Nós também podemos reter logs de serviores que incluem o endereço de IP em cada requisição para o nosso servidor.</p>
+
+      <h3 id="use">Para que usamos essas informações?</h3>
+
+      <p>Quaisquer das informações que coletamos podem ser usadas das seguintes formas:</p>
+
+      <ul>
+        <li>Para personalizar a sua experiência &mdash; suas informações nos ajudam a nos adequar melhor às suas necessidades individuais.</li>
+        <li>Para melhorar nosso site &mdash; nós continuamente nos esforçamos para aprimorar nosso site baseados na informação e no feedback que recebemos de você.</li>
+        <li>Para aprimorar o serviço ao consumidor &mdash; suas informações nos ajudam a responder efetivamente às suas requisições e solicitações por suporte.</li>
+        <li>Para mandar e-mails periódicos &mdash; O endereço de e-mail que você forneceu pode ser usado para lhe enviar informações, notificações que você requisitar sobre mudanças a determinados tópicos ou menções ao seu nome de usuário, responder requisições e/ou solicitações e perguntas.</li>
+      </ul>
+
+      <h3 id="protect">Como protegemos as suas informações?</h3>
+
+      <p>Nós implementamos uma variedade de medidas de segurança para manter a segurança de suas informações pessoais quando você insere, submete ou acessa as suas informações pessoais.</p>
+
+      <h3 id="data-retention">Qual a sua política de retenção de dados?</h3>
+
+      <p>Faremos esforços de boa fé para:</p>
+
+      <ul>
+        <li>Reter logs de servidor contendo o endereço IP de todas as requisições a este servidor por não mais que 90 dias.</li>
+        <li>Reter os endereços IP associados a usuários cadastrados e suas postagens por não mais que 5 anos.</li>
+      </ul>
+
+      <h3 id="cookies">Nós usamos cookies?</h3>
+
+      <p>Sim. Cookies são pequenos arquivos que um site ou o provedor de serviço transfere para o armazenamento interno de seu computador através de seu navegador (se você permitir). Estes cookies habilitam o site para reconhecer o seu navegador e, se você ter um cadastro, associá-lo a esta conta.</p>
+
+      <p>Nós usamos cookies para entender e salvar as suas preferências para futuras visitas e compilar dados agregados sobre o tráfego do site para que possamos oferecer melhores experiências e ferramentas no futuro. Nós podemos contratar serviços de terceiros para nos auxiliar a entender melhor nossos visitantes. Estes provedores de serviço não são autoriza usar as informações coletadas em nosso nome exceto para nos ajudar a conduzir e aprimorar nosso funcionamento.</p>
+
+      <h3 id="disclose">Nós revelamos informações para terceiros?</h3>
+
+      <p>Nós não vendemos, tocamos ou transferimos para terceiros informações pessoais que te identificam. Isso não inclui partes em que confiamos para nos ajudar a operar nosso site, conduzir nosso funcionamento ou servir você desde que estes terceiros concordem em manter essas informações em segredo. Nós também podemos prover as suas informações para obedecer ordens judiciais, reforçar nossas políticas ou proteger nossos direitos ou de outrem, propriedades ou segurança. Entretanto, informações pessoais não identificáveis podem ser enviadas para outras partes para marketing, propaganda e outros usos.</p>
+
+      <h3 id="third-party">Links de terceiros</h3>
+
+      <p>Ocasionalmente, à nossa discrição, podemos icluir ou oferecer produtos ou serviços de terceiros em nosso site. Estes terceiros têm políticas de privacidade separadas e independentes. Nós, portanto, não nos responsabilizamos pelo conteúdo e atividades destes sites de terceiros. Occasionally, at our discretion, we may include or offer third party products or services on our site. Não obstante, nós procuramos proteger a integridade de nosso site e todo feedback sobre estes sites de terceiros é bem-vindo.</p>
+
+      <h3 id="coppa">Obediência ao Ato de Proteção da Privacidade Online de Crianças</h3>
+
+      <p>Nosso site, produtos e serviços são todos direcionados a pessoas que têm pelo menos 13 anos de idade. Se este servidor estiver nos EUA, e você tiver menos de 13 anos, pelos requerimentos da COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) não use este site.</p>
+
+      <h3 id="online">Política de Apenas Privacidade Online</h3>
+
+      <p>Esta política de privacidade online se aplica somente a informações coletadas por nosso site e não a informações coletadas offline.</p>
+
+      <h3 id="consent">Seu Consentimento</h3>
+
+      <p>Usando o nosso site, você concorda com a nossa política de privacidade.</p>
+
+      <h3 id="changes">Mudanças em nossa Política de Privacidade</h3>
+
+      <p>Se decidirmos mudar a nossa política de privacidade, publicaremos as mudanças nesta página.</p>
+
+      <p>Este documento é CC-BY-SA. A sua última atualização aconteceu em 31 de maio de 2013.</p>
+
+      <p>Originalmente adaptado da <a href="https://github.com/discourse/discourse">política de privacidade do Discourse</a>.</p>
+    title: "%{instance} Termos de Serviço e Política de Privacidade"
   time:
     formats:
-      default: "%d %b, %Y, %H:%M"
+      default: "%b %d, %Y, %H:%M"
   two_factor_authentication:
-    code_hint: Entre o código gerado pelo seu aplicativo para confirmar
-    description_html: Se você habilitar <strong>autenticação two-factor </strong>, quando logar será necessário o seu telefone que vai gerar os tokens usado.
-    disable: Disabilitar
-    enable: Habilitar
-    enabled_success: Autenticador Two-factor habilitador com sucesso
-    generate_recovery_codes: Gerar códigos para recuperar conta
-    instructions_html: "<strong>Scaneie este código QR no seu Google Authenticator ou aplicativo similar no seu telefone</strong>. A partir de agora seu aplicativo irá gerar tokens que deverão ser digitados para você logar."
-    lost_recovery_codes: Códigos de recuperação permite que você recupere o acesso a sua conta se você perder seu telefone. Se você perder os códigos de recuperação, você pode regera-los aqui. Seus códigos antigos serão invalidados.
-    manual_instructions: 'Se você não puder scanear o código QR e precisa digita-los manualmente, aqui está o segredo em texto.:'
-    recovery_codes_regenerated: Códigos de recuperação foram gerados com sucesso
-    recovery_instructions_html: Se algum dia você perder o acesso ao seu telefone, você pode usar um dos códigos de abaixo para recupera o acesso a sua conta. Guarde os códigos de acesso em local seguro, por exemplo imprimindo ou guardados com documentos importantes.
+    code_hint: Insira o código gerado pelo seu aplicativo auteticador para confirmar
+    description_html: Se você ativar a <strong>autenticação em dois passos</strong>, o acesso à sua conta exigirá posse de seu celular, que irá gerar tokens para validação.
+    disable: Desativar
+    enable: Ativar
+    enabled: A autenticação em dois passos está ativada
+    enabled_success: Autenticação em dois passos ativada com sucesso
+    generate_recovery_codes: Gerar códigos de recuperação
+    instructions_html: "<strong>Escaneie este QR Code no Google Authenticator ou aplicativo TOTP similar com o seu celular</strong>. De agora em diante, este aplicativo irá gerar tokens que você terá que inserir quando desejar acessar a sua conta."
+    lost_recovery_codes: Códigos de recuperação permitem que você recupere acesso à sua conta caso perca o seu celular. Se você perdeu seus códigos de recuperação, você pode gerá-los novamente aqui. Seus códigos de recuperaçãp anteriores serão invalidados.
+    manual_instructions: 'Se você não pode escanear o QR code e precisa inserí-lo manualmente, aqui está o segredo em texto:'
+    recovery_codes: Códigos de recuperação de reserva
+    recovery_codes_regenerated: Códigos de recuperação regenerados com sucesso
+    recovery_instructions_html: Se você perder acesso ao seu celular, você pode usar um dos códigos de recuperação abaixo para reganhar acesso à sua conta. <strong>Mantenha os códigos de recuperação em um local seguro</strong>. Por exemplo, você pode imprimi-los e guardá-los com outros documentos importantes.
     setup: Configurar
-    wrong_code: O código digitado é inválido! Os relógios do servidor e do dispositivo estão corretos?
+    wrong_code: O código inserido é invalido! O horário do servidor e o horário do seu aparelho estão corretos?
   users:
-    invalid_email: Endereço e-mail inválido
-    invalid_otp_token: Código two-factor inválido
+    invalid_email: O endereço de e-mail é inválido
+    invalid_otp_token: Código de autenticação inválido
+    signed_in_as: 'Acesso como:'
diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml
index b2b33c8ff..932b166d7 100644
--- a/config/locales/simple_form.ar.yml
+++ b/config/locales/simple_form.ar.yml
@@ -4,10 +4,10 @@ ar:
     hints:
       defaults:
         avatar: PNG, GIF أو JPG. على الأكثر 2 ميغابيت . سوف يتم تصغيرها إلى 120x120px
-        display_name: '<span class="name-counter">%{count}</span> أحرف متبقية'
+        display_name: <span class="name-counter">%{count}</span> أحرف متبقية
         header: PNG, GIF or JPG. على الأكثر 2 ميغابيت . سوف يتم تصغيرها إلى 700x335px
         locked: يتطلب منك الموافقة يدويا على كل طلب للإشتراك بحسابك و منشوراتك تعرض لمتابعيك فقط دون غيرهم
-        note: '<span class="note-counter">%{count}</span> أحرف متبقية'
+        note: <span class="note-counter">%{count}</span> أحرف متبقية
       imports:
         data: ملف CSV تم تصديره من خادوم مثيل  آخر لماستدون
       sessions:
diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml
index 55b80277d..2ea70d51c 100644
--- a/config/locales/simple_form.bg.yml
+++ b/config/locales/simple_form.bg.yml
@@ -39,8 +39,8 @@ bg:
         follow_request: Изпращай e-mail, когато някой пожелае да те последва
         mention: Изпращай e-mail, когато някой те спомене
         reblog: Изпращай e-mail, когато някой сподели твоя публикация
-    'no': 'Не'
+    'no': Не
     required:
       mark: "*"
       text: задължително
-    'yes': 'Да'
+    'yes': Да
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index 7bcc21e66..b5b7f26d2 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -8,8 +8,8 @@ ca:
           one: <span class="name-counter">1</span> character left
           other: <span class="name-counter">%{count}</span> characters left
         header: PNG, GIF o JPG. Màxim 2MB. Serà escalat a 700x335px
-        locked: Requereix que aprovis manualment seguidors i les publicacions seran mostrades només als teus seguidors 
-        note: 
+        locked: Requereix que aprovis manualment seguidors i les publicacions seran mostrades només als teus seguidors
+        note:
           one: <span class="note-counter">1</span> character left
           other: <span class="note-counter">%{count}</span> characters left
       imports:
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index c07dc2846..a6ba839c6 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -4,10 +4,10 @@ de:
     hints:
       defaults:
         avatar: PNG, GIF oder JPG. Maximal 2MB. Wird auf 120x120px herunterskaliert
-        display_name: '<span class="name-counter">%{count}</span> Zeichen verbleiben'
+        display_name: <span class="name-counter">%{count}</span> Zeichen verbleiben
         header: PNG, GIF oder JPG. Maximal 2MB. Wird auf 700x335px herunterskaliert
         locked: Erlaubt dir, Profile zu überprüfen, bevor sie dir folgen können
-        note: '<span class="note-counter">%{count}</span> Zeichen verbleiben'
+        note: <span class="note-counter">%{count}</span> Zeichen verbleiben
       imports:
         data: CSV-Datei, die von einer anderen Mastodon-Instanz exportiert wurde
       sessions:
@@ -44,8 +44,8 @@ de:
         follow_request: E-Mail senden, wenn mir jemand folgen möchte
         mention: E-Mail senden, wenn mich jemand erwähnt
         reblog: E-Mail senden, wenn jemand meinen Beitrag teilt
-    'no': 'Nein'
+    'no': Nein
     required:
       mark: "*"
       text: Pflichtfeld
-    'yes': 'Ja'
+    'yes': Ja
diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml
index 7c501bb93..a3824d349 100644
--- a/config/locales/simple_form.eo.yml
+++ b/config/locales/simple_form.eo.yml
@@ -39,8 +39,8 @@ eo:
         follow_request: Sendi retpoŝt-mesaĝon, kiam iu petas sekvi vin
         mention: Sendi retpoŝt-mesaĝon, kiam iu mencias vin
         reblog: Sendi retpoŝt-mesaĝon, kiam iu diskonigas mesaĝon de vi
-    'no': 'Ne'
+    'no': Ne
     required:
       mark: "*"
       text: bezonata
-    'yes': 'Jes'
+    'yes': Jes
diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml
index f260e600c..63a0710d1 100644
--- a/config/locales/simple_form.es.yml
+++ b/config/locales/simple_form.es.yml
@@ -4,12 +4,21 @@ es:
     hints:
       defaults:
         avatar: PNG, GIF o JPG. Máximo 2MB. Será escalado a 120x120px
-        display_name: Máximo 30 caracteres
+        display_name:
+          one: <span class="name-counter">1</span> caracter restante
+          other: <span class="name-counter">%{count}</span> caracteres restantes
         header: PNG, GIF o JPG. Máximo 2MB. Será escalado a 700x335px
-        locked: Requiere que manualmente apruebes seguidores y las publicaciones serán mostradas solamente a tus seguidores 
-        note: Máximo 160 caracteres
+        locked: Requiere que manualmente apruebes seguidores y las publicaciones serán mostradas solamente a tus seguidores
+        note:
+          one: <span class="name-counter">1</span> caracter restante
+          other: <span class="name-counter">%{count}</span> caracteres restantes
+        setting_noindex: Afecta a tu perfil público y páginas de estado
       imports:
         data: Archivo CSV exportado desde otra instancia de Mastodon
+      sessions:
+        otp: Ingresa el código de autenticación de dos factores de tu teléfono o usa uno de tus códigos de recuperación.
+      user:
+        filtered_languages: Los lenguajes seleccionados serán removidos de tus líneas de tiempo públicas.
     labels:
       defaults:
         avatar: Avatar
diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml
index dd72a19bd..bdc4f32bf 100644
--- a/config/locales/simple_form.fa.yml
+++ b/config/locales/simple_form.fa.yml
@@ -5,13 +5,13 @@ fa:
       defaults:
         avatar: یکی از قالب‌های PNG یا  GIF یا JPG. بیشترین اندازه ۲ مگابایت. تصویر به اندازهٔ ۱۲۰×۱۲۰ پیکسل تبدیل خواهد شد.
         display_name:
-          one: '<span class="name-counter">1</span> حرف باقی مانده'
-          other: '<span class="name-counter">%{count}</span> حرف باقی مانده'
+          one: <span class="name-counter">1</span> حرف باقی مانده
+          other: <span class="name-counter">%{count}</span> حرف باقی مانده
         header: یکی از قالب‌های PNG یا  GIF یا JPG. بیشترین اندازه ۲ مگابایت. تصویر به اندازهٔ ۳۳۵×۷۰۰ پیکسل تبدیل خواهد شد
         locked: باید پیگیران تازه را خودتان تأیید کنید. حریم خصوصی پیش‌فرض نوشته‌ها را روی پیگیران تنظیم می‌کند
         note:
-          one: '<span class="note-counter">1</span> حرف باقی مانده'
-          other: '<span class="note-counter">%{count}</span> حرف باقی مانده'
+          one: <span class="note-counter">1</span> حرف باقی مانده
+          other: <span class="note-counter">%{count}</span> حرف باقی مانده
         setting_noindex: روی نمایهٔ عمومی و صفحهٔ نوشته‌های شما تأثیر می‌گذارد
       imports:
         data: پروندهٔ CSV که از سرور ماستدون دیگری برون‌سپاری شده
@@ -57,8 +57,8 @@ fa:
         follow_request: وقتی کسی درخواست پیگیری کرد ایمیل بفرست
         mention: وقتی کسی از شما نام برد ایمیل بفرست
         reblog: وقتی کسی نوشتهٔ شما را بازبوقید ایمیل بفرست
-    'no': 'خیر'
+    'no': خیر
     required:
       mark: "*"
       text: ضروری
-    'yes': 'بله'
+    'yes': بله
diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml
index 2bacd6d2c..8ca4378b1 100644
--- a/config/locales/simple_form.fi.yml
+++ b/config/locales/simple_form.fi.yml
@@ -39,8 +39,8 @@ fi:
         follow_request: Lähetä s-posti kun joku pyytää seurata sinua
         mention: Lähetä s-posti kun joku mainitsee sinut
         reblog: Lähetä s-posti kun joku buustaa julkaisusi
-    'no': 'Ei'
+    'no': Ei
     required:
       mark: "*"
       text: vaaditaan
-    'yes': 'Kyllä'
+    'yes': Kyllä
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index 532054449..d6af5e7ac 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -3,19 +3,19 @@ he:
   simple_form:
     hints:
       defaults:
-        avatar: 'PNG, GIF או JPG. מקסימום 2MB. גודל התמונה יוקטן ל-120x120px'
+        avatar: PNG, GIF או JPG. מקסימום 2MB. גודל התמונה יוקטן ל-120x120px
         display_name:
-          one: 'נותרה אות<span class="name-counter">אחת</span>'
-          other: 'נותרו<span class="name-counter">%{count}</span> אותיות'
-        header: 'PNG, GIF או JPG. מקסימום 2MB. גודל התמונה יוקטן 700x335px'
-        locked: 'מחייב אישור עוקבים באופן ידני. פרטיות ההודעות תהיה עוקבים-בלבד אלא אם יצוין אחרת'
+          one: נותרה אות<span class="name-counter">אחת</span>
+          other: נותרו<span class="name-counter">%{count}</span> אותיות
+        header: PNG, GIF או JPG. מקסימום 2MB. גודל התמונה יוקטן 700x335px
+        locked: מחייב אישור עוקבים באופן ידני. פרטיות ההודעות תהיה עוקבים-בלבד אלא אם יצוין אחרת
         note:
-          one: 'נותרה אות<span class="note-counter">אחת</span>'
-          other: 'נותרו <span class="note-counter">%{count}</span> אותיות'
+          one: נותרה אות<span class="note-counter">אחת</span>
+          other: נותרו <span class="note-counter">%{count}</span> אותיות
       imports:
-        data: 'קובץ CSV שיוצא משרת מסטודון אחר'
+        data: קובץ CSV שיוצא משרת מסטודון אחר
       sessions:
-        otp: 'נא להקליד קוד אימות דו-שלבי ממכשירך או קוד אחזור גישה.'
+        otp: נא להקליד קוד אימות דו-שלבי ממכשירך או קוד אחזור גישה.
       user:
         filtered_languages: בחירת שפות להסתרה מציר הזמן הציבורי שלך.
     labels:
@@ -26,7 +26,7 @@ he:
         current_password: סיסמא נוכחית
         data: מידע
         display_name: שם להצגה
-        email: 'כתובת דוא"ל'
+        email: כתובת דוא"ל
         header: ראשה
         locale: שפה
         locked: הפוך חשבון לפרטי
@@ -44,14 +44,14 @@ he:
         must_be_follower: חסימת התראות משאינם עוקבים
         must_be_following: חסימת התראות משאינם נעקבים
       notification_emails:
-        digest: 'שליחת הודעות דוא"ל מסכמות'
-        favourite: 'שליחת דוא"ל כשמחבבים חצרוץ'
-        follow: 'שליחת דוא"ל כשנוספות עוקבות'
-        follow_request: 'שליחת דוא"ל כשמבקשים לעקוב'
-        mention: 'שליחת דוא"ל כשפונים אלייך'
-        reblog: 'שליחת דוא"ל כשמהדהדים חצרוץ שלך'
-    'no': 'לא'
+        digest: שליחת הודעות דוא"ל מסכמות
+        favourite: שליחת דוא"ל כשמחבבים חצרוץ
+        follow: שליחת דוא"ל כשנוספות עוקבות
+        follow_request: שליחת דוא"ל כשמבקשים לעקוב
+        mention: שליחת דוא"ל כשפונים אלייך
+        reblog: שליחת דוא"ל כשמהדהדים חצרוץ שלך
+    'no': לא
     required:
       mark: "*"
       text: שדה חובה
-    'yes': 'כן'
+    'yes': כן
diff --git a/config/locales/simple_form.hr.yml b/config/locales/simple_form.hr.yml
index 0b7761a7f..3010423a7 100644
--- a/config/locales/simple_form.hr.yml
+++ b/config/locales/simple_form.hr.yml
@@ -40,8 +40,8 @@ hr:
         follow_request: Pošalji mi e-mail kad mi netko pošalje zahtjev da me želi slijediti
         mention: Pošalji mi e-mail kad me netko spomene
         reblog: Pošalji mi e-mail kad netko rebloga moj status
-    'no': 'Ne'
+    'no': Ne
     required:
       mark: "*"
       text: traženo
-    'yes': 'Da'
+    'yes': Da
diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml
index 89eb70767..c4dc3aead 100644
--- a/config/locales/simple_form.hu.yml
+++ b/config/locales/simple_form.hu.yml
@@ -20,8 +20,8 @@ hu:
         follow: E-mail küldése amikor valaki követni kezd téged
         mention: E-mail küldése amikor valaki megemlít téged
         reblog: E-mail küldése amikor valaki reblogolja az állapotod
-    'no': 'Nem'
+    'no': Nem
     required:
       mark: "*"
       text: kötelező
-    'yes': 'Igen'
+    'yes': Igen
diff --git a/config/locales/simple_form.id.yml b/config/locales/simple_form.id.yml
index a6e6a77a1..b00b4ef4f 100644
--- a/config/locales/simple_form.id.yml
+++ b/config/locales/simple_form.id.yml
@@ -43,8 +43,8 @@ id:
         follow_request: Kirim email saat seseorang meminta untuk mengikuti anda
         mention: Kirim email saat seseorang menyebut anda
         reblog: Kirim email saat seseorang mem-boost status anda
-    'no': 'Tidak'
+    'no': Tidak
     required:
       mark: "*"
       text: wajib
-    'yes': 'Ya'
+    'yes': Ya
diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml
index b7dbc8bef..abbad7430 100644
--- a/config/locales/simple_form.ko.yml
+++ b/config/locales/simple_form.ko.yml
@@ -51,8 +51,8 @@ ko:
         follow_request: 누군가 나를 팔로우 하길 원할 때 이메일 보내기
         mention: 누군가 나에게 답장했을 때 이메일 보내기
         reblog: 누군가 내 Toot을 부스트 했을 때 이메일 보내기
-    'no': '아니오'
+    'no': 아니오
     required:
       mark: "*"
       text: 필수 항목
-    'yes': '네'
+    'yes': 네
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
index 351d1800c..fabb5840a 100644
--- a/config/locales/simple_form.nl.yml
+++ b/config/locales/simple_form.nl.yml
@@ -7,13 +7,16 @@ nl:
         display_name: Maximaal 30 tekens
         header: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 700x335px
         locked: Vereist dat je handmatig volgers moet accepteren en stelt de privacy van toots standaard in op alleen volgers
-        note: Maximaal 160 tekens
+        note:
+          one: <span class="note-counter">1</span> teken over
+          other: <span class="note-counter">%{count}</span> tekens over
+        setting_noindex: Heeft invloed op jouw openbare profiel en toots
       imports:
-        data: CSV-bestand dat op een andere Mastodon-server werd geëxporteerd 
+        data: CSV-bestand dat op een andere Mastodon-server werd geëxporteerd
       sessions:
-        otp: Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcode's.        
+        otp: Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcode's.
       user:
-        filtered_languages: De geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd.        
+        filtered_languages: De geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd.
     labels:
       defaults:
         avatar: Avatar
@@ -32,8 +35,13 @@ nl:
         otp_attempt: Tweestaps-aanmeldcode
         password: Wachtwoord
         setting_auto_play_gif: Speel geanimeerde GIF's automatisch af
-        setting_boost_modal: Vraag voor het boosten een bevestiging
-        setting_default_privacy: Tootprivacy
+        setting_boost_modal: Vraag voor het boosten van een toot een bevestiging
+        setting_default_privacy: Zichtbaarheid toots
+        setting_default_sensitive: Media altijd als gevoelig markeren
+        setting_delete_modal: Vraag voor het verwijderen van een toot een bevestiging
+        setting_noindex: Jouw toots niet door zoekmachines laten indexeren
+        setting_system_font_ui: Standaardlettertype van jouw systeem gebruiken
+        setting_unfollow_modal: Vraag voor het ontvolgen van iemand een bevestiging
         type: Importtype
         username: gebruikersnaam
       interactions:
@@ -46,8 +54,8 @@ nl:
         follow_request: Verstuur een e-mail wanneer iemand jou wilt volgen
         mention: Verstuur een e-mail wanneer iemand jou vermeld
         reblog: Verstuur een e-mail wanneer iemand jouw toot heeft geboost
-    'no': 'Nee'
+    'no': Nee
     required:
       mark: "*"
       text: vereist
-    'yes': 'Ja'
+    'yes': Ja
diff --git a/config/locales/simple_form.no.yml b/config/locales/simple_form.no.yml
index cc9ad4c6f..194e1582f 100644
--- a/config/locales/simple_form.no.yml
+++ b/config/locales/simple_form.no.yml
@@ -1,19 +1,17 @@
+---
 'no':
   simple_form:
     hints:
       defaults:
-        avatar: 'PNG, GIF eller JPG. Maksimalt 2MB. Vil bli nedskalert til 120x120px'
+        avatar: PNG, GIF eller JPG. Maksimalt 2MB. Vil bli nedskalert til 120x120px
         display_name: Maksimalt 30 tegn
-        header: 'PNG, GIF eller JPG. Maksimalt 2MB. Vil bli nedskalert til 700x335px'
-        locked: >-
-          Krever at du manuelt godkjenner følgere og setter standardbeskyttelse
-          av poster til kun-følgere
+        header: PNG, GIF eller JPG. Maksimalt 2MB. Vil bli nedskalert til 700x335px
+        locked: Krever at du manuelt godkjenner følgere og setter standardbeskyttelse av poster til kun-følgere
         note: Maksimalt 160 tegn
       imports:
         data: CSV-fil eksportert fra en annen Mastodon instans
       sessions:
-        otp: >-
-          Skriv tofaktorkoden fra din telefon eller bruk en av gjenopprettingskodene.
+        otp: Skriv tofaktorkoden fra din telefon eller bruk en av gjenopprettingskodene.
     labels:
       defaults:
         avatar: Profilbilde
@@ -48,7 +46,6 @@
         reblog: Send e-post når noen fremhever din status
     'no': Nei
     required:
-      mark: '*'
+      mark: "*"
       text: påkrevd
     'yes': Ja
-
diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml
index 43cb52201..442a7d4b6 100644
--- a/config/locales/simple_form.oc.yml
+++ b/config/locales/simple_form.oc.yml
@@ -12,6 +12,7 @@ oc:
         note:
           one: Demòra encara <span class="name-counter">1</span> caractèr
           other: Demòran encara <span class="name-counter">%{count}</span> caractèrs
+        setting_noindex: Aquò es destinat a vòstre perfil public e vòstra pagina d’estatuts
       imports:
         data: Fichièr CSV exportat d’una autra instància Mastodon
       sessions:
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index 733e16892..22cae5271 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -1,51 +1,64 @@
 ---
 pt-BR:
   simple_form:
-    labels:
+    hints:
       defaults:
-        avatar: PNG, GIF or JPG. Máximo 2MB. Será reduzido para 120x120px
-        display_name: '<span class="name-counter">%{count}</span> caracteres restantes'
-        header: PNG, GIF or JPG. Máximo 2MB. Será reduzido para 700x335px
-        locked: Requer você aprovação manualmente seguidores e posts padrão de privacidade para seguidores-apenas
-        note: '<span class="note-counter">%{count}</span> caracteres restantes'
+        avatar: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 120x120px
+        display_name:
+          one: <span class="name-counter">1</span> caracter restante
+          other: <span class="name-counter">%{count}</span> caracteres restantes
+        header: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 700x335px
+        locked: Requer aprovação manual de seguidores
+        note:
+          one: <span class="note-counter">1</span> caracter restante
+          other: <span class="note-counter">%{count}</span> caracteres restantes
+        setting_noindex: Afeta seu perfil público e as páginas de suas postagens
       imports:
-        data: Arquivo CSV exportado de outra instancia Mastodon
+        data: Arquivo CSV exportado de outra instância do Mastodon
       sessions:
-        otp: Entre com o código de 2 passos do seu telefone ou use os códiogos de recuperação.
+        otp: Insira o código de autenticação do seu celular ou use um dos códigos de recuperação.
+      user:
+        filtered_languages: Selecione os idiomas que devem ser removidos de suas timelines públicas.
     labels:
       defaults:
         avatar: Avatar
-        confirm_new_password: Confirme nova senha
-        confirm_password: Confirme a senha
+        confirm_new_password: Confirmar nova senha
+        confirm_password: Confirmar senha
         current_password: Senha atual
         data: Dados
-        display_name: Nome
-        email: Endereço de email
-        header: Header
-        locale: Linguagem
-        locked: Conta bloqueada
+        display_name: Nome de exibição
+        email: Endereço de e-mail
+        filtered_languages: Idiomas filtrados
+        header: Cabeçalho
+        locale: Idioma
+        locked: Trancar conta
         new_password: Nova senha
-        note: Biografia
-        otp_attempt: Autenticação 2 passos
+        note: Bio
+        otp_attempt: Código de autenticação em dois passos
         password: Senha
-        setting_auto_play_gif: Tocar GIFs animados automaticamente
-        setting_boost_modal: Mostrar dialogo de confirmação antes de fazer boost
-        setting_default_privacy: Postar privacidade
-        severity: Severidade
-        type: Importar tipo
-        username: Usuário
+        setting_auto_play_gif: Reproduzir GIFs automaticamente
+        setting_boost_modal: Mostrar diálogo de confirmação antes de compartilhar postagem
+        setting_default_privacy: Privacidade das postagens
+        setting_default_sensitive: Sempre marcar mídia como sensível
+        setting_delete_modal: Mostrar diálogo de confirmação antes de deletar uma postagem
+        setting_noindex: Não quero ser indexado por mecanismos de busca
+        setting_system_font_ui: Usar a fonte padrão de seu sistema
+        setting_unfollow_modal: Mostrar diálogo de confirmação antes de deixar de seguir alguém
+        severity: Gravidade
+        type: Tipo de importação
+        username: Nome de usuário
       interactions:
         must_be_follower: Bloquear notificações de não-seguidores
-        must_be_following: Bloquear notificações de pessoas que você
+        must_be_following: Bloquear notificações de pessoas que você não segue
       notification_emails:
-        digest: Enviar resumo de emails
-        favourite: Enviar email quando alguém favorita um post seu
-        follow: Enviar email quando alguém seguir você
-        follow_request: Enviar email quando alguém requisitar te seguir
-        mention: Enviar email quando alguém mencionar você
-        reblog: Enviar email quando alguém reblogar um post seu
-    'no': 'Não'
+        digest: Mandar e-mails com relatórios
+        favourite: Mandar um e-mail quando alguém favoritar suas postagens
+        follow: Mandar um e-mail quando alguém te seguir
+        follow_request: Mandar um e-maill quando alguém solicitar ser seu seguidor
+        mention: Mandar um e-mail quando alguém te mencionar
+        reblog: Mandar um e-mail quando alguém compartilhar suas postagens
+    'no': Não
     required:
       mark: "*"
-      text: necessário
-    'yes': 'Sim'
+      text: obrigatório
+    'yes': Sim
diff --git a/config/locales/simple_form.pt.yml b/config/locales/simple_form.pt.yml
index ba3326b23..0df7a9bb9 100644
--- a/config/locales/simple_form.pt.yml
+++ b/config/locales/simple_form.pt.yml
@@ -42,8 +42,8 @@ pt:
         follow: Enviar email quando alguém te segue
         mention: Enviar email quando alguém te menciona
         reblog: Enviar email quando alguém partilhar um post teu
-    'no': 'Não'
+    'no': Não
     required:
       mark: "*"
       text: necessário
-    'yes': 'Sim'
+    'yes': Sim
diff --git a/config/locales/simple_form.th.yml b/config/locales/simple_form.th.yml
index c58758da4..87c673600 100644
--- a/config/locales/simple_form.th.yml
+++ b/config/locales/simple_form.th.yml
@@ -5,13 +5,13 @@ th:
       defaults:
         avatar: PNG, GIF or JPG. At most 2MB. Will be downscaled to 120x120px
         display_name:
-          one: '<span class="name-counter">1</span> character left'
-          other: '<span class="name-counter">%{count}</span> characters left'
+          one: <span class="name-counter">1</span> character left
+          other: <span class="name-counter">%{count}</span> characters left
         header: PNG, GIF or JPG. At most 2MB. Will be downscaled to 700x335px
         locked: Requires you to manually approve followers and defaults post privacy to followers-only
         note:
-          one: '<span class="name-counter">1</span> character left'
-          other: '<span class="note-counter">%{count}</span> characters left'
+          one: <span class="name-counter">1</span> character left
+          other: <span class="note-counter">%{count}</span> characters left
       imports:
         data: CSV file exported from another Mastodon instance
       sessions:
@@ -48,8 +48,8 @@ th:
         follow_request: Send e-mail when someone requests to follow you
         mention: Send e-mail when someone mentions you
         reblog: Send e-mail when someone boosts your status
-    'no': 'ไม่'
+    'no': ไม่
     required:
       mark: "*"
       text: required
-    'yes': 'ใช่'
+    'yes': ใช่
diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml
index 80b6c771c..0e171b793 100644
--- a/config/locales/simple_form.tr.yml
+++ b/config/locales/simple_form.tr.yml
@@ -1,14 +1,13 @@
-
 ---
 tr:
   simple_form:
     hints:
       defaults:
         avatar: En fazla 2MB olacak şekilde PNG, GIF veya JPG formatında yükleyiniz. 120x120px büyüklüğüne indirgenecektir
-        display_name: '<span class="name-counter">%{count}</span> karakter kaldı'
+        display_name: <span class="name-counter">%{count}</span> karakter kaldı
         header: En fazla 2MB olacak şekilde PNG, GIF veya JPG formatında yükleyiniz. 700x335px büyüklüğüne indirgenecektir.
         locked: Takipçilerinizi manuel olarak kabul etmenizi ve gönderilerinizi varsayılan olarak sadece takipçilerinizin göreceği şekilde paylaşmanızı sağlar.
-        note: '<span class="note-counter">%{count}</span> karakter kaldı'
+        note: <span class="note-counter">%{count}</span> karakter kaldı
       imports:
         data: Diğer Mastodon sunucusundan dışarı aktardığınız CSV dosyası
       sessions:
@@ -45,9 +44,8 @@ tr:
         follow_request: Biri bana takip isteği gönderdiğinde, bana e-posta gönder
         mention: Biri benden bahsettiğinde, bana e-posta gönder
         reblog: Biri durumumu paylaştığında, bana e-posta gönder
-    'no': 'Hayır'
+    'no': Hayır
     required:
       mark: "*"
       text: gerekli
-    'yes': 'Evet'
-
+    'yes': Evet
diff --git a/config/locales/simple_form.uk.yml b/config/locales/simple_form.uk.yml
index 16608c129..ff7f2d4ca 100644
--- a/config/locales/simple_form.uk.yml
+++ b/config/locales/simple_form.uk.yml
@@ -44,8 +44,8 @@ uk:
         follow_request: Надсилати листа, коли хтось запитує дозволу на підписку
         mention: Надсилати листа, коли хтось згадує Вас
         reblog: Надсилати листа, коли хтось передмухує Ваш статус
-    'no': 'Ні'
+    'no': Ні
     required:
       mark: "*"
       text: обов'язкове
-    'yes': 'Так'
+    'yes': Так
diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml
index 6f4edaf48..eafaa972e 100644
--- a/config/locales/simple_form.zh-CN.yml
+++ b/config/locales/simple_form.zh-CN.yml
@@ -46,8 +46,8 @@ zh-CN:
         follow_request: 当有用户要求关注你时,发电邮通知
         mention: 当有用户在嘟文中提及你时,发电邮通知
         reblog: 当有用户转嘟了你的嘟文时,发电邮通知
-    'no': '否'
+    'no': 否
     required:
       mark: "*"
       text: 必填
-    'yes': '是'
+    'yes': 是
diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml
index 0bb135ca1..c82f07e2d 100644
--- a/config/locales/simple_form.zh-TW.yml
+++ b/config/locales/simple_form.zh-TW.yml
@@ -42,8 +42,8 @@ zh-TW:
         follow_request: 當有使用者要求關注您時,發信通知
         mention: 當有使用者在文章提及您時,發信通知
         reblog: 當有使用者轉推您的文章時,發信通知
-    'no': '否'
+    'no': 否
     required:
       mark: "*"
       text: 必填
-    'yes': '是'
+    'yes': 是
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 95c24d0bc..44d0f3803 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -65,9 +65,9 @@ zh-CN:
       salmon_url: Salmon 反馈 URL
       search: 搜索
       show:
-        created_reports: 这个账户创建的报告
+        created_reports: 这个帐户创建的报告
         report: 报告
-        targeted_reports: 关于这个账户的报告
+        targeted_reports: 关于这个帐户的报告
       silence: 静音
       statuses: 嘟文
       title: 用户
@@ -82,7 +82,7 @@ zh-CN:
       domain: 域名阻隔
       new:
         create: 添加域名阻隔
-        hint: "「域名阻隔」不会隔绝该域名用户的嘟账户入本站数据库,但会嘟文抵达后,自动套用特定的审批操作。"
+        hint: "「域名阻隔」不会隔绝该域名用户的嘟帐户入本站数据库,但会嘟文抵达后,自动套用特定的审批操作。"
         severity:
           desc_html: "「<strong>自动静音</strong>」令该域名用户的嘟文,设为只对关注者显示,没有关注的人会看不到。 「<strong>自动除名</strong>」会自动将该域名用户的嘟文、媒体文件、个人资料从本服务器实例删除。"
           silence: 自动静音
@@ -96,11 +96,11 @@ zh-CN:
       severity: 阻隔程度
       show:
         affected_accounts:
-          one: 数据库中有1个账户受影响
-          other: 数据库中有%{count}个账户受影响
+          one: 数据库中有1个帐户受影响
+          other: 数据库中有%{count}个帐户受影响
         retroactive:
-          silence: 对此域名的所有账户取消静音
-          suspend: 对此域名的所有账户取消除名
+          silence: 对此域名的所有帐户取消静音
+          suspend: 对此域名的所有帐户取消除名
         title: 撤销 %{domain} 的域名阻隔
         undo: 撤销
       title: 域名阻隔
@@ -209,12 +209,12 @@ zh-CN:
     domain: 域名
     explanation_html: 想要保护你的嘟文的话,请慎重考虑关注你的人。<strong>你的受保护的嘟文会发送到有你的关注者的所有实例上</strong>。你也许想要复查一下关注者列表来移除那些你无法信任的关注者。
     followers_count: 关注者数量
-    lock_link: 保护你的账户
+    lock_link: 保护你的帐户
     purge: 从关注者中移除
     success: 从 %{count} 个域名中移除了关注者。
     true_privacy_html: "<strong>真正的隐私只能靠端到端加密来实现</strong>!"
     unlocked_warning_html: 任何人都可以关注你然后查看被保护的嘟文, %{lock_link} 可以复核和拒绝关注请求。
-    unlocked_warning_title: 你的账户没被保护
+    unlocked_warning_title: 你的帐户没被保护
   generic:
     changes_saved_msg: 更改已被保存。
     powered_by: 基于 %{link} 构建
@@ -231,7 +231,7 @@ zh-CN:
       muting: 静音名单
     upload: 上载
   landing_strip_html: "<strong>%{name}</strong> 是一个在 %{link_to_root_path} 的用户。只要你是象毛世界里(Mastodon、GNU social)任一服务器实例的用户,便可以跨站关注此站用户并与其沟通。"
-  landing_strip_signup_html: 如果你没有这类账户,欢迎在<a href="%{sign_up_path}">此处登记</a>。
+  landing_strip_signup_html: 如果你没有这类帐户,欢迎在<a href="%{sign_up_path}">此处登记</a>。
   media_attachments:
     validations:
       images_and_video: 无法添加视频到一个已经包含图片的嘟文中
@@ -247,8 +247,8 @@ zh-CN:
         one: "你有一个新通知 \U0001F418"
         other: "%{count} 个通知太多,赶快去看看 \U0001F418"
     favourite:
-      body: "%{name} 赞你"
-      subject: "%{name} 对你点赞"
+      body: "%{name} 收藏了你"
+      subject: "%{name} 给你点了收藏"
     follow:
       body: "%{name} 关注了你"
       subject: "%{name} 关注了你"
@@ -278,7 +278,7 @@ zh-CN:
     truncate: "……"
   remote_follow:
     acct: 请输入你的︰用户名称@实例域名
-    missing_resource: 无法找到您的账户转接网址
+    missing_resource: 无法找到您的帐户转接网址
     proceed: 下一步
     prompt: 你正准备关注︰
   settings:
@@ -317,10 +317,10 @@ zh-CN:
     enabled_success: 已成功启用两步认证
     generate_recovery_codes: 生成恢复代码
     instructions_html: "<strong>请用你手机的认证器应用(如 Google Authenticator、Authy),扫描这里的 QR 二维码</strong>。在两步认证启用后,你登录时将需要使用此应用程序产生的认证码。"
-    lost_recovery_codes: 如果你丢了手机,你可以用恢复代码重新访问你的账户。如果你丢了恢复代码,也可以在这里重新生成一个,不过以前的恢复代码就失效了。<del>(废话)</del>
+    lost_recovery_codes: 如果你丢了手机,你可以用恢复代码重新访问你的帐户。如果你丢了恢复代码,也可以在这里重新生成一个,不过以前的恢复代码就失效了。<del>(废话)</del>
     manual_instructions: 如果你无法扫描 QR 二维码,请手动输入这个文本密码︰
     recovery_codes_regenerated: 已成功重新生成恢复代码
-    recovery_instructions_html: 如果你的手机无法使用,你可以使用下面的任何恢复代码来恢复你的账号。请保管好你的恢复代码以防泄漏(例如你可以打印好它们并和重要文档一起保存)。
+    recovery_instructions_html: 如果你的手机无法使用,你可以使用下面的任何恢复代码来恢复你的帐号。请保管好你的恢复代码以防泄漏(例如你可以打印好它们并和重要文档一起保存)。
     setup: 设置
     wrong_code: 你输入的认证码并不正确!可能服务器时间和你手机不一致,请检查你手机的时钟,或与本站管理员联系。
   users:
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index aa6b1ea6a..8ff6d1bf8 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -242,8 +242,8 @@ zh-HK:
         one: "自從上次登入以來,你收到 1 則新的通知 \U0001F418"
         other: "自從上次登入以來,你收到 %{count} 則新的通知 \U0001F418"
     favourite:
-      body: 你的文章獲得 %{name} 的喜愛
-      subject: "%{name} 喜歡你的文章"
+      body: 你的文章是 %{name} 的最愛!
+      subject: "%{name} 收藏了你的文章"
     follow:
       body: "%{name} 開始關注你!"
       subject: "%{name} 現正關注你"
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 299a92da7..7a66a64ca 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -197,8 +197,8 @@ zh-TW:
         one: "自從上次登入以來,您收到 1 則新的通知 \U0001F418"
         other: "自從上次登入以來,您收到 %{count} 則新的通知 \U0001F418"
     favourite:
-      body: 您的文章被 %{name} 喜歡
-      subject: "%{name} 喜歡您的文章"
+      body: 您的文章被 %{name} 收藏
+      subject: "%{name} 將您的文章加入了最愛"
     follow:
       body: "%{name} 開始關注您!"
       subject: "%{name} 開始關注您"
diff --git a/config/navigation.rb b/config/navigation.rb
index 4b454b3fc..0a6ab6d3d 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -28,6 +28,7 @@ SimpleNavigation::Configuration.run do |navigation|
       admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }
       admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }
       admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url
+      admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis}
     end
 
     primary.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' }
diff --git a/config/routes.rb b/config/routes.rb
index dc93fc6fe..ce7a884f9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -98,6 +98,8 @@ Rails.application.routes.draw do
   resources :media, only: [:show]
   resources :tags,  only: [:show]
 
+  get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy
+
   # Remote follow
   resource :authorize_follow, only: [:show, :create]
   resource :share, only: [:show, :create]
@@ -134,6 +136,8 @@ Rails.application.routes.draw do
     resources :users, only: [] do
       resource :two_factor_authentication, only: [:destroy]
     end
+
+    resources :custom_emojis, only: [:index, :new, :create, :destroy]
   end
 
   get '/admin', to: redirect('/admin/settings/edit', status: 302)
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index a502f5593..4c35dcd43 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -7,20 +7,23 @@
   - mailers
 :schedule:
   subscriptions_scheduler:
-    cron: '0 5 * * *'
+    cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *'
     class: Scheduler::SubscriptionsScheduler
   media_cleanup_scheduler:
-    cron: '5 4 * * *'
+    cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
     class: Scheduler::MediaCleanupScheduler
   feed_cleanup_scheduler:
-    cron: '0 0 * * *'
+    cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * *'
     class: Scheduler::FeedCleanupScheduler
   doorkeeper_cleanup_scheduler:
-    cron: '1 1 * * 0'
+    cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * 0'
     class: Scheduler::DoorkeeperCleanupScheduler
   user_cleanup_scheduler:
-    cron: '4 5 * * *'
+    cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *'
     class: Scheduler::UserCleanupScheduler
   subscriptions_cleanup_scheduler:
-    cron: '2 2 * * 0'
+    cron: '<%= Random.rand(0..59) %> <%= Random.rand(1..3) %> * * 0'
     class: Scheduler::SubscriptionsCleanupScheduler
+  ip_cleanup_scheduler:
+    cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
+    class: Scheduler::IpCleanupScheduler
diff --git a/db/migrate/20170913000752_create_site_uploads.rb b/db/migrate/20170913000752_create_site_uploads.rb
new file mode 100644
index 000000000..2246e48cd
--- /dev/null
+++ b/db/migrate/20170913000752_create_site_uploads.rb
@@ -0,0 +1,10 @@
+class CreateSiteUploads < ActiveRecord::Migration[5.1]
+  def change
+    create_table :site_uploads do |t|
+      t.string :var, default: '', null: false, index: { unique: true }
+      t.attachment :file
+      t.json :meta
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20170917153509_create_custom_emojis.rb b/db/migrate/20170917153509_create_custom_emojis.rb
new file mode 100644
index 000000000..4040c8312
--- /dev/null
+++ b/db/migrate/20170917153509_create_custom_emojis.rb
@@ -0,0 +1,13 @@
+class CreateCustomEmojis < ActiveRecord::Migration[5.1]
+  def change
+    create_table :custom_emojis do |t|
+      t.string :shortcode, null: false, default: ''
+      t.string :domain
+      t.attachment :image
+
+      t.timestamps
+    end
+
+    add_index :custom_emojis, [:shortcode, :domain], unique: true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 52edfa497..b72f8f1a1 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20170914032032) do
+ActiveRecord::Schema.define(version: 20170917153509) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -89,6 +89,18 @@ ActiveRecord::Schema.define(version: 20170914032032) do
     t.index ["uri"], name: "index_conversations_on_uri", unique: true
   end
 
+  create_table "custom_emojis", force: :cascade do |t|
+    t.string "shortcode", default: "", null: false
+    t.string "domain"
+    t.string "image_file_name"
+    t.string "image_content_type"
+    t.integer "image_file_size"
+    t.datetime "image_updated_at"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
+  end
+
   create_table "domain_blocks", id: :serial, force: :cascade do |t|
     t.string "domain", default: "", null: false
     t.datetime "created_at", null: false
@@ -289,6 +301,18 @@ ActiveRecord::Schema.define(version: 20170914032032) do
     t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true
   end
 
+  create_table "site_uploads", force: :cascade do |t|
+    t.string "var", default: "", null: false
+    t.string "file_file_name"
+    t.string "file_content_type"
+    t.integer "file_file_size"
+    t.datetime "file_updated_at"
+    t.json "meta"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["var"], name: "index_site_uploads_on_var", unique: true
+  end
+
   create_table "status_pins", force: :cascade do |t|
     t.bigint "account_id", null: false
     t.bigint "status_id", null: false
diff --git a/lib/mastodon/unique_retry_job_middleware.rb b/lib/mastodon/unique_retry_job_middleware.rb
deleted file mode 100644
index 75da8a0c9..000000000
--- a/lib/mastodon/unique_retry_job_middleware.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-class Mastodon::UniqueRetryJobMiddleware
-  def call(_worker_class, item, _queue, _redis_pool)
-    return if item['unique_retry'] && retried?(item)
-    yield
-  end
-
-  private
-
-  def retried?(item)
-    # Use unique digest key of SidekiqUniqueJobs
-    unique_key = SidekiqUniqueJobs::UNIQUE_DIGEST_KEY
-    unique_digest = item[unique_key]
-    class_name = item['class']
-    retries = Sidekiq::RetrySet.new
-
-    retries.any? { |job| job.item['class'] == class_name && job.item[unique_key] == unique_digest }
-  end
-end
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index b6fdb10c6..8b692c29d 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,7 +13,7 @@ module Mastodon
     end
 
     def patch
-      0
+      1
     end
 
     def pre
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 3c65ece4b..5614ddf48 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -83,16 +83,15 @@ namespace :mastodon do
 
       MediaAttachment.where.not(remote_url: '').where('created_at < ?', time_ago).find_each do |media|
         media.file.destroy
-        media.type = :unknown
         media.save
       end
     end
 
     desc 'Set unknown attachment type for remote-only attachments'
     task set_unknown: :environment do
-      Rails.logger.debug 'Setting unknown attachment type for remote-only attachments...'
+      puts 'Setting unknown attachment type for remote-only attachments...'
       MediaAttachment.where(file_file_name: nil).where.not(type: :unknown).in_batches.update_all(type: :unknown)
-      Rails.logger.debug 'Done!'
+      puts 'Done!'
     end
 
     desc 'Redownload avatars/headers of remote users. Optionally limit to a particular domain with DOMAIN'
@@ -188,24 +187,24 @@ namespace :mastodon do
   namespace :maintenance do
     desc 'Update counter caches'
     task update_counter_caches: :environment do
-      Rails.logger.debug 'Updating counter caches for accounts...'
+      puts 'Updating counter caches for accounts...'
 
-      Account.unscoped.select('id').find_in_batches do |batch|
+      Account.unscoped.where.not(protocol: :activitypub).select('id').find_in_batches do |batch|
         Account.where(id: batch.map(&:id)).update_all('statuses_count = (select count(*) from statuses where account_id = accounts.id), followers_count = (select count(*) from follows where target_account_id = accounts.id), following_count = (select count(*) from follows where account_id = accounts.id)')
       end
 
-      Rails.logger.debug 'Updating counter caches for statuses...'
+      puts 'Updating counter caches for statuses...'
 
       Status.unscoped.select('id').find_in_batches do |batch|
         Status.where(id: batch.map(&:id)).update_all('favourites_count = (select count(*) from favourites where favourites.status_id = statuses.id), reblogs_count = (select count(*) from statuses as reblogs where reblogs.reblog_of_id = statuses.id)')
       end
 
-      Rails.logger.debug 'Done!'
+      puts 'Done!'
     end
 
     desc 'Generate static versions of GIF avatars/headers'
     task add_static_avatars: :environment do
-      Rails.logger.debug 'Generating static avatars/headers for GIF ones...'
+      puts 'Generating static avatars/headers for GIF ones...'
 
       Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account|
         begin
@@ -217,7 +216,7 @@ namespace :mastodon do
         end
       end
 
-      Rails.logger.debug 'Done!'
+      puts 'Done!'
     end
 
     desc 'Ensure referencial integrity'
diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb
index d44d720b1..1077a7288 100644
--- a/spec/controllers/home_controller_spec.rb
+++ b/spec/controllers/home_controller_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe HomeController, type: :controller do
   describe 'GET #index' do
     context 'when not signed in' do
       it 'redirects to about page' do
+        @request.path = '/'
         get :index
         expect(response).to redirect_to(about_path)
       end
@@ -13,6 +14,7 @@ RSpec.describe HomeController, type: :controller do
 
     context 'when signed in' do
       let(:user) { Fabricate(:user) }
+
       subject do
         sign_in(user)
         get :index
diff --git a/spec/fabricators/custom_emoji_fabricator.rb b/spec/fabricators/custom_emoji_fabricator.rb
new file mode 100644
index 000000000..18a7d23dc
--- /dev/null
+++ b/spec/fabricators/custom_emoji_fabricator.rb
@@ -0,0 +1,5 @@
+Fabricator(:custom_emoji) do
+  shortcode 'coolcat'
+  domain    nil
+  image     { File.open(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png')) }
+end
diff --git a/spec/fabricators/site_upload_fabricator.rb b/spec/fabricators/site_upload_fabricator.rb
new file mode 100644
index 000000000..8f4e43ac9
--- /dev/null
+++ b/spec/fabricators/site_upload_fabricator.rb
@@ -0,0 +1,3 @@
+Fabricator(:site_upload) do
+
+end
diff --git a/spec/fixtures/files/emojo.png b/spec/fixtures/files/emojo.png
new file mode 100644
index 000000000..cb5993499
--- /dev/null
+++ b/spec/fixtures/files/emojo.png
Binary files differdiff --git a/spec/fixtures/requests/activitypub-actor-noinbox.txt b/spec/fixtures/requests/activitypub-actor-noinbox.txt
new file mode 100644
index 000000000..95b4650e0
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-actor-noinbox.txt
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Date: Sun, 17 Sep 2017 06:51:23 GMT
+Content-Type: application/json; charset=utf-8
+X-XSS-Protection: 1; mode=block
+Link: <https://ap.example.com/.well-known/webfinger?resource=acct%3Afoo%40ap.example.com>; rel="lrdd"; type="application/xrd+xml", <https://ap.example.com/users/foo.atom>; rel="alternate"; type="application/atom+xml"
+Vary: Accept-Encoding
+Cache-Control: max-age=0, private, must-revalidate
+
+{"@context":"https://www.w3.org/ns/activitystreams","id":"https://ap.example.com/users/foo","type":"Person","following":"https://ap.example.com/users/foo/following","followers":"https://ap.example.com/users/foo/followers","inbox":null,"outbox":"https://ap.example.com/users/foo/outbox","preferredUsername":"foo","name":"","summary":"\u003cp\u003etest\u003c/p\u003e","icon":"https://quitter.no/avatar/7477-300-20160211190340.png","image":"/headers/original/missing.png","publicKey":{"id":"https://ap.example.com/users/foo#main-key","owner":"https://ap.example.com/users/foo","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3L4vnpNLzVH31MeWI39\n4F0wKeJFsLDAsNXGeOu0QF2x+h1zLWZw/agqD2R3JPU9/kaDJGPIV2Sn5zLyUA9S\n6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh\n8lDET6X4Pyw+ZJU0/OLo/41q9w+OrGtlsTm/PuPIeXnxa6BLqnDaxC+4IcjG/FiP\nahNCTINl/1F/TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq+t8nhQYkgAkt64euW\npva3qL5KD1mTIZQEP+LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3Qvu\nHQIDAQAB\n-----END PUBLIC KEY-----\n"}}
\ No newline at end of file
diff --git a/spec/fixtures/requests/activitypub-actor.txt b/spec/fixtures/requests/activitypub-actor.txt
new file mode 100644
index 000000000..6514241cb
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-actor.txt
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/activity+json; charset=utf-8
+Link: <https://ap.example.com/.well-known/webfinger?resource=acct%3Afoo%40ap.example.com>; rel="lrdd"; type="application/xrd+xml", <https://ap.example.com/users/foo.atom>; rel="alternate"; type="application/atom+xml", <https://ap.example.com/users/foo>; rel="alternate"; type="application/activity+json"
+Vary: Accept-Encoding
+X-Content-Type-Options: nosniff
+X-Xss-Protection: 1; mode=block
+
+{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation"}],"id":"https://ap.example.com/users/foo","type":"Person","following":"https://ap.example.com/users/foo/following","followers":"https://ap.example.com/users/foo/followers","inbox":"https://ap.example.com/users/foo/inbox","outbox":"https://ap.example.com/users/foo/outbox","preferredUsername":"foo","name":"","summary":"\u003cp\u003etest\u003c/p\u003e","url":"https://ap.example.com/@foo","manuallyApprovesFollowers":false,"publicKey":{"id":"https://ap.example.com/users/foo#main-key","owner":"https://ap.example.com/users/foo","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3L4vnpNLzVH31MeWI39\n4F0wKeJFsLDAsNXGeOu0QF2x+h1zLWZw/agqD2R3JPU9/kaDJGPIV2Sn5zLyUA9S\n6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh\n8lDET6X4Pyw+ZJU0/OLo/41q9w+OrGtlsTm/PuPIeXnxa6BLqnDaxC+4IcjG/FiP\nahNCTINl/1F/TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq+t8nhQYkgAkt64euW\npva3qL5KD1mTIZQEP+LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3Qvu\nHQIDAQAB\n-----END PUBLIC KEY-----\n"},"endpoints":{"sharedInbox":"https://ap.example.com/inbox"},"icon":{"type":"Image","url":"https://quitter.no/avatar/7477-300-20160211190340.png"}}
\ No newline at end of file
diff --git a/spec/fixtures/requests/activitypub-feed.txt b/spec/fixtures/requests/activitypub-feed.txt
new file mode 100644
index 000000000..84fd414c3
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-feed.txt
@@ -0,0 +1,47 @@
+HTTP/1.1 200 OK
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/atom+xml; charset=utf-8
+Link: <https://ap.example.com/.well-known/webfinger?resource=acct%3Afoo%40ap.example.com>; rel="lrdd"; type="application/xrd+xml", <https://ap.example.com/users/foo.atom>; rel="alternate"; type="application/atom+xml", <https://ap.example.com/users/foo>; rel="alternate"; type="application/activity+json"
+Vary: Accept-Encoding
+Date: Sun, 17 Sep 2017 06:33:53 GMT
+
+<?xml version="1.0"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
+  <id>https://ap.example.com/users/foo.atom</id>
+  <title>foo</title>
+  <subtitle>test</subtitle>
+  <updated>2017-09-16T18:50:09Z</updated>
+  <logo>https://ap.example.com/system/accounts/avatars/000/000/001/original/141ee5846d159cba.png?1505587809</logo>
+  <author>
+    <id>https://ap.example.com/users/foo</id>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+    <uri>https://ap.example.com/users/foo</uri>
+    <name>foo</name>
+    <email>foo@ap.example.com</email>
+    <summary type="html">&lt;p&gt;test&lt;/p&gt;</summary>
+    <link rel="alternate" type="text/html" href="https://ap.example.com/@foo"/>
+    <link rel="avatar" type="image/jpeg" media:width="120" media:height="120" href="https://quitter.no/avatar/7477-300-20160211190340.png"/>
+    <poco:preferredUsername>foo</poco:preferredUsername>
+    <poco:note>test</poco:note>
+    <mastodon:scope>public</mastodon:scope>
+  </author>
+  <link rel="alternate" type="text/html" href="https://ap.example.com/@foo"/>
+  <link rel="self" type="application/atom+xml" href="https://ap.example.com/users/foo.atom"/>
+  <link rel="hub" href="https://ap.example.com/api/push"/>
+  <link rel="salmon" href="https://ap.example.com/api/salmon/1"/>
+  <entry>
+    <id>https://ap.example.com/users/foo/statuses/11076</id>
+    <published>2017-09-13T01:23:19Z</published>
+    <updated>2017-09-13T01:23:19Z</updated>
+    <title>New status by foo</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <link rel="alternate" type="application/activity+json" href="https://ap.example.com/users/foo/statuses/11076"/>
+    <content type="html" xml:lang="ja">&lt;p&gt;test&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://ap.example.com/@foo/11076"/>
+    <link rel="self" type="application/atom+xml" href="https://ap.example.com/users/foo/updates/11015.atom"/>
+    <ostatus:conversation ref="tag:ap.example.com,2017-09-13:objectId=7412:objectType=Conversation"/>
+  </entry>
+</feed>
diff --git a/spec/fixtures/requests/activitypub-webfinger.txt b/spec/fixtures/requests/activitypub-webfinger.txt
new file mode 100644
index 000000000..465066d84
--- /dev/null
+++ b/spec/fixtures/requests/activitypub-webfinger.txt
@@ -0,0 +1,7 @@
+HTTP/1.1 200 OK
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/jrd+json; charset=utf-8
+X-Content-Type-Options: nosniff
+Date: Sun, 17 Sep 2017 06:22:50 GMT
+
+{"subject":"acct:foo@ap.example.com","aliases":["https://ap.example.com/@foo","https://ap.example.com/users/foo"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://ap.example.com/@foo"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://ap.example.com/users/foo.atom"},{"rel":"self","type":"application/activity+json","href":"https://ap.example.com/users/foo"},{"rel":"salmon","href":"https://ap.example.com/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.u3L4vnpNLzVH31MeWI394F0wKeJFsLDAsNXGeOu0QF2x-h1zLWZw_agqD2R3JPU9_kaDJGPIV2Sn5zLyUA9S6swCCMOtn7BBR9g9sucgXJmUFB0tACH2QSgHywMAybGfmSb3LsEMNKsGJ9VsvYoh8lDET6X4Pyw-ZJU0_OLo_41q9w-OrGtlsTm_PuPIeXnxa6BLqnDaxC-4IcjG_FiPahNCTINl_1F_TgSSDZ4Taf4U9XFEIFw8wmgploELozzIzKq-t8nhQYkgAkt64euWpva3qL5KD1mTIZQEP-LZvh3s2WHrLi3fhbdRuwQ2c0KkJA2oSTFPDpqqbPGZ3QvuHQ==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://ap.example.com/authorize_follow?acct={uri}"}]}
\ No newline at end of file
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index fcb044ebc..1a9520f04 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe ActivityPub::Activity::Create do
 
   before do
     stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
+    stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
   end
 
   describe '#perform' do
@@ -217,5 +218,29 @@ RSpec.describe ActivityPub::Activity::Create do
         expect(status.tags.map(&:name)).to include('test')
       end
     end
+
+    context 'with emojis' do
+      let(:object_json) do
+        {
+          id: 'bar',
+          type: 'Note',
+          content: 'Lorem ipsum :tinking:',
+          tag: [
+            {
+              type: 'Emoji',
+              href: 'http://example.com/emoji.png',
+              name: 'tinking',
+            },
+          ],
+        }
+      end
+
+      it 'creates status' do
+        status = sender.statuses.first
+
+        expect(status).to_not be_nil
+        expect(status.emojis.map(&:shortcode)).to include('tinking')
+      end
+    end
   end
 end
diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb
index ab04ccbab..71b6b78d2 100644
--- a/spec/lib/formatter_spec.rb
+++ b/spec/lib/formatter_spec.rb
@@ -89,6 +89,54 @@ RSpec.describe Formatter do
       end
     end
 
+    context 'matches a URL with Japanese path string' do
+      let(:text) { 'https://ja.wikipedia.org/wiki/日本' }
+
+      it 'has valid URL' do
+        is_expected.to include 'href="https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC"'
+      end
+    end
+
+    context 'matches a URL with Korean path string' do
+      let(:text) { 'https://ko.wikipedia.org/wiki/대한민국' }
+
+      it 'has valid URL' do
+        is_expected.to include 'href="https://ko.wikipedia.org/wiki/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD"'
+      end
+    end
+
+    context 'matches a URL with Simplified Chinese path string' do
+      let(:text) { 'https://baike.baidu.com/item/中华人民共和国' }
+
+      it 'has valid URL' do
+        is_expected.to include 'href="https://baike.baidu.com/item/%E4%B8%AD%E5%8D%8E%E4%BA%BA%E6%B0%91%E5%85%B1%E5%92%8C%E5%9B%BD"'
+      end
+    end
+
+    context 'matches a URL with Traditional Chinese path string' do
+      let(:text) { 'https://zh.wikipedia.org/wiki/臺灣' }
+
+      it 'has valid URL' do
+        is_expected.to include 'href="https://zh.wikipedia.org/wiki/%E8%87%BA%E7%81%A3"'
+      end
+    end
+
+    context 'contains unsafe URL (XSS attack, visible part)' do
+      let(:text) { %q{http://example.com/b<del>b</del>} }
+
+      it 'has escaped HTML' do
+        is_expected.to include '&lt;del&gt;b&lt;/del&gt;'
+      end
+    end
+
+    context 'contains unsafe URL (XSS attack, invisible part)' do
+      let(:text) { %q{http://example.com/blahblahblahblah/a<script>alert("Hello")</script>} }
+
+      it 'has escaped HTML' do
+        is_expected.to include '&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;'
+      end
+    end
+
     context 'contains HTML (script tag)' do
       let(:text) { '<script>alert("Hello")</script>' }
 
@@ -175,6 +223,45 @@ RSpec.describe Formatter do
 
         include_examples 'encode and link URLs'
       end
+
+      context 'with custom_emojify option' do
+        let!(:emoji) { Fabricate(:custom_emoji) }
+        let(:status) { Fabricate(:status, account: local_account, text: text) }
+
+        subject { Formatter.instance.format(status, custom_emojify: true) }
+
+        context 'with emoji at the start' do
+          let(:text) { ':coolcat: Beep boop' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+
+        context 'with emoji in the middle' do
+          let(:text) { 'Beep :coolcat: boop' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+
+        context 'with concatenated emoji' do
+          let(:text) { ':coolcat::coolcat:' }
+
+          it 'does not touch the shortcodes' do
+            is_expected.to match(/:coolcat::coolcat:/)
+          end
+        end
+
+        context 'with emoji at the end' do
+          let(:text) { 'Beep boop :coolcat:' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/boop <img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+      end
     end
 
     context 'with remote status' do
@@ -183,6 +270,45 @@ RSpec.describe Formatter do
       it 'reformats' do
         is_expected.to eq 'Beep boop'
       end
+
+      context 'with custom_emojify option' do
+        let!(:emoji) { Fabricate(:custom_emoji, domain: remote_account.domain) }
+        let(:status) { Fabricate(:status, account: remote_account, text: text) }
+
+        subject { Formatter.instance.format(status, custom_emojify: true) }
+
+        context 'with emoji at the start' do
+          let(:text) { '<p>:coolcat: Beep boop<br />' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+
+        context 'with emoji in the middle' do
+          let(:text) { '<p>Beep :coolcat: boop</p>' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+
+        context 'with concatenated emoji' do
+          let(:text) { '<p>:coolcat::coolcat:</p>' }
+
+          it 'does not touch the shortcodes' do
+            is_expected.to match(/<p>:coolcat::coolcat:<\/p>/)
+          end
+        end
+
+        context 'with emoji at the end' do
+          let(:text) { '<p>Beep boop<br />:coolcat:</p>' }
+
+          it 'converts shortcode to image tag' do
+            is_expected.to match(/<br><img draggable="false" class="emojione" alt=":coolcat:"/)
+          end
+        end
+      end
     end
   end
 
diff --git a/spec/lib/language_detector_spec.rb b/spec/lib/language_detector_spec.rb
index ec39cb6a0..d17026511 100644
--- a/spec/lib/language_detector_spec.rb
+++ b/spec/lib/language_detector_spec.rb
@@ -3,10 +3,10 @@
 require 'rails_helper'
 
 describe LanguageDetector do
-  describe 'prepared_text' do
+  describe 'prepare_text' do
     it 'returns unmodified string without special cases' do
       string = 'just a regular string'
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
 
       expect(result).to eq string
     end
@@ -14,33 +14,35 @@ describe LanguageDetector do
     it 'collapses spacing in strings' do
       string = 'The formatting   in    this is very        odd'
 
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
       expect(result).to eq 'The formatting in this is very odd'
     end
 
     it 'strips usernames from strings before detection' do
       string = '@username Yeah, very surreal...! also @friend'
 
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
       expect(result).to eq 'Yeah, very surreal...! also'
     end
 
     it 'strips URLs from strings before detection' do
       string = 'Our website is https://example.com and also http://localhost.dev'
 
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
       expect(result).to eq 'Our website is and also'
     end
 
     it 'strips #hashtags from strings before detection' do
       string = 'Hey look at all the #animals and #fish'
 
-      result = described_class.new(string).prepared_text
+      result = described_class.instance.send(:prepare_text, string)
       expect(result).to eq 'Hey look at all the and'
     end
   end
 
-  describe 'to_iso_s' do
+  describe 'detect' do
+    let(:account_without_user_locale) { Fabricate(:user, locale: nil).account }
+
     it 'detects english language for basic strings' do
       strings = [
         "Hello and welcome to mastodon how are you today?",
@@ -48,7 +50,7 @@ describe LanguageDetector do
         "a lot of people just want to feel righteous all the time and that's all that matters",
       ]
       strings.each do |string|
-        result = described_class.new(string).to_iso_s
+        result = described_class.instance.detect(string, account_without_user_locale)
 
         expect(result).to eq(:en), string
       end
@@ -56,14 +58,14 @@ describe LanguageDetector do
 
     it 'detects spanish language' do
       string = 'Obtener un Hola y bienvenidos a Mastodon'
-      result = described_class.new(string).to_iso_s
+      result = described_class.instance.detect(string, account_without_user_locale)
 
       expect(result).to eq :es
     end
 
     describe 'when language can\'t be detected' do
       it 'uses nil when sent an empty document' do
-        result = described_class.new('').to_iso_s
+        result = described_class.instance.detect('', account_without_user_locale)
         expect(result).to eq nil
       end
 
@@ -73,7 +75,7 @@ describe LanguageDetector do
           cld_result = CLD3::NNetLanguageIdentifier.new(0, 2048).find_language(string)
           expect(cld_result).not_to eq :en
 
-          result = described_class.new(string).to_iso_s
+          result = described_class.instance.detect(string, account_without_user_locale)
 
           expect(result).to eq nil
         end
@@ -82,14 +84,13 @@ describe LanguageDetector do
       describe 'with an account' do
         it 'uses the account locale when present' do
           account = double(user_locale: 'fr')
-          result  = described_class.new('', account).to_iso_s
+          result  = described_class.instance.detect('', account)
 
           expect(result).to eq :fr
         end
 
         it 'uses nil when account is present but has no locale' do
-          account = double(user_locale: nil)
-          result  = described_class.new('', account).to_iso_s
+          result  = described_class.instance.detect('', account_without_user_locale)
 
           expect(result).to eq nil
         end
@@ -97,8 +98,7 @@ describe LanguageDetector do
 
       describe 'with an `en` default locale' do
         it 'uses nil for undetectable string' do
-          string = ''
-          result = described_class.new(string).to_iso_s
+          result = described_class.instance.detect('', account_without_user_locale)
 
           expect(result).to eq nil
         end
@@ -114,7 +114,7 @@ describe LanguageDetector do
 
         it 'uses nil for undetectable string' do
           string = ''
-          result = described_class.new(string).to_iso_s
+          result = described_class.instance.detect(string, account_without_user_locale)
 
           expect(result).to eq nil
         end
diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb
index 0451eceeb..b2480a53b 100644
--- a/spec/lib/ostatus/atom_serializer_spec.rb
+++ b/spec/lib/ostatus/atom_serializer_spec.rb
@@ -97,11 +97,23 @@ RSpec.describe OStatus::AtomSerializer do
 
       mentioned = element.nodes.find do |node|
         node.name == 'link' &&
-        node[:rel] == 'mentioned' &&
-        node['ostatus:object-type'] == TagManager::TYPES[:person]
+          node[:rel] == 'mentioned' &&
+          node['ostatus:object-type'] == TagManager::TYPES[:person]
       end
+
       expect(mentioned[:href]).to eq 'https://cb6e6126.ngrok.io/users/username'
     end
+
+    it 'appends link elements for emojis' do
+      Fabricate(:custom_emoji)
+
+      status  = Fabricate(:status, text: ':coolcat:')
+      element = serialize(status)
+      emoji   = element.nodes.find { |node| node.name == 'link' && node[:rel] == 'emoji' }
+
+      expect(emoji[:name]).to eq 'coolcat'
+      expect(emoji[:href]).to_not be_blank
+    end
   end
 
   describe 'render' do
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index 1cd6e0a6f..6c7830231 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -63,23 +63,23 @@ RSpec.describe TagManager do
 
   describe '#local_url?' do
     around do |example|
-      original_local_domain = Rails.configuration.x.local_domain
+      original_web_domain = Rails.configuration.x.web_domain
       example.run
-      Rails.configuration.x.local_domain = original_local_domain
+      Rails.configuration.x.web_domain = original_web_domain
     end
 
     it 'returns true if the normalized string with port is local URL' do
-      Rails.configuration.x.local_domain = 'domain:42'
+      Rails.configuration.x.web_domain = 'domain:42'
       expect(TagManager.instance.local_url?('https://DoMaIn:42/')).to eq true
     end
 
     it 'returns true if the normalized string without port is local URL' do
-      Rails.configuration.x.local_domain = 'domain'
+      Rails.configuration.x.web_domain = 'domain'
       expect(TagManager.instance.local_url?('https://DoMaIn/')).to eq true
     end
 
     it 'returns false for string with irrelevant characters' do
-      Rails.configuration.x.local_domain = 'domain'
+      Rails.configuration.x.web_domain = 'domain'
       expect(TagManager.instance.local_url?('https://domainn/')).to eq false
     end
   end
diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb
new file mode 100644
index 000000000..cb51e9519
--- /dev/null
+++ b/spec/models/custom_emoji_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+RSpec.describe CustomEmoji, type: :model do
+  describe '.from_text' do
+    let!(:emojo) { Fabricate(:custom_emoji) }
+
+    subject { described_class.from_text(text, nil) }
+
+    context 'with plain text' do
+      let(:text) { 'Hello :coolcat:' }
+
+      it 'returns records used via shortcodes in text' do
+        is_expected.to include(emojo)
+      end
+    end
+
+    context 'with html' do
+      let(:text) { '<p>Hello :coolcat:</p>' }
+
+      it 'returns records used via shortcodes in text' do
+        is_expected.to include(emojo)
+      end
+    end
+  end
+end
diff --git a/spec/models/site_upload_spec.rb b/spec/models/site_upload_spec.rb
new file mode 100644
index 000000000..8745d54b8
--- /dev/null
+++ b/spec/models/site_upload_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe SiteUpload, type: :model do
+
+end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 484effd5e..12efcae61 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -173,19 +173,6 @@ RSpec.describe Status, type: :model do
     end
   end
 
-  describe '.local_only' do
-    it 'returns only statuses from local accounts' do
-      local_account = Fabricate(:account, domain: nil)
-      remote_account = Fabricate(:account, domain: 'test.com')
-      local_status = Fabricate(:status, account: local_account)
-      remote_status = Fabricate(:status, account: remote_account)
-
-      results = described_class.local_only
-      expect(results).to include(local_status)
-      expect(results).not_to include(remote_status)
-    end
-  end
-
   describe '.as_home_timeline' do
     let(:account) { Fabricate(:account) }
     let(:followed) { Fabricate(:account) }
@@ -529,6 +516,14 @@ RSpec.describe Status, type: :model do
     end
   end
 
+  describe 'validation' do
+    it 'disallow empty uri for remote status' do
+      alice.update(domain: 'example.com')
+      status = Fabricate.build(:status, uri: '', account: alice)
+      expect(status).to model_have_error_on_field(:uri)
+    end
+  end
+
   describe 'after_create' do
     it 'saves ActivityPub uri as uri for local status' do
       status = Status.create(account: alice, text: 'foo')
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index b0aa740ac..ba61d22c3 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe FetchLinkCardService do
     stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt'))
     stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
     stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
+    stub_request(:head, 'http://example.com/日本語').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
+    stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt'))
     stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404)
 
     subject.call(status)
@@ -52,6 +54,15 @@ RSpec.describe FetchLinkCardService do
         expect(status.preview_cards.first.title).to eq("Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.")
       end
     end
+
+    context do
+      let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') }
+
+      it 'works with Japanese path string' do
+        expect(a_request(:get, 'http://example.com/日本語')).to have_been_made.at_least_once
+        expect(status.preview_cards.first.title).to eq("SJISのページ")
+      end
+    end
   end
 
   context 'in a remote status' do
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 4182c4e1f..91902ff69 100644
--- a/spec/services/post_status_service_spec.rb
+++ b/spec/services/post_status_service_spec.rb
@@ -65,15 +65,12 @@ RSpec.describe PostStatusService do
   end
 
   it 'creates a status with a language set' do
-    detector = double(to_iso_s: :en)
-    allow(LanguageDetector).to receive(:new).and_return(detector)
-
     account = Fabricate(:account)
-    text = 'test status text'
+    text = 'This is an English text.'
 
-    subject.call(account, text)
+    status = subject.call(account, text)
 
-    expect(LanguageDetector).to have_received(:new).with(text, account)
+    expect(status.language).to eq 'en'
   end
 
   it 'processes mentions' do
diff --git a/spec/services/resolve_remote_account_service_spec.rb b/spec/services/resolve_remote_account_service_spec.rb
index d0eab2310..d0bb6a137 100644
--- a/spec/services/resolve_remote_account_service_spec.rb
+++ b/spec/services/resolve_remote_account_service_spec.rb
@@ -72,6 +72,39 @@ RSpec.describe ResolveRemoteAccountService do
   end
 
   context 'with an ActivityPub account' do
+    before do
+      stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt'))
+      stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt'))
+      stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt'))
+      stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404)
+    end
+
+    it 'fallback to OStatus if actor json could not be fetched' do
+      stub_request(:get, "https://ap.example.com/users/foo").to_return(status: 404)
+
+      account = subject.call('foo@ap.example.com')
+
+      expect(account.ostatus?).to eq true
+      expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom'
+    end
+
+    it 'fallback to OStatus if actor json did not have inbox_url' do
+      stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-noinbox.txt'))
+
+      account = subject.call('foo@ap.example.com')
+
+      expect(account.ostatus?).to eq true
+      expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom'
+    end
+
+    it 'returns new remote account' do
+      account = subject.call('foo@ap.example.com')
+
+      expect(account.activitypub?).to eq true
+      expect(account.domain).to eq 'ap.example.com'
+      expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
+    end
+
     pending
   end
 
diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb
index 95a8a6323..b2f2658de 100644
--- a/spec/views/about/show.html.haml_spec.rb
+++ b/spec/views/about/show.html.haml_spec.rb
@@ -17,6 +17,7 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do
                                 version_number: '1.0',
                                 source_url: 'https://github.com/tootsuite/mastodon',
                                 open_registrations: false,
+                                thumbnail: nil,
                                 closed_registrations_message: 'yes',
                                 commit_hash: commit_hash)
 
diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb
index 6cc3b117a..59ea40990 100644
--- a/spec/views/stream_entries/show.html.haml_spec.rb
+++ b/spec/views/stream_entries/show.html.haml_spec.rb
@@ -80,9 +80,9 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
 
     header_tags = view.content_for(:header_tags)
 
-    expect(header_tags).to match(%r{<meta content='.+' property='og:title'>})
-    expect(header_tags).to match(%r{<meta content='article' property='og:type'>})
-    expect(header_tags).to match(%r{<meta content='.+' property='og:image'>})
-    expect(header_tags).to match(%r{<meta content='http://.+' property='og:url'>})
+    expect(header_tags).to match(%r{<meta content=".+" property="og:title" />})
+    expect(header_tags).to match(%r{<meta content="article" property="og:type" />})
+    expect(header_tags).to match(%r{<meta content=".+" property="og:image" />})
+    expect(header_tags).to match(%r{<meta content="http://.+" property="og:url" />})
   end
 end