about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/api/v1/push/subscriptions_controller.rb50
-rw-r--r--app/controllers/api/web/push_subscriptions_controller.rb11
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/concerns/session_tracking_concern.rb22
-rw-r--r--app/controllers/invites_controller.rb12
-rw-r--r--app/controllers/shares_controller.rb1
-rw-r--r--app/javascript/flavours/glitch/actions/notifications.js8
-rw-r--r--app/javascript/flavours/glitch/components/modal_root.js3
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts.js2
-rw-r--r--app/javascript/flavours/glitch/styles/about.scss76
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss120
-rw-r--r--app/javascript/flavours/glitch/styles/components/accounts.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/containers.scss1
-rw-r--r--app/javascript/flavours/glitch/styles/forms.scss25
-rw-r--r--app/javascript/flavours/glitch/styles/rtl.scss16
-rw-r--r--app/javascript/flavours/glitch/styles/stream_entries.scss38
-rw-r--r--app/javascript/flavours/glitch/styles/tables.scss116
-rw-r--r--app/javascript/flavours/glitch/util/html.js6
-rw-r--r--app/javascript/mastodon/features/compose/components/upload.js2
-rw-r--r--app/javascript/mastodon/locales/ar.json1
-rw-r--r--app/javascript/mastodon/locales/bg.json1
-rw-r--r--app/javascript/mastodon/locales/ca.json1
-rw-r--r--app/javascript/mastodon/locales/co.json19
-rw-r--r--app/javascript/mastodon/locales/de.json1
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json8
-rw-r--r--app/javascript/mastodon/locales/el.json11
-rw-r--r--app/javascript/mastodon/locales/en.json3
-rw-r--r--app/javascript/mastodon/locales/eo.json1
-rw-r--r--app/javascript/mastodon/locales/es.json1
-rw-r--r--app/javascript/mastodon/locales/eu.json1
-rw-r--r--app/javascript/mastodon/locales/fa.json11
-rw-r--r--app/javascript/mastodon/locales/fi.json21
-rw-r--r--app/javascript/mastodon/locales/fr.json5
-rw-r--r--app/javascript/mastodon/locales/gl.json1
-rw-r--r--app/javascript/mastodon/locales/he.json1
-rw-r--r--app/javascript/mastodon/locales/hr.json1
-rw-r--r--app/javascript/mastodon/locales/hu.json1
-rw-r--r--app/javascript/mastodon/locales/hy.json1
-rw-r--r--app/javascript/mastodon/locales/id.json1
-rw-r--r--app/javascript/mastodon/locales/io.json1
-rw-r--r--app/javascript/mastodon/locales/it.json1
-rw-r--r--app/javascript/mastodon/locales/ja.json1
-rw-r--r--app/javascript/mastodon/locales/ko.json55
-rw-r--r--app/javascript/mastodon/locales/nl.json21
-rw-r--r--app/javascript/mastodon/locales/no.json1
-rw-r--r--app/javascript/mastodon/locales/oc.json1
-rw-r--r--app/javascript/mastodon/locales/pl.json1
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json1
-rw-r--r--app/javascript/mastodon/locales/pt.json1
-rw-r--r--app/javascript/mastodon/locales/ru.json1
-rw-r--r--app/javascript/mastodon/locales/sk.json1
-rw-r--r--app/javascript/mastodon/locales/sl.json297
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json1
-rw-r--r--app/javascript/mastodon/locales/sr.json1
-rw-r--r--app/javascript/mastodon/locales/sv.json1
-rw-r--r--app/javascript/mastodon/locales/te.json1
-rw-r--r--app/javascript/mastodon/locales/th.json1
-rw-r--r--app/javascript/mastodon/locales/tr.json1
-rw-r--r--app/javascript/mastodon/locales/uk.json1
-rw-r--r--app/javascript/mastodon/locales/whitelist_sl.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json1
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json1
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json1
-rw-r--r--app/lib/request.rb11
-rw-r--r--app/models/concerns/remotable.rb3
-rw-r--r--app/models/user.rb2
-rw-r--r--app/models/web/push_subscription.rb47
-rw-r--r--app/serializers/initial_state_serializer.rb8
-rw-r--r--app/serializers/rest/web_push_subscription_serializer.rb13
-rw-r--r--app/serializers/web/notification_serializer.rb2
-rw-r--r--app/services/notify_service.rb21
-rw-r--r--app/views/accounts/show.html.haml1
-rw-r--r--app/views/tags/_og.html.haml2
-rw-r--r--app/views/tags/show.html.haml2
-rw-r--r--app/workers/web/push_notification_worker.rb18
-rw-r--r--app/workers/web_push_notification_worker.rb25
78 files changed, 917 insertions, 241 deletions
diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb
new file mode 100644
index 000000000..5038cc03c
--- /dev/null
+++ b/app/controllers/api/v1/push/subscriptions_controller.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class Api::V1::Push::SubscriptionsController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :push }
+  before_action :require_user!
+  before_action :set_web_push_subscription
+
+  def create
+    @web_subscription&.destroy!
+
+    @web_subscription = ::Web::PushSubscription.create!(
+      endpoint: subscription_params[:endpoint],
+      key_p256dh: subscription_params[:keys][:p256dh],
+      key_auth: subscription_params[:keys][:auth],
+      data: data_params,
+      user_id: current_user.id,
+      access_token_id: doorkeeper_token.id
+    )
+
+    render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
+  end
+
+  def update
+    raise ActiveRecord::RecordNotFound if @web_subscription.nil?
+
+    @web_subscription.update!(data: data_params)
+
+    render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
+  end
+
+  def destroy
+    @web_subscription&.destroy!
+    render_empty
+  end
+
+  private
+
+  def set_web_push_subscription
+    @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
+  end
+
+  def subscription_params
+    params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
+  end
+
+  def data_params
+    return {} if params[:data].blank?
+    params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
+  end
+end
diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index 249e7c186..fe8e42580 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -31,22 +31,23 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
       endpoint: subscription_params[:endpoint],
       key_p256dh: subscription_params[:keys][:p256dh],
       key_auth: subscription_params[:keys][:auth],
-      data: data
+      data: data,
+      user_id: active_session.user_id,
+      access_token_id: active_session.access_token_id
     )
 
     active_session.update!(web_push_subscription: web_subscription)
 
-    render json: web_subscription.as_payload
+    render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
   end
 
   def update
     params.require([:id])
 
     web_subscription = ::Web::PushSubscription.find(params[:id])
-
     web_subscription.update!(data: data_params)
 
-    render json: web_subscription.as_payload
+    render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
   end
 
   private
@@ -56,6 +57,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
   end
 
   def data_params
-    @data_params ||= params.require(:data).permit(:alerts)
+    @data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
   end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 158c0c10e..27ebc3333 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base
 
   include Localized
   include UserTrackingConcern
+  include SessionTrackingConcern
 
   helper_method :current_account
   helper_method :current_session
diff --git a/app/controllers/concerns/session_tracking_concern.rb b/app/controllers/concerns/session_tracking_concern.rb
new file mode 100644
index 000000000..45361b019
--- /dev/null
+++ b/app/controllers/concerns/session_tracking_concern.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module SessionTrackingConcern
+  extend ActiveSupport::Concern
+
+  UPDATE_SIGN_IN_HOURS = 24
+
+  included do
+    before_action :set_session_activity
+  end
+
+  private
+
+  def set_session_activity
+    return unless session_needs_update?
+    current_session.touch
+  end
+
+  def session_needs_update?
+    !current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago
+  end
+end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 189e4072e..2e9f73bb8 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -9,9 +9,9 @@ class InvitesController < ApplicationController
   before_action :set_pack
 
   def index
-    authorize :invite, :create?
+    authorize :invite, :index?
 
-    @invites = Invite.where(user: current_user)
+    @invites = invites
     @invite  = Invite.new(expires_in: 1.day.to_i)
   end
 
@@ -24,13 +24,13 @@ class InvitesController < ApplicationController
     if @invite.save
       redirect_to invites_path
     else
-      @invites = Invite.where(user: current_user)
+      @invites = invites
       render :index
     end
   end
 
   def destroy
-    @invite = Invite.where(user: current_user).find(params[:id])
+    @invite = invites.find(params[:id])
     authorize @invite, :destroy?
     @invite.expire!
     redirect_to invites_path
@@ -42,6 +42,10 @@ class InvitesController < ApplicationController
     use_pack 'settings'
   end
 
+  def invites
+    Invite.where(user: current_user)
+  end
+
   def resource_params
     params.require(:invite).permit(:max_uses, :expires_in)
   end
diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb
index 3cbaccb35..4624c29a6 100644
--- a/app/controllers/shares_controller.rb
+++ b/app/controllers/shares_controller.rb
@@ -16,6 +16,7 @@ class SharesController < ApplicationController
 
   def initial_state_params
     text = [params[:title], params[:text], params[:url]].compact.join(' ')
+
     {
       settings: Web::Setting.find_by(user: current_user)&.data || {},
       push_subscription: current_account.user.web_push_subscription(current_session),
diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index cf27eff90..0d52100b9 100644
--- a/app/javascript/flavours/glitch/actions/notifications.js
+++ b/app/javascript/flavours/glitch/actions/notifications.js
@@ -3,6 +3,7 @@ import { List as ImmutableList } from 'immutable';
 import IntlMessageFormat from 'intl-messageformat';
 import { fetchRelationships } from './accounts';
 import { defineMessages } from 'react-intl';
+import { unescapeHTML } from 'flavours/glitch/util/html';
 
 export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
 
@@ -40,13 +41,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
   }
 };
 
-const unescapeHTML = (html) => {
-  const wrapper = document.createElement('div');
-  html = html.replace(/<br \/>|<br>|\n/g, ' ');
-  wrapper.innerHTML = html;
-  return wrapper.textContent;
-};
-
 export function updateNotifications(notification, intlMessages, intlLocale) {
   return (dispatch, getState) => {
     const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js
index 789e117c7..89f81f58e 100644
--- a/app/javascript/flavours/glitch/components/modal_root.js
+++ b/app/javascript/flavours/glitch/components/modal_root.js
@@ -6,6 +6,7 @@ export default class ModalRoot extends React.PureComponent {
   static propTypes = {
     children: PropTypes.node,
     onClose: PropTypes.func.isRequired,
+    noEsc: PropTypes.bool,
   };
 
   state = {
@@ -16,7 +17,7 @@ export default class ModalRoot extends React.PureComponent {
 
   handleKeyUp = (e) => {
     if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
-         && !!this.props.children && !this.props.props.noEsc) {
+         && !!this.props.children && !this.props.noEsc) {
       this.props.onClose();
     }
   }
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 15bd6b365..464c73c9a 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -111,7 +111,7 @@ export default class Header extends ImmutablePureComponent {
                 {fields.map((pair, i) => (
                   <dl key={i}>
                     <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
-                    <dd dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} title={pair.get('value')} />
+                    <dd dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} title={pair.get('value_plain')} />
                  </dl>
                 ))}
               </div>
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index 320c039a4..cdb6dc9d0 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -59,7 +59,7 @@ export default class ModalRoot extends React.PureComponent {
     const visible = !!type;
 
     return (
-      <Base onClose={onClose}>
+      <Base onClose={onClose} noEsc={props.noEsc}>
         {visible && (
           <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
             {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js
index 23fbd999c..e8127a871 100644
--- a/app/javascript/flavours/glitch/reducers/accounts.js
+++ b/app/javascript/flavours/glitch/reducers/accounts.js
@@ -57,6 +57,7 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import emojify from 'flavours/glitch/util/emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 import escapeTextContentForBrowser from 'escape-html';
+import { unescapeHTML } from 'flavours/glitch/util/html';
 
 const normalizeAccount = (state, account) => {
   account = { ...account };
@@ -74,6 +75,7 @@ const normalizeAccount = (state, account) => {
       ...pair,
       name_emojified: emojify(escapeTextContentForBrowser(pair.name)),
       value_emojified: emojify(pair.value),
+      value_plain: unescapeHTML(pair.value),
     }));
   }
 
diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss
index 55f31266f..c9c0e3081 100644
--- a/app/javascript/flavours/glitch/styles/about.scss
+++ b/app/javascript/flavours/glitch/styles/about.scss
@@ -322,6 +322,11 @@ $small-breakpoint: 960px;
     border: 0;
     border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
     margin: 20px 0;
+
+    &.spacer {
+      height: 1px;
+      border: 0;
+    }
   }
 
   .container-alt {
@@ -681,6 +686,54 @@ $small-breakpoint: 960px;
       margin-bottom: 0;
     }
 
+    .account {
+      border-bottom: 0;
+      padding: 0;
+
+      &__display-name {
+        align-items: center;
+        display: flex;
+        margin-right: 5px;
+      }
+
+      div.account__display-name {
+        &:hover {
+          .display-name strong {
+            text-decoration: none;
+          }
+        }
+
+        .account__avatar {
+          cursor: default;
+        }
+      }
+
+      &__avatar-wrapper {
+        margin-left: 0;
+        flex: 0 0 auto;
+      }
+
+      &__avatar {
+        width: 44px;
+        height: 44px;
+        background-size: 44px 44px;
+      }
+
+      .display-name {
+        font-size: 15px;
+
+        &__account {
+          font-size: 14px;
+        }
+      }
+    }
+
+    @media screen and (max-width: $small-breakpoint) {
+      .contact {
+        margin-top: 30px;
+      }
+    }
+
     @media screen and (max-width: $column-breakpoint) {
       padding: 25px 20px;
     }
@@ -816,6 +869,8 @@ $small-breakpoint: 960px;
       font-size: 16px;
       line-height: inherit;
       font-weight: inherit;
+      margin: 0;
+      padding: 0;
     }
 
     .column {
@@ -852,8 +907,13 @@ $small-breakpoint: 960px;
   }
 
   &__features {
+    & > p {
+      padding-right: 60px;
+    }
+
     .features-list {
-      margin: 40px 0 !important;
+      margin: 40px 0;
+      margin-top: 30px;
     }
 
     &__action {
@@ -862,17 +922,11 @@ $small-breakpoint: 960px;
   }
 
   .features-list {
-    margin-top: 20px;
-
     .features-list__row {
       display: flex;
       padding: 10px 0;
       justify-content: space-between;
 
-      &:first-child {
-        padding-top: 0;
-      }
-
       .visual {
         flex: 0 0 auto;
         display: flex;
@@ -898,6 +952,14 @@ $small-breakpoint: 960px;
         }
       }
     }
+
+    @media screen and (min-width: $small-breakpoint) {
+      display: grid;
+      grid-gap: 30px;
+      grid-template-columns: 1fr 1fr;
+      grid-auto-columns: 50%;
+      grid-auto-rows: max-content;
+    }
   }
 
   .extended-description {
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index b077df145..1948a2a23 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -141,14 +141,15 @@
     }
 
     hr {
-      margin: 20px 0;
+      width: 100%;
+      height: 0;
       border: 0;
-      background: transparent;
-      border-bottom: 1px solid $ui-base-color;
+      border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
+      margin: 20px 0;
 
-      &.section-break {
-        margin: 30px 0;
-        border-bottom: 2px solid $ui-base-lighter-color;
+      &.spacer {
+        height: 1px;
+        border: 0;
       }
     }
 
@@ -273,22 +274,6 @@
   }
 }
 
-.flavour-screen {
-  display: block;
-  margin: 10px auto;
-  max-width: 100%;
-}
-
-.flavour-description {
-  display: block;
-  font-size: 16px;
-  margin: 10px 0;
-
-  & > p {
-    margin: 10px 0;
-  }
-}
-
 .report-accounts {
   display: flex;
   flex-wrap: wrap;
@@ -351,34 +336,9 @@
   }
 }
 
-.report-note__comment {
-  margin-bottom: 20px;
-}
-
-.report-note__form {
-  margin-bottom: 20px;
-
-  .report-note__textarea {
-    box-sizing: border-box;
-    border: 0;
-    padding: 7px 4px;
-    margin-bottom: 10px;
-    font-size: 16px;
-    color: $inverted-text-color;
-    display: block;
-    width: 100%;
-    outline: 0;
-    font-family: inherit;
-    resize: vertical;
-  }
-
-  .report-note__buttons {
-    text-align: right;
-  }
-
-  .report-note__button {
-    margin: 0 0 5px 5px;
-  }
+.simple_form.new_report_note,
+.simple_form.new_account_moderation_note {
+  max-width: 100%;
 }
 
 .batch-form-box {
@@ -406,13 +366,6 @@
   }
 }
 
-.batch-checkbox,
-.batch-checkbox-all {
-  display: flex;
-  align-items: center;
-  margin-right: 5px;
-}
-
 .back-link {
   margin-bottom: 10px;
   font-size: 14px;
@@ -432,7 +385,7 @@
 }
 
 .log-entry {
-  margin-bottom: 8px;
+  margin-bottom: 20px;
   line-height: 20px;
 
   &__header {
@@ -530,9 +483,12 @@
   }
 }
 
+a.name-tag,
 .name-tag {
   display: flex;
   align-items: center;
+  text-decoration: none;
+  color: $secondary-text-color;
 
   .avatar {
     display: block;
@@ -544,4 +500,52 @@
   .username {
     font-weight: 500;
   }
+
+  &.suspended {
+    .username {
+      text-decoration: line-through;
+      color: lighten($error-red, 12%);
+    }
+
+    .avatar {
+      filter: grayscale(100%);
+      opacity: 0.8;
+    }
+  }
+}
+
+.speech-bubble {
+  margin-bottom: 20px;
+  border-left: 4px solid $ui-highlight-color;
+
+  &.positive {
+    border-left-color: $success-green;
+  }
+
+  &.negative {
+    border-left-color: lighten($error-red, 12%);
+  }
+
+  &__bubble {
+    padding: 16px;
+    padding-left: 14px;
+    font-size: 15px;
+    line-height: 20px;
+    border-radius: 4px 4px 4px 0;
+    position: relative;
+    font-weight: 500;
+
+    a {
+      color: $darker-text-color;
+    }
+  }
+
+  &__owner {
+    padding: 8px;
+    padding-left: 12px;
+  }
+
+  time {
+    color: $dark-text-color;
+  }
 }
diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss
index 5167a507e..dadfa6d57 100644
--- a/app/javascript/flavours/glitch/styles/components/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/components/accounts.scss
@@ -32,7 +32,8 @@
 
 .account__avatar-wrapper {
   float: left;
-  margin: 6px 16px 6px 6px;
+  margin-left: 12px;
+  margin-right: 12px;
 }
 
 .account__avatar {
diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss
index 9d5ab66a4..c40b38a5a 100644
--- a/app/javascript/flavours/glitch/styles/containers.scss
+++ b/app/javascript/flavours/glitch/styles/containers.scss
@@ -60,6 +60,7 @@
   }
 }
 
+.card-standalone__body,
 .media-gallery-standalone__body {
   overflow: hidden;
 }
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index 0b12742a9..f97890187 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -280,6 +280,11 @@ code {
   .actions {
     margin-top: 30px;
     display: flex;
+
+    &.actions--top {
+      margin-top: 0;
+      margin-bottom: 30px;
+    }
   }
 
   button,
@@ -563,9 +568,27 @@ code {
 
 .post-follow-actions {
   text-align: center;
-  color: $ui-primary-color;
+  color: $darker-text-color;
 
   div {
     margin-bottom: 4px;
   }
 }
+
+.alternative-login {
+  margin-top: 20px;
+  margin-bottom: 20px;
+
+  h4 {
+    font-size: 16px;
+    color: $primary-text-color;
+    text-align: center;
+    margin-bottom: 20px;
+    border: 0;
+    padding: 0;
+  }
+
+  .button {
+    display: block;
+  }
+}
diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss
index 77420c84b..e9099a9e9 100644
--- a/app/javascript/flavours/glitch/styles/rtl.scss
+++ b/app/javascript/flavours/glitch/styles/rtl.scss
@@ -1,6 +1,22 @@
 body.rtl {
   direction: rtl;
 
+  .column-header > button {
+    text-align: right;
+    padding-left: 0;
+    padding-right: 15px;
+  }
+
+  .landing-page__logo {
+    margin-right: 0;
+    margin-left: 20px;
+  }
+
+  .landing-page .features-list .features-list__row .visual {
+    margin-left: 0;
+    margin-right: 15px;
+  }
+
   .column-link__icon,
   .column-header__icon {
     margin-right: 0;
diff --git a/app/javascript/flavours/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/stream_entries.scss
index b505c1580..40963ae84 100644
--- a/app/javascript/flavours/glitch/styles/stream_entries.scss
+++ b/app/javascript/flavours/glitch/styles/stream_entries.scss
@@ -6,7 +6,8 @@
     background: $simple-background-color;
 
     .detailed-status.light,
-    .status.light {
+    .status.light,
+    .more.light {
       border-bottom: 1px solid $ui-secondary-color;
       animation: none;
     }
@@ -65,6 +66,10 @@
     }
   }
 
+  .media-gallery__gifv__label {
+    bottom: 9px;
+  }
+
   .status.light {
     padding: 14px 14px 14px (48px + 14px * 2);
     position: relative;
@@ -145,10 +150,10 @@
 
       a.status__content__spoiler-link {
         color: $primary-text-color;
-        background: $ui-primary-color;
+        background: $ui-base-color;
 
         &:hover {
-          background: lighten($ui-primary-color, 8%);
+          background: lighten($ui-base-color, 8%);
         }
       }
     }
@@ -215,10 +220,10 @@
 
       a.status__content__spoiler-link {
         color: $primary-text-color;
-        background: $ui-primary-color;
+        background: $ui-base-color;
 
         &:hover {
-          background: lighten($ui-primary-color, 8%);
+          background: lighten($ui-base-color, 8%);
         }
       }
     }
@@ -261,16 +266,8 @@
   }
 
   .media-spoiler {
-    background: $ui-primary-color;
-    color: $inverted-text-color;
-    transition: all 100ms linear;
-
-    &:hover,
-    &:active,
-    &:focus {
-      background: darken($ui-primary-color, 5%);
-      color: unset;
-    }
+    background: $ui-base-color;
+    color: $darker-text-color;
   }
 
   .pre-header {
@@ -299,6 +296,17 @@
       text-decoration: underline;
     }
   }
+
+  .more {
+    color: $darker-text-color;
+    display: block;
+    padding: 14px;
+    text-align: center;
+
+    &:not(:hover) {
+      text-decoration: none;
+    }
+  }
 }
 
 .embed {
diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index c12d84f1c..fa876e603 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -11,6 +11,7 @@
     vertical-align: top;
     border-top: 1px solid $ui-base-color;
     text-align: left;
+    background: darken($ui-base-color, 4%);
   }
 
   & > thead > tr > th {
@@ -48,9 +49,38 @@
     }
   }
 
-  &.inline-table > tbody > tr:nth-child(odd) > td,
-  &.inline-table > tbody > tr:nth-child(odd) > th {
-    background: transparent;
+  &.inline-table {
+    & > tbody > tr:nth-child(odd) {
+      & > td,
+      & > th {
+        background: transparent;
+      }
+    }
+
+    & > tbody > tr:first-child {
+      & > td,
+      & > th {
+        border-top: 0;
+      }
+    }
+  }
+
+  &.batch-table {
+    & > thead > tr > th {
+      background: $ui-base-color;
+      border-top: 1px solid darken($ui-base-color, 8%);
+      border-bottom: 1px solid darken($ui-base-color, 8%);
+
+      &:first-child {
+        border-radius: 4px 0 0;
+        border-left: 1px solid darken($ui-base-color, 8%);
+      }
+
+      &:last-child {
+        border-radius: 0 4px 0 0;
+        border-right: 1px solid darken($ui-base-color, 8%);
+      }
+    }
   }
 }
 
@@ -63,6 +93,13 @@ samp {
   font-family: 'mastodon-font-monospace', monospace;
 }
 
+button.table-action-link {
+  background: transparent;
+  border: 0;
+  font: inherit;
+}
+
+button.table-action-link,
 a.table-action-link {
   text-decoration: none;
   display: inline-block;
@@ -79,4 +116,77 @@ a.table-action-link {
     font-weight: 400;
     margin-right: 5px;
   }
+
+  &:first-child {
+    padding-left: 0;
+  }
+}
+
+.batch-table {
+  &__toolbar,
+  &__row {
+    display: flex;
+
+    &__select {
+      box-sizing: border-box;
+      padding: 8px 16px;
+      cursor: pointer;
+      min-height: 100%;
+
+      input {
+        margin-top: 8px;
+      }
+    }
+
+    &__actions,
+    &__content {
+      padding: 8px 0;
+      padding-right: 16px;
+      flex: 1 1 auto;
+    }
+  }
+
+  &__toolbar {
+    border: 1px solid darken($ui-base-color, 8%);
+    background: $ui-base-color;
+    border-radius: 4px 0 0;
+    height: 47px;
+    align-items: center;
+
+    &__actions {
+      text-align: right;
+      padding-right: 16px - 5px;
+    }
+  }
+
+  &__row {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    background: darken($ui-base-color, 4%);
+
+    &:hover {
+      background: darken($ui-base-color, 2%);
+    }
+
+    &:nth-child(even) {
+      background: $ui-base-color;
+
+      &:hover {
+        background: lighten($ui-base-color, 2%);
+      }
+    }
+
+    &__content {
+      padding-top: 12px;
+      padding-bottom: 16px;
+    }
+  }
+
+  .status__content {
+    padding-top: 0;
+
+    strong {
+      font-weight: 700;
+    }
+  }
 }
diff --git a/app/javascript/flavours/glitch/util/html.js b/app/javascript/flavours/glitch/util/html.js
new file mode 100644
index 000000000..0b646ce58
--- /dev/null
+++ b/app/javascript/flavours/glitch/util/html.js
@@ -0,0 +1,6 @@
+export const unescapeHTML = (html) => {
+  const wrapper = document.createElement('div');
+  html = html.replace(/<br \/>|<br>|\n/g, ' ');
+  wrapper.innerHTML = html;
+  return wrapper.textContent;
+};
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index 61b2d19e0..bfa2b4727 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -77,7 +77,7 @@ export default class Upload extends ImmutablePureComponent {
           {({ scale }) => (
             <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
               <div className={classNames('compose-form__upload__actions', { active })}>
-                <button className='icon-button' onClick={this.handleUndoClick}><i className='fa fa-times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Undo' /></button>
+                <button className='icon-button' onClick={this.handleUndoClick}><i className='fa fa-times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
                 {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
               </div>
 
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index d531d8fd5..a4f27cd31 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "حظر @{name}",
   "account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}",
   "account.blocked": "محظور",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 971475114..dd747ab3b 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Блокирай",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index f2e3699d5..22c5453ca 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Bloca @{name}",
   "account.block_domain": "Amaga-ho tot de {domain}",
   "account.blocked": "Bloquejat",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index 2d7427c55..9d8d950a8 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -1,9 +1,10 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Bluccà @{name}",
   "account.block_domain": "Piattà tuttu da {domain}",
   "account.blocked": "Bluccatu",
   "account.direct": "Missaghju direttu @{name}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "Ghjè pussibule chì l’infurmazione quì sottu ùn rifletta micca u prufile sanu di l’utilizatore.",
   "account.domain_blocked": "Duminiu piattatu",
   "account.edit_profile": "Mudificà u prufile",
   "account.follow": "Siguità",
@@ -28,7 +29,7 @@
   "account.unfollow": "Ùn siguità più",
   "account.unmute": "Ùn piattà più @{name}",
   "account.unmute_notifications": "Ùn piattà più nutificazione da @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "Vede tuttu u prufile",
   "alert.unexpected.message": "Un prublemu inaspettatu hè accadutu.",
   "alert.unexpected.title": "Uups!",
   "boost_modal.combo": "Pudete appughjà nant'à {combo} per saltà quessa a prussima volta",
@@ -78,7 +79,7 @@
   "confirmations.delete.message": "Site sicuru·a che vulete supprime stu statutu?",
   "confirmations.delete_list.confirm": "Toglie",
   "confirmations.delete_list.message": "Site sicuru·a che vulete supprime sta lista?",
-  "confirmations.domain_block.confirm": "Piattà tuttu u duminiu?",
+  "confirmations.domain_block.confirm": "Piattà tuttu u duminiu",
   "confirmations.domain_block.message": "Site sicuru·a che vulete piattà tuttu à {domain}? Saria forse abbastanza di bluccà ò piattà alcuni conti da quallà.",
   "confirmations.mute.confirm": "Piattà",
   "confirmations.mute.message": "Site sicuru·a che vulete piattà @{name}?",
@@ -107,13 +108,13 @@
   "empty_column.home.public_timeline": "a linea pubblica",
   "empty_column.list": "Ùn c'hè ancu nunda quì. Quandu membri di sta lista manderanu novi statuti, i vidarete quì.",
   "empty_column.notifications": "Ùn avete ancu nisuna nutificazione. Interact with others to start the conversation.",
-  "empty_column.public": "Ùn c'hè nunda quì! Scrivete qualcosa in pubblicu o seguitate utilizatori d'altre istanze per empie a linea pubblica.",
+  "empty_column.public": "Ùn c'hè nunda quì! Scrivete qualcosa in pubblicu o seguitate utilizatori d'altre istanze per empie a linea pubblica",
   "follow_request.authorize": "Auturizà",
   "follow_request.reject": "Righjittà",
   "getting_started.appsshort": "Applicazione",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Per principià",
-  "getting_started.open_source_notice": "Mastodon ghjè un lugiziale liberu. Pudete cuntribuisce à u codice o a traduzione, o palisà un bug, nant'à GitHub: {github}",
+  "getting_started.open_source_notice": "Mastodon ghjè un lugiziale liberu. Pudete cuntribuisce à u codice o a traduzione, o palisà un bug, nant'à GitHub: {github}.",
   "getting_started.userguide": "Guida d'utilizazione",
   "home.column_settings.advanced": "Avanzati",
   "home.column_settings.basic": "Bàsichi",
@@ -200,7 +201,7 @@
   "onboarding.page_six.apps_available": "Ci sò {apps} dispunibule per iOS, Android è altre piattaforme.",
   "onboarding.page_six.github": "Mastodon ghjè un lugiziale liberu. Pudete cuntribuisce à u codice o a traduzione, o palisà un prublemu, nant'à {github}.",
   "onboarding.page_six.guidelines": "regule di a cumunità",
-  "onboarding.page_six.read_guidelines": "Ùn vi scurdate di leghje e {guidelines} di {domain}",
+  "onboarding.page_six.read_guidelines": "Ùn vi scurdate di leghje e {guidelines} di {domain}!",
   "onboarding.page_six.various_app": "applicazione pè u telefuninu",
   "onboarding.page_three.profile": "Pudete mudificà u prufile per cambia u ritrattu, a descrizzione è u nome affissatu. Ci sò ancu alcun'altre preferenze.",
   "onboarding.page_three.search": "Fate usu di l'area di ricerca per truvà altre persone è vede hashtag cum'è {illustration} o {introductions}. Per vede qualcunu ch'ùn hè micca nant'à st'istanza, cercate u so identificatore complettu (pare un'email).",
@@ -231,13 +232,13 @@
   "report.target": "Signalamentu",
   "search.placeholder": "Circà",
   "search_popout.search_format": "Ricerca avanzata",
-  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.full_text": "I testi simplici rimandanu i statuti ch'avete scritti, aghjunti à i vostri favuriti, spartuti o induve quelli site mintuvatu·a, è ancu i cugnomi, nomi pubblichi è hashtag chì currispondenu.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "statutu",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.text": "Un testu simplice rimanda i nomi pubblichi, cugnomi è hashtag",
   "search_popout.tips.user": "utilizatore",
   "search_results.accounts": "Ghjente",
-  "search_results.hashtags": "Hashtags",
+  "search_results.hashtags": "Hashtag",
   "search_results.statuses": "Statuti",
   "search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}",
   "standalone.public_title": "Una vista di...",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index f442e0675..fe5bc4443 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "@{name} blocken",
   "account.block_domain": "Alles von {domain} verstecken",
   "account.blocked": "Blockiert",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index b400349d2..74bf97361 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -552,6 +552,10 @@
       {
         "defaultMessage": "Domain hidden",
         "id": "account.domain_blocked"
+      },
+      {
+        "defaultMessage": "Bot",
+        "id": "account.badges.bot"
       }
     ],
     "path": "app/javascript/mastodon/features/account/components/header.json"
@@ -815,7 +819,7 @@
         "id": "upload_form.description"
       },
       {
-        "defaultMessage": "Undo",
+        "defaultMessage": "Delete",
         "id": "upload_form.undo"
       },
       {
@@ -1866,4 +1870,4 @@
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
   }
-]
\ No newline at end of file
+]
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index 5b36348eb..d1421be76 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Απόκλεισε τον/την @{name}",
   "account.block_domain": "Απόκρυψε τα πάντα από τον/την",
   "account.blocked": "Αποκλεισμένος/η",
@@ -111,7 +112,7 @@
   "follow_request.authorize": "Ενέκρινε",
   "follow_request.reject": "Απέρριψε",
   "getting_started.appsshort": "Εφαρμογές",
-  "getting_started.faq": "FAQ",
+  "getting_started.faq": "Συχνές Ερωτήσεις",
   "getting_started.heading": "Ξεκινώντας",
   "getting_started.open_source_notice": "Το Mastodon είναι ελεύθερο λογισμικό. Μπορείς να συνεισφέρεις ή να αναφέρεις ζητήματα στο GitHub στο {github}.",
   "getting_started.userguide": "Οδηγός Χρηστών",
@@ -174,7 +175,7 @@
   "notification.follow": "{name} followed you",
   "notification.mention": "{name} mentioned you",
   "notification.reblog": "{name} boosted your status",
-  "notifications.clear": "Clear notifications",
+  "notifications.clear": "Καθαρισμός ειδοποιήσεων",
   "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
   "notifications.column_settings.alert": "Desktop notifications",
   "notifications.column_settings.favourite": "Favourites:",
@@ -194,7 +195,7 @@
   "onboarding.page_one.full_handle": "Your full handle",
   "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
   "onboarding.page_one.welcome": "Welcome to Mastodon!",
-  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
+  "onboarding.page_six.admin": "Ο διαχειριστής του κόμβου σου είναι ο/η {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.",
@@ -248,13 +249,13 @@
   "status.direct": "Direct message @{name}",
   "status.embed": "Embed",
   "status.favourite": "Favourite",
-  "status.load_more": "Load more",
+  "status.load_more": "Φόρτωσε περισσότερα",
   "status.media_hidden": "Media hidden",
   "status.mention": "Mention @{name}",
   "status.more": "More",
   "status.mute": "Mute @{name}",
   "status.mute_conversation": "Mute conversation",
-  "status.open": "Expand this status",
+  "status.open": "Διεύρυνε αυτή την κατάσταση",
   "status.pin": "Pin on profile",
   "status.pinned": "Pinned toot",
   "status.reblog": "Boost",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index d8e69fd3c..35c5a86e9 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
@@ -286,7 +287,7 @@
   "upload_button.label": "Add media",
   "upload_form.description": "Describe for the visually impaired",
   "upload_form.focus": "Crop",
-  "upload_form.undo": "Undo",
+  "upload_form.undo": "Delete",
   "upload_progress.label": "Uploading...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 1ab6d2aa0..4aa3d80a3 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Bloki @{name}",
   "account.block_domain": "Kaŝi ĉion de {domain}",
   "account.blocked": "Blokita",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 41d7db9da..8aac2d77d 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Bloquear",
   "account.block_domain": "Ocultar todo de {domain}",
   "account.blocked": "Bloqueado",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 49cdf5630..e927547e3 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokeatu @{name}",
   "account.block_domain": "{domain}(e)ko guztia ezkutatu",
   "account.blocked": "Blokeatuta",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 99aba00c3..e19b1cd85 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "مسدودسازی @{name}",
   "account.block_domain": "پنهان‌سازی همه چیز از سرور {domain}",
   "account.blocked": "مسدودشده",
@@ -125,17 +126,17 @@
   "keyboard_shortcuts.boost": "برای بازبوقیدن",
   "keyboard_shortcuts.column": "برای برجسته‌کردن یک نوشته در یکی از ستون‌ها",
   "keyboard_shortcuts.compose": "برای فعال‌کردن کادر نوشتهٔ تازه",
-  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.description": "توضیح",
   "keyboard_shortcuts.down": "برای پایین‌رفتن در فهرست",
-  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.enter": "برای گشودن نوشته",
   "keyboard_shortcuts.favourite": "برای پسندیدن",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.heading": "میان‌برهای صفحه‌کلید",
   "keyboard_shortcuts.hotkey": "میان‌بر",
   "keyboard_shortcuts.legend": "برای نمایش این راهنما",
   "keyboard_shortcuts.mention": "برای نام‌بردن از نویسنده",
   "keyboard_shortcuts.reply": "برای پاسخ‌دادن",
   "keyboard_shortcuts.search": "برای فعال‌کردن جستجو",
-  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toggle_hidden": "برای نمایش/نهفتن نوشتهٔ پشت هشدار محتوا",
   "keyboard_shortcuts.toot": "برای آغاز یک بوق تازه",
   "keyboard_shortcuts.unfocus": "برای برداشتن توجه از نوشتن/جستجو",
   "keyboard_shortcuts.up": "برای بالا رفتن در فهرست",
@@ -211,7 +212,7 @@
   "privacy.direct.short": "مستقیم",
   "privacy.private.long": "تنها به پیگیران نشان بده",
   "privacy.private.short": "خصوصی",
-  "privacy.public.long": "در فهرست عمومی نشان بده",
+  "privacy.public.long": "نمایش در فهرست عمومی",
   "privacy.public.short": "عمومی",
   "privacy.unlisted.long": "عمومی، ولی فهرست نکن",
   "privacy.unlisted.short": "فهرست‌نشده",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 07d4d9aa5..cfe9c4f33 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Estä @{name}",
   "account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}",
   "account.blocked": "Estetty",
@@ -29,8 +30,8 @@
   "account.unmute": "Poista käyttäjän @{name} mykistys",
   "account.unmute_notifications": "Poista mykistys käyttäjän @{name} ilmoituksilta",
   "account.view_full_profile": "Näytä koko profiili",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "Tapahtui odottamaton virhe.",
+  "alert.unexpected.title": "Hups!",
   "boost_modal.combo": "Ensi kerralla voit ohittaa tämän painamalla {combo}",
   "bundle_column_error.body": "Jokin meni vikaan komponenttia ladattaessa.",
   "bundle_column_error.retry": "Yritä uudestaan",
@@ -41,7 +42,7 @@
   "column.blocks": "Estetyt käyttäjät",
   "column.community": "Paikallinen aikajana",
   "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.domain_blocks": "Piilotetut verkkotunnukset",
   "column.favourites": "Suosikit",
   "column.follow_requests": "Seuraamispyynnöt",
   "column.home": "Koti",
@@ -59,7 +60,7 @@
   "column_header.unpin": "Poista kiinnitys",
   "column_subheading.navigation": "Navigaatio",
   "column_subheading.settings": "Asetukset",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Tämä tuuttaus näkyy vain mainituille käyttäjille.",
   "compose_form.hashtag_warning": "Tämä tuuttaus ei näy hashtag-hauissa, koska se on listaamaton. Hashtagien avulla voi hakea vain julkisia tuuttauksia.",
   "compose_form.lock_disclaimer": "Tilisi ei ole {locked}. Kuka tahansa voi seurata tiliäsi ja nähdä vain seuraajille rajaamasi julkaisut.",
   "compose_form.lock_disclaimer.lock": "lukittu",
@@ -135,7 +136,7 @@
   "keyboard_shortcuts.mention": "mainitse julkaisija",
   "keyboard_shortcuts.reply": "vastaa",
   "keyboard_shortcuts.search": "siirry hakukenttään",
-  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toggle_hidden": "näytä/piilota sisältövaroituksella merkitty teksti",
   "keyboard_shortcuts.toot": "ala kirjoittaa uutta tuuttausta",
   "keyboard_shortcuts.unfocus": "siirry pois tekstikentästä tai hakukentästä",
   "keyboard_shortcuts.up": "siirry listassa ylöspäin",
@@ -158,7 +159,7 @@
   "navigation_bar.blocks": "Estetyt käyttäjät",
   "navigation_bar.community_timeline": "Paikallinen aikajana",
   "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.domain_blocks": "Piilotetut verkkotunnukset",
   "navigation_bar.edit_profile": "Muokkaa profiilia",
   "navigation_bar.favourites": "Suosikit",
   "navigation_bar.follow_requests": "Seuraamispyynnöt",
@@ -242,10 +243,10 @@
   "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
   "standalone.public_title": "Kurkistus sisälle...",
   "status.block": "Estä @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Peru buustaus",
   "status.cannot_reblog": "Tätä julkaisua ei voi buustata",
   "status.delete": "Poista",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Viesti käyttäjälle @{name}",
   "status.embed": "Upota",
   "status.favourite": "Tykkää",
   "status.load_more": "Lataa lisää",
@@ -258,7 +259,7 @@
   "status.pin": "Kiinnitä profiiliin",
   "status.pinned": "Kiinnitetty tuuttaus",
   "status.reblog": "Buustaa",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Buustaa alkuperäiselle yleisölle",
   "status.reblogged_by": "{name} buustasi",
   "status.reply": "Vastaa",
   "status.replyAll": "Vastaa ketjuun",
@@ -276,7 +277,7 @@
   "tabs_bar.home": "Koti",
   "tabs_bar.local_timeline": "Paikallinen",
   "tabs_bar.notifications": "Ilmoitukset",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Hae",
   "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.",
   "upload_area.title": "Lataa raahaamalla ja pudottamalla tähän",
   "upload_button.label": "Lisää mediaa",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 16bf4033c..b0bda275c 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Bloquer @{name}",
   "account.block_domain": "Tout masquer venant de {domain}",
   "account.blocked": "Bloqué",
@@ -187,7 +188,7 @@
   "notifications.column_settings.sound": "Émettre un son",
   "onboarding.done": "Effectué",
   "onboarding.next": "Suivant",
-  "onboarding.page_five.public_timelines": "Le fil public global affiche les messages de toutes les personnes suivies par les membres de {domain}. Le fil public local est identique mais se limite aux membres de {domain}.",
+  "onboarding.page_five.public_timelines": "Le fil public global affiche les messages de toutes les personnes suivies par les membres de {domain}. Le fil public local est identique, mais se limite aux membres de {domain}.",
   "onboarding.page_four.home": "L’Accueil affiche les messages des personnes que vous suivez.",
   "onboarding.page_four.notifications": "La colonne de notification vous avertit lors d'une interaction avec vous.",
   "onboarding.page_one.federation": "Mastodon est un réseau de serveurs indépendants qui se joignent pour former un réseau social plus vaste. Nous appelons ces serveurs des instances.",
@@ -225,7 +226,7 @@
   "reply_indicator.cancel": "Annuler",
   "report.forward": "Transférer à {target}",
   "report.forward_hint": "Le compte provient d'un autre serveur. Envoyez également une copie anonyme du rapport ?",
-  "report.hint": "Le rapport sera envoyé aux modérateurs de votre instance. Vous pouvez expliquer pourquoi vous signalez ce compte ci-dessous :",
+  "report.hint": "Le rapport sera envoyé aux modérateurs de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous :",
   "report.placeholder": "Commentaires additionnels",
   "report.submit": "Envoyer",
   "report.target": "Signalement",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 652ca31d1..29885b896 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Ocultar calquer contido de {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 0ffbb14f3..a4bcba8ff 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "חסימת @{name}",
   "account.block_domain": "להסתיר הכל מהקהילה {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index c41cc3ea1..bd1dfac4c 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokiraj @{name}",
   "account.block_domain": "Sakrij sve sa {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index a0c186184..5059a45d4 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "@{name} letiltása",
   "account.block_domain": "Minden elrejtése innen: {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index a0442bad4..7fe723892 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Արգելափակել @{name}֊ին",
   "account.block_domain": "Թաքցնել ամենը հետեւյալ տիրույթից՝ {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 2fd922544..07b26a0a5 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokir @{name}",
   "account.block_domain": "Sembunyikan segalanya dari {domain}",
   "account.blocked": "Terblokir",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index ed45ee11e..a8ab72339 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokusar @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index a7ca62015..9d3486152 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blocca @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Bloccato",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index dbb4562de..1bdf34a81 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "@{name}さんをブロック",
   "account.block_domain": "{domain}全体を非表示",
   "account.blocked": "ブロック済み",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 2a2734673..e72a7dff9 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "@{name}을 차단",
   "account.block_domain": "{domain} 전체를 숨김",
   "account.blocked": "차단 됨",
@@ -29,9 +30,9 @@
   "account.unmute": "뮤트 해제",
   "account.unmute_notifications": "@{name}의 알림 뮤트 해제",
   "account.view_full_profile": "전체 프로필 보기",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
-  "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
+  "alert.unexpected.message": "예측하지 못한 에러가 발생했습니다.",
+  "alert.unexpected.title": "앗!",
+  "boost_modal.combo": "{combo}를 누르면 다음부터 이 과정을 건너뛸 수 있습니다",
   "bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
   "bundle_column_error.retry": "다시 시도",
   "bundle_column_error.title": "네트워크 에러",
@@ -40,8 +41,8 @@
   "bundle_modal_error.retry": "다시 시도",
   "column.blocks": "차단 중인 사용자",
   "column.community": "로컬 타임라인",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "다이렉트 메시지",
+  "column.domain_blocks": "숨겨진 도메인",
   "column.favourites": "즐겨찾기",
   "column.follow_requests": "팔로우 요청",
   "column.home": "홈",
@@ -59,17 +60,17 @@
   "column_header.unpin": "고정 해제",
   "column_subheading.navigation": "내비게이션",
   "column_subheading.settings": "설정",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "이 툿은 멘션 된 유저들에게만 보여집니다.",
   "compose_form.hashtag_warning": "이 툿은 어떤 해시태그로도 검색 되지 않습니다. 전체공개로 게시 된 툿만이 해시태그로 검색 될 수 있습니다.",
   "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
   "compose_form.lock_disclaimer.lock": "비공개",
   "compose_form.placeholder": "지금 무엇을 하고 있나요?",
   "compose_form.publish": "툿",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.sensitive.marked": "미디어가 열람주의로 설정되어 있습니다",
+  "compose_form.sensitive.unmarked": "미디어가 열람주의로 설정 되어 있지 않습니다",
+  "compose_form.spoiler.marked": "열람주의가 설정되어 있습니다",
+  "compose_form.spoiler.unmarked": "열람주의가 설정 되어 있지 않습니다",
   "compose_form.spoiler_placeholder": "경고",
   "confirmation_modal.cancel": "취소",
   "confirmations.block.confirm": "차단",
@@ -101,13 +102,13 @@
   "emoji_button.symbols": "기호",
   "emoji_button.travel": "여행과 장소",
   "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "아직 다이렉트 메시지가 없습니다. 다이렉트 메시지를 보내거나 받은 경우, 여기에 표시 됩니다.",
   "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
   "empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.",
   "empty_column.home.public_timeline": "연합 타임라인",
   "empty_column.list": "리스트에 아직 아무 것도 없습니다.",
-  "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!",
-  "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!",
+  "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요.",
+  "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스의 유저를 팔로우 해서 채워보세요",
   "follow_request.authorize": "허가",
   "follow_request.reject": "거부",
   "getting_started.appsshort": "애플리케이션",
@@ -135,7 +136,7 @@
   "keyboard_shortcuts.mention": "멘션",
   "keyboard_shortcuts.reply": "답장",
   "keyboard_shortcuts.search": "검색창에 포커스",
-  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toggle_hidden": "CW로 가려진 텍스트를 표시/비표시",
   "keyboard_shortcuts.toot": "새 툿 작성",
   "keyboard_shortcuts.unfocus": "작성창에서 포커스 해제",
   "keyboard_shortcuts.up": "리스트에서 위로 이동",
@@ -157,8 +158,8 @@
   "mute_modal.hide_notifications": "이 사용자로부터의 알림을 뮤트하시겠습니까?",
   "navigation_bar.blocks": "차단한 사용자",
   "navigation_bar.community_timeline": "로컬 타임라인",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "다이렉트 메시지",
+  "navigation_bar.domain_blocks": "숨겨진 도메인",
   "navigation_bar.edit_profile": "프로필 편집",
   "navigation_bar.favourites": "즐겨찾기",
   "navigation_bar.follow_requests": "팔로우 요청",
@@ -177,12 +178,12 @@
   "notifications.clear": "알림 지우기",
   "notifications.clear_confirmation": "정말로 알림을 삭제하시겠습니까?",
   "notifications.column_settings.alert": "데스크탑 알림",
-  "notifications.column_settings.favourite": "즐겨찾기",
-  "notifications.column_settings.follow": "새 팔로워",
-  "notifications.column_settings.mention": "답글",
+  "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.reblog": "부스트:",
   "notifications.column_settings.show": "컬럼에 표시",
   "notifications.column_settings.sound": "효과음 재생",
   "onboarding.done": "완료",
@@ -200,7 +201,7 @@
   "onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.",
   "onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
   "onboarding.page_six.guidelines": "커뮤니티 가이드라인",
-  "onboarding.page_six.read_guidelines": "{guidelines}을 확인하는 것을 잊지 마세요.",
+  "onboarding.page_six.read_guidelines": "{domain}의 {guidelines}을 확인하는 것을 잊지 마세요!",
   "onboarding.page_six.various_app": "다양한 모바일 애플리케이션",
   "onboarding.page_three.profile": "[프로필 편집] 에서 자기 소개나 이름을 변경할 수 있습니다. 또한 다른 설정도 변경할 수 있습니다.",
   "onboarding.page_three.search": "검색 바에서 {illustration} 나 {introductions} 와 같이 특정 해시태그가 달린 포스트를 보거나, 사용자를 찾을 수 있습니다.",
@@ -242,10 +243,10 @@
   "search_results.total": "{count, number}건의 결과",
   "standalone.public_title": "지금 이런 이야기를 하고 있습니다…",
   "status.block": "@{name} 차단",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "부스트 취소",
   "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
   "status.delete": "삭제",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "@{name}에게 다이렉트 메시지",
   "status.embed": "공유하기",
   "status.favourite": "즐겨찾기",
   "status.load_more": "더 보기",
@@ -258,7 +259,7 @@
   "status.pin": "고정",
   "status.pinned": "고정 된 툿",
   "status.reblog": "부스트",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "원래의 수신자들에게 부스트",
   "status.reblogged_by": "{name}님이 부스트 했습니다",
   "status.reply": "답장",
   "status.replyAll": "전원에게 답장",
@@ -267,16 +268,16 @@
   "status.sensitive_warning": "민감한 미디어",
   "status.share": "공유",
   "status.show_less": "숨기기",
-  "status.show_less_all": "Show less for all",
+  "status.show_less_all": "모두 접기",
   "status.show_more": "더 보기",
-  "status.show_more_all": "Show more for all",
+  "status.show_more_all": "모두 펼치기",
   "status.unmute_conversation": "이 대화의 뮤트 해제하기",
   "status.unpin": "고정 해제",
   "tabs_bar.federated_timeline": "연합",
   "tabs_bar.home": "홈",
   "tabs_bar.local_timeline": "로컬",
   "tabs_bar.notifications": "알림",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "검색",
   "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
   "upload_area.title": "드래그 & 드롭으로 업로드",
   "upload_button.label": "미디어 추가",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index adc1d19a7..a6bfe36f9 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokkeer @{name}",
   "account.block_domain": "Negeer alles van {domain}",
   "account.blocked": "Geblokkeerd",
@@ -50,7 +51,7 @@
   "column.notifications": "Meldingen",
   "column.pins": "Vastgezette toots",
   "column.public": "Globale tijdlijn",
-  "column_back_button.label": "terug",
+  "column_back_button.label": "Terug",
   "column_header.hide_settings": "Instellingen verbergen",
   "column_header.moveLeft_settings": "Kolom naar links verplaatsen",
   "column_header.moveRight_settings": "Kolom naar rechts verplaatsen",
@@ -61,7 +62,7 @@
   "column_subheading.settings": "Instellingen",
   "compose_form.direct_message_warning": "Deze toot zal alleen zichtbaar zijn voor alle vermelde gebruikers.",
   "compose_form.hashtag_warning": "Deze toot valt niet onder een hashtag te bekijken, omdat deze niet op openbare tijdlijnen wordt getoond. Alleen openbare toots kunnen via hashtags gevonden worden.",
-  "compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en toots zien die je alleen aan volgers hebt gericht.",
+  "compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en kan de toots zien die je alleen aan jouw volgers hebt gericht.",
   "compose_form.lock_disclaimer.lock": "besloten",
   "compose_form.placeholder": "Wat wil je kwijt?",
   "compose_form.publish": "Toot",
@@ -76,10 +77,10 @@
   "confirmations.block.message": "Weet je het zeker dat je {name} wilt blokkeren?",
   "confirmations.delete.confirm": "Verwijderen",
   "confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
-  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.confirm": "Verwijderen",
   "confirmations.delete_list.message": "Weet je zeker dat je deze lijst definitief wilt verwijderen?",
   "confirmations.domain_block.confirm": "Negeer alles van deze server",
-  "confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
+  "confirmations.domain_block.message": "Weet je het echt heel erg zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gepaster.",
   "confirmations.mute.confirm": "Negeren",
   "confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?",
   "confirmations.unfollow.confirm": "Ontvolgen",
@@ -106,13 +107,13 @@
   "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
   "empty_column.list": "Er is nog niks in deze lijst. Wanneer lijstleden nieuwe toots publiceren, zijn deze hier te zien.",
-  "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
+  "empty_column.notifications": "Je hebt nog geen meldingen. Begin met iemand een gesprek.",
   "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere servers om het te vullen",
   "follow_request.authorize": "Goedkeuren",
   "follow_request.reject": "Afkeuren",
   "getting_started.appsshort": "Apps",
   "getting_started.faq": "FAQ",
-  "getting_started.heading": "Beginnen",
+  "getting_started.heading": "Aan de slag",
   "getting_started.open_source_notice": "Mastodon is vrije software. Je kunt bijdragen of problemen melden op GitHub via {github}.",
   "getting_started.userguide": "Gebruikersgids",
   "home.column_settings.advanced": "Geavanceerd",
@@ -131,7 +132,7 @@
   "keyboard_shortcuts.favourite": "om als favoriet te markeren",
   "keyboard_shortcuts.heading": "Sneltoetsen",
   "keyboard_shortcuts.hotkey": "Sneltoets",
-  "keyboard_shortcuts.legend": "om deze legenda weer te geven",
+  "keyboard_shortcuts.legend": "om deze legenda te tonen",
   "keyboard_shortcuts.mention": "om de auteur te vermelden",
   "keyboard_shortcuts.reply": "om te reageren",
   "keyboard_shortcuts.search": "om het zoekvak te focussen",
@@ -187,7 +188,7 @@
   "notifications.column_settings.sound": "Geluid afspelen",
   "onboarding.done": "Klaar",
   "onboarding.next": "Volgende",
-  "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodonservers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.",
+  "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodonservers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te leren kennen.",
   "onboarding.page_four.home": "Deze tijdlijn laat toots zien van mensen die jij volgt.",
   "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodongebruikers hebt.",
   "onboarding.page_one.federation": "Mastodon is een netwerk van onafhankelijke servers die samen een groot sociaal netwerk vormen.",
@@ -231,7 +232,7 @@
   "report.target": "Rapporteer {target}",
   "search.placeholder": "Zoeken",
   "search_popout.search_format": "Geavanceerd zoeken",
-  "search_popout.tips.full_text": "Gebruik gewone tekst om te zoeken naar jouw toots, gebooste toots, favorieten en naar toots waarin jij bent vermeldt, en naar gebruikersnamen, weergavenamen en hashtags.",
+  "search_popout.tips.full_text": "Gebruik gewone tekst om te zoeken in jouw toots, gebooste toots, favorieten en in toots waarin jij bent vermeldt, en tevens naar gebruikersnamen, weergavenamen en hashtags.",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "toot",
   "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags",
@@ -253,7 +254,7 @@
   "status.mention": "Vermeld @{name}",
   "status.more": "Meer",
   "status.mute": "Negeer @{name}",
-  "status.mute_conversation": "Negeer conversatie",
+  "status.mute_conversation": "Negeer gesprek",
   "status.open": "Toot volledig tonen",
   "status.pin": "Aan profielpagina vastmaken",
   "status.pinned": "Vastgemaakte toot",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 0ee6d0722..05dae0630 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokkér @{name}",
   "account.block_domain": "Skjul alt fra {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index c4cb996cf..ead57d63f 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blocar @{name}",
   "account.block_domain": "Tot amagar del domeni {domain}",
   "account.blocked": "Blocat",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 6d6db7c82..120edcb1c 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokuj @{name}",
   "account.block_domain": "Blokuj wszystko z {domain}",
   "account.blocked": "Zablokowany",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 7f8690f91..2360c41e7 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo de {domain}",
   "account.blocked": "Bloqueado",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index ce816dc41..3ac92dd57 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo do domínio {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 8eeebaf73..599873439 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Блокировать",
   "account.block_domain": "Блокировать все с {domain}",
   "account.blocked": "Заблокирован(а)",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index d69648ccf..1cf7dee80 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokovať @{name}",
   "account.block_domain": "Ukryť všetko z {domain}",
   "account.blocked": "Blokovaný/á",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
new file mode 100644
index 000000000..202dc4bd0
--- /dev/null
+++ b/app/javascript/mastodon/locales/sl.json
@@ -0,0 +1,297 @@
+{
+  "account.badges.bot": "Bot",
+  "account.block": "Blokiraj @{name}",
+  "account.block_domain": "Skrij vse iz {domain}",
+  "account.blocked": "Blokirano",
+  "account.direct": "Neposredno sporočilo @{name}",
+  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.domain_blocked": "Domain hidden",
+  "account.edit_profile": "Edit profile",
+  "account.follow": "Follow",
+  "account.followers": "Followers",
+  "account.follows": "Follows",
+  "account.follows_you": "Follows you",
+  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.media": "Media",
+  "account.mention": "Mention @{name}",
+  "account.moved_to": "{name} has moved to:",
+  "account.mute": "Mute @{name}",
+  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.muted": "Muted",
+  "account.posts": "Toots",
+  "account.posts_with_replies": "Toots and replies",
+  "account.report": "Report @{name}",
+  "account.requested": "Awaiting approval. Click to cancel follow request",
+  "account.share": "Share @{name}'s profile",
+  "account.show_reblogs": "Show boosts from @{name}",
+  "account.unblock": "Unblock @{name}",
+  "account.unblock_domain": "Unhide {domain}",
+  "account.unfollow": "Unfollow",
+  "account.unmute": "Unmute @{name}",
+  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.view_full_profile": "View full profile",
+  "alert.unexpected.message": "An unexpected error occurred.",
+  "alert.unexpected.title": "Oops!",
+  "boost_modal.combo": "You can press {combo} to skip this next time",
+  "bundle_column_error.body": "Something went wrong while loading this component.",
+  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.title": "Network error",
+  "bundle_modal_error.close": "Close",
+  "bundle_modal_error.message": "Something went wrong while loading this component.",
+  "bundle_modal_error.retry": "Try again",
+  "column.blocks": "Blocked users",
+  "column.community": "Local timeline",
+  "column.direct": "Direct messages",
+  "column.domain_blocks": "Hidden domains",
+  "column.favourites": "Favourites",
+  "column.follow_requests": "Follow requests",
+  "column.home": "Home",
+  "column.lists": "Lists",
+  "column.mutes": "Muted users",
+  "column.notifications": "Notifications",
+  "column.pins": "Pinned toot",
+  "column.public": "Federated timeline",
+  "column_back_button.label": "Back",
+  "column_header.hide_settings": "Hide settings",
+  "column_header.moveLeft_settings": "Move column to the left",
+  "column_header.moveRight_settings": "Move column to the right",
+  "column_header.pin": "Pin",
+  "column_header.show_settings": "Show settings",
+  "column_header.unpin": "Unpin",
+  "column_subheading.navigation": "Navigation",
+  "column_subheading.settings": "Settings",
+  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
+  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
+  "compose_form.lock_disclaimer.lock": "locked",
+  "compose_form.placeholder": "What is on your mind?",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive.marked": "Media is marked as sensitive",
+  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
+  "compose_form.spoiler.marked": "Text is hidden behind warning",
+  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.spoiler_placeholder": "Write your warning here",
+  "confirmation_modal.cancel": "Cancel",
+  "confirmations.block.confirm": "Block",
+  "confirmations.block.message": "Are you sure you want to block {name}?",
+  "confirmations.delete.confirm": "Delete",
+  "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.domain_block.confirm": "Hide entire domain",
+  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
+  "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.custom": "Custom",
+  "emoji_button.flags": "Flags",
+  "emoji_button.food": "Food & Drink",
+  "emoji_button.label": "Insert emoji",
+  "emoji_button.nature": "Nature",
+  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objects",
+  "emoji_button.people": "People",
+  "emoji_button.recent": "Frequently used",
+  "emoji_button.search": "Search...",
+  "emoji_button.search_results": "Search results",
+  "emoji_button.symbols": "Symbols",
+  "emoji_button.travel": "Travel & Places",
+  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.hashtag": "There is nothing in this hashtag yet.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
+  "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
+  "empty_column.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",
+  "getting_started.faq": "FAQ",
+  "getting_started.heading": "Getting started",
+  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {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",
+  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.enter": "to open status",
+  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.mention": "to mention author",
+  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+  "keyboard_shortcuts.up": "to move up in the list",
+  "lightbox.close": "Close",
+  "lightbox.next": "Next",
+  "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among people you follow",
+  "lists.subheading": "Your lists",
+  "loading_indicator.label": "Loading...",
+  "media_gallery.toggle_visible": "Toggle visibility",
+  "missing_indicator.label": "Not found",
+  "missing_indicator.sublabel": "This resource could not be found",
+  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "navigation_bar.blocks": "Blocked users",
+  "navigation_bar.community_timeline": "Local timeline",
+  "navigation_bar.direct": "Direct messages",
+  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.edit_profile": "Edit profile",
+  "navigation_bar.favourites": "Favourites",
+  "navigation_bar.follow_requests": "Follow requests",
+  "navigation_bar.info": "Extended information",
+  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
+  "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",
+  "notification.follow": "{name} followed you",
+  "notification.mention": "{name} mentioned you",
+  "notification.reblog": "{name} boosted your status",
+  "notifications.clear": "Clear notifications",
+  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+  "notifications.column_settings.alert": "Desktop notifications",
+  "notifications.column_settings.favourite": "Favourites:",
+  "notifications.column_settings.follow": "New followers:",
+  "notifications.column_settings.mention": "Mentions:",
+  "notifications.column_settings.push": "Push notifications",
+  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.show": "Show in column",
+  "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.full_handle": "Your full handle",
+  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
+  "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",
+  "privacy.change": "Adjust status privacy",
+  "privacy.direct.long": "Post to mentioned users only",
+  "privacy.direct.short": "Direct",
+  "privacy.private.long": "Post to followers only",
+  "privacy.private.short": "Followers-only",
+  "privacy.public.long": "Post to public timelines",
+  "privacy.public.short": "Public",
+  "privacy.unlisted.long": "Do not show in public timelines",
+  "privacy.unlisted.short": "Unlisted",
+  "regeneration_indicator.label": "Loading…",
+  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancel",
+  "report.forward": "Forward to {target}",
+  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.placeholder": "Additional comments",
+  "report.submit": "Submit",
+  "report.target": "Report {target}",
+  "search.placeholder": "Search",
+  "search_popout.search_format": "Advanced search format",
+  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.status": "status",
+  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.user": "user",
+  "search_results.accounts": "People",
+  "search_results.hashtags": "Hashtags",
+  "search_results.statuses": "Toots",
+  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "standalone.public_title": "A look inside...",
+  "status.block": "Block @{name}",
+  "status.cancel_reblog_private": "Unboost",
+  "status.cannot_reblog": "This post cannot be boosted",
+  "status.delete": "Delete",
+  "status.direct": "Direct message @{name}",
+  "status.embed": "Embed",
+  "status.favourite": "Favourite",
+  "status.load_more": "Load more",
+  "status.media_hidden": "Media hidden",
+  "status.mention": "Mention @{name}",
+  "status.more": "More",
+  "status.mute": "Mute @{name}",
+  "status.mute_conversation": "Mute conversation",
+  "status.open": "Expand this status",
+  "status.pin": "Pin on profile",
+  "status.pinned": "Pinned toot",
+  "status.reblog": "Boost",
+  "status.reblog_private": "Boost to original audience",
+  "status.reblogged_by": "{name} boosted",
+  "status.reply": "Reply",
+  "status.replyAll": "Reply to thread",
+  "status.report": "Report @{name}",
+  "status.sensitive_toggle": "Click to view",
+  "status.sensitive_warning": "Sensitive content",
+  "status.share": "Share",
+  "status.show_less": "Show less",
+  "status.show_less_all": "Show less for all",
+  "status.show_more": "Show more",
+  "status.show_more_all": "Show more for all",
+  "status.unmute_conversation": "Unmute conversation",
+  "status.unpin": "Unpin from profile",
+  "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notifications",
+  "tabs_bar.search": "Search",
+  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "upload_area.title": "Drag & drop to upload",
+  "upload_button.label": "Add media",
+  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.focus": "Crop",
+  "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"
+}
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index b1ea0d179..490b3a51a 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blokiraj korisnika @{name}",
   "account.block_domain": "Sakrij sve sa domena {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index aa978675f..b331f9f48 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Блокирај корисника @{name}",
   "account.block_domain": "Сакриј све са домена {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 4efe88a7e..36b200764 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Blockera @{name}",
   "account.block_domain": "Dölj allt från {domain}",
   "account.blocked": "Blockerad",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index a56720fee..038ac6abb 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 82b44fe30..8d24395af 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Block @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 056fbfe8f..a377e7b74 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Engelle @{name}",
   "account.block_domain": "Hide everything from {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 1a7b58789..613d1a00d 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "Заблокувати",
   "account.block_domain": "Заглушити {domain}",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/whitelist_sl.json b/app/javascript/mastodon/locales/whitelist_sl.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_sl.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index a3a4de0af..073dbe6cb 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "屏蔽 @{name}",
   "account.block_domain": "隐藏来自 {domain} 的内容",
   "account.blocked": "Blocked",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 7719e08a6..0c012aa7d 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的一切文章",
   "account.blocked": "封鎖",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 84ff25e03..0f4d04947 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -1,4 +1,5 @@
 {
+  "account.badges.bot": "Bot",
   "account.block": "封鎖 @{name}",
   "account.block_domain": "隱藏來自 {domain} 的一切貼文",
   "account.blocked": "已被封鎖的",
diff --git a/app/lib/request.rb b/app/lib/request.rb
index fc7d398e0..731bf7687 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -51,16 +51,17 @@ class Request
   end
 
   def headers
-    (@account ? @headers.merge('Signature' => signature) : @headers).reverse_merge('Accept-Encoding' => 'gzip').without(REQUEST_TARGET)
+    (@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
   end
 
   private
 
   def set_common_headers!
-    @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}"
-    @headers['User-Agent']   = user_agent
-    @headers['Host']         = @url.host
-    @headers['Date']         = Time.now.utc.httpdate
+    @headers[REQUEST_TARGET]    = "#{@verb} #{@url.path}"
+    @headers['User-Agent']      = user_agent
+    @headers['Host']            = @url.host
+    @headers['Date']            = Time.now.utc.httpdate
+    @headers['Accept-Encoding'] = 'gzip' if @verb != :head
   end
 
   def set_digest!
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 7f1ef5191..20ddbca53 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -41,6 +41,9 @@ module Remotable
         rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
           Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
           nil
+        rescue Paperclip::Error => e
+          Rails.logger.debug "Error processing remote #{attachment_name}: #{e}"
+          nil
         end
       end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 82d165079..d5ca9be36 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -245,7 +245,7 @@ class User < ApplicationRecord
   end
 
   def web_push_subscription(session)
-    session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
+    session.web_push_subscription.nil? ? nil : session.web_push_subscription
   end
 
   def invite_code=(code)
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index 1736106f7..df549c6d3 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -3,38 +3,51 @@
 #
 # Table name: web_push_subscriptions
 #
-#  id         :bigint(8)        not null, primary key
-#  endpoint   :string           not null
-#  key_p256dh :string           not null
-#  key_auth   :string           not null
-#  data       :json
-#  created_at :datetime         not null
-#  updated_at :datetime         not null
+#  id              :bigint(8)        not null, primary key
+#  endpoint        :string           not null
+#  key_p256dh      :string           not null
+#  key_auth        :string           not null
+#  data            :json
+#  created_at      :datetime         not null
+#  updated_at      :datetime         not null
+#  access_token_id :bigint(8)
+#  user_id         :bigint(8)
 #
 
-require 'webpush'
-
 class Web::PushSubscription < ApplicationRecord
+  belongs_to :user, optional: true
+  belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', optional: true
+
   has_one :session_activation
 
   def push(notification)
-    I18n.with_locale(session_activation.user.locale || I18n.default_locale) do
+    I18n.with_locale(associated_user.locale || I18n.default_locale) do
       push_payload(message_from(notification), 48.hours.seconds)
     end
   end
 
   def pushable?(notification)
-    data&.key?('alerts') && data['alerts'][notification.type.to_s]
+    data&.key?('alerts') && ActiveModel::Type::Boolean.new.cast(data['alerts'][notification.type.to_s])
   end
 
-  def as_payload
-    payload = { id: id, endpoint: endpoint }
-    payload[:alerts] = data['alerts'] if data&.key?('alerts')
-    payload
+  def associated_user
+    return @associated_user if defined?(@associated_user)
+
+    @associated_user = if user_id.nil?
+                         session_activation.user
+                       else
+                         user
+                       end
   end
 
-  def access_token
-    find_or_create_access_token.token
+  def associated_access_token
+    return @associated_access_token if defined?(@associated_access_token)
+
+    @associated_access_token = if access_token_id.nil?
+                                 find_or_create_access_token.token
+                               else
+                                 access_token
+                               end
   end
 
   private
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 1d17e2b0a..eefd853ff 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -2,19 +2,15 @@
 
 class InitialStateSerializer < ActiveModel::Serializer
   attributes :meta, :compose, :accounts,
-             :media_attachments, :settings, :push_subscription,
+             :media_attachments, :settings
              :max_toot_chars
 
-  has_many :custom_emojis, serializer: REST::CustomEmojiSerializer
+  has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
 
   def max_toot_chars
     StatusLengthValidator::MAX_CHARS
   end
 
-  def custom_emojis
-    CustomEmoji.local.where(disabled: false)
-  end
-
   def meta
     store = {
       streaming_api_base_url: Rails.configuration.x.streaming_api_base_url,
diff --git a/app/serializers/rest/web_push_subscription_serializer.rb b/app/serializers/rest/web_push_subscription_serializer.rb
new file mode 100644
index 000000000..7fd952a56
--- /dev/null
+++ b/app/serializers/rest/web_push_subscription_serializer.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer
+  attributes :id, :endpoint, :alerts, :server_key
+
+  def alerts
+    object.data&.dig('alerts') || {}
+  end
+
+  def server_key
+    Rails.configuration.x.vapid_public_key
+  end
+end
diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb
index e5524fe7a..31c703832 100644
--- a/app/serializers/web/notification_serializer.rb
+++ b/app/serializers/web/notification_serializer.rb
@@ -54,7 +54,7 @@ class Web::NotificationSerializer < ActiveModel::Serializer
 
     def access_token
       return if actions.empty?
-      current_push_subscription.access_token
+      current_push_subscription.associated_access_token
     end
 
     def message
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index ba086449c..6490d2735 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -9,6 +9,7 @@ class NotifyService < BaseService
     return if recipient.user.nil? || blocked?
 
     create_notification
+    push_notification if @notification.browserable?
     send_email if email_enabled?
   rescue ActiveRecord::RecordInvalid
     return
@@ -101,25 +102,27 @@ class NotifyService < BaseService
 
   def create_notification
     @notification.save!
-    return unless @notification.browserable?
+  end
+
+  def push_notification
+    return if @notification.activity.nil?
+
     Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
     send_push_notifications
   end
 
   def send_push_notifications
-    # HACK: Can be caused by quickly unfavouriting a status, since creating
-    # a favourite and creating a notification are not wrapped in a transaction.
-    return if @notification.activity.nil?
-
-    sessions_with_subscriptions = @recipient.user.session_activations.where.not(web_push_subscription: nil)
-    sessions_with_subscriptions_ids = sessions_with_subscriptions.select { |session| session.web_push_subscription.pushable? @notification }.map(&:id)
+    subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id)
+                                               .select { |subscription| subscription.pushable?(@notification) }
+                                               .map(&:id)
 
-    WebPushNotificationWorker.push_bulk(sessions_with_subscriptions_ids) do |session_activation_id|
-      [session_activation_id, @notification.id]
+    ::Web::PushNotificationWorker.push_bulk(subscriptions_ids) do |subscription_id|
+      [subscription_id, @notification.id]
     end
   end
 
   def send_email
+    return if @notification.activity.nil?
     NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later
   end
 
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index bbf2139a5..cfdd3a945 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -8,6 +8,7 @@
     %meta{ name: 'robots', content: 'noindex' }/
 
   %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
+  %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
   %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) }/
 
diff --git a/app/views/tags/_og.html.haml b/app/views/tags/_og.html.haml
index 853a499ae..a7c289bcb 100644
--- a/app/views/tags/_og.html.haml
+++ b/app/views/tags/_og.html.haml
@@ -2,5 +2,5 @@
 = opengraph 'og:url', tag_url(@tag)
 = opengraph 'og:type', 'website'
 = opengraph 'og:title', "##{@tag.name}"
-= opengraph 'og:description', t('about.about_hashtag_html', hashtag: @tag.name)
+= opengraph 'og:description', strip_tags(t('about.about_hashtag_html', hashtag: @tag.name))
 = opengraph 'twitter:card', 'summary'
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index c2202a9eb..2b46e58c7 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -2,6 +2,8 @@
   = "##{@tag.name}"
 
 - content_for :header_tags do
+  %link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/
+
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
   = render 'og'
 
diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb
new file mode 100644
index 000000000..4a40e5c8b
--- /dev/null
+++ b/app/workers/web/push_notification_worker.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Web::PushNotificationWorker
+  include Sidekiq::Worker
+
+  sidekiq_options backtrace: true
+
+  def perform(subscription_id, notification_id)
+    subscription = ::Web::PushSubscription.find(subscription_id)
+    notification = Notification.find(notification_id)
+
+    subscription.push(notification) unless notification.activity.nil?
+  rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
+    subscription.destroy!
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/app/workers/web_push_notification_worker.rb b/app/workers/web_push_notification_worker.rb
deleted file mode 100644
index eacea04c3..000000000
--- a/app/workers/web_push_notification_worker.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-class WebPushNotificationWorker
-  include Sidekiq::Worker
-
-  sidekiq_options backtrace: true
-
-  def perform(session_activation_id, notification_id)
-    session_activation = SessionActivation.find(session_activation_id)
-    notification       = Notification.find(notification_id)
-
-    return if session_activation.web_push_subscription.nil? || notification.activity.nil?
-
-    session_activation.web_push_subscription.push(notification)
-  rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
-    # Subscription expiration is not currently implemented in any browser
-
-    session_activation.web_push_subscription.destroy!
-    session_activation.update!(web_push_subscription: nil)
-
-    true
-  rescue ActiveRecord::RecordNotFound
-    true
-  end
-end