From 1fcffa573cf1ca9373ac1b65b2f7e805cc691927 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 4 Sep 2024 14:54:15 +0200
Subject: [PATCH 01/91] Fix 500 error in `GET /api/v2_alpha/notifications` when
 there are no notifications to return (#31746)

---
 .../api/v2_alpha/notifications_controller.rb        |  2 ++
 spec/requests/api/v2_alpha/notifications_spec.rb    | 13 +++++++++++++
 2 files changed, 15 insertions(+)

diff --git a/app/controllers/api/v2_alpha/notifications_controller.rb b/app/controllers/api/v2_alpha/notifications_controller.rb
index bd6979955..e8aa0b9e4 100644
--- a/app/controllers/api/v2_alpha/notifications_controller.rb
+++ b/app/controllers/api/v2_alpha/notifications_controller.rb
@@ -77,6 +77,8 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
   end
 
   def load_grouped_notifications
+    return [] if @notifications.empty?
+
     MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do
       NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
     end
diff --git a/spec/requests/api/v2_alpha/notifications_spec.rb b/spec/requests/api/v2_alpha/notifications_spec.rb
index 7663d215e..8009e7edc 100644
--- a/spec/requests/api/v2_alpha/notifications_spec.rb
+++ b/spec/requests/api/v2_alpha/notifications_spec.rb
@@ -116,6 +116,19 @@ RSpec.describe 'Notifications' do
 
     it_behaves_like 'forbidden for wrong scope', 'write write:notifications'
 
+    context 'when there are no notifications' do
+      before do
+        user.account.notifications.destroy_all
+      end
+
+      it 'returns 0 notifications' do
+        subject
+
+        expect(response).to have_http_status(200)
+        expect(body_as_json[:notification_groups]).to eq []
+      end
+    end
+
     context 'with no options' do
       it 'returns expected notification types', :aggregate_failures do
         subject

From fab29ebbe864f0aec84857fc3d87f0d56f4f6b9b Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 4 Sep 2024 15:28:16 +0200
Subject: [PATCH 02/91] Fix all notification types being stored without
 filtering when polling (#31745)

---
 .../mastodon/actions/notification_groups.ts   | 33 ++++++++++---------
 1 file changed, 17 insertions(+), 16 deletions(-)

diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts
index 51f83f1d2..2ee46500a 100644
--- a/app/javascript/mastodon/actions/notification_groups.ts
+++ b/app/javascript/mastodon/actions/notification_groups.ts
@@ -18,7 +18,7 @@ import {
   selectSettingsNotificationsQuickFilterActive,
   selectSettingsNotificationsShows,
 } from 'mastodon/selectors/settings';
-import type { AppDispatch } from 'mastodon/store';
+import type { AppDispatch, RootState } from 'mastodon/store';
 import {
   createAppAsyncThunk,
   createDataLoadingThunk,
@@ -32,6 +32,14 @@ function excludeAllTypesExcept(filter: string) {
   return allNotificationTypes.filter((item) => item !== filter);
 }
 
+function getExcludedTypes(state: RootState) {
+  const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
+
+  return activeFilter === 'all'
+    ? selectSettingsNotificationsExcludedTypes(state)
+    : excludeAllTypesExcept(activeFilter);
+}
+
 function dispatchAssociatedRecords(
   dispatch: AppDispatch,
   notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
@@ -62,17 +70,8 @@ function dispatchAssociatedRecords(
 
 export const fetchNotifications = createDataLoadingThunk(
   'notificationGroups/fetch',
-  async (_params, { getState }) => {
-    const activeFilter =
-      selectSettingsNotificationsQuickFilterActive(getState());
-
-    return apiFetchNotifications({
-      exclude_types:
-        activeFilter === 'all'
-          ? selectSettingsNotificationsExcludedTypes(getState())
-          : excludeAllTypesExcept(activeFilter),
-    });
-  },
+  async (_params, { getState }) =>
+    apiFetchNotifications({ exclude_types: getExcludedTypes(getState()) }),
   ({ notifications, accounts, statuses }, { dispatch }) => {
     dispatch(importFetchedAccounts(accounts));
     dispatch(importFetchedStatuses(statuses));
@@ -92,9 +91,11 @@ export const fetchNotifications = createDataLoadingThunk(
 
 export const fetchNotificationsGap = createDataLoadingThunk(
   'notificationGroups/fetchGap',
-  async (params: { gap: NotificationGap }) =>
-    apiFetchNotifications({ max_id: params.gap.maxId }),
-
+  async (params: { gap: NotificationGap }, { getState }) =>
+    apiFetchNotifications({
+      max_id: params.gap.maxId,
+      exclude_types: getExcludedTypes(getState()),
+    }),
   ({ notifications, accounts, statuses }, { dispatch }) => {
     dispatch(importFetchedAccounts(accounts));
     dispatch(importFetchedStatuses(statuses));
@@ -109,6 +110,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
   async (_params, { getState }) => {
     return apiFetchNotifications({
       max_id: undefined,
+      exclude_types: getExcludedTypes(getState()),
       // In slow mode, we don't want to include notifications that duplicate the already-displayed ones
       since_id: usePendingItems
         ? getState().notificationGroups.groups.find(
@@ -183,7 +185,6 @@ export const setNotificationsFilter = createAppAsyncThunk(
       path: ['notifications', 'quickFilter', 'active'],
       value: filterType,
     });
-    // dispatch(expandNotifications({ forceLoad: true }));
     void dispatch(fetchNotifications());
     dispatch(saveSettings());
   },

From 585e369e0bb0e17ff5e025385dfefde1e9e81fa3 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 4 Sep 2024 15:43:08 +0200
Subject: [PATCH 03/91] Fix display name being displayed instead of domain in
 remote reports (#31613)

---
 .../components/notification_admin_report.tsx     | 16 ++++------------
 1 file changed, 4 insertions(+), 12 deletions(-)

diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_admin_report.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_admin_report.tsx
index fda5798ae..e41a6b273 100644
--- a/app/javascript/mastodon/features/notifications_v2/components/notification_admin_report.tsx
+++ b/app/javascript/mastodon/features/notifications_v2/components/notification_admin_report.tsx
@@ -42,19 +42,11 @@ export const NotificationAdminReport: React.FC<{
 
   if (!account || !targetAccount) return null;
 
+  const domain = account.acct.split('@')[1];
+
   const values = {
-    name: (
-      <bdi
-        dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
-      />
-    ),
-    target: (
-      <bdi
-        dangerouslySetInnerHTML={{
-          __html: targetAccount.get('display_name_html'),
-        }}
-      />
-    ),
+    name: <bdi>{domain ?? `@${account.acct}`}</bdi>,
+    target: <bdi>@{targetAccount.acct}</bdi>,
     category: intl.formatMessage(messages[report.category]),
     count: report.status_ids.length,
   };

From 9ba81eae3e526724dda1c693117aae5c0c235fe9 Mon Sep 17 00:00:00 2001
From: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
Date: Wed, 4 Sep 2024 16:10:26 +0200
Subject: [PATCH 04/91] Streaming: Improve Redis connection options handling
 (#31623)

---
 streaming/index.js |  64 +++++++++++++++++-------
 streaming/redis.js | 120 +++++++++++++++++++++++++++++++++++----------
 2 files changed, 142 insertions(+), 42 deletions(-)

diff --git a/streaming/index.js b/streaming/index.js
index 5ef1f6f31..b302565a4 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -111,6 +111,35 @@ const startServer = async () => {
   const server = http.createServer();
   const wss = new WebSocketServer({ noServer: true });
 
+  /**
+   * Adds a namespace to Redis keys or channel names
+   * Fixes: https://github.com/redis/ioredis/issues/1910
+   * @param {string} keyOrChannel
+   * @returns {string}
+   */
+  function redisNamespaced(keyOrChannel) {
+    if (redisConfig.namespace) {
+      return `${redisConfig.namespace}:${keyOrChannel}`;
+    } else {
+      return keyOrChannel;
+    }
+  }
+
+  /**
+   * Removes the redis namespace from a channel name
+   * @param {string} channel
+   * @returns {string}
+   */
+  function redisUnnamespaced(channel) {
+    if (typeof redisConfig.namespace === "string") {
+      // Note: this removes the configured namespace and the colon that is used
+      // to separate it:
+      return channel.slice(redisConfig.namespace.length + 1);
+    } else {
+      return channel;
+    }
+  }
+
   // Set the X-Request-Id header on WebSockets:
   wss.on("headers", function onHeaders(headers, req) {
     headers.push(`X-Request-Id: ${req.id}`);
@@ -200,7 +229,6 @@ const startServer = async () => {
   const subs = {};
 
   const redisSubscribeClient = Redis.createClient(redisConfig, logger);
-  const { redisPrefix } = redisConfig;
 
   // When checking metrics in the browser, the favicon is requested this
   // prevents the request from falling through to the API Router, which would
@@ -222,7 +250,7 @@ const startServer = async () => {
     const interval = 6 * 60;
 
     const tellSubscribed = () => {
-      channels.forEach(channel => redisClient.set(`${redisPrefix}subscribed:${channel}`, '1', 'EX', interval * 3));
+      channels.forEach(channel => redisClient.set(redisNamespaced(`subscribed:${channel}`), '1', 'EX', interval * 3));
     };
 
     tellSubscribed();
@@ -240,11 +268,10 @@ const startServer = async () => {
    */
   const onRedisMessage = (channel, message) => {
     metrics.redisMessagesReceived.inc();
+    logger.debug(`New message on channel ${channel}`);
 
-    const callbacks = subs[channel];
-
-    logger.debug(`New message on channel ${redisPrefix}${channel}`);
-
+    const key = redisUnnamespaced(channel);
+    const callbacks = subs[key];
     if (!callbacks) {
       return;
     }
@@ -273,7 +300,8 @@ const startServer = async () => {
 
     if (subs[channel].length === 0) {
       logger.debug(`Subscribe ${channel}`);
-      redisSubscribeClient.subscribe(channel, (err, count) => {
+
+      redisSubscribeClient.subscribe(redisNamespaced(channel), (err, count) => {
         if (err) {
           logger.error(`Error subscribing to ${channel}`);
         } else if (typeof count === 'number') {
@@ -300,7 +328,9 @@ const startServer = async () => {
 
     if (subs[channel].length === 0) {
       logger.debug(`Unsubscribe ${channel}`);
-      redisSubscribeClient.unsubscribe(channel, (err, count) => {
+
+      // FIXME: https://github.com/redis/ioredis/issues/1910
+      redisSubscribeClient.unsubscribe(redisNamespaced(channel), (err, count) => {
         if (err) {
           logger.error(`Error unsubscribing to ${channel}`);
         } else if (typeof count === 'number') {
@@ -481,14 +511,14 @@ const startServer = async () => {
     });
 
     res.on('close', () => {
-      unsubscribe(`${redisPrefix}${accessTokenChannelId}`, listener);
-      unsubscribe(`${redisPrefix}${systemChannelId}`, listener);
+      unsubscribe(accessTokenChannelId, listener);
+      unsubscribe(systemChannelId, listener);
 
       metrics.connectedChannels.labels({ type: 'eventsource', channel: 'system' }).dec(2);
     });
 
-    subscribe(`${redisPrefix}${accessTokenChannelId}`, listener);
-    subscribe(`${redisPrefix}${systemChannelId}`, listener);
+    subscribe(accessTokenChannelId, listener);
+    subscribe(systemChannelId, listener);
 
     metrics.connectedChannels.labels({ type: 'eventsource', channel: 'system' }).inc(2);
   };
@@ -805,11 +835,11 @@ const startServer = async () => {
     };
 
     channelIds.forEach(id => {
-      subscribe(`${redisPrefix}${id}`, listener);
+      subscribe(id, listener);
     });
 
     if (typeof attachCloseHandler === 'function') {
-      attachCloseHandler(channelIds.map(id => `${redisPrefix}${id}`), listener);
+      attachCloseHandler(channelIds, listener);
     }
 
     return listener;
@@ -1156,7 +1186,7 @@ const startServer = async () => {
     }
 
     channelIds.forEach(channelId => {
-      unsubscribe(`${redisPrefix}${channelId}`, subscription.listener);
+      unsubscribe(channelId, subscription.listener);
     });
 
     metrics.connectedChannels.labels({ type: 'websocket', channel: subscription.channelName }).dec();
@@ -1200,8 +1230,8 @@ const startServer = async () => {
       },
     });
 
-    subscribe(`${redisPrefix}${accessTokenChannelId}`, listener);
-    subscribe(`${redisPrefix}${systemChannelId}`, listener);
+    subscribe(accessTokenChannelId, listener);
+    subscribe(systemChannelId, listener);
 
     subscriptions[accessTokenChannelId] = {
       channelName: 'system',
diff --git a/streaming/redis.js b/streaming/redis.js
index 208d6ae07..2a36b89dc 100644
--- a/streaming/redis.js
+++ b/streaming/redis.js
@@ -4,44 +4,114 @@ import { parseIntFromEnvValue } from './utils.js';
 
 /**
  * @typedef RedisConfiguration
- * @property {import('ioredis').RedisOptions} redisParams
- * @property {string} redisPrefix
- * @property {string|undefined} redisUrl
+ * @property {string|undefined} namespace
+ * @property {string|undefined} url
+ * @property {import('ioredis').RedisOptions} options
  */
 
+/**
+ *
+ * @param {NodeJS.ProcessEnv} env
+ * @returns {boolean}
+ */
+function hasSentinelConfiguration(env) {
+  return (
+    typeof env.REDIS_SENTINELS === 'string' &&
+    env.REDIS_SENTINELS.length > 0 &&
+    typeof env.REDIS_SENTINEL_MASTER === 'string' &&
+    env.REDIS_SENTINEL_MASTER.length > 0
+  );
+}
+
+/**
+ *
+ * @param {NodeJS.ProcessEnv} env
+ * @param {import('ioredis').SentinelConnectionOptions} commonOptions
+ * @returns {import('ioredis').SentinelConnectionOptions}
+ */
+function getSentinelConfiguration(env, commonOptions) {
+  const redisDatabase = parseIntFromEnvValue(env.REDIS_DB, 0, 'REDIS_DB');
+  const sentinelPort = parseIntFromEnvValue(env.REDIS_SENTINEL_PORT, 26379, 'REDIS_SENTINEL_PORT');
+
+  const sentinels = env.REDIS_SENTINELS.split(',').map((sentinel) => {
+    const [host, port] = sentinel.split(':', 2);
+
+    /** @type {import('ioredis').SentinelAddress} */
+    return {
+      host: host,
+      port: port ?? sentinelPort,
+      // Force support for both IPv6 and IPv4, by default ioredis sets this to 4,
+      // only allowing IPv4 connections:
+      // https://github.com/redis/ioredis/issues/1576
+      family: 0
+    };
+  });
+
+  return {
+    db: redisDatabase,
+    name: env.REDIS_SENTINEL_MASTER,
+    username: env.REDIS_USERNAME,
+    password: env.REDIS_PASSWORD,
+    sentinelUsername: env.REDIS_SENTINEL_USERNAME ?? env.REDIS_USERNAME,
+    sentinelPassword: env.REDIS_SENTINEL_PASSWORD ?? env.REDIS_PASSWORD,
+    sentinels,
+    ...commonOptions,
+  };
+}
+
 /**
  * @param {NodeJS.ProcessEnv} env the `process.env` value to read configuration from
  * @returns {RedisConfiguration} configuration for the Redis connection
  */
 export function configFromEnv(env) {
-  // ioredis *can* transparently add prefixes for us, but it doesn't *in some cases*,
-  // which means we can't use it. But this is something that should be looked into.
-  const redisPrefix = env.REDIS_NAMESPACE ? `${env.REDIS_NAMESPACE}:` : '';
+  const redisNamespace = env.REDIS_NAMESPACE;
 
+  // These options apply for both REDIS_URL based connections and connections
+  // using the other REDIS_* environment variables:
+  const commonOptions = {
+    // Force support for both IPv6 and IPv4, by default ioredis sets this to 4,
+    // only allowing IPv4 connections:
+    // https://github.com/redis/ioredis/issues/1576
+    family: 0
+    // Note: we don't use auto-prefixing of keys since this doesn't apply to
+    // subscribe/unsubscribe which have "channel" instead of "key" arguments
+  };
+
+  // If we receive REDIS_URL, don't continue parsing any other REDIS_*
+  // environment variables:
+  if (typeof env.REDIS_URL === 'string' && env.REDIS_URL.length > 0) {
+    return {
+      url: env.REDIS_URL,
+      options: commonOptions,
+      namespace: redisNamespace
+    };
+  }
+
+  // If we have configuration for Redis Sentinel mode, prefer that:
+  if (hasSentinelConfiguration(env)) {
+    return {
+      options: getSentinelConfiguration(env, commonOptions),
+      namespace: redisNamespace
+    };
+  }
+
+  // Finally, handle all the other REDIS_* environment variables:
   let redisPort = parseIntFromEnvValue(env.REDIS_PORT, 6379, 'REDIS_PORT');
   let redisDatabase = parseIntFromEnvValue(env.REDIS_DB, 0, 'REDIS_DB');
 
   /** @type {import('ioredis').RedisOptions} */
-  const redisParams = {
-    host: env.REDIS_HOST || '127.0.0.1',
+  const options = {
+    host: env.REDIS_HOST ?? '127.0.0.1',
     port: redisPort,
-    // Force support for both IPv6 and IPv4, by default ioredis sets this to 4,
-    // only allowing IPv4 connections:
-    // https://github.com/redis/ioredis/issues/1576
-    family: 0,
     db: redisDatabase,
-    password: env.REDIS_PASSWORD || undefined,
+    username: env.REDIS_USERNAME,
+    password: env.REDIS_PASSWORD,
+    ...commonOptions,
   };
 
-  // redisParams.path takes precedence over host and port.
-  if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) {
-    redisParams.path = env.REDIS_URL.slice(7);
-  }
-
   return {
-    redisParams,
-    redisPrefix,
-    redisUrl: typeof env.REDIS_URL === 'string' ? env.REDIS_URL : undefined,
+    options,
+    namespace: redisNamespace
   };
 }
 
@@ -50,13 +120,13 @@ export function configFromEnv(env) {
  * @param {import('pino').Logger} logger
  * @returns {Redis}
  */
-export function createClient({ redisParams, redisUrl }, logger) {
+export function createClient({ url, options }, logger) {
   let client;
 
-  if (typeof redisUrl === 'string') {
-    client = new Redis(redisUrl, redisParams);
+  if (typeof url === 'string') {
+    client = new Redis(url, options);
   } else {
-    client = new Redis(redisParams);
+    client = new Redis(options);
   }
 
   client.on('error', (err) => logger.error({ err }, 'Redis Client Error!'));

From ef2bc8ea261838cf31fe4fe11b2954a19c864295 Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Wed, 4 Sep 2024 16:10:45 +0200
Subject: [PATCH 05/91] Add redis sentinel support to ruby part of code
 (#31744)

---
 lib/mastodon/redis_configuration.rb           | 102 ++++++++++--------
 spec/lib/mastodon/redis_configuration_spec.rb |  56 ++++++++++
 2 files changed, 112 insertions(+), 46 deletions(-)

diff --git a/lib/mastodon/redis_configuration.rb b/lib/mastodon/redis_configuration.rb
index 3cd121e4a..9139d8758 100644
--- a/lib/mastodon/redis_configuration.rb
+++ b/lib/mastodon/redis_configuration.rb
@@ -1,34 +1,33 @@
 # frozen_string_literal: true
 
 class Mastodon::RedisConfiguration
+  DEFAULTS = {
+    host: 'localhost',
+    port: 6379,
+    db: 0,
+  }.freeze
+
   def base
-    @base ||= {
-      url: setup_base_redis_url,
-      driver: driver,
-      namespace: base_namespace,
-    }
+    @base ||= setup_config(prefix: nil, defaults: DEFAULTS)
+              .merge(namespace: base_namespace)
   end
 
   def sidekiq
-    @sidekiq ||= {
-      url: setup_prefixed_redis_url(:sidekiq),
-      driver: driver,
-      namespace: sidekiq_namespace,
-    }
+    @sidekiq ||= setup_config(prefix: 'SIDEKIQ_')
+                 .merge(namespace: sidekiq_namespace)
   end
 
   def cache
-    @cache ||= {
-      url: setup_prefixed_redis_url(:cache),
-      driver: driver,
-      namespace: cache_namespace,
-      expires_in: 10.minutes,
-      connect_timeout: 5,
-      pool: {
-        size: Sidekiq.server? ? Sidekiq[:concurrency] : Integer(ENV['MAX_THREADS'] || 5),
-        timeout: 5,
-      },
-    }
+    @cache ||= setup_config(prefix: 'CACHE_')
+               .merge({
+                 namespace: cache_namespace,
+                 expires_in: 10.minutes,
+                 connect_timeout: 5,
+                 pool: {
+                   size: Sidekiq.server? ? Sidekiq[:concurrency] : Integer(ENV['MAX_THREADS'] || 5),
+                   timeout: 5,
+                 },
+               })
   end
 
   private
@@ -55,42 +54,53 @@ class Mastodon::RedisConfiguration
     namespace ? "#{namespace}_cache" : 'cache'
   end
 
-  def setup_base_redis_url
-    url = ENV.fetch('REDIS_URL', nil)
-    return url if url.present?
+  def setup_config(prefix: nil, defaults: {})
+    prefix = "#{prefix}REDIS_"
 
-    user     = ENV.fetch('REDIS_USER', '')
-    password = ENV.fetch('REDIS_PASSWORD', '')
-    host     = ENV.fetch('REDIS_HOST', 'localhost')
-    port     = ENV.fetch('REDIS_PORT', 6379)
-    db       = ENV.fetch('REDIS_DB', 0)
+    url       = ENV.fetch("#{prefix}URL", nil)
+    user      = ENV.fetch("#{prefix}USER", nil)
+    password  = ENV.fetch("#{prefix}PASSWORD", nil)
+    host      = ENV.fetch("#{prefix}HOST", defaults[:host])
+    port      = ENV.fetch("#{prefix}PORT", defaults[:port])
+    db        = ENV.fetch("#{prefix}DB", defaults[:db])
+    name      = ENV.fetch("#{prefix}SENTINEL_MASTER", nil)
+    sentinels = parse_sentinels(ENV.fetch("#{prefix}SENTINELS", nil))
 
-    construct_uri(host, port, db, user, password)
-  end
+    return { url:, driver: } if url
 
-  def setup_prefixed_redis_url(prefix)
-    prefix = "#{prefix.to_s.upcase}_"
-    url = ENV.fetch("#{prefix}REDIS_URL", nil)
-
-    return url if url.present?
-
-    user     = ENV.fetch("#{prefix}REDIS_USER", nil)
-    password = ENV.fetch("#{prefix}REDIS_PASSWORD", nil)
-    host     = ENV.fetch("#{prefix}REDIS_HOST", nil)
-    port     = ENV.fetch("#{prefix}REDIS_PORT", nil)
-    db       = ENV.fetch("#{prefix}REDIS_DB", nil)
-
-    if host.nil?
-      base[:url]
+    if name.present? && sentinels.present?
+      host = name
+      port = nil
+      db ||= 0
     else
-      construct_uri(host, port, db, user, password)
+      sentinels = nil
+    end
+
+    url = construct_uri(host, port, db, user, password)
+
+    if url.present?
+      { url:, driver:, name:, sentinels: }
+    else
+      # Fall back to base config. This has defaults for the URL
+      # so this cannot lead to an endless loop.
+      base
     end
   end
 
   def construct_uri(host, port, db, user, password)
+    return nil if host.blank?
+
     Addressable::URI.parse("redis://#{host}:#{port}/#{db}").tap do |uri|
       uri.user = user if user.present?
       uri.password = password if password.present?
     end.normalize.to_str
   end
+
+  def parse_sentinels(sentinels_string)
+    (sentinels_string || '').split(',').map do |sentinel|
+      host, port = sentinel.split(':')
+      port = port.present? ? port.to_i : 26_379
+      { host: host, port: port }
+    end.presence
+  end
 end
diff --git a/spec/lib/mastodon/redis_configuration_spec.rb b/spec/lib/mastodon/redis_configuration_spec.rb
index c7326fd41..a48ffc80e 100644
--- a/spec/lib/mastodon/redis_configuration_spec.rb
+++ b/spec/lib/mastodon/redis_configuration_spec.rb
@@ -45,6 +45,20 @@ RSpec.describe Mastodon::RedisConfiguration do
       it 'uses the url from the base config' do
         expect(subject[:url]).to eq 'redis://localhost:6379/0'
       end
+
+      context 'when the base config uses sentinel' do
+        around do |example|
+          ClimateControl.modify REDIS_SENTINELS: '192.168.0.1:3000,192.168.0.2:4000', REDIS_SENTINEL_MASTER: 'mainsentinel' do
+            example.run
+          end
+        end
+
+        it 'uses the sentinel configuration from base config' do
+          expect(subject[:url]).to eq 'redis://mainsentinel/0'
+          expect(subject[:name]).to eq 'mainsentinel'
+          expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 3000 }, { host: '192.168.0.2', port: 4000 })
+        end
+      end
     end
 
     context "when the `#{prefix}_REDIS_URL` environment variable is present" do
@@ -72,6 +86,39 @@ RSpec.describe Mastodon::RedisConfiguration do
     end
   end
 
+  shared_examples 'sentinel support' do |prefix = nil|
+    prefix = prefix ? "#{prefix}_" : ''
+
+    context 'when configuring sentinel support' do
+      around do |example|
+        ClimateControl.modify "#{prefix}REDIS_PASSWORD": 'testpass1', "#{prefix}REDIS_HOST": 'redis2.example.com', "#{prefix}REDIS_SENTINELS": '192.168.0.1:3000,192.168.0.2:4000', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
+          example.run
+        end
+      end
+
+      it 'constructs the url using the sentinel master name' do
+        expect(subject[:url]).to eq 'redis://:testpass1@mainsentinel/0'
+      end
+
+      it 'includes the sentinel master name and list of sentinels' do
+        expect(subject[:name]).to eq 'mainsentinel'
+        expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 3000 }, { host: '192.168.0.2', port: 4000 })
+      end
+    end
+
+    context 'when giving sentinels without port numbers' do
+      around do |example|
+        ClimateControl.modify "#{prefix}REDIS_SENTINELS": '192.168.0.1,192.168.0.2', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
+          example.run
+        end
+      end
+
+      it 'uses the default sentinel port' do
+        expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 26_379 }, { host: '192.168.0.2', port: 26_379 })
+      end
+    end
+  end
+
   describe '#base' do
     subject { redis_environment.base }
 
@@ -81,6 +128,8 @@ RSpec.describe Mastodon::RedisConfiguration do
           url: 'redis://localhost:6379/0',
           driver: :hiredis,
           namespace: nil,
+          name: nil,
+          sentinels: nil,
         })
       end
     end
@@ -113,12 +162,15 @@ RSpec.describe Mastodon::RedisConfiguration do
           url: 'redis://:testpass@redis.example.com:3333/3',
           driver: :hiredis,
           namespace: nil,
+          name: nil,
+          sentinels: nil,
         })
       end
     end
 
     include_examples 'setting a different driver'
     include_examples 'setting a namespace'
+    include_examples 'sentinel support'
   end
 
   describe '#sidekiq' do
@@ -127,6 +179,7 @@ RSpec.describe Mastodon::RedisConfiguration do
     include_examples 'secondary configuration', 'SIDEKIQ'
     include_examples 'setting a different driver'
     include_examples 'setting a namespace'
+    include_examples 'sentinel support', 'SIDEKIQ'
   end
 
   describe '#cache' do
@@ -139,6 +192,8 @@ RSpec.describe Mastodon::RedisConfiguration do
         namespace: 'cache',
         expires_in: 10.minutes,
         connect_timeout: 5,
+        name: nil,
+        sentinels: nil,
         pool: {
           size: 5,
           timeout: 5,
@@ -166,5 +221,6 @@ RSpec.describe Mastodon::RedisConfiguration do
 
     include_examples 'secondary configuration', 'CACHE'
     include_examples 'setting a different driver'
+    include_examples 'sentinel support', 'CACHE'
   end
 end

From fe04291af46d7cb9d3439fa73739b2ffb2b53d72 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 4 Sep 2024 13:19:53 -0400
Subject: [PATCH 06/91] Use more accurate beginning/ending times in annual
 report source (#31751)

---
 app/lib/annual_report/source.rb | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/app/lib/annual_report/source.rb b/app/lib/annual_report/source.rb
index 1ccb62267..d56a1fccc 100644
--- a/app/lib/annual_report/source.rb
+++ b/app/lib/annual_report/source.rb
@@ -11,6 +11,16 @@ class AnnualReport::Source
   protected
 
   def year_as_snowflake_range
-    (Mastodon::Snowflake.id_at(DateTime.new(year, 1, 1))..Mastodon::Snowflake.id_at(DateTime.new(year, 12, 31)))
+    (beginning_snowflake_id..ending_snowflake_id)
+  end
+
+  private
+
+  def beginning_snowflake_id
+    Mastodon::Snowflake.id_at DateTime.new(year).beginning_of_year
+  end
+
+  def ending_snowflake_id
+    Mastodon::Snowflake.id_at DateTime.new(year).end_of_year
   end
 end

From e1b5f3fc6f1bb6e77a7cad725a963d008c7ce983 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 4 Sep 2024 13:29:05 -0400
Subject: [PATCH 07/91] Use `response.parsed_body` for html response checks
 (#31750)

---
 spec/controllers/admin/accounts_controller_spec.rb |  2 +-
 .../admin/export_domain_blocks_controller_spec.rb  |  2 +-
 .../controllers/admin/instances_controller_spec.rb |  2 +-
 .../auth/registrations_controller_spec.rb          |  2 +-
 spec/requests/account_show_page_spec.rb            | 14 ++++++++------
 5 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 89a7239f5..ca399fbd9 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Admin::AccountsController do
     end
 
     def accounts_table_rows
-      Nokogiri::Slop(response.body).css('table.accounts-table tr')
+      response.parsed_body.css('table.accounts-table tr')
     end
   end
 
diff --git a/spec/controllers/admin/export_domain_blocks_controller_spec.rb b/spec/controllers/admin/export_domain_blocks_controller_spec.rb
index 39195716c..564f5a88c 100644
--- a/spec/controllers/admin/export_domain_blocks_controller_spec.rb
+++ b/spec/controllers/admin/export_domain_blocks_controller_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Admin::ExportDomainBlocksController do
     end
 
     def batch_table_rows
-      Nokogiri::Slop(response.body).css('body div.batch-table__row')
+      response.parsed_body.css('body div.batch-table__row')
     end
   end
 
diff --git a/spec/controllers/admin/instances_controller_spec.rb b/spec/controllers/admin/instances_controller_spec.rb
index a64bbb2c9..1e65373e1 100644
--- a/spec/controllers/admin/instances_controller_spec.rb
+++ b/spec/controllers/admin/instances_controller_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Admin::InstancesController do
     end
 
     def instance_directory_links
-      Nokogiri::Slop(response.body).css('div.directory__tag a')
+      response.parsed_body.css('div.directory__tag a')
     end
   end
 
diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb
index 75ab28765..6118edf4e 100644
--- a/spec/controllers/auth/registrations_controller_spec.rb
+++ b/spec/controllers/auth/registrations_controller_spec.rb
@@ -342,7 +342,7 @@ RSpec.describe Auth::RegistrationsController do
       end
 
       def username_error_text
-        Nokogiri::Slop(response.body).css('.user_account_username .error').text
+        response.parsed_body.css('.user_account_username .error').text
       end
     end
 
diff --git a/spec/requests/account_show_page_spec.rb b/spec/requests/account_show_page_spec.rb
index d0857c898..7f3ea2595 100644
--- a/spec/requests/account_show_page_spec.rb
+++ b/spec/requests/account_show_page_spec.rb
@@ -18,14 +18,16 @@ RSpec.describe 'The account show page' do
   end
 
   def head_link_icons
-    head_section.css('link[rel=icon]')
+    response
+      .parsed_body
+      .search('html head link[rel=icon]')
   end
 
   def head_meta_content(property)
-    head_section.meta("[@property='#{property}']")[:content]
-  end
-
-  def head_section
-    Nokogiri::Slop(response.body).html.head
+    response
+      .parsed_body
+      .search("html head meta[property='#{property}']")
+      .attr('content')
+      .text
   end
 end

From 559958f8c540a28c9f41da040fa23a228fadad0b Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 4 Sep 2024 19:35:40 +0200
Subject: [PATCH 08/91] Fix email language when recipient has no selected
 locale (#31747)

---
 app/mailers/application_mailer.rb |  2 +-
 app/mailers/user_mailer.rb        | 24 ++++++++++++------------
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 3312183d4..35f0b5fee 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -12,7 +12,7 @@ class ApplicationMailer < ActionMailer::Base
   protected
 
   def locale_for_account(account, &block)
-    I18n.with_locale(account.user_locale || I18n.locale || I18n.default_locale, &block)
+    I18n.with_locale(account.user_locale || I18n.default_locale, &block)
   end
 
   def set_autoreply_headers!
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 81a2c0c6d..5c9e5c96d 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -33,7 +33,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: default_devise_subject
     end
   end
@@ -43,7 +43,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: default_devise_subject
     end
   end
@@ -53,7 +53,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: default_devise_subject
     end
   end
@@ -63,7 +63,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: default_devise_subject
     end
   end
@@ -73,7 +73,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: default_devise_subject
     end
   end
@@ -83,7 +83,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: default_devise_subject
     end
   end
@@ -93,7 +93,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: default_devise_subject
     end
   end
@@ -103,7 +103,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: default_devise_subject
     end
   end
@@ -114,7 +114,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
     end
   end
@@ -125,7 +125,7 @@ class UserMailer < Devise::Mailer
 
     return unless @resource.active_for_authentication?
 
-    I18n.with_locale(locale) do
+    I18n.with_locale(locale(use_current_locale: true)) do
       mail subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
     end
   end
@@ -219,7 +219,7 @@ class UserMailer < Devise::Mailer
     @instance = Rails.configuration.x.local_domain
   end
 
-  def locale
-    @resource.locale.presence || I18n.locale || I18n.default_locale
+  def locale(use_current_locale: false)
+    @resource.locale.presence || (use_current_locale && I18n.locale) || I18n.default_locale
   end
 end

From 4678473e54de33200919ad39f08162aed9350e8a Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 4 Sep 2024 15:50:33 -0400
Subject: [PATCH 09/91] Add `AnnualReport::Source#report_statuses` method for
 subclasses to use (#31753)

---
 app/lib/annual_report/archetype.rb                 | 12 ++++--------
 .../commonly_interacted_with_accounts.rb           |  2 +-
 app/lib/annual_report/most_reblogged_accounts.rb   |  2 +-
 app/lib/annual_report/most_used_apps.rb            |  2 +-
 app/lib/annual_report/percentiles.rb               |  2 +-
 app/lib/annual_report/source.rb                    |  7 +++++++
 app/lib/annual_report/time_series.rb               |  2 +-
 app/lib/annual_report/top_hashtags.rb              |  2 +-
 app/lib/annual_report/top_statuses.rb              |  2 +-
 app/lib/annual_report/type_distribution.rb         | 14 ++++----------
 10 files changed, 22 insertions(+), 25 deletions(-)

diff --git a/app/lib/annual_report/archetype.rb b/app/lib/annual_report/archetype.rb
index ea9ef366d..c02b28dfd 100644
--- a/app/lib/annual_report/archetype.rb
+++ b/app/lib/annual_report/archetype.rb
@@ -28,22 +28,18 @@ class AnnualReport::Archetype < AnnualReport::Source
   end
 
   def polls_count
-    @polls_count ||= base_scope.where.not(poll_id: nil).count
+    @polls_count ||= report_statuses.where.not(poll_id: nil).count
   end
 
   def reblogs_count
-    @reblogs_count ||= base_scope.where.not(reblog_of_id: nil).count
+    @reblogs_count ||= report_statuses.where.not(reblog_of_id: nil).count
   end
 
   def replies_count
-    @replies_count ||= base_scope.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count
+    @replies_count ||= report_statuses.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count
   end
 
   def standalone_count
-    @standalone_count ||= base_scope.without_replies.without_reblogs.count
-  end
-
-  def base_scope
-    @account.statuses.where(id: year_as_snowflake_range)
+    @standalone_count ||= report_statuses.without_replies.without_reblogs.count
   end
 end
diff --git a/app/lib/annual_report/commonly_interacted_with_accounts.rb b/app/lib/annual_report/commonly_interacted_with_accounts.rb
index af5e854c2..e7482f0d5 100644
--- a/app/lib/annual_report/commonly_interacted_with_accounts.rb
+++ b/app/lib/annual_report/commonly_interacted_with_accounts.rb
@@ -17,6 +17,6 @@ class AnnualReport::CommonlyInteractedWithAccounts < AnnualReport::Source
   private
 
   def commonly_interacted_with_accounts
-    @account.statuses.reorder(nil).where(id: year_as_snowflake_range).where.not(in_reply_to_account_id: @account.id).group(:in_reply_to_account_id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('in_reply_to_account_id, count(*) AS total'))
+    report_statuses.where.not(in_reply_to_account_id: @account.id).group(:in_reply_to_account_id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('in_reply_to_account_id, count(*) AS total'))
   end
 end
diff --git a/app/lib/annual_report/most_reblogged_accounts.rb b/app/lib/annual_report/most_reblogged_accounts.rb
index e3e8a7c90..39ed3868e 100644
--- a/app/lib/annual_report/most_reblogged_accounts.rb
+++ b/app/lib/annual_report/most_reblogged_accounts.rb
@@ -17,6 +17,6 @@ class AnnualReport::MostRebloggedAccounts < AnnualReport::Source
   private
 
   def most_reblogged_accounts
-    @account.statuses.reorder(nil).where(id: year_as_snowflake_range).where.not(reblog_of_id: nil).joins(reblog: :account).group('accounts.id').having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('accounts.id, count(*) as total'))
+    report_statuses.where.not(reblog_of_id: nil).joins(reblog: :account).group('accounts.id').having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('accounts.id, count(*) as total'))
   end
 end
diff --git a/app/lib/annual_report/most_used_apps.rb b/app/lib/annual_report/most_used_apps.rb
index 85ff1ff86..fb1ca1d16 100644
--- a/app/lib/annual_report/most_used_apps.rb
+++ b/app/lib/annual_report/most_used_apps.rb
@@ -17,6 +17,6 @@ class AnnualReport::MostUsedApps < AnnualReport::Source
   private
 
   def most_used_apps
-    @account.statuses.reorder(nil).where(id: year_as_snowflake_range).joins(:application).group('oauth_applications.name').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('oauth_applications.name, count(*) as total'))
+    report_statuses.joins(:application).group('oauth_applications.name').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('oauth_applications.name, count(*) as total'))
   end
 end
diff --git a/app/lib/annual_report/percentiles.rb b/app/lib/annual_report/percentiles.rb
index 9fe4698ee..0251cb66a 100644
--- a/app/lib/annual_report/percentiles.rb
+++ b/app/lib/annual_report/percentiles.rb
@@ -17,7 +17,7 @@ class AnnualReport::Percentiles < AnnualReport::Source
   end
 
   def statuses_created
-    @statuses_created ||= @account.statuses.where(id: year_as_snowflake_range).count
+    @statuses_created ||= report_statuses.count
   end
 
   def total_with_fewer_followers
diff --git a/app/lib/annual_report/source.rb b/app/lib/annual_report/source.rb
index d56a1fccc..cb9f7b16e 100644
--- a/app/lib/annual_report/source.rb
+++ b/app/lib/annual_report/source.rb
@@ -10,6 +10,13 @@ class AnnualReport::Source
 
   protected
 
+  def report_statuses
+    @account
+      .statuses
+      .where(id: year_as_snowflake_range)
+      .reorder(nil)
+  end
+
   def year_as_snowflake_range
     (beginning_snowflake_id..ending_snowflake_id)
   end
diff --git a/app/lib/annual_report/time_series.rb b/app/lib/annual_report/time_series.rb
index a144bac0d..65a188eda 100644
--- a/app/lib/annual_report/time_series.rb
+++ b/app/lib/annual_report/time_series.rb
@@ -17,7 +17,7 @@ class AnnualReport::TimeSeries < AnnualReport::Source
   private
 
   def statuses_per_month
-    @statuses_per_month ||= @account.statuses.reorder(nil).where(id: year_as_snowflake_range).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h
+    @statuses_per_month ||= report_statuses.group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h
   end
 
   def following_per_month
diff --git a/app/lib/annual_report/top_hashtags.rb b/app/lib/annual_report/top_hashtags.rb
index 488dacb1b..32bd10d69 100644
--- a/app/lib/annual_report/top_hashtags.rb
+++ b/app/lib/annual_report/top_hashtags.rb
@@ -17,6 +17,6 @@ class AnnualReport::TopHashtags < AnnualReport::Source
   private
 
   def top_hashtags
-    Tag.joins(:statuses).where(statuses: { id: @account.statuses.where(id: year_as_snowflake_range).reorder(nil).select(:id) }).group(:id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('COALESCE(tags.display_name, tags.name), count(*) AS total'))
+    Tag.joins(:statuses).where(statuses: { id: report_statuses.select(:id) }).group(:id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('COALESCE(tags.display_name, tags.name), count(*) AS total'))
   end
 end
diff --git a/app/lib/annual_report/top_statuses.rb b/app/lib/annual_report/top_statuses.rb
index 1ab170952..c5abeaa58 100644
--- a/app/lib/annual_report/top_statuses.rb
+++ b/app/lib/annual_report/top_statuses.rb
@@ -16,6 +16,6 @@ class AnnualReport::TopStatuses < AnnualReport::Source
   end
 
   def base_scope
-    @account.statuses.public_visibility.joins(:status_stat).where(id: year_as_snowflake_range).reorder(nil)
+    report_statuses.public_visibility.joins(:status_stat)
   end
 end
diff --git a/app/lib/annual_report/type_distribution.rb b/app/lib/annual_report/type_distribution.rb
index fc12a6f1f..fe38d8a8a 100644
--- a/app/lib/annual_report/type_distribution.rb
+++ b/app/lib/annual_report/type_distribution.rb
@@ -4,17 +4,11 @@ class AnnualReport::TypeDistribution < AnnualReport::Source
   def generate
     {
       type_distribution: {
-        total: base_scope.count,
-        reblogs: base_scope.where.not(reblog_of_id: nil).count,
-        replies: base_scope.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count,
-        standalone: base_scope.without_replies.without_reblogs.count,
+        total: report_statuses.count,
+        reblogs: report_statuses.where.not(reblog_of_id: nil).count,
+        replies: report_statuses.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count,
+        standalone: report_statuses.without_replies.without_reblogs.count,
       },
     }
   end
-
-  private
-
-  def base_scope
-    @account.statuses.where(id: year_as_snowflake_range)
-  end
 end

From 4d5c91e99a3897addd737b12b8b6e3baded6d2d9 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 4 Sep 2024 15:51:40 -0400
Subject: [PATCH 10/91] Remove `before` block in spec with TODOs which have
 been TO-DONE already (#31754)

---
 spec/requests/instance_actor_spec.rb | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/spec/requests/instance_actor_spec.rb b/spec/requests/instance_actor_spec.rb
index 7e4784203..bb294b04a 100644
--- a/spec/requests/instance_actor_spec.rb
+++ b/spec/requests/instance_actor_spec.rb
@@ -4,11 +4,6 @@ require 'rails_helper'
 
 RSpec.describe 'Instance actor endpoint' do
   describe 'GET /actor' do
-    before do
-      integration_session.https! # TODO: Move to global rails_helper for all request specs?
-      host! Rails.configuration.x.local_domain # TODO: Move to global rails_helper for all request specs?
-    end
-
     let!(:original_federation_mode) { Rails.configuration.x.limited_federation_mode }
 
     shared_examples 'instance actor endpoint' do

From 7d91723f052f42a2b5fed365b007767e5717a9bb Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Thu, 5 Sep 2024 11:26:49 +0200
Subject: [PATCH 11/91] Support `REDIS_SENTINEL_PORT` variables (#31767)

---
 lib/mastodon/redis_configuration.rb           | 23 ++++++------
 spec/lib/mastodon/redis_configuration_spec.rb | 36 ++++++++++++++++---
 2 files changed, 44 insertions(+), 15 deletions(-)

diff --git a/lib/mastodon/redis_configuration.rb b/lib/mastodon/redis_configuration.rb
index 9139d8758..5a096a1bf 100644
--- a/lib/mastodon/redis_configuration.rb
+++ b/lib/mastodon/redis_configuration.rb
@@ -57,17 +57,20 @@ class Mastodon::RedisConfiguration
   def setup_config(prefix: nil, defaults: {})
     prefix = "#{prefix}REDIS_"
 
-    url       = ENV.fetch("#{prefix}URL", nil)
-    user      = ENV.fetch("#{prefix}USER", nil)
-    password  = ENV.fetch("#{prefix}PASSWORD", nil)
-    host      = ENV.fetch("#{prefix}HOST", defaults[:host])
-    port      = ENV.fetch("#{prefix}PORT", defaults[:port])
-    db        = ENV.fetch("#{prefix}DB", defaults[:db])
-    name      = ENV.fetch("#{prefix}SENTINEL_MASTER", nil)
-    sentinels = parse_sentinels(ENV.fetch("#{prefix}SENTINELS", nil))
+    url           = ENV.fetch("#{prefix}URL", nil)
+    user          = ENV.fetch("#{prefix}USER", nil)
+    password      = ENV.fetch("#{prefix}PASSWORD", nil)
+    host          = ENV.fetch("#{prefix}HOST", defaults[:host])
+    port          = ENV.fetch("#{prefix}PORT", defaults[:port])
+    db            = ENV.fetch("#{prefix}DB", defaults[:db])
+    name          = ENV.fetch("#{prefix}SENTINEL_MASTER", nil)
+    sentinel_port = ENV.fetch("#{prefix}SENTINEL_PORT", 26_379)
+    sentinel_list = ENV.fetch("#{prefix}SENTINELS", nil)
 
     return { url:, driver: } if url
 
+    sentinels = parse_sentinels(sentinel_list, default_port: sentinel_port)
+
     if name.present? && sentinels.present?
       host = name
       port = nil
@@ -96,10 +99,10 @@ class Mastodon::RedisConfiguration
     end.normalize.to_str
   end
 
-  def parse_sentinels(sentinels_string)
+  def parse_sentinels(sentinels_string, default_port: 26_379)
     (sentinels_string || '').split(',').map do |sentinel|
       host, port = sentinel.split(':')
-      port = port.present? ? port.to_i : 26_379
+      port = (port || default_port).to_i
       { host: host, port: port }
     end.presence
   end
diff --git a/spec/lib/mastodon/redis_configuration_spec.rb b/spec/lib/mastodon/redis_configuration_spec.rb
index a48ffc80e..d14adf951 100644
--- a/spec/lib/mastodon/redis_configuration_spec.rb
+++ b/spec/lib/mastodon/redis_configuration_spec.rb
@@ -107,14 +107,40 @@ RSpec.describe Mastodon::RedisConfiguration do
     end
 
     context 'when giving sentinels without port numbers' do
-      around do |example|
-        ClimateControl.modify "#{prefix}REDIS_SENTINELS": '192.168.0.1,192.168.0.2', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
-          example.run
+      context "when no default port is given via `#{prefix}REDIS_SENTINEL_PORT`" do
+        around do |example|
+          ClimateControl.modify "#{prefix}REDIS_SENTINELS": '192.168.0.1,192.168.0.2', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
+            example.run
+          end
+        end
+
+        it 'uses the default sentinel port' do
+          expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 26_379 }, { host: '192.168.0.2', port: 26_379 })
         end
       end
 
-      it 'uses the default sentinel port' do
-        expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 26_379 }, { host: '192.168.0.2', port: 26_379 })
+      context 'when adding port numbers to some, but not all sentinels' do
+        around do |example|
+          ClimateControl.modify "#{prefix}REDIS_SENTINELS": '192.168.0.1:5678,192.168.0.2', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
+            example.run
+          end
+        end
+
+        it 'uses the given port number when available and the default otherwise' do
+          expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 5678 }, { host: '192.168.0.2', port: 26_379 })
+        end
+      end
+
+      context "when a default port is given via `#{prefix}REDIS_SENTINEL_PORT`" do
+        around do |example|
+          ClimateControl.modify "#{prefix}REDIS_SENTINEL_PORT": '1234', "#{prefix}REDIS_SENTINELS": '192.168.0.1,192.168.0.2', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
+            example.run
+          end
+        end
+
+        it 'uses the given port number' do
+          expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 1234 }, { host: '192.168.0.2', port: 1234 })
+        end
       end
     end
   end

From ec4c49082edb5f4941bd4e129900628c6b30101e Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 5 Sep 2024 11:39:59 +0200
Subject: [PATCH 12/91] Change design of unread conversations in web UI
 (#31763)

---
 .../components/conversation.jsx               |  2 +-
 .../styles/mastodon/components.scss           | 19 ++-----------------
 2 files changed, 3 insertions(+), 18 deletions(-)

diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
index 6588c8b76..0d154db1e 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
@@ -170,7 +170,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
 
   return (
     <HotKeys handlers={handlers}>
-      <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
+      <div className={classNames('conversation focusable muted', { unread })} tabIndex={0}>
         <div className='conversation__avatar' onClick={handleClick} role='presentation'>
           <AvatarComposite accounts={accounts} size={48} />
         </div>
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c1e507570..d892c008b 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -8522,22 +8522,6 @@ noscript {
       word-break: break-word;
     }
   }
-
-  &--unread {
-    background: lighten($ui-base-color, 2%);
-
-    &:focus {
-      background: lighten($ui-base-color, 4%);
-    }
-
-    .conversation__content__info {
-      font-weight: 700;
-    }
-
-    .conversation__content__relative-time {
-      color: $primary-text-color;
-    }
-  }
 }
 
 .announcements {
@@ -8732,7 +8716,8 @@ noscript {
 }
 
 .notification,
-.status__wrapper {
+.status__wrapper,
+.conversation {
   position: relative;
 
   &.unread {

From eb23d9f0f6d8415551e31d264418de733f71d83d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 5 Sep 2024 09:40:38 +0000
Subject: [PATCH 13/91] New Crowdin Translations (automated) (#31765)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/ar.json    |  22 ++-
 app/javascript/mastodon/locales/fr-CA.json |   1 +
 app/javascript/mastodon/locales/fr.json    |   1 +
 app/javascript/mastodon/locales/pt-PT.json |  78 ++++-----
 config/locales/activerecord.pt-PT.yml      |  10 +-
 config/locales/ar.yml                      |  23 ++-
 config/locales/devise.pt-PT.yml            | 106 ++++++-------
 config/locales/doorkeeper.pt-PT.yml        |  72 ++++-----
 config/locales/gl.yml                      |   4 +-
 config/locales/pt-PT.yml                   | 175 +++++++++++----------
 config/locales/simple_form.ar.yml          |   2 +-
 config/locales/simple_form.pt-PT.yml       |  24 +--
 12 files changed, 272 insertions(+), 246 deletions(-)

diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index c1cb734a6..d83a42a6c 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -95,6 +95,8 @@
   "block_modal.title": "أتريد حظر هذا المستخدم؟",
   "block_modal.you_wont_see_mentions": "لن تر المنشورات التي يُشار فيهم إليه.",
   "boost_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة",
+  "boost_modal.reblog": "أتريد إعادة نشر المنشور؟",
+  "boost_modal.undo_reblog": "أتريد إلغاء إعادة نشر المنشور؟",
   "bundle_column_error.copy_stacktrace": "انسخ تقرير الخطأ",
   "bundle_column_error.error.body": "لا يمكن تقديم الصفحة المطلوبة. قد يكون بسبب خطأ في التعليمات البرمجية، أو مشكلة توافق المتصفح.",
   "bundle_column_error.error.title": "أوه لا!",
@@ -230,7 +232,7 @@
   "domain_pill.who_they_are": "بما أن المعرفات تقول من هو الشخص ومكان وجوده، يمكنك التفاعل مع الناس عبر الويب الاجتماعي لل <button>منصات التي تعمل ب ActivityPub</button>.",
   "domain_pill.who_you_are": "بما أن معرفك يقول من أنت ومكان وجوده، يمكن للناس التفاعل معك عبر الويب الاجتماعي لل <button>منصات التي تعمل ب ActivityPub</button>.",
   "domain_pill.your_handle": "عنوانك الكامل:",
-  "domain_pill.your_server": "منزلك الرقمي، حيث تعيش جميع مشاركاتك. لا تحب هذا؟ إنقل الخوادم في أي وقت واخضر متابعينك أيضًا.",
+  "domain_pill.your_server": "موطِنك الرقمي، حيث توجد فيه كافة منشوراتك. ألا يعجبك المكان؟ يمكنك الانتقال بين الخوادم في أي وقت واصطحاب متابعيك أيضاً.",
   "domain_pill.your_username": "معرفك الفريد على هذا الخادم. من الممكن العثور على مستخدمين بنفس إسم المستخدم على خوادم مختلفة.",
   "embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
   "embed.preview": "إليك ما سيبدو عليه:",
@@ -290,7 +292,7 @@
   "filter_modal.added.review_and_configure": "لمراجعة وزيادة تكوين فئة عوامل التصفية هذه، انتقل إلى {settings_link}.",
   "filter_modal.added.review_and_configure_title": "إعدادات التصفية",
   "filter_modal.added.settings_link": "صفحة الإعدادات",
-  "filter_modal.added.short_explanation": "تمت إضافة هذه المشاركة إلى فئة الفلاتر التالية: {title}.",
+  "filter_modal.added.short_explanation": "تمت إضافة هذا المنشور إلى فئة عوامل التصفية التالية: {title}.",
   "filter_modal.added.title": "تمت إضافة عامل التصفية!",
   "filter_modal.select_filter.context_mismatch": "لا ينطبق على هذا السياق",
   "filter_modal.select_filter.expired": "منتهية الصلاحيّة",
@@ -348,6 +350,9 @@
   "hashtag.follow": "اتبع الوسم",
   "hashtag.unfollow": "ألغِ متابعة الوسم",
   "hashtags.and_other": "…و {count, plural, zero {} one {# واحد آخر} two {# اثنان آخران} few {# آخرون} many {# آخَرًا}other {# آخرون}}",
+  "hints.profiles.see_more_followers": "عرض المزيد من المتابعين على {domain}",
+  "hints.profiles.see_more_posts": "عرض المزيد من المنشورات من {domain}",
+  "hints.threads.see_more": "اطلع على المزيد من الردود على {domain}",
   "home.column_settings.show_reblogs": "اعرض المعاد نشرها",
   "home.column_settings.show_replies": "اعرض الردود",
   "home.hide_announcements": "إخفاء الإعلانات",
@@ -356,8 +361,10 @@
   "home.pending_critical_update.title": "تحديث أمان حرج متوفر!",
   "home.show_announcements": "إظهار الإعلانات",
   "ignore_notifications_modal.disclaimer": "لا يمكن لـ Mastodon إبلاغ المستخدمين بأنك قد تجاهلت إشعاراتهم. تجاهل الإشعارات لن يمنع إرسال الرسائل نفسها.",
+  "ignore_notifications_modal.filter_instead": "تصفيتها بدلا من ذلك",
   "ignore_notifications_modal.ignore": "تجاهل الإشعارات",
   "ignore_notifications_modal.limited_accounts_title": "تجاهل الإشعارات من الحسابات التي هي تحت الإشراف؟",
+  "ignore_notifications_modal.new_accounts_title": "تجاهل الإشعارات الصادرة من الحسابات الجديدة؟",
   "interaction_modal.description.favourite": "بفضل حساب على ماستدون، يمكنك إضافة هذا المنشور إلى مفضلتك لإبلاغ الناشر عن تقديرك وكذا للاحتفاظ بالمنشور إلى وقت لاحق.",
   "interaction_modal.description.follow": "بفضل حساب في ماستدون، يمكنك متابعة {name} وتلقي منشوراته في موجزات خيطك الرئيس.",
   "interaction_modal.description.reblog": "مع حساب في ماستدون، يمكنك تعزيز هذا المنشور ومشاركته مع مُتابِعيك.",
@@ -447,6 +454,7 @@
   "mute_modal.you_wont_see_mentions": "لن تر المنشورات التي يُشار فيها إليه.",
   "mute_modal.you_wont_see_posts": "سيكون بإمكانه رؤية منشوراتك، لكنك لن ترى منشوراته.",
   "navigation_bar.about": "عن",
+  "navigation_bar.administration": "الإدارة",
   "navigation_bar.advanced_interface": "افتحه في واجهة الويب المتقدمة",
   "navigation_bar.blocks": "الحسابات المحجوبة",
   "navigation_bar.bookmarks": "الفواصل المرجعية",
@@ -463,6 +471,7 @@
   "navigation_bar.follows_and_followers": "المتابِعون والمتابَعون",
   "navigation_bar.lists": "القوائم",
   "navigation_bar.logout": "خروج",
+  "navigation_bar.moderation": "الإشراف",
   "navigation_bar.mutes": "الحسابات المكتومة",
   "navigation_bar.opened_in_classic_interface": "تُفتَح المنشورات والحسابات وغيرها من الصفحات الخاصة بشكل مبدئي على واجهة الويب التقليدية.",
   "navigation_bar.personal": "شخصي",
@@ -484,7 +493,7 @@
   "notification.mention": "إشارة",
   "notification.moderation-warning.learn_more": "اعرف المزيد",
   "notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف",
-  "notification.moderation_warning.action_delete_statuses": "تم إزالة بعض مشاركاتك.",
+  "notification.moderation_warning.action_delete_statuses": "تم حذف بعض من منشوراتك.",
   "notification.moderation_warning.action_disable": "تم تعطيل حسابك.",
   "notification.moderation_warning.action_mark_statuses_as_sensitive": "بعض من منشوراتك تم تصنيفها على أنها حساسة.",
   "notification.moderation_warning.action_none": "لقد تلقى حسابك تحذيرا بالإشراف.",
@@ -502,12 +511,15 @@
   "notification.status": "{name} نشر للتو",
   "notification.update": "عدّلَ {name} منشورًا",
   "notification_requests.accept": "موافقة",
+  "notification_requests.confirm_accept_multiple.title": "قبول طلبات الإشعار؟",
+  "notification_requests.confirm_dismiss_multiple.title": "تجاهل طلبات الإشعار؟",
   "notification_requests.dismiss": "تخطي",
   "notification_requests.edit_selection": "تعديل",
   "notification_requests.exit_selection": "تمّ",
   "notification_requests.explainer_for_limited_account": "تم تصفية الإشعارات من هذا الحساب لأن الحساب تم تقييده من قبل مشرف.",
   "notification_requests.notifications_from": "إشعارات من {name}",
   "notification_requests.title": "الإشعارات المصفاة",
+  "notification_requests.view": "عرض الإشعارات",
   "notifications.clear": "مسح الإشعارات",
   "notifications.clear_confirmation": "متأكد من أنك تود مسح جميع الإشعارات الخاصة بك و المتلقاة إلى حد الآن ؟",
   "notifications.clear_title": "أترغب في مسح الإشعارات؟",
@@ -520,7 +532,7 @@
   "notifications.column_settings.filter_bar.advanced": "عرض جميع الفئات",
   "notifications.column_settings.filter_bar.category": "شريط التصفية السريعة",
   "notifications.column_settings.follow": "متابعُون جُدُد:",
-  "notifications.column_settings.follow_request": "الطلبات الجديد لِمتابَعتك:",
+  "notifications.column_settings.follow_request": "الطلبات الجديدة لِمتابَعتك:",
   "notifications.column_settings.mention": "الإشارات:",
   "notifications.column_settings.poll": "نتائج استطلاع الرأي:",
   "notifications.column_settings.push": "الإشعارات",
@@ -747,7 +759,7 @@
   "status.history.edited": "عدله {name} {date}",
   "status.load_more": "حمّل المزيد",
   "status.media.open": "اضغط للفتح",
-  "status.media.show": "اضغط للإظهار",
+  "status.media.show": "اضغط لإظهاره",
   "status.media_hidden": "وسائط مخفية",
   "status.mention": "أذكُر @{name}",
   "status.more": "المزيد",
diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json
index 983f737a7..a1d4061eb 100644
--- a/app/javascript/mastodon/locales/fr-CA.json
+++ b/app/javascript/mastodon/locales/fr-CA.json
@@ -356,6 +356,7 @@
   "hints.profiles.follows_may_be_missing": "Les abonnements pour ce profil peuvent être manquants.",
   "hints.profiles.posts_may_be_missing": "Certains messages de ce profil peuvent être manquants.",
   "hints.profiles.see_more_followers": "Afficher plus d'abonné·e·s sur {domain}",
+  "hints.profiles.see_more_follows": "Afficher plus d'abonné·e·s sur {domain}",
   "hints.profiles.see_more_posts": "Voir plus de messages sur {domain}",
   "hints.threads.replies_may_be_missing": "Les réponses provenant des autres serveurs pourraient être manquantes.",
   "hints.threads.see_more": "Afficher plus de réponses sur {domain}",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 2b22f4ba3..9f55634b2 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -356,6 +356,7 @@
   "hints.profiles.follows_may_be_missing": "Les abonnements pour ce profil peuvent être manquants.",
   "hints.profiles.posts_may_be_missing": "Certains messages de ce profil peuvent être manquants.",
   "hints.profiles.see_more_followers": "Afficher plus d'abonné·e·s sur {domain}",
+  "hints.profiles.see_more_follows": "Afficher plus d'abonné·e·s sur {domain}",
   "hints.profiles.see_more_posts": "Voir plus de messages sur {domain}",
   "hints.threads.replies_may_be_missing": "Les réponses provenant des autres serveurs pourraient être manquantes.",
   "hints.threads.see_more": "Afficher plus de réponses sur {domain}",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index c5d84b9ac..2d0013d60 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -43,7 +43,7 @@
   "account.in_memoriam": "Em Memória.",
   "account.joined_short": "Juntou-se a",
   "account.languages": "Alterar línguas subscritas",
-  "account.link_verified_on": "A posse desta ligação foi verificada em {date}",
+  "account.link_verified_on": "O proprietário desta hiperligação foi verificado em {date}",
   "account.locked_info": "Esta conta é privada. O proprietário revê manualmente quem o pode seguir.",
   "account.media": "Média",
   "account.mention": "Mencionar @{name}",
@@ -154,15 +154,15 @@
   "compose_form.lock_disclaimer": "A sua conta não é {locked}. Qualquer pessoa pode segui-lo e ver as publicações direcionadas apenas a seguidores.",
   "compose_form.lock_disclaimer.lock": "fechada",
   "compose_form.placeholder": "Em que está a pensar?",
-  "compose_form.poll.duration": "Duração do inquérito",
+  "compose_form.poll.duration": "Duração da sondagem",
   "compose_form.poll.multiple": "Escolha múltipla",
   "compose_form.poll.option_placeholder": "Opção {number}",
   "compose_form.poll.single": "Escolha uma",
-  "compose_form.poll.switch_to_multiple": "Alterar o inquérito para permitir várias respostas",
-  "compose_form.poll.switch_to_single": "Alterar o inquérito para permitir uma única resposta",
+  "compose_form.poll.switch_to_multiple": "Alterar a sondagem para permitir várias respostas",
+  "compose_form.poll.switch_to_single": "Alterar a sondagem para permitir uma única resposta",
   "compose_form.poll.type": "Estilo",
   "compose_form.publish": "Publicar",
-  "compose_form.publish_form": "Publicar",
+  "compose_form.publish_form": "Nova publicação",
   "compose_form.reply": "Responder",
   "compose_form.save_changes": "Atualizar",
   "compose_form.spoiler.marked": "Texto escondido atrás de aviso",
@@ -189,7 +189,7 @@
   "confirmations.redraft.message": "Tem a certeza de que quer eliminar e reescrever esta publicação? Os favoritos e partilhas perder-se-ão e as respostas à publicação original ficarão órfãs.",
   "confirmations.redraft.title": "Eliminar e reescrever publicação?",
   "confirmations.reply.confirm": "Responder",
-  "confirmations.reply.message": "Responder agora irá reescrever a mensagem que está a compor actualmente. Tem a certeza que quer continuar?",
+  "confirmations.reply.message": "Se responder agora, a mensagem que está a escrever será substituída. Tem a certeza que pretende continuar?",
   "confirmations.reply.title": "Sobrescrever publicação?",
   "confirmations.unfollow.confirm": "Deixar de seguir",
   "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?",
@@ -206,14 +206,14 @@
   "directory.federated": "Do fediverso conhecido",
   "directory.local": "Apenas de {domain}",
   "directory.new_arrivals": "Recém chegados",
-  "directory.recently_active": "Com actividade recente",
+  "directory.recently_active": "Recentemente ativo",
   "disabled_account_banner.account_settings": "Definições da conta",
   "disabled_account_banner.text": "A sua conta {disabledAccount} está presentemente desativada.",
   "dismissable_banner.community_timeline": "Estas são as publicações públicas mais recentes de pessoas cujas contas são hospedadas por {domain}.",
   "dismissable_banner.dismiss": "Descartar",
   "dismissable_banner.explore_links": "Essas histórias de notícias estão, no momento, a ser faladas por pessoas neste e noutros servidores da rede descentralizada.",
   "dismissable_banner.explore_statuses": "Estas são publicações de toda a rede social que estão a ganhar popularidade atualmente. As mensagens mais recentes com mais partilhas e favoritos obtêm uma classificação mais elevada.",
-  "dismissable_banner.explore_tags": "Estas #etiquetas estão presentemente a ganhar atenção entre as pessoas neste e noutros servidores da rede descentralizada.",
+  "dismissable_banner.explore_tags": "Estas são hashtags que estão a ganhar força na rede social atualmente. As hashtags que são utilizadas por mais pessoas diferentes têm uma classificação mais elevada.",
   "dismissable_banner.public_timeline": "Estas são as publicações públicas mais recentes de pessoas na rede social que as pessoas em {domain} seguem.",
   "domain_block_modal.block": "Bloquear servidor",
   "domain_block_modal.block_account_instead": "Bloquear @{name} em alternativa",
@@ -238,7 +238,7 @@
   "domain_pill.your_username": "O seu identificador único neste servidor. É possível encontrar utilizadores com o mesmo nome de utilizador em diferentes servidores.",
   "embed.instructions": "Incorpore esta publicação no seu site copiando o código abaixo.",
   "embed.preview": "Podes ver aqui como irá ficar:",
-  "emoji_button.activity": "Actividade",
+  "emoji_button.activity": "Atividade",
   "emoji_button.clear": "Limpar",
   "emoji_button.custom": "Personalizar",
   "emoji_button.flags": "Bandeiras",
@@ -246,7 +246,7 @@
   "emoji_button.label": "Inserir Emoji",
   "emoji_button.nature": "Natureza",
   "emoji_button.not_found": "Nenhum emoji correspondente encontrado",
-  "emoji_button.objects": "Objectos",
+  "emoji_button.objects": "Objetos",
   "emoji_button.people": "Pessoas",
   "emoji_button.recent": "Utilizados regularmente",
   "emoji_button.search": "Pesquisar...",
@@ -258,24 +258,24 @@
   "empty_column.account_timeline": "Sem publicações por aqui!",
   "empty_column.account_unavailable": "Perfil indisponível",
   "empty_column.blocks": "Ainda não bloqueaste qualquer utilizador.",
-  "empty_column.bookmarked_statuses": "Ainda não adicionou nenhuma publicação aos itens salvos. Quando adicionar, eles serão exibidos aqui.",
+  "empty_column.bookmarked_statuses": "Ainda não tem nenhuma publicação marcada. Quando marcar uma, ela aparecerá aqui.",
   "empty_column.community": "A cronologia local está vazia. Escreve algo público para começar!",
   "empty_column.direct": "Ainda não tem qualquer menção privada. Quando enviar ou receber uma, ela irá aparecer aqui.",
   "empty_column.domain_blocks": "Ainda não há qualquer domínio escondido.",
-  "empty_column.explore_statuses": "Nada está em alta no momento. Volte mais tarde!",
+  "empty_column.explore_statuses": "Nada é tendência neste momento. Volte mais tarde!",
   "empty_column.favourited_statuses": "Ainda não assinalou qualquer publicação como favorita. Quando o fizer, aparecerá aqui.",
   "empty_column.favourites": "Ainda ninguém assinalou esta publicação como favorita. Quando alguém o fizer, aparecerá aqui.",
   "empty_column.follow_requests": "Ainda não tens nenhum pedido de seguidor. Quando receberes algum, ele irá aparecer aqui.",
   "empty_column.followed_tags": "Ainda não segue nenhuma hashtag. Quando o fizer, ela aparecerá aqui.",
   "empty_column.hashtag": "Não foram encontradas publicações com essa #etiqueta.",
-  "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
+  "empty_column.home": "A sua linha cronológica inicial está vazia! Siga mais pessoas para a preencher.",
   "empty_column.list": "Ainda não existem publicações nesta lista. Quando membros desta lista fizerem novas publicações, elas aparecerão aqui.",
   "empty_column.lists": "Ainda não tem qualquer lista. Quando criar uma, ela irá aparecer aqui.",
   "empty_column.mutes": "Ainda não silenciaste qualquer utilizador.",
   "empty_column.notification_requests": "Tudo limpo! Não há nada aqui. Quando você receber novas notificações, elas aparecerão aqui conforme as suas configurações.",
   "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
   "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para veres aqui os conteúdos públicos",
-  "error.unexpected_crash.explanation": "Devido a um erro no nosso código ou a uma compatilidade com o seu navegador, esta página não pôde ser apresentada correctamente.",
+  "error.unexpected_crash.explanation": "Devido a um erro no nosso código ou a um problema de compatibilidade do navegador, esta página não pôde ser apresentada corretamente.",
   "error.unexpected_crash.explanation_addons": "Esta página não pôde ser exibida corretamente. Este erro provavelmente é causado por um complemento do navegador ou ferramentas de tradução automática.",
   "error.unexpected_crash.next_steps": "Tente atualizar a página. Se isso não ajudar, pode usar o Mastodon através de um navegador diferente ou uma aplicação nativa.",
   "error.unexpected_crash.next_steps_addons": "Tente desabilitá-los e atualizar a página. Se isso não ajudar, você ainda poderá usar o Mastodon por meio de um navegador diferente ou de um aplicativo nativo.",
@@ -355,10 +355,10 @@
   "hashtags.and_other": "…e {count, plural, other {mais #}}",
   "hints.profiles.followers_may_be_missing": "Podem faltar seguidores neste perfil.",
   "hints.profiles.follows_may_be_missing": "O número de perfis seguidos por este perfil pode faltar.",
-  "hints.profiles.posts_may_be_missing": "Podem faltar alguns posts deste perfil.",
+  "hints.profiles.posts_may_be_missing": "Podem faltar algumas publicações deste perfil.",
   "hints.profiles.see_more_followers": "Ver mais seguidores no {domain}",
   "hints.profiles.see_more_follows": "Veja mais perfis seguidos em {domain}",
-  "hints.profiles.see_more_posts": "Ver mais posts em {domain}",
+  "hints.profiles.see_more_posts": "Ver mais publicações em {domain}",
   "home.column_settings.show_reblogs": "Mostrar impulsos",
   "home.column_settings.show_replies": "Mostrar respostas",
   "home.hide_announcements": "Ocultar comunicações",
@@ -406,7 +406,7 @@
   "keyboard_shortcuts.my_profile": "para abrir o teu perfil",
   "keyboard_shortcuts.notifications": "para abrir a coluna das notificações",
   "keyboard_shortcuts.open_media": "para abrir media",
-  "keyboard_shortcuts.pinned": "para abrir a lista dos toots fixados",
+  "keyboard_shortcuts.pinned": "Abrir lista de publicações fixadas",
   "keyboard_shortcuts.profile": "para abrir o perfil do autor",
   "keyboard_shortcuts.reply": "para responder",
   "keyboard_shortcuts.requests": "para abrir a lista dos pedidos de seguidor",
@@ -475,7 +475,7 @@
   "navigation_bar.mutes": "Utilizadores silenciados",
   "navigation_bar.opened_in_classic_interface": "Por norma, publicações, contas, e outras páginas específicas são abertas na interface web clássica.",
   "navigation_bar.personal": "Pessoal",
-  "navigation_bar.pins": "Toots afixados",
+  "navigation_bar.pins": "Publicações fixadas",
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.public_timeline": "Cronologia federada",
   "navigation_bar.search": "Pesquisar",
@@ -504,8 +504,8 @@
   "notification.moderation_warning.action_sensitive": "As suas publicações serão, a partir de agora, assinaladas como sensíveis.",
   "notification.moderation_warning.action_silence": "A sua conta foi limitada.",
   "notification.moderation_warning.action_suspend": "A sua conta foi suspensa.",
-  "notification.own_poll": "A sua votação terminou",
-  "notification.poll": "Uma votação em que participaste chegou ao fim",
+  "notification.own_poll": "A sua sondagem terminou",
+  "notification.poll": "Terminou uma sondagem em que votou",
   "notification.reblog": "{name} reforçou a tua publicação",
   "notification.relationships_severance_event": "Perdeu as ligações com {name}",
   "notification.relationships_severance_event.account_suspension": "Um administrador de {from} suspendeu {target}, o que significa que já não pode receber atualizações dele ou interagir com ele.",
@@ -536,7 +536,7 @@
   "notifications.column_settings.follow": "Novos seguidores:",
   "notifications.column_settings.follow_request": "Novos pedidos de seguidor:",
   "notifications.column_settings.mention": "Menções:",
-  "notifications.column_settings.poll": "Resultados do inquérito:",
+  "notifications.column_settings.poll": "Resultados da sondagem:",
   "notifications.column_settings.push": "Notificações Push",
   "notifications.column_settings.reblog": "Reforços:",
   "notifications.column_settings.show": "Mostrar na coluna",
@@ -550,7 +550,7 @@
   "notifications.filter.favourites": "Favoritos",
   "notifications.filter.follows": "Seguidores",
   "notifications.filter.mentions": "Menções",
-  "notifications.filter.polls": "Resultados do inquérito",
+  "notifications.filter.polls": "Resultados da sondagem",
   "notifications.filter.statuses": "Atualizações de pessoas que você segue",
   "notifications.grant_permission": "Conceder permissão.",
   "notifications.group": "{count} notificações",
@@ -573,7 +573,7 @@
   "notifications_permission_banner.title": "Nunca perca nada",
   "onboarding.action.back": "Voltar atrás",
   "onboarding.actions.back": "Voltar atrás",
-  "onboarding.actions.go_to_explore": "Veja as tendências atuais",
+  "onboarding.actions.go_to_explore": "Ver tendências atuais",
   "onboarding.actions.go_to_home": "Ir para a sua página inicial",
   "onboarding.compose.template": "Olá #Mastodon!",
   "onboarding.follows.empty": "Infelizmente, não é possível mostrar resultados neste momento. Pode tentar utilizar a pesquisa ou navegar na página \"Explorar\" para encontrar pessoas para seguir ou tentar novamente mais tarde.",
@@ -599,7 +599,7 @@
   "onboarding.start.title": "Conseguiu!",
   "onboarding.steps.follow_people.body": "Seguir pessoas interessantes é o propósito do Mastodon. ",
   "onboarding.steps.follow_people.title": "Personalize o seu feed",
-  "onboarding.steps.publish_status.body": "Diga olá ao mundo com texto, fotos, vídeos ou votos {emoji}",
+  "onboarding.steps.publish_status.body": "Diga olá ao mundo com texto, fotos, vídeos ou sondagens {emoji}",
   "onboarding.steps.publish_status.title": "Faça a sua primeira publicação",
   "onboarding.steps.setup_profile.body": "Promova as suas interações para ter um perfil preenchido. ",
   "onboarding.steps.setup_profile.title": "Personalize o seu perfil",
@@ -608,19 +608,19 @@
   "onboarding.tips.2fa": "<strong>Sabia?</strong> Pode proteger a sua conta ativando a autenticação em duas etapas nas configurações de conta. Funciona com qualquer aplicativo TOTP à sua escolha, sem necessitar de um número de telefone!",
   "onboarding.tips.accounts_from_other_servers": "<strong>Sabia?</strong> Como o Mastodon é descentralizado, alguns perfis que encontra estarão hospedados noutros servidores que não os seus. E ainda assim pode interagir com eles perfeitamente! O servidor deles está na segunda metade do nome de utilizador!",
   "onboarding.tips.migration": "<strong>Sabia?</strong> Se sentir que o {domain} não é um bom servidor para si, no futuro pode mudar para outro servidor Mastodon sem perder os seus seguidores. Pode até mesmo hospedar o seu próprio servidor!",
-  "onboarding.tips.verification": "<strong>Sabia que?</strong> Pode fazer a verificação do seu site, adicionando o link do seu perfil à primeira página do seu site, como também pode adicionar o seu site ao seu perfil? Sem taxas ou documentos!",
+  "onboarding.tips.verification": "<strong>Sabia que?</strong> Pode verificar a sua conta colocando uma hiperligação para o seu perfil Mastodon no seu próprio site e adicionando o site ao seu perfil. Sem taxas ou documentos!",
   "password_confirmation.exceeds_maxlength": "A confirmação da palavra-passe excedeu o tamanho máximo ",
   "password_confirmation.mismatching": "A confirmação da palavra-passe não corresponde",
   "picture_in_picture.restore": "Colocá-lo de volta",
   "poll.closed": "Fechado",
-  "poll.refresh": "Recarregar",
+  "poll.refresh": "Atualizar",
   "poll.reveal": "Ver resultados",
   "poll.total_people": "{count, plural, one {# pessoa} other {# pessoas}}",
   "poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
   "poll.vote": "Votar",
-  "poll.voted": "Votaste nesta resposta",
+  "poll.voted": "Votou nesta resposta",
   "poll.votes": "{votes, plural, one {# voto } other {# votos}}",
-  "poll_button.add_poll": "Adicionar votação",
+  "poll_button.add_poll": "Adicionar uma sondagem",
   "poll_button.remove_poll": "Remover sondagem",
   "privacy.change": "Ajustar a privacidade da publicação",
   "privacy.direct.long": "Todos os mencionados na publicação",
@@ -635,7 +635,7 @@
   "privacy_policy.last_updated": "Última atualização em {date}",
   "privacy_policy.title": "Política de privacidade",
   "recommended": "Recomendado",
-  "refresh": "Actualizar",
+  "refresh": "Atualizar",
   "regeneration_indicator.label": "A carregar…",
   "regeneration_indicator.sublabel": "A tua página inicial está a ser preparada!",
   "relative_time.days": "{number}d",
@@ -677,7 +677,7 @@
   "report.reasons.other": "É outra coisa",
   "report.reasons.other_description": "O problema não se encaixa nas outras categorias",
   "report.reasons.spam": "É spam",
-  "report.reasons.spam_description": "Hiperligações maliciosas, contactos falsos, ou respostas repetitivas",
+  "report.reasons.spam_description": "Hiperligações maliciosas, contactos falsos ou respostas repetitivas",
   "report.reasons.violation": "Viola as regras do servidor",
   "report.reasons.violation_description": "Está ciente de que infringe regras específicas",
   "report.rules.subtitle": "Selecione tudo o que se aplicar",
@@ -733,8 +733,8 @@
   "sign_in_banner.create_account": "Criar conta",
   "sign_in_banner.follow_anyone": "Siga alguém no fediverso e veja tudo em ordem cronológica. Sem algoritmos, anúncios ou clickbait à vista.",
   "sign_in_banner.mastodon_is": "O Mastodon é a melhor maneira de acompanhar o que está a acontecer.",
-  "sign_in_banner.sign_in": "Iniciar Sessão",
-  "sign_in_banner.sso_redirect": "Inicie Sessão ou Registe-se",
+  "sign_in_banner.sign_in": "Iniciar sessão",
+  "sign_in_banner.sso_redirect": "Inicie sessão ou registe-se",
   "status.admin_account": "Abrir a interface de moderação para @{name}",
   "status.admin_domain": "Abrir interface de moderação para {domain}",
   "status.admin_status": "Abrir esta publicação na interface de moderação",
@@ -742,7 +742,7 @@
   "status.bookmark": "Guardar nos marcadores",
   "status.cancel_reblog_private": "Deixar de reforçar",
   "status.cannot_reblog": "Não é possível partilhar esta publicação",
-  "status.copy": "Copiar ligação para a publicação",
+  "status.copy": "Copiar hiperligação para a publicação",
   "status.delete": "Eliminar",
   "status.detailed_status": "Vista pormenorizada da conversa",
   "status.direct": "Mencionar @{name} em privado",
@@ -801,15 +801,15 @@
   "time_remaining.moments": "Momentos restantes",
   "time_remaining.seconds": "{número, plural, um {# second} outro {# seconds}} faltam",
   "trends.counter_by_accounts": "{count, plural, one {{counter} pessoa} other {{counter} pessoas}} {days, plural, one {no último dia} other {nos últimos {days} dias}}",
-  "trends.trending_now": "Em alta neste momento",
+  "trends.trending_now": "Tendências atuais",
   "ui.beforeunload": "O teu rascunho será perdido se abandonares o Mastodon.",
   "units.short.billion": "{count}MM",
   "units.short.million": "{count}M",
   "units.short.thousand": "{count}m",
-  "upload_area.title": "Arraste e solte para enviar",
-  "upload_button.label": "Juntar imagens, um vídeo, ou um ficheiro de som",
-  "upload_error.limit": "Limite máximo do ficheiro a carregar excedido.",
-  "upload_error.poll": "O carregamento de ficheiros não é permitido em sondagens.",
+  "upload_area.title": "Arrastar e largar para enviar",
+  "upload_button.label": "Adicionar imagens, um vídeo ou um ficheiro de som",
+  "upload_error.limit": "Limite de envio de ficheiros excedido.",
+  "upload_error.poll": "Não é permitido o envio de ficheiros em sondagens.",
   "upload_form.audio_description": "Descreva para pessoas com diminuição da acuidade auditiva",
   "upload_form.description": "Descreva para pessoas com diminuição da acuidade visual",
   "upload_form.edit": "Editar",
@@ -820,7 +820,7 @@
   "upload_modal.applying": "A aplicar…",
   "upload_modal.choose_image": "Escolher imagem",
   "upload_modal.description_placeholder": "Grave e cabisbaixo, o filho justo zelava pela querida mãe doente",
-  "upload_modal.detect_text": "Detectar texto na imagem",
+  "upload_modal.detect_text": "Detetar texto na imagem",
   "upload_modal.edit_media": "Editar media",
   "upload_modal.hint": "Clique ou arraste o círculo na pré-visualização para escolher o ponto focal que será sempre visível em todas as miniaturas.",
   "upload_modal.preparing_ocr": "A preparar o reconhecimento de caracteres (OCR)…",
diff --git a/config/locales/activerecord.pt-PT.yml b/config/locales/activerecord.pt-PT.yml
index ba738741f..60f3def5d 100644
--- a/config/locales/activerecord.pt-PT.yml
+++ b/config/locales/activerecord.pt-PT.yml
@@ -6,7 +6,7 @@ pt-PT:
         expires_at: Prazo
         options: Escolhas
       user:
-        agreement: Acordo de serviço
+        agreement: Contrato de prestação de serviço
         email: Endereço de correio electrónico
         locale: Região
         password: Palavra-passe
@@ -19,7 +19,7 @@ pt-PT:
         account:
           attributes:
             username:
-              invalid: apenas letras, números e underscores
+              invalid: deve conter apenas letras, números e traços inferiores
               reserved: está reservado
         admin/webhook:
           attributes:
@@ -43,15 +43,15 @@ pt-PT:
               blocked: usa um fornecedor de e-mail que não é permitido
               unreachable: não parece existir
             role_id:
-              elevated: não pode ser maior que o da sua função atual
+              elevated: não pode ser superior à sua função atual
         user_role:
           attributes:
             permissions_as_keys:
               dangerous: incluir permissões que não são seguras para a função base
-              elevated: não pode incluir permissões que a sua função atual não possui
+              elevated: não pode incluir permissões que a sua função atual não possua
               own_role: não pode ser alterado com a sua função atual
             position:
-              elevated: não pode ser maior que o da sua função atual
+              elevated: não pode ser superior à sua função atual
               own_role: não pode ser alterado com a sua função atual
         webhook:
           attributes:
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 027d80215..7ab1b4f07 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -906,6 +906,7 @@ ar:
       moderation:
         title: الحالة
       newest: الأحدث
+      reset: إعادة التعيين
       review: حالة المراجعة
       search: البحث
       title: الوسوم
@@ -985,6 +986,7 @@ ar:
           other: مستخدَم من قِبل %{count} شخص خلال الأسبوع الماضي
           two: مستخدَم من قِبل %{count} شخصين خلال الأسبوع الماضي
           zero: مستخدَم من قِبل %{count} شخص خلال الأسبوع الماضي
+      title: التوصيات والرائجة
       trending: المتداولة
     warning_presets:
       add_new: إضافة واحد جديد
@@ -1141,8 +1143,10 @@ ar:
     security: الأمان
     set_new_password: إدخال كلمة مرور جديدة
     setup:
+      email_below_hint_html: قم بفحص مجلد البريد المزعج الخاص بك، أو قم بطلب آخر. يمكنك تصحيح عنوان بريدك الإلكتروني إن كان خاطئا.
       email_settings_hint_html: انقر على الرابط الذي أرسلناه لك للتحقق من %{email}. سننتظر هنا.
       link_not_received: ألم تحصل على رابط؟
+      new_confirmation_instructions_sent: سوف تتلقى رسالة بريد إلكتروني جديدة مع رابط التأكيد في غضون بضع دقائق!
       title: تحقَّق من بريدك الوارِد
     sign_in:
       preamble_html: قم بتسجيل الدخول باستخدام بيانات الاعتماد الخاصة بك على <strong>%{domain}</strong>. إن استُضيف حسابك على خادم مختلف عن هذا الخادم، لن تتمكن من الولوج هنا.
@@ -1153,7 +1157,9 @@ ar:
       title: دعنا نجهّز %{domain}.
     status:
       account_status: حالة الحساب
+      confirming: في انتظار اكتمال تأكيد البريد الإلكتروني.
       functional: حسابك يعمل بشكل كامل.
+      pending: إن طلبك قيد المراجعة من قبل فريقنا. قد يستغرق هذا بعض الوقت. سوف تتلقى بريدا إلكترونيا إذا تمت الموافقة على طلبك.
       redirecting_to: حسابك غير نشط لأنه تم تحويله حاليا إلى %{acct}.
       self_destruct: نظرًا لإغلاق %{domain}، ستحصل فقط على وصول محدود إلى حسابك.
       view_strikes: عرض العقوبات السابقة المُطَبَّقة ضد حسابك
@@ -1196,6 +1202,9 @@ ar:
       before: 'يرجى قراءة هذه الملاحظات بتأنّي قبل المواصلة:'
       caches: قد يبقى المحتوى الذي تم تخزينه مؤقتًا مِن طرف الخوادم الأخرى
       data_removal: سوف تُحذَف منشوراتك والبيانات الأخرى نهائيا
+      email_change_html: بإمكانك <a href="%{path}">تغيير عنوان بريدك الإلكتروني</a> دون أن يُحذف حسابك
+      email_contact_html: إن لم تتلقّ أي شيء ، يمكنك مراسلة <a href="mailto:%{email}">%{email}</a> لطلب المساعدة
+      email_reconfirmation_html: إن لم تتلقّ الرسالة الإلكترونية للتأكيد ، بإمكانك <a href="%{path}">إعادة طلبها ثانيةً</a>
       irreversible: لن يكون بإمكانك استرجاع أو إعادة تنشيط حسابك
       more_details_html: للمزيد مِن التفاصيل ، يرجى الإطلاع على <a href="%{terms_path}">سياسة الخصوصية</a>.
       username_available: سيصبح اسم مستخدمك متوفرا ثانية
@@ -1448,7 +1457,7 @@ ar:
       two: "%{count} استخدامات"
       zero: "%{count} استخدامات"
     max_uses_prompt: بلا حدود
-    prompt: توليد و مشاركة روابط للسماح للآخَرين بالنفاذ إلى مثيل الخادم هذا
+    prompt: توليد و مشاركة روابط للسماح للآخَرين النفاذ إلى هذا الخادم
     table:
       expires_at: تنتهي مدة صلاحيتها في
       uses: عدد الاستخدامات
@@ -1460,6 +1469,7 @@ ar:
     authentication_methods:
       otp: تطبيق المصادقة الثنائية
       password: كلمة المرور
+      sign_in_token: رمز الأمان للبريد الإلكتروني
       webauthn: مفاتيح الأمان
     description_html: إذا رأيت النشاط الذي لا تتعرف عليه، فكر في تغيير كلمة المرور الخاصة بك وتفعيل المصادقة ذات العاملين.
     empty: لا يوجد سجل مصادقة متاح
@@ -1553,6 +1563,7 @@ ar:
     update:
       subject: قام %{name} بتحرير منشور
   notifications:
+    administration_emails: إشعارات البريد الإلكتروني الإدارية
     email_events_hint: 'اختر الأحداث التي تريد أن تصِلَك اشعارات عنها:'
   number:
     human:
@@ -1811,13 +1822,13 @@ ar:
     keep_media: الاحتفاظ بالمنشورات ذات وسائط مرفقة
     keep_media_hint: لن تُحذف أي من منشوراتك التي تحتوي على وسائط مرفقة
     keep_pinned: الاحتفاظ بالمنشورات المثبتة
-    keep_pinned_hint: لم تقوم بحذف أي من مشاركتك المثبتة
+    keep_pinned_hint: لن تحذف أي من منشوراتك المثبتة
     keep_polls: الاحتفاظ باستطلاعات الرأي
     keep_polls_hint: لم تقم بحذف أي من استطلاعاتك
     keep_self_bookmark: احتفظ بالمنشورات التي أدرجتها في الفواصل المرجعية
-    keep_self_bookmark_hint: لم تقم بحذف مشاركاتك الخاصة إذا قمت بوضع علامة مرجعية عليها
+    keep_self_bookmark_hint: لن تحذف منشوراتك الخاصة إذا قمت بوضع علامة مرجعية عليها
     keep_self_fav: احتفظ بالمنشورات التي أدرجتها في المفضلة
-    keep_self_fav_hint: لم تقم بحذف مشاركاتك الخاصة إذا كنت قد فضلتهم
+    keep_self_fav_hint: لن تحذف منشوراتك الخاصة إذا كنت قد فضلتها
     min_age:
       '1209600': أسبوعان
       '15778476': 6 أشهر
@@ -1828,9 +1839,9 @@ ar:
       '63113904': سنتان
       '7889238': 3 أشهر
     min_age_label: عتبة العمر
-    min_favs: إبقاء المشاركات المفضلة أكثر من
+    min_favs: إبقاء المنشورات المفضلة على الأقل
     min_favs_hint: لن تُحذف أي من منشوراتك التي تلقّت على الأقل هذا العدد من المفضلات. اتركه فارغاً لحذف المنشورات مهما كان عدد المفضلات التي تلقتها
-    min_reblogs: إبقاء المنشورات المعاد نشرها أكثر من
+    min_reblogs: إبقاء المنشورات المعاد نشرها على الأقل
     min_reblogs_hint: لن تُحذف أي من منشوراتك التي أعيد مشاركتها أكثر من هذا العدد من المرات. اتركه فارغاً لحذف المنشورات بغض النظر عن عدد إعادات النشر
   stream_entries:
     sensitive_content: محتوى حساس
diff --git a/config/locales/devise.pt-PT.yml b/config/locales/devise.pt-PT.yml
index c66181fc5..edfb79850 100644
--- a/config/locales/devise.pt-PT.yml
+++ b/config/locales/devise.pt-PT.yml
@@ -2,117 +2,117 @@
 pt-PT:
   devise:
     confirmations:
-      confirmed: O seu endereço correio electrónico foi correctamente confirmado.
-      send_instructions: Vais receber um e-mail com as instruções para confirmar o teu endereço de e-mail dentro de alguns minutos. Por favor, verifica a caixa de spam se não recebeu o e-mail.
-      send_paranoid_instructions: Se o teu endereço de e-mail já existir na nossa base de dados, vais receber um e-mail com as instruções de confirmação dentro de alguns minutos. Por favor, verifica a caixa de spam se não recebeu o e-mail.
+      confirmed: O seu endereço de e-mail foi corretamente confirmado.
+      send_instructions: Irá receber um e-mail com instruções sobre como confirmar o seu endereço de e-mail dentro de alguns minutos. Verifique a sua pasta de spam se não recebeu este e-mail.
+      send_paranoid_instructions: Se o seu endereço de e-mail existir na nossa base de dados, receberá um e-mail com instruções sobre como confirmar o seu endereço de e-mail dentro de alguns minutos. Verifique a sua pasta de spam se não recebeu este e-mail.
     failure:
-      already_authenticated: A tua sessão já está aberta.
-      inactive: A tua conta ainda não está ativada.
+      already_authenticated: Já tem sessão iniciada.
+      inactive: A sua conta ainda não está ativada.
       invalid: "%{authentication_keys} ou palavra-passe inválida."
       last_attempt: Tem só mais uma tentativa antes da sua conta ser bloqueada.
-      locked: A tua conta está bloqueada.
+      locked: A sua conta está bloqueada.
       not_found_in_database: "%{authentication_keys} ou palavra-passe inválida."
       omniauth_user_creation_failure: Erro ao criar uma conta para esta identidade.
       pending: A sua conta está ainda a aguardar revisão.
-      timeout: A tua sessão expirou. Por favor, entra de novo para continuares.
-      unauthenticated: Precisas de entrar na tua conta ou de te registares antes de continuar.
-      unconfirmed: Tens de confirmar o teu endereço de e-mail antes de continuar.
+      timeout: A sua sessão expirou. Inicie sessão novamente para continuar.
+      unauthenticated: É necessário iniciar sessão ou registar-se antes de continuar.
+      unconfirmed: Tem de confirmar o seu endereço de e-mail antes de continuar.
     mailer:
       confirmation_instructions:
         action: Verificar o endereço de e-mail
         action_with_app: Confirmar e regressar a %{app}
-        explanation: Criou uma conta em %{host} com este endereço de e-mail. Está a um clique de ativá-la. Se não foi você que fez este registo, por favor ignore esta mensagem.
+        explanation: Foi criada uma conta em %{host} com este endereço de e-mail. Está a um clique de ativá-la. Se não foi você que fez este registo, por favor ignore esta mensagem.
         explanation_when_pending: Candidatou-se com um convite para %{host} com este endereço de e-mail. Logo que confirme o seu endereço de e-mail, iremos rever a sua candidatura. Pode iniciar sessão para alterar os seus dados ou eliminar a sua conta, mas não poderá aceder à maioria das funções até que a sua conta seja aprovada. Se a sua inscrição for indeferida, os seus dados serão eliminados, pelo que não será necessária qualquer ação adicional da sua parte. Se não solicitou este convite, queira ignorar este e-mail.
-        extra_html: Por favor leia as <a href="%{terms_path}">regras da instância</a> e os <a href="%{policy_path}">nossos termos de serviço</a>.
-        subject: 'Mastodon: Instruções de confirmação %{instance}'
+        extra_html: Por favor leia as <a href="%{terms_path}">regras do servidor</a> e os <a href="%{policy_path}">nossos termos de serviço</a>.
+        subject: 'Mastodon: instruções de confirmação para %{instance}'
         title: Verificar o endereço de e-mail
       email_changed:
         explanation: 'O e-mail associado à sua conta será alterado para:'
-        extra: Se não alterou o seu e-mail, é possível que alguém tenha conseguido aceder à sua conta. Por favor altere a sua palavra-passe imediatamente ou entra em contacto com um administrador da instância se tiver ficado sem acesso à sua conta.
-        subject: 'Mastodon: E-mail alterado'
+        extra: Se não alterou o seu e-mail, é possível que alguém tenha conseguido aceder à sua conta. Por favor altere a sua palavra-passe imediatamente ou entre em contacto com um administrador do servidor se tiver ficado sem acesso à sua conta.
+        subject: 'Mastodon: e-mail alterado'
         title: Novo endereço de e-mail
       password_change:
-        explanation: A palavra-passe da tua conta foi alterada.
-        extra: Se não alterou a sua palavra-passe, é possível que alguém tenha conseguido aceder à sua conta. Por favor altere a sua palavra-passe imediatamente ou entre em contacto com um administrador da instância se tiver ficado sem acesso à sua conta.
+        explanation: A palavra-passe da sua conta foi alterada.
+        extra: Se não alterou a sua palavra-passe, é possível que alguém tenha conseguido aceder à sua conta. Por favor altere a sua palavra-passe imediatamente ou entre em contacto com um administrador do servidor se tiver ficado sem acesso à sua conta.
         subject: 'Mastodon: palavra-passe alterada'
         title: Palavra-passe alterada
       reconfirmation_instructions:
         explanation: Confirme o seu novo endereço para alterar o e-mail.
-        extra: Se esta mudança não foi iniciada por si, queira ignorar este e-mail. O endereço de correio electrónico da sua conta do Mastodon não irá mudar enquanto não aceder à hiperligação acima.
-        subject: 'Mastodon: Confirmação de e-mail %{instance}'
+        extra: Se esta alteração não foi iniciada por si, ignore este e-mail. O endereço de e-mail da conta Mastodon não será alterado até aceder à hiperligação acima.
+        subject: 'Mastodon: confirmação de e-mail para %{instance}'
         title: Validar o endereço de e-mail
       reset_password_instructions:
         action: Alterar palavra-passe
-        explanation: Pediste a alteração da palavra-passe da tua conta.
-        extra: Se não fez este pedido, queira ignorar este e-mail. A sua palavra-passe não irá mudar se não aceder à hiperligação acima e criar uma nova.
-        subject: 'Mastodon: Instruções para redefinir a palavra-passe'
+        explanation: Solicitou uma nova palavra-passe para a sua conta.
+        extra: Se não solicitou esta alteração, ignore este e-mail. A sua palavra-passe não será alterada até aceder à hiperligação acima e criar uma nova.
+        subject: 'Mastodon: instruções para redefinir a palavra-passe'
         title: Solicitar nova palavra-passe
       two_factor_disabled:
-        explanation: O acesso agora é possível usando apenas o endereço de correio eletrónico e palavra-passe.
-        subject: 'Mastodon: Autenticação de duas etapas desativada'
+        explanation: O início de sessão é agora possível utilizando apenas o endereço de e-mail e a palavra-passe.
+        subject: 'Mastodon: autenticação de duas etapas desativada'
         subtitle: A autenticação de dois fatores foi desativada para a sua conta.
         title: 2FA desativado
       two_factor_enabled:
-        explanation: Um token gerado pelo aplicativo TOTP emparelhado será necessário para login.
-        subject: 'Mastodon: Autenticação em duas etapas ativada'
-        subtitle: A autenticação de dois fatores foi habilitada para sua conta.
+        explanation: Para iniciar sessão, será necessário um token gerado pela aplicação TOTP emparelhada.
+        subject: 'Mastodon: autenticação em duas etapas ativada'
+        subtitle: A autenticação de dois fatores foi ativada para a sua conta.
         title: 2FA ativado
       two_factor_recovery_codes_changed:
-        explanation: Os códigos de recuperação anteriores foram invalidados e novos foram gerados.
-        subject: 'Mastodon: Gerados novos códigos de recuperação em duas etapas'
-        subtitle: Os códigos de recuperação anteriores foram invalidados e novos foram gerados.
+        explanation: Os códigos de recuperação anteriores foram invalidados e foram gerados novos códigos.
+        subject: 'Mastodon: gerados novos códigos de recuperação em duas etapas'
+        subtitle: Os códigos de recuperação anteriores foram invalidados e foram gerados novos códigos.
         title: Códigos de recuperação 2FA alterados
       unlock_instructions:
-        subject: 'Mastodon: Instruções para desbloquear a tua conta'
+        subject: 'Mastodon: instruções para desbloquear'
       webauthn_credential:
         added:
           explanation: A seguinte chave de segurança foi adicionada à sua conta
-          subject: 'Mastodon: Nova chave de segurança'
+          subject: 'Mastodon: nova chave de segurança'
           title: Foi adicionada uma nova chave de segurança
         deleted:
           explanation: A seguinte chave de segurança foi eliminada da sua conta
-          subject: 'Mastodon: Chave de segurança eliminada'
+          subject: 'Mastodon: chave de segurança eliminada'
           title: Uma das suas chaves de segurança foi eliminada
       webauthn_disabled:
         explanation: A autenticação com chaves de segurança foi desativada para sua conta.
-        extra: O login agora é possível usando apenas o token gerado pelo aplicativo TOTP emparelhado.
-        subject: 'Mastodon: Autenticação com chave de segurança desativada'
+        extra: O início de sessão é agora possível utilizando apenas o token gerado pela aplicação TOTP emparelhada.
+        subject: 'Mastodon: autenticação com chave de segurança desativada'
         title: Chaves de segurança desativadas
       webauthn_enabled:
-        explanation: A autenticação da chave de segurança foi habilitada para sua conta.
-        extra: Sua chave de segurança agora pode ser usada para login.
-        subject: 'Mastodon: Autenticação com chave de segurança ativada'
+        explanation: A autenticação por chave de segurança foi ativada para a sua conta.
+        extra: A sua chave de segurança pode agora ser utilizada para iniciar sessão.
+        subject: 'Mastodon: autenticação com chave de segurança ativada'
         title: Chaves de segurança ativadas
     omniauth_callbacks:
       failure: Não foi possível autenticar %{kind} porque "%{reason}".
       success: Autenticado correctamente na conta %{kind}.
     passwords:
-      no_token: Não pode aceder a esta página se não vier através da ligação enviada por e-mail para alteração da sua palavra-passe. Se de facto usou essa ligação para chegar até aqui, queira garantir de que usou o endereço URL completo.
-      send_instructions: Vai receber um e-mail com instruções para alterar a palavra-passe dentro de alguns minutos.
-      send_paranoid_instructions: Se o seu endereço de e-mail existir na nossa base de dados, dentro de alguns minutos irá receber uma ligação para recuperar a palavra-passe.
-      updated: A tua palavra-passe foi alterada. Estás agora autenticado na tua conta.
+      no_token: Não pode aceder a esta página se não vier através da hiperligação enviada por e-mail para alteração da sua palavra-passe. Se de facto usou essa hiperligação para chegar até aqui, verifique se usou o endereço URL completo.
+      send_instructions: Se o seu endereço de e-mail existir na nossa base de dados, receberá uma hiperligação de recuperação da palavra-passe no seu endereço de e-mail dentro de alguns minutos. Verifique a sua pasta de spam se não recebeu esta mensagem de correio eletrónico.
+      send_paranoid_instructions: Se o seu endereço de e-mail existir na nossa base de dados, receberá uma hiperligação de recuperação da palavra-passe no seu endereço de e-mail dentro de alguns minutos. Verifique a sua pasta de spam se não recebeu esta mensagem de correio eletrónico.
+      updated: A sua palavra-passe foi alterada com sucesso. Está agora autenticado.
       updated_not_active: A tua palavra-passe foi alterada.
     registrations:
-      destroyed: Adeus! A tua conta foi cancelada. Esperamos ver-te em breve.
-      signed_up: Seja bem-vindo! A sua conta foi correctamente registada.
-      signed_up_but_inactive: A tua conta foi registada. No entanto ainda não está activa.
-      signed_up_but_locked: A sua conta foi correctamente registada. Contudo, não pudemos iniciar sessão porque a sua conta está bloqueada.
-      signed_up_but_pending: Foi enviada uma hiperligação de confirmação para o seu correio electrónico. Só depois de clicar na hiperligação avaliaremos a sua inscrição. Será notificado caso a sua conta seja aprovada.
-      signed_up_but_unconfirmed: Foi enviada uma hiperligação de confirmação para o seu correio electrónico. Queira usar essa hiperligação para activar a sua conta.
-      update_needs_confirmation: Solicitou uma alteração da informação da sua conta, mas para tal é necessário confirmá-la. Queira ver o seu correio electrónico e seguir a hiperligação para a confirmar. Se não encontrar essa mensagem, veja se está na pasta de lixo electrónico.
-      updated: A sua conta foi correctamente actualizada.
+      destroyed: Adeus! A sua conta foi cancelada com sucesso. Esperamos voltar a vê-lo em breve.
+      signed_up: Bem-vindo! A sua conta foi registada com sucesso.
+      signed_up_but_inactive: Registou-se com sucesso. No entanto, não foi possível iniciar sessão porque a sua conta ainda não está ativada.
+      signed_up_but_locked: Registou-se com sucesso. No entanto, não foi possível iniciar sessão porque a sua conta está bloqueada.
+      signed_up_but_pending: Foi enviada uma mensagem com uma hiperligação de confirmação para o seu endereço de e-mail. Depois de clicar na hiperligação, analisaremos a sua candidatura. Será notificado se for aprovado.
+      signed_up_but_unconfirmed: Foi enviada para o seu endereço de e-mail uma mensagem com uma hiperligação de confirmação. Siga a hiperligação para ativar a sua conta. Verifique a sua pasta de spam se não recebeu esta mensagem de e-mail.
+      update_needs_confirmation: Atualizou a sua conta com sucesso, mas temos de verificar o seu novo endereço de e-mail. Verifique o seu e-mail e siga a hiperligação de confirmação para confirmar o seu novo endereço de e-mail. Verifique a sua pasta de spam se não recebeu esta mensagem de correio eletrónico.
+      updated: A sua conta foi corretamente atualizada.
     sessions:
       already_signed_out: Sessão encerrada.
       signed_in: Sessão iniciada.
       signed_out: Sessão encerrada.
     unlocks:
-      send_instructions: Dentro de alguns momentos, irá receber um e-mail com instruções para desbloquear a sua conta. Consulte a sua pasta de lixo electrónico se não o encontrar.
-      send_paranoid_instructions: Se a sua conta existir, dentro de momentos irá receber um e-mail com instruções para a desbloquear. Consulte a sua pasta de lixo electrónico se não o encontrar.
-      unlocked: A sua conta foi correctamente desbloqueada. Queira iniciar uma nova sessão para continuar.
+      send_instructions: Receberá um e-mail com instruções sobre como desbloquear a sua conta dentro de alguns minutos. Verifique a sua pasta de spam se não recebeu este e-mail.
+      send_paranoid_instructions: Se a sua conta existir, receberá um e-mail com instruções sobre como a desbloquear dentro de alguns minutos. Verifique a sua pasta de spam se não recebeu este e-mail.
+      unlocked: A sua conta foi desbloqueada com sucesso. Inicie sessão para continuar.
   errors:
     messages:
       already_confirmed: já confirmado, por favor tente iniciar sessão
-      confirmation_period_expired: tem de ser confirmado durante %{period}, por favor tenta outra vez
+      confirmation_period_expired: tem de ser confirmado dentro de %{period}, por favor tente outra vez
       expired: expirou, por favor tente outra vez
       not_found: não encontrado
       not_locked: não estava bloqueada
diff --git a/config/locales/doorkeeper.pt-PT.yml b/config/locales/doorkeeper.pt-PT.yml
index 30ea62d9e..3b4439584 100644
--- a/config/locales/doorkeeper.pt-PT.yml
+++ b/config/locales/doorkeeper.pt-PT.yml
@@ -4,9 +4,9 @@ pt-PT:
     attributes:
       doorkeeper/application:
         name: Nome da aplicação
-        redirect_uri: URI de redireccionamento
+        redirect_uri: URI de redirecionamento
         scopes: Âmbitos
-        website: Página na teia da aplicação
+        website: Site da aplicação
     errors:
       models:
         doorkeeper/application:
@@ -31,11 +31,11 @@ pt-PT:
       form:
         error: Ups! Verifique que o formulário não tem erros
       help:
-        native_redirect_uri: Usa %{native_redirect_uri} para testes locais
-        redirect_uri: Utiliza uma linha por URI
-        scopes: Separe as esferas de acção com espaços. Deixe em branco para usar autorizações predefinidas.
+        native_redirect_uri: Usar %{native_redirect_uri} para testes locais
+        redirect_uri: Usar uma linha por URI
+        scopes: Separe os âmbitos de aplicação com espaços. Deixe em branco para utilizar os âmbitos de aplicação predefinidos.
       index:
-        application: Aplicações
+        application: Aplicação
         callback_url: URL de retorno
         delete: Eliminar
         empty: Não tem aplicações.
@@ -48,31 +48,31 @@ pt-PT:
         title: Nova aplicação
       show:
         actions: Ações
-        application_id: Id de Aplicação
-        callback_urls: Callback urls
-        scopes: Autorizações
+        application_id: Chave da aplicação
+        callback_urls: URLs de retorno
+        scopes: Âmbitos
         secret: Segredo
         title: 'Aplicação: %{name}'
     authorizations:
       buttons:
-        authorize: Autorize
-        deny: Não autorize
+        authorize: Autorizar
+        deny: Negar
       error:
         title: Ocorreu um erro
       new:
         prompt_html: "%{client_name} pretende ter permissão para aceder à sua conta. É uma aplicação de terceiros. <strong>Se não confia nesta aplicação, então não deve autorizá-la.</strong>"
         review_permissions: Rever permissões
-        title: Autorização é necessária
+        title: Autorização necessária
       show:
-        title: Copiar o código desta autorização e colar na aplicação.
+        title: Copie este código de autorização e cole-o na aplicação.
     authorized_applications:
       buttons:
         revoke: Revogar
       confirmations:
-        revoke: Tens a certeza?
+        revoke: Tem a certeza?
       index:
         authorized_at: Autorizado em %{date}
-        description_html: Estas são aplicações que podem aceder à sua conta utilizando a API. Se encontrar aqui aplicativos que não reconhece, ou um aplicativo está com mau funcionamento, pode revogar o seu acesso.
+        description_html: Estas são as aplicações que podem aceder à sua conta utilizando a API. Se houver aplicações que não reconhece aqui, ou se uma aplicação estiver a comportar-se mal, pode revogar o seu acesso.
         last_used_at: Última utilização em %{date}
         never_used: Nunca utilizado
         scopes: Permissões
@@ -81,15 +81,15 @@ pt-PT:
     errors:
       messages:
         access_denied: O proprietário do recurso ou servidor de autorização negou o pedido.
-        credential_flow_not_configured: As credenciais da palavra-passe do proprietário do recurso falhou devido a que Doorkeeper.configure.resource_owner_from_credentials não foram configuradas.
-        invalid_client: Autenticação do cliente falhou por causa de um cliente desconhecido, nenhum cliente de autenticação incluído ou método de autenticação não suportado.
+        credential_flow_not_configured: Falha no fluxo de credenciais da palavra-passe do proprietário do recurso porque Doorkeeper.configure.resource_owner_from_credentials não está configurado.
+        invalid_client: A autenticação do cliente falhou devido a cliente desconhecido, sem autenticação de cliente incluída ou método de autenticação não suportado.
         invalid_code_challenge_method: O método de validação do código tem de ser S256, o método simples não é suportado.
         invalid_grant: A concessão de autorização fornecida é inválida, expirou, foi revogada, não corresponde à URI de redirecionamento usada no pedido de autorização ou foi emitida para outro cliente.
         invalid_redirect_uri: A URI de redirecionamento incluída não é válida.
         invalid_request:
           missing_param: 'Parâmetro requerido em falta: %{value}.'
-          request_not_authorized: O pedido precisa ser autorizado. O parâmetro requerido para autorização da solicitação está ausente ou é inválido.
-          unknown: A solicitação não possui um parâmetro requerido, inclui um valor de parâmetro não suportado ou tem outro tipo de formato incorreto.
+          request_not_authorized: O pedido tem de ser autorizado. O parâmetro necessário para autorizar o pedido está em falta ou é inválido.
+          unknown: O pedido não tem um parâmetro obrigatório, inclui um valor de parâmetro não suportado ou está mal formado.
         invalid_resource_owner: As credenciais do proprietário do recurso não são válidas ou o proprietário do recurso não pode ser encontrado
         invalid_scope: O âmbito solicitado é inválido, desconhecido ou tem um formato incorreto.
         invalid_token:
@@ -98,7 +98,7 @@ pt-PT:
           unknown: O token de acesso é inválido
         resource_owner_authenticator_not_configured: A procura pelo proprietário do recurso falhou porque Doorkeeper.configure.resource_owner_authenticator não foi configurado.
         server_error: O servidor de autorização encontrou uma condição inesperada que impediu o cumprimento do pedido .
-        temporarily_unavailable: O servidor de autorização não é capaz de lidar com o pedido devido a uma sobrecarga ou mantenimento do servidor.
+        temporarily_unavailable: O servidor de autorização não pode atualmente tratar o pedido devido a uma sobrecarga temporária ou à manutenção do servidor.
         unauthorized_client: O cliente não está autorizado a realizar esta solicitação usando este método.
         unsupported_grant_type: O tipo de concessão de autorização não é suportado pelo servidor de autorização.
         unsupported_response_type: O servidor de autorização não suporta este tipo de resposta.
@@ -125,12 +125,12 @@ pt-PT:
         admin/reports: Administração de denúncias
         all: Acesso total à sua conta Mastodon
         blocks: Bloqueios
-        bookmarks: Itens Salvos
+        bookmarks: Marcadores
         conversations: Conversas
-        crypto: Encriptação ponta-a-ponta
+        crypto: Encriptação ponta a ponta
         favourites: Favoritos
         filters: Filtros
-        follow: A seguir, a silenciar, e a bloquear
+        follow: A seguir, a silenciar e a bloquear
         follows: Seguidores
         lists: Listas
         media: Anexos de media
@@ -139,7 +139,7 @@ pt-PT:
         profile: O seu perfil Mastodon
         push: Notificações push
         reports: Denúncias
-        search: Pesquisa
+        search: Pesquisar
         statuses: Publicações
     layouts:
       admin:
@@ -165,14 +165,14 @@ pt-PT:
       admin:write:email_domain_blocks: executar ações de moderação no bloqueio de domínios de e-mail
       admin:write:ip_blocks: executar ações de moderação no bloqueio de IPs
       admin:write:reports: executar ações de moderação em denúncias
-      crypto: usa encriptação ponta-a-ponta
-      follow: siga, bloqueie, desbloqueie, e deixa de seguir contas
-      profile: apenas ler as informações do perfil da sua conta
+      crypto: usar encriptação ponta a ponta
+      follow: alterar relações de conta
+      profile: ler apenas as informações do perfil da sua conta
       push: receber as suas notificações push
-      read: tenha acesso aos dados da tua conta
+      read: ler todos os dados da sua conta
       read:accounts: ver as informações da conta
       read:blocks: ver os seus bloqueios
-      read:bookmarks: ver os seus favoritos
+      read:bookmarks: ver os seus marcadores
       read:favourites: ver os seus favoritos
       read:filters: ver os seus filtros
       read:follows: ver quem você segue
@@ -181,18 +181,18 @@ pt-PT:
       read:notifications: ver as suas notificações
       read:reports: ver as suas denúncias
       read:search: pesquisar em seu nome
-      read:statuses: ver todos os estados
-      write: publique por ti
-      write:accounts: modificar o seu perfil
+      read:statuses: ver todas as publicações
+      write: alterar todos os dados da sua conta
+      write:accounts: alterar o seu perfil
       write:blocks: bloquear contas e domínios
-      write:bookmarks: estado dos favoritos
+      write:bookmarks: marcar publicações
       write:conversations: silenciar e eliminar conversas
-      write:favourites: assinalar como favoritas
+      write:favourites: favoritar publicações
       write:filters: criar filtros
       write:follows: seguir pessoas
       write:lists: criar listas
-      write:media: carregar arquivos de media
+      write:media: enviar ficheiros de media
       write:mutes: silenciar pessoas e conversas
       write:notifications: limpar as suas notificações
       write:reports: denunciar outras pessoas
-      write:statuses: publicar estado
+      write:statuses: publicar publicações
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index b43ec5d17..86010b066 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -343,7 +343,7 @@ gl:
       title: Emoticonas personalizadas
       uncategorized: Sen categoría
       unlist: Non listar
-      unlisted: Sen listar
+      unlisted: Fóra das listas
       update_failed_msg: Non foi posíbel actualizar a emoticona
       updated_msg: Actualizouse a emoticona de xeito correcto!
       upload: Subir
@@ -1756,7 +1756,7 @@ gl:
       private_long: Mostrar só as seguidoras
       public: Público
       public_long: Visible para calquera
-      unlisted: Sen listar
+      unlisted: Fóra das listas
       unlisted_long: Visible para calquera, pero non en cronoloxías públicas
   statuses_cleanup:
     enabled: Borrar automáticamente publicacións antigas
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index 262063698..1bd724595 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -12,8 +12,8 @@ pt-PT:
       one: Seguidor
       other: Seguidores
     following: A seguir
-    instance_actor_flash: Esta conta é um actor virtual usado para representar a própria instância e não um utilizador individual. É usada para motivos de federação e não deve ser suspenso.
-    last_active: última vez activo
+    instance_actor_flash: Esta conta é um ator virtual utilizado para representar o servidor em si e não um utilizador individual. É utilizada para efeitos de federação e não deve ser suspensa.
+    last_active: última atividade
     link_verified_on: A posse desta hiperligação foi verificada em %{date}
     nothing_here: Não há nada aqui!
     pin_errors:
@@ -25,19 +25,19 @@ pt-PT:
   admin:
     account_actions:
       action: Executar acção
-      title: Executar acção de moderação em %{acct}
+      title: Executar ação de moderação em %{acct}
     account_moderation_notes:
       create: Deixar uma nota
-      created_msg: Nota de moderação correctamente criada!
+      created_msg: Nota de moderação criada com sucesso!
       destroyed_msg: Nota de moderação destruída!
     accounts:
       approve: Aprovar
-      approved_msg: Inscrição de %{username} aprovada correctamente
+      approved_msg: Inscrição de %{username} aprovada com sucesso
       are_you_sure: Tens a certeza?
       avatar: Imagem de perfil
       by_domain: Domínio
       change_email:
-        changed_msg: Endereço de correio electrónico alterado!
+        changed_msg: E-mail alterado com sucesso!
         current_email: E-mail atual
         label: Alterar e-mail
         new_email: Novo e-mail
@@ -57,12 +57,12 @@ pt-PT:
       demote: Despromoveu
       destroyed_msg: Os dados de %{username} estão agora em fila de espera para serem eliminados de imediato
       disable: Congelar
-      disable_two_factor_authentication: Desactivar autenticação por dois factores (2FA)
+      disable_two_factor_authentication: Desativar autenticação por dois fatores (2FA)
       disabled: Congelada
       display_name: Nome a mostrar
       domain: Domínio
       edit: Editar
-      email: Correio electrónico
+      email: E-mail
       email_status: Estado do e-mail
       enable: Descongelar
       enabled: Ativado
@@ -86,7 +86,7 @@ pt-PT:
       memorialized: Em memória
       memorialized_msg: Conta %{username} transformada com sucesso em memorial
       moderation:
-        active: Activo
+        active: Ativo
         all: Todos
         disabled: Desativado
         pending: Pendente
@@ -94,7 +94,7 @@ pt-PT:
         suspended: Supensos
         title: Moderação
       moderation_notes: Notas de moderação
-      most_recent_activity: Actividade mais recente
+      most_recent_activity: Atividade mais recente
       most_recent_ip: IP mais recente
       no_account_selected: Nenhuma conta foi alterada porque nenhuma foi selecionada
       no_limits_imposed: Sem limites impostos
@@ -111,9 +111,9 @@ pt-PT:
       public: Público
       push_subscription_expires: A Inscrição PuSH expira
       redownload: Atualizar perfil
-      redownloaded_msg: Perfil de %{username} correctamente actualizado a partir da origem
+      redownloaded_msg: Perfil de %{username} atualizado a partir da origem com sucesso
       reject: Rejeitar
-      rejected_msg: Inscrição de %{username} correctamente rejeitada
+      rejected_msg: Inscrição de %{username} rejeitada com sucesso
       remote_suspension_irreversible: Os dados desta conta foram eliminados irreversivelmente.
       remote_suspension_reversible_hint_html: A conta foi suspensa no servidor de origem e os seus dados serão totalmente eliminados em %{date}. Até lá, o servidor de origem poderá recuperar esta conta sem quaisquer efeitos negativos. Se desejar eliminar todos os dados desta conta imediatamente, pode fazê-lo em baixo.
       remove_avatar: Remover a imagem de perfil
@@ -152,7 +152,7 @@ pt-PT:
       title: Contas
       unblock_email: Desbloquear endereço de e-mail
       unblocked_email_msg: Endereço de e-mail de %{username} desbloqueado
-      unconfirmed_email: Correio electrónico por confirmar
+      unconfirmed_email: E-mail por confirmar
       undo_sensitized: Desmarcar como problemático
       undo_silenced: Desfazer silenciar
       undo_suspension: Desfazer supensão
@@ -226,7 +226,7 @@ pt-PT:
         create_account_warning_html: "%{name} enviou um aviso para %{target}"
         create_announcement_html: "%{name} criou o novo anúncio %{target}"
         create_custom_emoji_html: "%{name} carregou o novo emoji %{target}"
-        create_domain_allow_html: "%{name} habilitou a federação com o domínio %{target}"
+        create_domain_allow_html: "%{name} permitiu a federação com o domínio %{target}"
         create_domain_block_html: "%{name} bloqueou o domínio %{target}"
         create_ip_block_html: "%{name} criou regra para o IP %{target}"
         create_unavailable_domain_html: "%{name} parou a entrega ao domínio %{target}"
@@ -244,7 +244,7 @@ pt-PT:
         disable_2fa_user_html: "%{name} desativou o requerimento de autenticação em dois passos para o utilizador %{target}"
         disable_custom_emoji_html: "%{name} desabilitou o emoji %{target}"
         disable_user_html: "%{name} desativou o acesso para o utilizador %{target}"
-        enable_custom_emoji_html: "%{name} habilitou o emoji %{target}"
+        enable_custom_emoji_html: "%{name} ativou o emoji %{target}"
         enable_user_html: "%{name} ativou o acesso para o utilizador %{target}"
         memorialize_account_html: "%{name} transformou a conta de %{target} em um memorial"
         promote_user_html: "%{name} promoveu o utilizador %{target}"
@@ -276,22 +276,22 @@ pt-PT:
       title: Registo de auditoria
       unavailable_instance: "(nome de domínio indisponível)"
     announcements:
-      destroyed_msg: Comunicado correctamente eliminado!
+      destroyed_msg: Comunicado eliminado com sucesso!
       edit:
         title: Editar comunicado
       empty: Nenhum comunicado encontrado.
-      live: Em directo
+      live: Em direto
       new:
         create: Criar comunicado
         title: Novo comunicado
       publish: Publicar
-      published_msg: Comunicado correctamente publicado!
+      published_msg: Comunicado publicado com sucesso!
       scheduled_for: Agendado para %{time}
       scheduled_msg: Comunicado agendado para publicação!
       title: Comunicados
       unpublish: Anular publicação
-      unpublished_msg: Comunicado correctamente retirado de publicação!
-      updated_msg: Comunicado correctamente actualizado!
+      unpublished_msg: Comunicado corretamente retirado de publicação!
+      updated_msg: Comunicado atualizado com sucesso!
     critical_update_pending: Atualização crítica pendente
     custom_emojis:
       assign_category: Atribuir categoria
@@ -319,16 +319,16 @@ pt-PT:
       not_permitted: Não está autorizado a executar esta ação
       overwrite: Escrever por cima
       shortcode: Código de atalho
-      shortcode_hint: Pelo menos 2 caracteres, apenas caracteres alfanuméricos e underscores
+      shortcode_hint: Pelo menos 2 caracteres, apenas caracteres alfanuméricos e traços inferiores
       title: Emojis personalizados
       uncategorized: Não categorizados
       unlist: Não listar
       unlisted: Não inventariado
       update_failed_msg: Não foi possível atualizar esse emoji
-      updated_msg: Emoji correctamente actualizado!
+      updated_msg: Emoji atualizado com sucesso!
       upload: Enviar
     dashboard:
-      active_users: utilizadores activos
+      active_users: utilizadores ativos
       interactions: interações
       media_storage: Armazenamento de media
       new_users: novos utilizadores
@@ -350,8 +350,8 @@ pt-PT:
       sources: Origem de inscrições
       space: Utilização do espaço
       title: Painel de controlo
-      top_languages: Principais idiomas activos
-      top_servers: Servidores mais activos
+      top_languages: Principais idiomas ativos
+      top_servers: Servidores mais ativos
       website: Página na teia
     disputes:
       appeals:
@@ -359,7 +359,7 @@ pt-PT:
         title: Recursos
     domain_allows:
       add_new: Permitir federação com o domínio
-      created_msg: Permissão correctamente atribuída para federação
+      created_msg: O domínio foi autorizado com êxito para a federação
       destroyed_msg: Revogada a permissão de federação para o domínio
       export: Exportar
       import: Importar
@@ -385,14 +385,14 @@ pt-PT:
       import: Importar
       new:
         create: Criar bloqueio
-        hint: O bloqueio por domínio não vai prevenir a criação de contas na base de dados, mas irá retroactiva e automaticamente aplicar certos métodos de moderação nessas contas.
+        hint: O bloqueio do domínio não impedirá a criação de registos de contas na base de dados, mas aplicará retroativamente e automaticamente métodos de moderação específicos a essas contas.
         severity:
           desc_html: "<strong>Limitar</strong> tornará as mensagens das contas neste domínio invisíveis a qualquer pessoa que não as esteja a seguir. <strong>Suspender</strong> removerá do seu servidor todo o conteúdo, media, e dados de perfil das contas deste domínio. Utilize <strong>Nenhum</strong> se apenas quiser rejeitar ficheiros media."
           noop: Nenhum
           silence: Limitar
           suspend: Suspender
         title: Novo bloqueio de domínio
-      no_domain_block_selected: Nenhum bloqueio de domínio foi alterado, pois nenhum foi seleccionado
+      no_domain_block_selected: Nenhum bloqueio de domínio foi alterado pois nenhum foi selecionado
       not_permitted: Não está autorizado a executar esta ação
       obfuscate: Ofuscar nome de domínio
       obfuscate_hint: Ofuscar parcialmente o nome de domínio na lista, caso estejam definidas limitações na publicação da lista de domínios
@@ -527,7 +527,7 @@ pt-PT:
       title: Convites
     ip_blocks:
       add_new: Criar regra
-      created_msg: Nova regra de IP correctamente adicionada
+      created_msg: Nova regra de IP corretamente adicionada
       delete: Eliminar
       expires_in:
         '1209600': 2 semanas
@@ -547,15 +547,15 @@ pt-PT:
       delete: Eliminar
       description_html: Um <strong>repetidor de federação</strong> é um servidor intermediário que troca grandes volumes de publicações públicas entre instâncias que o subscrevem e publicam. <strong>Ele pode ajudar pequenas e medias instâncias a descobrir conteúdo do fediverso</strong> que, de outro modo, exigiria que os utilizadores locais seguissem manualmente outras pessoas em instâncias remotas.
       disable: Desativar
-      disabled: Desactivado
-      enable: Activar
+      disabled: Desativado
+      enable: Ativar
       enable_hint: Uma vez ativado, a tua instância irá assinar todas as publicações deste repetidor e irá começar a enviar as suas publicações públicas para lá.
       enabled: Ativado
       inbox_url: URL do repetidor
       pending: À espera da aprovação do repetidor
       save_and_enable: Guardar e ativar
       setup: Configurar uma ligação ao repetidor
-      signatures_not_enabled: Os repetidores não funcionarão correctamente enquanto o modo seguro ou o modo de lista branca estiverem activados
+      signatures_not_enabled: Os repetidores não funcionarão corretamente enquanto o modo seguro ou o modo de lista branca estiverem ativados
       status: Estado
       title: Retransmissores
     report_notes:
@@ -618,27 +618,27 @@ pt-PT:
       reported_by: Denunciado por
       reported_with_application: Reportado com a aplicação
       resolved: Resolvido
-      resolved_msg: Denúncia correctamente resolvida!
+      resolved_msg: Denúncia resolvida com sucesso!
       skip_to_actions: Passar para as ações
       status: Estado
       statuses: Conteúdo denunciado
       statuses_description_html: O conteúdo ofensivo será citado na comunicação com a conta denunciada
       summary:
         action_preambles:
-          delete_html: 'Você está prestes a <strong>remover</strong> algumas das publicações de <strong>@%{acct}</strong>. Isto irá:'
-          mark_as_sensitive_html: 'Você está prestes a <strong>marcar</strong> alguns dos posts de <strong>@%{acct}</strong>como <strong>sensível</strong>. Isto irá:'
-          silence_html: 'Você está prestes a <strong>limitar a conta do</strong> <strong>@%{acct}</strong>. Isto irá:'
-          suspend_html: 'Você está prestes a <strong>suspender a conta de</strong> <strong>@%{acct}</strong>. Isto irá:'
+          delete_html: 'Está prestes a <strong>remover</strong> algumas das publicações de <strong>@%{acct}</strong>. Isto irá:'
+          mark_as_sensitive_html: 'Está prestes a <strong>marcar</strong> algumas das publicações de <strong>@%{acct}</strong>como <strong>sensível</strong>. Isto irá:'
+          silence_html: 'Está prestes a <strong>limitar a conta de</strong> <strong>@%{acct}</strong>. Isto irá:'
+          suspend_html: 'Está prestes a <strong>suspender a conta de</strong> <strong>@%{acct}</strong>. Isto irá:'
         actions:
           delete_html: Excluir as publicações ofensivas
           mark_as_sensitive_html: Marcar a mídia dos posts ofensivos como sensível
           silence_html: Limitar firmemente o alcance de <strong>@%{acct}</strong>, tornando seus perfis e conteúdos apenas visíveis para pessoas que já os estão seguindo ou olhando manualmente no perfil
-          suspend_html: Suspender <strong>@%{acct}</strong>, tornando seu perfil e conteúdo inacessíveis e impossível de interagir com
+          suspend_html: Suspender <strong>@%{acct}</strong>, tornando seu perfil e conteúdo inacessíveis e impossível de interagir
         close_report: 'Marcar relatório #%{id} como resolvido'
-        close_reports_html: Marcar <strong>todos os</strong> relatórios contra <strong>@%{acct}</strong> como resolvidos
-        delete_data_html: Excluir <strong>@%{acct}</strong>perfil e conteúdo 30 dias a menos que sejam dessuspensos
+        close_reports_html: Marcar <strong>todas as</strong> denúncias contra <strong>@%{acct}</strong> como resolvidas
+        delete_data_html: Eliminar o perfil de <strong>@%{acct}</strong> e conteúdos daqui a 30 dias, a menos que entretanto sejam suspensos
         preview_preamble_html: "<strong>@%{acct}</strong> receberá um aviso com o seguinte conteúdo:"
-        record_strike_html: Registre um ataque contra <strong>@%{acct}</strong> para ajudá-lo a escalar futuras violações desta conta
+        record_strike_html: Registar um ataque contra <strong>@%{acct}</strong> para ajudar a escalar futuras violações desta conta
         warning_placeholder: Argumentos adicionais opcionais para a acção de moderação.
       target_origin: Origem da conta denunciada
       title: Denúncias
@@ -693,7 +693,7 @@ pt-PT:
         manage_settings: Gerir Configurações
         manage_settings_description: Permite aos utilizadores alterar as configurações do sítio na teia
         manage_taxonomies: Gerir Taxonomias
-        manage_taxonomies_description: 'Permite aos utilizadores avaliar o conteúdo em alta e atualizar as configurações de #etiquetas'
+        manage_taxonomies_description: Permite aos utilizadores rever o conteúdo em tendência e atualizar as configurações de hashtag
         manage_user_access: Gerir Acesso de Utilizador
         manage_users: Gerir Utilizadores
         manage_users_description: Permite aos utilizadores ver os detalhes de outros utilizadores e executar ações de moderação contra eles
@@ -743,13 +743,13 @@ pt-PT:
         publish_discovered_servers: Publicar servidores descobertos
         publish_statistics: Publicar estatísticas
         title: Descobrir
-        trends: Em alta
+        trends: Tendências
       domain_blocks:
         all: Para toda a gente
         disabled: Para ninguém
         users: Para utilizadores locais que se encontrem autenticados
       registrations:
-        moderation_recommandation: Por favor, certifique-se de que você tem uma equipe de moderação adequada e reativa antes de abrir os registros para todos!
+        moderation_recommandation: Certifique-se de que dispõe de uma equipa de moderação adequada e reativa antes de abrir as inscrições a todos!
         preamble: Controle quem pode criar uma conta no seu servidor.
         title: Inscrições
       registrations_mode:
@@ -757,7 +757,7 @@ pt-PT:
           approved: Registo sujeito a aprovação
           none: Ninguém se pode registar
           open: Qualquer pessoa se pode registar
-        warning_hint: Recomendamos o uso de "Aprovação necessária para se cadastrar", a menos que você esteja confiante de que sua equipe de moderação pode lidar com spam e registros maliciosos em tempo hábil.
+        warning_hint: Recomendamos a utilização de “É necessária aprovação para o registo”, a menos que esteja confiante de que a sua equipa de moderação pode tratar o spam e os registos maliciosos de forma atempada.
       security:
         authorized_fetch: Exigir autenticação de servidores federados
         authorized_fetch_hint: Exigir autenticação de servidores federados permite uma aplicação mais rigorosa de bloqueios tanto ao nível do utilizador como do servidor. No entanto, isso é feito à custa de uma diminuição de desempenho, reduz o alcance das suas respostas e pode introduzir problemas de compatibilidade com alguns serviços federados. Além disso, isso não impede os atores mais empenhados de aceder às suas publicações e contas públicas.
@@ -766,7 +766,7 @@ pt-PT:
       title: Definições do servidor
     site_uploads:
       delete: Eliminar arquivo carregado
-      destroyed_msg: Envio de sítio na teia correctamente eliminado!
+      destroyed_msg: Envio do site eliminado com sucesso!
     software_updates:
       critical_update: Crítico — por favor, atualize rapidamente
       documentation_link: Saber mais
@@ -800,7 +800,7 @@ pt-PT:
       reblogs: Re-publicacões
       status_changed: Publicação alterada
       title: Estado das contas
-      trending: Em alta
+      trending: Em tendência
       visibility: Visibilidade
       with_media: Com media
     strikes:
@@ -823,7 +823,7 @@ pt-PT:
       elasticsearch_health_yellow:
         message_html: O cluster elasticsearch não está de boa saúde (estado amarelo), pode querer investigar o motivo
       elasticsearch_index_mismatch:
-        message_html: Os mapeamentos elasticsearch estão desatualizados. Por favor, execute <code>tootctl search deploy --only=%{value}</code>
+        message_html: Os mapeamentos de índice Elasticsearch estão desatualizados. Execute <code>tootctl search deploy --only=%{value}</code>
       elasticsearch_preset:
         action: Ver a documentação
         message_html: O seu cluster elasticsearch tem mais de um nó, mas o Mastodon não está configurado para os usar.
@@ -831,9 +831,9 @@ pt-PT:
         action: Ver documentação
         message_html: O seu cluster elasticsearch tem apenas um nó, <code>ES_PRESET</code> deve ser configurado para <code>single_node_cluster</code>.
       elasticsearch_reset_chewy:
-        message_html: O seu índice de sistema elasticsearch está desatualizado devido a uma mudança de configuração. Por favor, execute <code>tootctl search deploy --reset-chewy</code> para o atualizar.
+        message_html: O seu índice de sistema Elasticsearch está desatualizado devido a uma mudança de configuração. Execute <code>tootctl search deploy --reset-chewy</code> para o atualizar.
       elasticsearch_running_check:
-        message_html: Não foi possível conectar ao Elasticsearch. Por favor, verifique se está em execução, ou desabilite a pesquisa de texto completo
+        message_html: Não foi possível conectar ao Elasticsearch. Verifique se está em execução ou desative a pesquisa de texto completo
       elasticsearch_version_check:
         message_html: 'Versão de Elasticsearch incompatível: %{value}'
         version_comparison: A versão de Elasticsearch %{running_version} está em execução. No entanto, é obrigatória a versão %{required_version}
@@ -872,14 +872,14 @@ pt-PT:
       review: Estado da revisão
       search: Pesquisar
       title: Hashtags
-      updated_msg: 'Definições de #etiquetas correctamente actualizadas'
+      updated_msg: 'Definições de #etiquetas atualizadas com sucesso'
     title: Administração
     trends:
       allow: Permitir
       approved: Aprovado
       disallow: Não permitir
       links:
-        allow: Permitir ligação
+        allow: Permitir hiperligação
         allow_provider: Permitir editor
         description_html: Estas são as ligações que presentemente estão a ser muito partilhadas por contas visíveis pelo seu servidor. Estas podem ajudar os seus utilizador a descobrir o que está a acontecer no mundo. Nenhuma ligação é exibida publicamente até que o editor a aprove. Também pode permitir ou rejeitar ligações em avulso.
         disallow: Não permitir ligação
@@ -890,15 +890,15 @@ pt-PT:
         shared_by_over_week:
           one: Partilhado por uma pessoa na última semana
           other: Partilhado por %{count} pessoas na última semana
-        title: Ligações em alta
+        title: Hiperligações em tendência
         usage_comparison: Partilhado %{today} vezes hoje, em comparação com %{yesterday} ontem
       not_allowed_to_trend: Não permitido para tendência
       only_allowed: Apenas permitidos
       pending_review: Pendente de revisão
       preview_card_providers:
-        allowed: Ligações deste editor poderão vir a ficar em alta
+        allowed: As hiperligações deste editor podem ser tendência
         description_html: Estes são os domínios a partir dos quais ligações são frequentemente partilhadas no seu servidor. As suas ligações não serão colocadas em alta a menos que o seu domínio de origem seja aprovado. A sua aprovação (ou rejeição) estende-se a subdomínios.
-        rejected: Ligações deste editor não serão postas em alta
+        rejected: As hiperligações deste editor não podem ser tendência
         title: Editores
       rejected: Rejeitado
       statuses:
@@ -907,18 +907,18 @@ pt-PT:
         description_html: Estas são publicações que o seu servidor conhece e que atualmente estão a ser frequentemente partilhadas e adicionadas aos favoritos. Isto pode ajudar os seus utilizadores, novos e retornados, a encontrar mais pessoas para seguir. Nenhuma publicação será exibida publicamente até que aprove o autor, e o autor permita que a sua conta seja sugerida a outros. Você também pode permitir ou rejeitar publicações individualmente.
         disallow: Não permitir publicação
         disallow_account: Não permitir autor
-        no_status_selected: Nenhuma publicação em alta foi alterada, pois nenhuma foi selecionada
+        no_status_selected: Não foram alteradas quaisquer publicações de tendências, uma vez que nenhuma foi selecionada
         not_discoverable: O autor optou por não permitir que a sua conta seja sugerida a outros
         shared_by:
           one: Partilhado ou adicionado aos marcadores uma vez
           other: Partilhado e adicionado aos marcadores %{friendly_count} vezes
-        title: Publicações em alta
+        title: Publicações em tendência
       tags:
         current_score: Pontuação atual %{score}
         dashboard:
           tag_accounts_measure: utilizadores únicos
           tag_languages_dimension: Idiomas mais populares
-          tag_servers_dimension: Topo de servidores
+          tag_servers_dimension: Servidores mais populares
           tag_servers_measure: servidores diferentes
           tag_uses_measure: utilizações totais
         description_html: 'Estas são as #etiquetas que aparecem atualmente com frequência em publicações visíveis pelo seu servidor. Isto pode ajudar os seus utilizadores a descobrir o que está ser mais falado no momento. Nenhuma #etiqueta será exibida publicamente até que a aprove.'
@@ -928,15 +928,16 @@ pt-PT:
         not_trendable: Não aparecerá nas tendências
         not_usable: Não pode ser utilizada
         peaked_on_and_decaying: Máximo em %{date}, agora a decair
-        title: Etiquetas em alta
-        trendable: Pode aparecer em alta
-        trending_rank: 'Em alta #%{rank}'
+        title: Etiquetas em tendência
+        trendable: Pode aparecer nas tendências
+        trending_rank: 'Tendência #%{rank}'
         usable: Pode ser utilizada
         usage_comparison: Utilizada %{today} vezes hoje, em comparação com %{yesterday} ontem
         used_by_over_week:
           one: Utilizada por uma pessoa na última semana
           other: Utilizada por %{count} pessoas na última semana
-      trending: Em alta
+      title: Recomendações e tendências
+      trending: Em tendência
     warning_presets:
       add_new: Adicionar novo
       delete: Eliminar
@@ -946,13 +947,13 @@ pt-PT:
     webhooks:
       add_new: Adicionar endpoint
       delete: Eliminar
-      description_html: Um <strong>webhook</strong> possibilita que o Mastodon envie <strong>notificações em tempo real</strong> de eventos seleccionados, para uma aplicação sua, de modo que esta possa <strong>espoletar ações automaticamente</strong>.
+      description_html: Um <strong>webhook</strong> possibilita que o Mastodon envie <strong>notificações em tempo real</strong> de eventos selecionados, para uma aplicação sua, de modo que esta possa <strong>despoletar ações automaticamente</strong>.
       disable: Desativar
       disabled: Desativado
       edit: Editar endpoint
       empty: Não tem ainda qualquer endpoint de webhook configurado.
       enable: Ativar
-      enabled: Activo
+      enabled: Ativo
       enabled_events:
         one: 1 evento ativado
         other: "%{count} eventos ativados"
@@ -993,18 +994,18 @@ pt-PT:
       body: Foram lançadas novas versões do Mastodon, talvez queira atualizar!
       subject: Estão disponíveis novas versões do Mastodon para %{instance}!
     new_trends:
-      body: 'Os seguintes itens precisam ser revistos antes de poderem ser exibidos publicamente:'
+      body: 'Os seguintes itens necessitam de uma revisão antes de poderem ser apresentados publicamente:'
       new_trending_links:
-        title: Ligações em alta
+        title: Hiperligações em tendência
       new_trending_statuses:
-        title: Publicações em alta
+        title: Publicações em tendência
       new_trending_tags:
-        title: Etiquetas em alta
+        title: Etiquetas em tendência
       subject: Novas tendências para revisão em %{instance}
   aliases:
     add_new: Criar pseudónimo
     created_msg: Criou com sucesso um novo pseudónimo. Pode agora iniciar a migração da conta antiga.
-    deleted_msg: O pseudónimo foi correctamente eliminado. Não será mais possível migrar a partir dessa conta.
+    deleted_msg: O pseudónimo foi removido com êxito. Deixará de ser possível passar dessa conta para esta.
     empty: Não tem pseudónimos.
     hint_html: Se quiser mudar de outra conta para esta, pode criar aqui um pseudónimo, que é necessário antes de poder prosseguir com a migração de seguidores da conta antiga para esta. Esta ação por si só é <strong>inofensiva e reversível</strong>. <strong>A migração da conta é iniciada a partir da conta antiga</strong>.
     remove: Desvincular pseudónimo
@@ -1026,8 +1027,8 @@ pt-PT:
     view_profile: Ver perfil
     view_status: Ver publicação
   applications:
-    created: Aplicação correctamente criada
-    destroyed: Aplicação correctamente eliminada
+    created: Aplicação criada com sucesso
+    destroyed: Aplicação eliminada com sucesso
     logout: Sair
     regenerate_token: Regenerar token de acesso
     token_regenerated: Token de acesso regenerado com sucesso
@@ -1036,7 +1037,7 @@ pt-PT:
   auth:
     apply_for_account: Solicitar uma conta
     captcha_confirmation:
-      help_html: Se tiver problemas a resolver o CAPTCHA, pode entrar em contacto conosco através de %{email} e poderemos ajudá-lo.
+      help_html: Se tiver problemas a resolver o CAPTCHA, pode entrar em contacto connosco através de %{email} e poderemos ajudá-lo.
       hint_html: Só mais uma coisa! Precisamos confirmar que você é um humano (isto para que possamos evitar spam!). Resolva o CAPTCHA abaixo e clique em "Continuar".
       title: Verificação de segurança
     confirmations:
@@ -1138,7 +1139,7 @@ pt-PT:
     confirm_password: Insira sua palavra-passe atual para verificar a sua identidade
     confirm_username: Insira seu nome de utilizador para confirmar o procedimento
     proceed: Eliminar conta
-    success_msg: A sua conta foi correctamente eliminada
+    success_msg: A sua conta foi eliminada com sucesso
     warning:
       before: 'Antes de continuar, por favor leia cuidadosamente estas notas:'
       caches: O conteúdo que foi armazenado em cache por outras instâncias pode perdurar
@@ -1335,11 +1336,11 @@ pt-PT:
       muting: Importando contas silenciadas
     type: Tipo de importação
     type_groups:
-      constructive: Seguidores e Marcadores
+      constructive: Seguidores e marcadores
       destructive: Bloqueios e silenciamentos
     types:
       blocking: Lista de bloqueio
-      bookmarks: Itens salvos
+      bookmarks: Marcadores
       domain_blocking: Lista de domínios bloqueados
       following: Lista de pessoas que estás a seguir
       lists: Listas
@@ -1395,7 +1396,7 @@ pt-PT:
     acct: Mudou-se para
     cancel: Cancelar redirecionamento
     cancel_explanation: Cancelar o redirecionamento irá reativar a sua conta atual, mas não trará de volta os seguidores que foram migrados para essa conta.
-    cancelled_msg: Cancelou correctamente o redireccionamento.
+    cancelled_msg: Cancelou corretamente o redirecionamento.
     errors:
       already_moved: é a mesma conta para a qual já migrou
       missing_also_known_as: não é um pseudónimo dessa conta
@@ -1405,13 +1406,13 @@ pt-PT:
     followers_count: Seguidores no momento da migração
     incoming_migrations: A migrar de uma conta diferente
     incoming_migrations_html: Para migrar de outra conta para esta, primeiro você precisa <a href="%{path}">criar um pseudónimo</a>.
-    moved_msg: A sua conta está agora a ser redireccionada para %{acct} e os seus seguidores estão a ser transferidos.
+    moved_msg: A sua conta está agora a ser redirecionada para %{acct} e os seus seguidores estão a ser transferidos.
     not_redirecting: A sua conta não está atualmente a ser redirecionada para nenhuma outra conta.
     on_cooldown: Migrou recentemente a sua conta. Esta função ficará disponível novamente em %{count} dias.
     past_migrations: Migrações anteriores
     proceed_with_move: Migrar seguidores
-    redirected_msg: A sua conta está agora a ser redireccionada para %{acct}.
-    redirecting_to: A sua conta está a ser redireccionada para %{acct}.
+    redirected_msg: A sua conta está agora a ser redirecionada para %{acct}.
+    redirecting_to: A sua conta está a ser redirecionada para %{acct}.
     set_redirect: Definir redirecionamento
     warning:
       backreference_required: A nova conta deve primeiro ser configurada para que esta seja referenciada
@@ -1419,7 +1420,7 @@ pt-PT:
       cooldown: Após a migração, há um período de tempo de espera durante o qual não poderá voltar a migrar
       disabled_account: Posteriormente, a sua conta atual não será totalmente utilizável. No entanto, continuará a ter acesso à exportação de dados, bem como à reativação.
       followers: Esta ação irá migrar todos os seguidores da conta atual para a nova conta
-      only_redirect_html: Em alternativa, pode <a href="%{path}">apenas colocar um redireccionamento no seu perfil</a>.
+      only_redirect_html: Em alternativa, pode <a href="%{path}">apenas colocar um redirecionamento no seu perfil</a>.
       other_data: Nenhum outro dado será migrado automaticamente
       redirect: O perfil da sua conta atual será atualizado com um aviso de redirecionamento e será excluído das pesquisas
   moderation:
@@ -1546,7 +1547,7 @@ pt-PT:
     remove_selected_follows: Deixar de seguir os utilizadores selecionados
     status: Estado da conta
   remote_follow:
-    missing_resource: Não foi possível encontrar o URL de redireccionamento para a sua conta
+    missing_resource: Não foi possível encontrar o URL de redirecionamento para a sua conta
   reports:
     errors:
       invalid_rules: não faz referência a regras válidas
@@ -1639,7 +1640,7 @@ pt-PT:
       user_domain_block: Bloqueou %{target_name}
     lost_followers: Seguidores perdidos
     lost_follows: Pessoas que segue perdidas
-    preamble: Pode perder seguidores e pessoas que segue quando bloqueia um domínio ou quando os seus moderadores decidem suspender um servidor remoto. Quando isso acontecer, poderá descarregar listas de relações cessadas, para serem inspeccionadas e possivelmente importadas para outro servidor.
+    preamble: Pode perder seguidores e pessoas que segue quando bloqueia um domínio ou quando os seus moderadores decidem suspender um servidor remoto. Quando isso acontecer, poderá descarregar listas de relações cortadas, para serem inspecionadas e possivelmente importadas para outro servidor.
     purged: Informações sobre este servidor foram purgadas pelos administradores do seu servidor.
     type: Evento
   statuses:
@@ -1706,8 +1707,8 @@ pt-PT:
     keep_pinned_hint: Não apagar nenhuma das suas publicações afixadas
     keep_polls: Manter sondagens
     keep_polls_hint: Não apaga nenhuma das suas sondagens
-    keep_self_bookmark: Manter as publicações que guardou
-    keep_self_bookmark_hint: Não apaga as suas próprias publicações se as tiver guardado
+    keep_self_bookmark: Manter as publicações que marcou
+    keep_self_bookmark_hint: Não elimina as suas próprias publicações se as tiver nos marcadores
     keep_self_fav: Manter as publicações que marcou
     keep_self_fav_hint: Não apaga as suas próprias publicações se as tiver marcado
     min_age:
@@ -1852,8 +1853,8 @@ pt-PT:
         one: "%{people} pessoa nos últimos 2 dias"
         other: "%{people} pessoas nos últimos 2 dias"
       hashtags_subtitle: Explore o que está em tendência desde os últimos 2 dias
-      hashtags_title: Trending hashtags
-      hashtags_view_more: Ver mais hashtags em alta
+      hashtags_title: Etiquetas em tendência
+      hashtags_view_more: Ver mais etiquetas em tendência
       post_action: Compor
       post_step: Diga olá para o mundo com texto, fotos, vídeos ou enquetes.
       post_title: Faça a sua primeira publicação
@@ -1874,7 +1875,7 @@ pt-PT:
     extra_instructions_html: <strong>Dica:</strong> A ligação no seu site pode ser invisível. A parte importante é <code>rel="me"</code> que impede a personificação em sites com conteúdo gerado pelo utilizador. Pode até utilizar uma etiqueta <code>link</code> no cabeçalho da página ao invés de <code>a</code>, mas o HTML deve ser acessível sem executar JavaScript.
     here_is_how: Veja como
     hint_html: "<strong>Verificar a sua identidade no Mastodon é para todos.</strong> Baseado em normas públicas da web, agora e para sempre gratuitas. Tudo o que precisa é de um site pessoal pelo qual as pessoas o reconheçam. Quando coloca no seu perfil uma ligação para esse site, vamos verificar que o site tem uma ligação de volta para o seu perfil e mostrar um indicador visual."
-    instructions_html: Copie e cole o código abaixo no HTML do seu site. Em seguida, adicione o endereço do seu site em um dos campos extras no seu perfil, na aba "Editar perfil" e salve as alterações.
+    instructions_html: Copie e cole o código abaixo no HTML do seu site. Em seguida, adicione o endereço do seu site num dos campos extras no seu perfil, na aba "Editar perfil" e guarde as alterações.
     verification: Verificação
     verified_links: As suas ligações verificadas
   webauthn_credentials:
diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml
index e552b42b1..81be19a44 100644
--- a/config/locales/simple_form.ar.yml
+++ b/config/locales/simple_form.ar.yml
@@ -206,7 +206,7 @@ ar:
         setting_aggregate_reblogs: جمّع المنشورات المعاد نشرها في الخيوط الزمنية
         setting_always_send_emails: ارسل إشعارات البريد الإلكتروني دائماً
         setting_auto_play_gif: تشغيل تلقائي لِوَسائط جيف المتحركة
-        setting_boost_modal: إظهار مربع حوار التأكيد قبل إعادة مشاركة أي منشور
+        setting_boost_modal: إظهار مربع حوار التأكيد قبل إعادة نشر أي منشور
         setting_default_language: لغة النشر
         setting_default_privacy: خصوصية المنشور
         setting_default_sensitive: اعتبر الوسائط دائما كمحتوى حساس
diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml
index a76076a5c..25348f277 100644
--- a/config/locales/simple_form.pt-PT.yml
+++ b/config/locales/simple_form.pt-PT.yml
@@ -52,7 +52,7 @@ pt-PT:
         locale: O idioma da interface de utilizador, e-mails e notificações push
         password: Use pelo menos 8 caracteres
         phrase: Será correspondido independentemente da capitalização ou do aviso de conteúdo duma publicação
-        scopes: Quais as API a que será concedido acesso. Se escolher uma abrangência de nível superior, não precisará de as seleccionar individualmente.
+        scopes: Quais as API a que a aplicação terá permissão para aceder. Se selecionar um âmbito de nível superior, não precisa de selecionar âmbitos individuais.
         setting_aggregate_reblogs: Não mostrar novos reforços de publicações recentemente reforçadas (só afecta publicações acabadas de reforçar)
         setting_always_send_emails: Normalmente as notificações por e-mail não serão enviadas quando estiver a utilizar ativamente o Mastodon
         setting_default_sensitive: Media problemática oculta por padrão, pode ser revelada com um clique
@@ -81,7 +81,7 @@ pt-PT:
         backups_retention_period: Os utilizadores têm a possibilidade de gerar arquivos das suas mensagens para descarregar mais tarde. Quando definido para um valor positivo, estes arquivos serão automaticamente eliminados do seu armazenamento após o número de dias especificado.
         bootstrap_timeline_accounts: Estas contas serão destacadas no topo das recomendações aos novos utilizadores.
         closed_registrations_message: Apresentado quando as inscrições estiverem encerradas
-        content_cache_retention_period: Todas as publicações de outros servidores (incluindo boosts e respostas) serão eliminadas após o número de dias especificado, independentemente de qualquer interação do utilizador local com essas publicações. Isto inclui publicações em que um utilizador local as tenha marcado como favoritas ou adicionado aos items salvos. As menções privadas entre utilizadores de instâncias diferentes também se perderão e serão impossíveis de restaurar. A utilização desta definição destina-se a instâncias para fins especiais e quebra muitas expectativas dos utilizadores quando implementada para utilização geral.
+        content_cache_retention_period: Todas as publicações de outros servidores (incluindo boosts e respostas) serão eliminadas após o número de dias especificado, independentemente de qualquer interação do utilizador local com essas publicações. Isto inclui publicações em que um utilizador local as tenha marcado ou favoritado. As menções privadas entre utilizadores de instâncias diferentes também se perderão e serão impossíveis de restaurar. A utilização desta definição destina-se a instâncias para fins especiais e quebra muitas expectativas dos utilizadores quando implementada para utilização geral.
         custom_css: Pode aplicar estilos personalizados na versão web do Mastodon.
         favicon: WEBP, PNG, GIF ou JPG. Substitui o ícone de favorito padrão do Mastodon por um ícone personalizado.
         mascot: Sobrepõe-se à ilustração na interface web avançada.
@@ -99,9 +99,9 @@ pt-PT:
         theme: Tema que os visitantes e os novos utilizadores veem.
         thumbnail: Uma imagem de cerca de 2:1, apresentada ao lado da informação do seu servidor.
         timeline_preview: Os visitantes sem sessão iniciada poderão consultar as publicações públicas mais recentes disponíveis no servidor.
-        trendable_by_default: Ignorar a revisão manual do conteúdo em alta. Elementos em avulso poderão ainda assim ser retirados das tendências mesmo após a sua apresentação.
-        trends: As publicações em alta mostram quais as publicações, etiquetas e notícias que estão a ganhar destaque no seu servidor.
-        trends_as_landing_page: Mostrar conteúdo de tendências para usuários logados e visitantes em vez de uma descrição deste servidor. Requer que as tendências sejam ativadas.
+        trendable_by_default: Ignorar a revisão manual do conteúdo de tendências. Os itens individuais ainda podem ser removidos das tendências após a apresentação.
+        trends: As tendências mostram quais as publicações, etiquetas e notícias que estão a ganhar destaque no seu servidor.
+        trends_as_landing_page: Mostrar conteúdo de tendências a utilizadores e visitantes com sessão terminada em vez de uma descrição deste servidor. Requer que as tendências estejam ativadas.
       form_challenge:
         current_password: Está a entrar numa área segura
       imports:
@@ -129,7 +129,7 @@ pt-PT:
       tag:
         name: Só pode alterar a capitalização das letras, por exemplo, para torná-las mais legíveis
       user:
-        chosen_languages: Quando seleccionado, só serão mostradas nas cronologias públicas as publicações nos idiomas escolhidos
+        chosen_languages: Quando selecionado, só serão mostradas nas cronologias públicas as publicações nos idiomas escolhidos
         role: A função controla que permissões o utilizador tem
       user_role:
         color: Cor a ser utilizada para a função em toda a interface de utilizador, como RGB no formato hexadecimal
@@ -222,7 +222,7 @@ pt-PT:
         setting_reduce_motion: Reduz movimento em animações
         setting_system_font_ui: Usar o tipo de letra padrão do sistema
         setting_theme: Tema do sítio
-        setting_trends: Mostrar o que está hoje em alta
+        setting_trends: Mostrar as tendências de hoje
         setting_unfollow_modal: Solicitar confirmação antes de deixar de seguir alguém
         setting_use_blurhash: Mostrar gradientes coloridos para medias ocultas
         setting_use_pending_items: Modo lento
@@ -253,7 +253,7 @@ pt-PT:
         mascot: Mascote personalizada (legado)
         media_cache_retention_period: Período de retenção de ficheiros de media em cache
         peers_api_enabled: Publicar lista de servidores descobertos na API
-        profile_directory: Habilitar diretório de perfis
+        profile_directory: Ativar o diretório de perfis
         registrations_mode: Quem se pode inscrever
         require_invite_text: Requerer uma razão para entrar
         show_domain_blocks: Mostrar domínios bloqueados
@@ -268,8 +268,8 @@ pt-PT:
         theme: Tema predefinido
         thumbnail: Miniatura do servidor
         timeline_preview: Permitir acesso não autenticado às cronologias públicas
-        trendable_by_default: Permitir publicações em alta sem revisão prévia
-        trends: Activar publicações em alta
+        trendable_by_default: Permitir tendências sem revisão prévia
+        trends: Ativar tendências
         trends_as_landing_page: Usar tendências como página inicial
       interactions:
         must_be_follower: Bloquear notificações de não-seguidores
@@ -303,7 +303,7 @@ pt-PT:
           label: Está disponível uma nova versão do Mastodon
           none: Nunca notificar atualizações (não recomendado)
           patch: Notificar sobre atualizações de correções de problemas
-        trending_tag: Uma nova publicação em alta requer avaliação
+        trending_tag: Uma nova publicação em tendência requer revisão
       rule:
         hint: Informação Adicional
         text: Regra
@@ -313,7 +313,7 @@ pt-PT:
       tag:
         listable: Permitir que esta etiqueta apareça em pesquisas e no diretório de perfis
         name: Etiqueta
-        trendable: Permitir que esta etiqueta apareça em alta
+        trendable: Permitir que esta etiqueta apareça nas tendências
         usable: Permitir que as publicações usem esta hashtag localmente
       user:
         role: Cargo

From b265a654d74f649b6a58debe39a439cc9b07b730 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 5 Sep 2024 11:46:11 +0200
Subject: [PATCH 14/91] Fix wrong width on content warnings and filters in web
 UI (#31761)

---
 app/javascript/styles/mastodon/components.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index d892c008b..2fc195a3b 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -10916,6 +10916,7 @@ noscript {
 }
 
 .content-warning {
+  box-sizing: border-box;
   background: rgba($ui-highlight-color, 0.05);
   color: $secondary-text-color;
   border-top: 1px solid;

From ba9fd1c32e760582041758105b2844debed640a3 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 5 Sep 2024 05:48:33 -0400
Subject: [PATCH 15/91] Add coverage for `Account#prepare_contents` callback
 (#31748)

---
 spec/models/account_spec.rb | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 27707fa89..1e8e4b1e4 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -723,6 +723,30 @@ RSpec.describe Account do
     end
   end
 
+  describe '#prepare_contents' do
+    subject { Fabricate.build :account, domain: domain, note: '  padded note  ', display_name: '  padded name  ' }
+
+    context 'with local account' do
+      let(:domain) { nil }
+
+      it 'strips values' do
+        expect { subject.valid? }
+          .to change(subject, :note).to('padded note')
+          .and(change(subject, :display_name).to('padded name'))
+      end
+    end
+
+    context 'with remote account' do
+      let(:domain) { 'host.example' }
+
+      it 'preserves values' do
+        expect { subject.valid? }
+          .to not_change(subject, :note)
+          .and(not_change(subject, :display_name))
+      end
+    end
+  end
+
   describe 'Normalizations' do
     describe 'username' do
       it { is_expected.to normalize(:username).from(" \u3000bob \t \u00a0 \n ").to('bob') }

From f9712fad1b62e93961280243970c6ccff4d3e3ff Mon Sep 17 00:00:00 2001
From: James May <fowl2@users.noreply.github.com>
Date: Thu, 5 Sep 2024 19:48:42 +1000
Subject: [PATCH 16/91] Direct link to each authorized_application entry with
 html anchor (#31677)

Co-authored-by: Matt Jankowski <matt@jankowski.online>
---
 app/views/oauth/authorized_applications/index.html.haml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml
index b6819bbd7..eb544d079 100644
--- a/app/views/oauth/authorized_applications/index.html.haml
+++ b/app/views/oauth/authorized_applications/index.html.haml
@@ -7,7 +7,7 @@
 
 .applications-list
   - @applications.each do |application|
-    .applications-list__item
+    .applications-list__item{ id: dom_id(application) }
       - if application.website.present?
         = link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer', class: 'announcements-list__item__title'
       - else

From bd8cd0c6e746356eea83ffc4d3185a51e4eeb5dc Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 5 Sep 2024 09:50:38 +0000
Subject: [PATCH 17/91] Update dependency cssnano to v7.0.6 (#31757)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 110 +++++++++++++++++++++++++++---------------------------
 1 file changed, 55 insertions(+), 55 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index c77ebfa44..1b3a7f1e4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6717,26 +6717,26 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cssnano-preset-default@npm:^7.0.5":
-  version: 7.0.5
-  resolution: "cssnano-preset-default@npm:7.0.5"
+"cssnano-preset-default@npm:^7.0.6":
+  version: 7.0.6
+  resolution: "cssnano-preset-default@npm:7.0.6"
   dependencies:
     browserslist: "npm:^4.23.3"
     css-declaration-sorter: "npm:^7.2.0"
     cssnano-utils: "npm:^5.0.0"
-    postcss-calc: "npm:^10.0.1"
+    postcss-calc: "npm:^10.0.2"
     postcss-colormin: "npm:^7.0.2"
-    postcss-convert-values: "npm:^7.0.3"
-    postcss-discard-comments: "npm:^7.0.2"
+    postcss-convert-values: "npm:^7.0.4"
+    postcss-discard-comments: "npm:^7.0.3"
     postcss-discard-duplicates: "npm:^7.0.1"
     postcss-discard-empty: "npm:^7.0.0"
     postcss-discard-overridden: "npm:^7.0.0"
-    postcss-merge-longhand: "npm:^7.0.3"
-    postcss-merge-rules: "npm:^7.0.3"
+    postcss-merge-longhand: "npm:^7.0.4"
+    postcss-merge-rules: "npm:^7.0.4"
     postcss-minify-font-values: "npm:^7.0.0"
     postcss-minify-gradients: "npm:^7.0.0"
     postcss-minify-params: "npm:^7.0.2"
-    postcss-minify-selectors: "npm:^7.0.3"
+    postcss-minify-selectors: "npm:^7.0.4"
     postcss-normalize-charset: "npm:^7.0.0"
     postcss-normalize-display-values: "npm:^7.0.0"
     postcss-normalize-positions: "npm:^7.0.0"
@@ -6750,10 +6750,10 @@ __metadata:
     postcss-reduce-initial: "npm:^7.0.2"
     postcss-reduce-transforms: "npm:^7.0.0"
     postcss-svgo: "npm:^7.0.1"
-    postcss-unique-selectors: "npm:^7.0.2"
+    postcss-unique-selectors: "npm:^7.0.3"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/ffa7c6fa16c6ad98b7732fc563de74d492e6ad6d243a9f00431c0cbdbc576bcd49226d2695d881465d32dea0a2916add40ac10e7560dd7b5de9fd0fa25ee081b
+  checksum: 10c0/5c827a9f6b35475267af0512d55f569994b8334eb06565498daa2070ef52f0cdd2013f5efc1cbc0b4664370f491b0080f93c8ee56a7730d38fdf451fb65b030c
   languageName: node
   linkType: hard
 
@@ -6767,14 +6767,14 @@ __metadata:
   linkType: hard
 
 "cssnano@npm:^7.0.0":
-  version: 7.0.5
-  resolution: "cssnano@npm:7.0.5"
+  version: 7.0.6
+  resolution: "cssnano@npm:7.0.6"
   dependencies:
-    cssnano-preset-default: "npm:^7.0.5"
+    cssnano-preset-default: "npm:^7.0.6"
     lilconfig: "npm:^3.1.2"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/cb43ed964787dca33efb44d8f4fea8a49c495db44d1d12940493f0dd5d63db78e01c5b140fe42b480b332733602a25f4c85186d00977eb3070b29f7422761985
+  checksum: 10c0/19ff09931a1531e7c0c0d8928da554d99213aa0bb1f3b93cc6b4987727d60a8cd5537b113a5cf4f95cc1db65bba3f2b35476bd63bb57e7469d4eab73e07d736d
   languageName: node
   linkType: hard
 
@@ -13278,15 +13278,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-calc@npm:^10.0.1":
-  version: 10.0.1
-  resolution: "postcss-calc@npm:10.0.1"
+"postcss-calc@npm:^10.0.2":
+  version: 10.0.2
+  resolution: "postcss-calc@npm:10.0.2"
   dependencies:
-    postcss-selector-parser: "npm:^6.1.1"
+    postcss-selector-parser: "npm:^6.1.2"
     postcss-value-parser: "npm:^4.2.0"
   peerDependencies:
     postcss: ^8.4.38
-  checksum: 10c0/5e38cc6f082f87e82067497b41684410784223ecd3701bf52242ea9f2f467f1fad6b5a561f8aa3be307d89435b4060f58aeb27c4064003586daf653cc4d91fef
+  checksum: 10c0/f57c9db7a7a2f3a0cdf45990089c051248d995bb2b9d1bd1fcd1634507851e92ea85bbc71a3594e359e9e9287ba0a820c90d6d292126a4b735cda364a86ce9cf
   languageName: node
   linkType: hard
 
@@ -13354,15 +13354,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-convert-values@npm:^7.0.3":
-  version: 7.0.3
-  resolution: "postcss-convert-values@npm:7.0.3"
+"postcss-convert-values@npm:^7.0.4":
+  version: 7.0.4
+  resolution: "postcss-convert-values@npm:7.0.4"
   dependencies:
     browserslist: "npm:^4.23.3"
     postcss-value-parser: "npm:^4.2.0"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/dbb6278bd8d8b11e448933d823426c883bff3f6abeaa23c7530cc4668b9da6f714e073840f280273f8a14022c3a99eb461ec732f7539e062b32f5281e1be6526
+  checksum: 10c0/9839b29f7c638672115c9fef5ed7df016aa43ea9dd42a4a2ace16e6a49c75246d2c19f3e03a6409ed3bc7c2fa4de6203bf5789cef8268c76618326b68e3bc591
   languageName: node
   linkType: hard
 
@@ -13420,14 +13420,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-discard-comments@npm:^7.0.2":
-  version: 7.0.2
-  resolution: "postcss-discard-comments@npm:7.0.2"
+"postcss-discard-comments@npm:^7.0.3":
+  version: 7.0.3
+  resolution: "postcss-discard-comments@npm:7.0.3"
   dependencies:
-    postcss-selector-parser: "npm:^6.1.1"
+    postcss-selector-parser: "npm:^6.1.2"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/c01632e643b6ec1f61ad59efe06a9e8dfc7fcedeb1551ae48fc33fa801353f6222e31954286cd97171c694f34c2b4c7f7a2213fd0f913e37c34d0353258ed234
+  checksum: 10c0/7700c8fb9a83c6ea5cc784267b9afd6e2968fda0358d583af5913baa28dfc91b0f2a4bd0b2bd62a86ebcb8dadb2547e287beae25b5a097e21c1f723367ccf112
   languageName: node
   linkType: hard
 
@@ -13572,29 +13572,29 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-merge-longhand@npm:^7.0.3":
-  version: 7.0.3
-  resolution: "postcss-merge-longhand@npm:7.0.3"
+"postcss-merge-longhand@npm:^7.0.4":
+  version: 7.0.4
+  resolution: "postcss-merge-longhand@npm:7.0.4"
   dependencies:
     postcss-value-parser: "npm:^4.2.0"
-    stylehacks: "npm:^7.0.3"
+    stylehacks: "npm:^7.0.4"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/b968c3d16f3edc6075b20219a1165c089dc454a6a42951dcdfc94adb932fb96ef7bcd465c6cd21b0e5b55ac08921355ddbbbc7cdcf87a345e4bef8b3cdd2e7e9
+  checksum: 10c0/6f50f7775dd361f83daf1acb3e0001d700ed2b7b9bea02df172143adc7fa196ce9209c9e482010ce36fd704512433b62692c5ab2eef5226db71ea3e694654dc7
   languageName: node
   linkType: hard
 
-"postcss-merge-rules@npm:^7.0.3":
-  version: 7.0.3
-  resolution: "postcss-merge-rules@npm:7.0.3"
+"postcss-merge-rules@npm:^7.0.4":
+  version: 7.0.4
+  resolution: "postcss-merge-rules@npm:7.0.4"
   dependencies:
     browserslist: "npm:^4.23.3"
     caniuse-api: "npm:^3.0.0"
     cssnano-utils: "npm:^5.0.0"
-    postcss-selector-parser: "npm:^6.1.1"
+    postcss-selector-parser: "npm:^6.1.2"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/3cd20484ab6d15c62eded408248d5eeaba52a573935943f933865680e070a0e75b3a7447802c575bc86e1fae667cf51d9d5766537835d9b8c090337b5adf928e
+  checksum: 10c0/fffdcef4ada68e92ab8e6dc34a3b9aa2b87188cd4d08f5ba0ff2aff7e3e3c7f086830748ff64db091b5ccb9ac59ac37cfaab1268ed3efb50ab9c4f3714eb5f6d
   languageName: node
   linkType: hard
 
@@ -13635,15 +13635,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-minify-selectors@npm:^7.0.3":
-  version: 7.0.3
-  resolution: "postcss-minify-selectors@npm:7.0.3"
+"postcss-minify-selectors@npm:^7.0.4":
+  version: 7.0.4
+  resolution: "postcss-minify-selectors@npm:7.0.4"
   dependencies:
     cssesc: "npm:^3.0.0"
-    postcss-selector-parser: "npm:^6.1.1"
+    postcss-selector-parser: "npm:^6.1.2"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/5211f63a1672f646a1bab57bd8eac0816d42adacb5e286ad5e6e342a795bb0d086bd6044a1b338311ca28f33f2c1833165ee611eaa671287379821ba3c5d68ad
+  checksum: 10c0/212b8f3d62eb2a27ed57d4e76b75b0886806ddb9e2497c0bb79308fa75dabaaaa4ed2b97734896e87603272d05231fd74aee2c256a48d77aa468b5b64cc7866a
   languageName: node
   linkType: hard
 
@@ -14004,7 +14004,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4, postcss-selector-parser@npm:^6.1.0, postcss-selector-parser@npm:^6.1.1, postcss-selector-parser@npm:^6.1.2":
+"postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4, postcss-selector-parser@npm:^6.1.0, postcss-selector-parser@npm:^6.1.2":
   version: 6.1.2
   resolution: "postcss-selector-parser@npm:6.1.2"
   dependencies:
@@ -14026,14 +14026,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-unique-selectors@npm:^7.0.2":
-  version: 7.0.2
-  resolution: "postcss-unique-selectors@npm:7.0.2"
+"postcss-unique-selectors@npm:^7.0.3":
+  version: 7.0.3
+  resolution: "postcss-unique-selectors@npm:7.0.3"
   dependencies:
-    postcss-selector-parser: "npm:^6.1.1"
+    postcss-selector-parser: "npm:^6.1.2"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/cc54c57cd1c5a6e3e166ec63cc036d9e2df80b05e508d9ce754ca4193bf8c1bfcc16b3c6f0d81b8352a3282201d249b90bb87abacfcfb9065c9e3705ea5d110e
+  checksum: 10c0/2eb90eb0745d1e29d411ea5108f1cd9737de5b8f739cabc717074872bc4015950c9963f870b23b33b9ef45e7887eecfe5560cffee56616d4e0b8d0fac4f7cb10
   languageName: node
   linkType: hard
 
@@ -16551,15 +16551,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"stylehacks@npm:^7.0.3":
-  version: 7.0.3
-  resolution: "stylehacks@npm:7.0.3"
+"stylehacks@npm:^7.0.4":
+  version: 7.0.4
+  resolution: "stylehacks@npm:7.0.4"
   dependencies:
     browserslist: "npm:^4.23.3"
-    postcss-selector-parser: "npm:^6.1.1"
+    postcss-selector-parser: "npm:^6.1.2"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/5030334b06ef705b5700444dab120b540b09159e935e75b60f25bd56db1d85f0d11755f0b0f64ce3f12c5a72ff1b6f57fea49c26d18eb0de2334d6a143b94f8d
+  checksum: 10c0/b4d0b280ba274503ecc04111cc11c713e0d65db079fbcd8b42d6350be1cca20e28611eddee93b419aa208176a0d3a5fff83d83ef958d1876713809b6a2787c0c
   languageName: node
   linkType: hard
 

From 8fd3e37747b6cdde100bae33ac5d84f3255c24ce Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 5 Sep 2024 06:20:27 -0400
Subject: [PATCH 18/91] Update `parser` and `rubocop` gems (#31760)

---
 .rubocop_todo.yml |  5 +----
 Gemfile.lock      | 13 +++++--------
 2 files changed, 6 insertions(+), 12 deletions(-)

diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 09acb795b..a6e51d6ae 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,6 +1,6 @@
 # This configuration was generated by
 # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
-# using RuboCop version 1.65.0.
+# using RuboCop version 1.66.1.
 # The point is for the user to remove these configuration records
 # one by one as the offenses are removed from the code base.
 # Note that changes in the inspected code, or installation of new
@@ -35,7 +35,6 @@ Rails/OutputSafety:
 # Configuration parameters: AllowedVars.
 Style/FetchEnvVar:
   Exclude:
-    - 'app/lib/redis_configuration.rb'
     - 'app/lib/translation_service.rb'
     - 'config/environments/production.rb'
     - 'config/initializers/2_limited_federation_mode.rb'
@@ -44,7 +43,6 @@ Style/FetchEnvVar:
     - 'config/initializers/devise.rb'
     - 'config/initializers/paperclip.rb'
     - 'config/initializers/vapid.rb'
-    - 'lib/mastodon/redis_config.rb'
     - 'lib/tasks/repo.rake'
 
 # This cop supports safe autocorrection (--autocorrect).
@@ -93,7 +91,6 @@ Style/OptionalBooleanParameter:
     - 'app/services/fetch_resource_service.rb'
     - 'app/workers/domain_block_worker.rb'
     - 'app/workers/unfollow_follow_worker.rb'
-    - 'lib/mastodon/redis_config.rb'
 
 # This cop supports unsafe autocorrection (--autocorrect-all).
 # Configuration parameters: EnforcedStyle.
diff --git a/Gemfile.lock b/Gemfile.lock
index a533b6624..c70823d05 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -584,7 +584,7 @@ GEM
     ostruct (0.6.0)
     ox (2.14.18)
     parallel (1.26.3)
-    parser (3.3.4.2)
+    parser (3.3.5.0)
       ast (~> 2.4.1)
       racc
     parslet (2.0.0)
@@ -698,8 +698,7 @@ GEM
     responders (3.1.1)
       actionpack (>= 5.2)
       railties (>= 5.2)
-    rexml (3.3.6)
-      strscan
+    rexml (3.3.7)
     rotp (6.3.0)
     rouge (4.3.0)
     rpam2 (4.0.2)
@@ -735,18 +734,17 @@ GEM
       rspec-mocks (~> 3.0)
       sidekiq (>= 5, < 8)
     rspec-support (3.13.1)
-    rubocop (1.65.1)
+    rubocop (1.66.1)
       json (~> 2.3)
       language_server-protocol (>= 3.17.0)
       parallel (~> 1.10)
       parser (>= 3.3.0.2)
       rainbow (>= 2.2.2, < 4.0)
       regexp_parser (>= 2.4, < 3.0)
-      rexml (>= 3.2.5, < 4.0)
-      rubocop-ast (>= 1.31.1, < 2.0)
+      rubocop-ast (>= 1.32.2, < 2.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (>= 2.4.0, < 3.0)
-    rubocop-ast (1.32.1)
+    rubocop-ast (1.32.3)
       parser (>= 3.3.1.0)
     rubocop-capybara (2.21.0)
       rubocop (~> 1.41)
@@ -826,7 +824,6 @@ GEM
     stringio (3.1.1)
     strong_migrations (2.0.0)
       activerecord (>= 6.1)
-    strscan (3.1.0)
     swd (1.3.0)
       activesupport (>= 3)
       attr_required (>= 0.0.5)

From 5b1ae15a368aa800fd045d6e3b6a7500c7196889 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 5 Sep 2024 13:06:05 +0200
Subject: [PATCH 19/91] Update docker.io/ruby Docker tag to v3.3.5 (#31758)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Dockerfile b/Dockerfile
index cd555f702..c7c02d9b4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,7 +12,7 @@ ARG BUILDPLATFORM=${BUILDPLATFORM}
 
 # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
 # renovate: datasource=docker depName=docker.io/ruby
-ARG RUBY_VERSION="3.3.4"
+ARG RUBY_VERSION="3.3.5"
 # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
 # renovate: datasource=node-version depName=node
 ARG NODE_MAJOR_VERSION="20"

From e820cc30b8798c1d9e30d4e780c511b5ab395345 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 5 Sep 2024 07:54:27 -0400
Subject: [PATCH 20/91] Convert invites controller spec to system/request specs
 (#31755)

---
 app/views/invites/_invite.html.haml           |  2 +-
 spec/controllers/invites_controller_spec.rb   | 84 ------------------
 spec/requests/invites_spec.rb                 | 31 +++++++
 .../support/matchers/private_cache_control.rb | 14 +++
 spec/system/invites_spec.rb                   | 86 +++++++++++++++++++
 spec/system/tags_spec.rb                      |  3 +
 6 files changed, 135 insertions(+), 85 deletions(-)
 delete mode 100644 spec/controllers/invites_controller_spec.rb
 create mode 100644 spec/requests/invites_spec.rb
 create mode 100644 spec/support/matchers/private_cache_control.rb
 create mode 100644 spec/system/invites_spec.rb

diff --git a/app/views/invites/_invite.html.haml b/app/views/invites/_invite.html.haml
index 94e1a7112..892fdc5a0 100644
--- a/app/views/invites/_invite.html.haml
+++ b/app/views/invites/_invite.html.haml
@@ -1,4 +1,4 @@
-%tr
+%tr{ id: dom_id(invite) }
   %td
     .input-copy
       .input-copy__wrapper
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
deleted file mode 100644
index 192c5b00b..000000000
--- a/spec/controllers/invites_controller_spec.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe InvitesController do
-  render_views
-
-  let(:user) { Fabricate(:user) }
-
-  before do
-    sign_in user
-  end
-
-  describe 'GET #index' do
-    before do
-      Fabricate(:invite, user: user)
-    end
-
-    context 'when everyone can invite' do
-      before do
-        UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users])
-        get :index
-      end
-
-      it 'returns http success' do
-        expect(response).to have_http_status(:success)
-      end
-
-      it 'returns private cache control headers' do
-        expect(response.headers['Cache-Control']).to include('private, no-store')
-      end
-    end
-
-    context 'when not everyone can invite' do
-      before do
-        UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users])
-        get :index
-      end
-
-      it 'returns http forbidden' do
-        expect(response).to have_http_status(403)
-      end
-    end
-  end
-
-  describe 'POST #create' do
-    subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } }
-
-    context 'when everyone can invite' do
-      before do
-        UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users])
-      end
-
-      it 'succeeds to create a invite' do
-        expect { subject }.to change(Invite, :count).by(1)
-        expect(subject).to redirect_to invites_path
-        expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10)
-      end
-    end
-
-    context 'when not everyone can invite' do
-      before do
-        UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users])
-      end
-
-      it 'returns http forbidden' do
-        expect(subject).to have_http_status(403)
-      end
-    end
-  end
-
-  describe 'DELETE #destroy' do
-    subject { delete :destroy, params: { id: invite.id } }
-
-    let(:invite) { Fabricate(:invite, user: user, expires_at: nil) }
-
-    it 'expires invite and redirects' do
-      expect { subject }
-        .to(change { invite.reload.expired? }.to(true))
-      expect(response)
-        .to redirect_to invites_path
-    end
-  end
-end
diff --git a/spec/requests/invites_spec.rb b/spec/requests/invites_spec.rb
new file mode 100644
index 000000000..8a5ad2ccd
--- /dev/null
+++ b/spec/requests/invites_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Invites' do
+  let(:user) { Fabricate(:user) }
+
+  before { sign_in user }
+
+  context 'when not everyone can invite' do
+    before { UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users]) }
+
+    describe 'GET /invites' do
+      it 'returns http forbidden' do
+        get invites_path
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+
+    describe 'POST /invites' do
+      it 'returns http forbidden' do
+        post invites_path, params: { invite: { max_users: '10', expires_in: 1800 } }
+
+        expect(response)
+          .to have_http_status(403)
+      end
+    end
+  end
+end
diff --git a/spec/support/matchers/private_cache_control.rb b/spec/support/matchers/private_cache_control.rb
new file mode 100644
index 000000000..7fcf56be3
--- /dev/null
+++ b/spec/support/matchers/private_cache_control.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :have_private_cache_control do
+  match do |page|
+    page.response_headers['Cache-Control'] == 'private, no-store'
+  end
+
+  failure_message do |page|
+    <<~ERROR
+      Expected page to have `Cache-Control` header with `private, no-store` but it has:
+        #{page.response_headers['Cache-Control']}
+    ERROR
+  end
+end
diff --git a/spec/system/invites_spec.rb b/spec/system/invites_spec.rb
new file mode 100644
index 000000000..648bbea82
--- /dev/null
+++ b/spec/system/invites_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Invites' do
+  include ActionView::RecordIdentifier
+
+  let(:user) { Fabricate :user }
+
+  before do
+    host! 'localhost:3000' # TODO: Move into before for all system specs?
+    sign_in user
+  end
+
+  describe 'Viewing invites' do
+    it 'Lists existing user invites' do
+      invite = Fabricate :invite, user: user
+
+      visit invites_path
+
+      within css_id(invite) do
+        expect(page)
+          .to have_content(invite.uses)
+          .and have_private_cache_control
+        expect(copyable_field.value)
+          .to eq(public_invite_url(invite_code: invite.code))
+      end
+    end
+  end
+
+  describe 'Creating a new invite' do
+    it 'Saves the invite for the user' do
+      visit invites_path
+
+      fill_invite_form
+
+      expect { submit_form }
+        .to change(user.invites, :count).by(1)
+    end
+  end
+
+  describe 'Deleting an existing invite' do
+    it 'Expires the invite' do
+      invite = Fabricate :invite, user: user
+
+      visit invites_path
+
+      expect { delete_invite(invite) }
+        .to change { invite.reload.expired? }.to(true)
+
+      within css_id(invite) do
+        expect(page).to have_content I18n.t('invites.expired')
+      end
+    end
+  end
+
+  private
+
+  def css_id(record)
+    "##{dom_id(record)}" # TODO: Extract to system spec helper?
+  end
+
+  def copyable_field
+    within '.input-copy' do
+      find(:field, type: :text, readonly: true)
+    end
+  end
+
+  def submit_form
+    click_on I18n.t('invites.generate')
+  end
+
+  def delete_invite(invite)
+    within css_id(invite) do
+      click_on I18n.t('invites.delete')
+    end
+  end
+
+  def fill_invite_form
+    select I18n.t('invites.max_uses', count: 100),
+           from: I18n.t('simple_form.labels.defaults.max_uses')
+    select I18n.t("invites.expires_in.#{30.minutes.to_i}"),
+           from: I18n.t('simple_form.labels.defaults.expires_in')
+    check I18n.t('simple_form.labels.defaults.autofollow')
+  end
+end
diff --git a/spec/system/tags_spec.rb b/spec/system/tags_spec.rb
index e9ad970a5..f39c6bf0d 100644
--- a/spec/system/tags_spec.rb
+++ b/spec/system/tags_spec.rb
@@ -6,11 +6,14 @@ RSpec.describe 'Tags' do
   describe 'Viewing a tag' do
     let(:tag) { Fabricate(:tag, name: 'test') }
 
+    before { sign_in Fabricate(:user) }
+
     it 'visits the tag page and renders the web app' do
       visit tag_path(tag)
 
       expect(page)
         .to have_css('noscript', text: /Mastodon/)
+        .and have_private_cache_control
     end
   end
 end

From b4b639ee4a059385874f143027b75516c8db17d9 Mon Sep 17 00:00:00 2001
From: Michael Stanclift <mx@vmstan.com>
Date: Thu, 5 Sep 2024 07:34:13 -0500
Subject: [PATCH 21/91] Fix radio checkbox visibility in Report dialogs
 (#31752)

---
 app/javascript/styles/mastodon/components.scss | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 2fc195a3b..5936ef8a6 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6437,7 +6437,7 @@ a.status-card {
   }
 
   .dialog-option .poll__input {
-    border-color: $inverted-text-color;
+    border-color: $darker-text-color;
     color: $ui-secondary-color;
     display: inline-flex;
     align-items: center;
@@ -6451,13 +6451,13 @@ a.status-card {
     &:active,
     &:focus,
     &:hover {
-      border-color: lighten($inverted-text-color, 15%);
+      border-color: $valid-value-color;
       border-width: 4px;
     }
 
     &.active {
-      border-color: $inverted-text-color;
-      background: $inverted-text-color;
+      border-color: $valid-value-color;
+      background: $valid-value-color;
     }
   }
 

From f85694acfdaf57c113bba49ca2aef949b16d4141 Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Thu, 5 Sep 2024 16:06:58 +0200
Subject: [PATCH 22/91] Add credentials to redis sentinel configuration
 (#31768)

---
 lib/mastodon/redis_configuration.rb           | 45 ++++++++++++-------
 spec/lib/mastodon/redis_configuration_spec.rb | 23 +++++++---
 2 files changed, 45 insertions(+), 23 deletions(-)

diff --git a/lib/mastodon/redis_configuration.rb b/lib/mastodon/redis_configuration.rb
index 5a096a1bf..3d739a2ac 100644
--- a/lib/mastodon/redis_configuration.rb
+++ b/lib/mastodon/redis_configuration.rb
@@ -57,39 +57,50 @@ class Mastodon::RedisConfiguration
   def setup_config(prefix: nil, defaults: {})
     prefix = "#{prefix}REDIS_"
 
-    url           = ENV.fetch("#{prefix}URL", nil)
-    user          = ENV.fetch("#{prefix}USER", nil)
-    password      = ENV.fetch("#{prefix}PASSWORD", nil)
-    host          = ENV.fetch("#{prefix}HOST", defaults[:host])
-    port          = ENV.fetch("#{prefix}PORT", defaults[:port])
-    db            = ENV.fetch("#{prefix}DB", defaults[:db])
-    name          = ENV.fetch("#{prefix}SENTINEL_MASTER", nil)
-    sentinel_port = ENV.fetch("#{prefix}SENTINEL_PORT", 26_379)
-    sentinel_list = ENV.fetch("#{prefix}SENTINELS", nil)
+    url      = ENV.fetch("#{prefix}URL", nil)
+    user     = ENV.fetch("#{prefix}USER", nil)
+    password = ENV.fetch("#{prefix}PASSWORD", nil)
+    host     = ENV.fetch("#{prefix}HOST", defaults[:host])
+    port     = ENV.fetch("#{prefix}PORT", defaults[:port])
+    db       = ENV.fetch("#{prefix}DB", defaults[:db])
 
     return { url:, driver: } if url
 
-    sentinels = parse_sentinels(sentinel_list, default_port: sentinel_port)
+    sentinel_options = setup_sentinels(prefix, default_user: user, default_password: password)
 
-    if name.present? && sentinels.present?
-      host = name
+    if sentinel_options.present?
+      host = sentinel_options[:name]
       port = nil
       db ||= 0
-    else
-      sentinels = nil
     end
 
     url = construct_uri(host, port, db, user, password)
 
     if url.present?
-      { url:, driver:, name:, sentinels: }
+      { url:, driver: }.merge(sentinel_options)
     else
-      # Fall back to base config. This has defaults for the URL
-      # so this cannot lead to an endless loop.
+      # Fall back to base config, which has defaults for the URL
+      # so this cannot lead to endless recursion.
       base
     end
   end
 
+  def setup_sentinels(prefix, default_user: nil, default_password: nil)
+    name              = ENV.fetch("#{prefix}SENTINEL_MASTER", nil)
+    sentinel_port     = ENV.fetch("#{prefix}SENTINEL_PORT", 26_379)
+    sentinel_list     = ENV.fetch("#{prefix}SENTINELS", nil)
+    sentinel_username = ENV.fetch("#{prefix}SENTINEL_USERNAME", default_user)
+    sentinel_password = ENV.fetch("#{prefix}SENTINEL_PASSWORD", default_password)
+
+    sentinels = parse_sentinels(sentinel_list, default_port: sentinel_port)
+
+    if name.present? && sentinels.present?
+      { name:, sentinels:, sentinel_username:, sentinel_password: }
+    else
+      {}
+    end
+  end
+
   def construct_uri(host, port, db, user, password)
     return nil if host.blank?
 
diff --git a/spec/lib/mastodon/redis_configuration_spec.rb b/spec/lib/mastodon/redis_configuration_spec.rb
index d14adf951..e36dcfba0 100644
--- a/spec/lib/mastodon/redis_configuration_spec.rb
+++ b/spec/lib/mastodon/redis_configuration_spec.rb
@@ -100,10 +100,27 @@ RSpec.describe Mastodon::RedisConfiguration do
         expect(subject[:url]).to eq 'redis://:testpass1@mainsentinel/0'
       end
 
+      it 'uses the redis password to authenticate with sentinels' do
+        expect(subject[:sentinel_password]).to eq 'testpass1'
+      end
+
       it 'includes the sentinel master name and list of sentinels' do
         expect(subject[:name]).to eq 'mainsentinel'
         expect(subject[:sentinels]).to contain_exactly({ host: '192.168.0.1', port: 3000 }, { host: '192.168.0.2', port: 4000 })
       end
+
+      context "when giving dedicated credentials in `#{prefix}REDIS_SENTINEL_USERNAME` and `#{prefix}REDIS_SENTINEL_PASSWORD`" do
+        around do |example|
+          ClimateControl.modify "#{prefix}REDIS_SENTINEL_USERNAME": 'sentinel_user', "#{prefix}REDIS_SENTINEL_PASSWORD": 'sentinel_pass1' do
+            example.run
+          end
+        end
+
+        it 'uses the credential to authenticate with sentinels' do
+          expect(subject[:sentinel_username]).to eq 'sentinel_user'
+          expect(subject[:sentinel_password]).to eq 'sentinel_pass1'
+        end
+      end
     end
 
     context 'when giving sentinels without port numbers' do
@@ -154,8 +171,6 @@ RSpec.describe Mastodon::RedisConfiguration do
           url: 'redis://localhost:6379/0',
           driver: :hiredis,
           namespace: nil,
-          name: nil,
-          sentinels: nil,
         })
       end
     end
@@ -188,8 +203,6 @@ RSpec.describe Mastodon::RedisConfiguration do
           url: 'redis://:testpass@redis.example.com:3333/3',
           driver: :hiredis,
           namespace: nil,
-          name: nil,
-          sentinels: nil,
         })
       end
     end
@@ -218,8 +231,6 @@ RSpec.describe Mastodon::RedisConfiguration do
         namespace: 'cache',
         expires_in: 10.minutes,
         connect_timeout: 5,
-        name: nil,
-        sentinels: nil,
         pool: {
           size: 5,
           timeout: 5,

From d58faa20181c95b82ace744494a683b81eea681a Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 5 Sep 2024 10:07:17 -0400
Subject: [PATCH 23/91] Remove references to deprecated `Import` model (#31759)

---
 app/views/settings/imports/index.html.haml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/views/settings/imports/index.html.haml b/app/views/settings/imports/index.html.haml
index bfddd4546..634631b5a 100644
--- a/app/views/settings/imports/index.html.haml
+++ b/app/views/settings/imports/index.html.haml
@@ -23,7 +23,7 @@
       = f.input :mode,
                 as: :radio_buttons,
                 collection_wrapper_tag: 'ul',
-                collection: Import::MODES,
+                collection: Form::Import::MODES,
                 item_wrapper_tag: 'li',
                 label_method: ->(mode) { safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }
 

From 850478dc140f16d407ade46f791e08f3ceb4bf2e Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 5 Sep 2024 10:41:14 -0400
Subject: [PATCH 24/91] Use `conflicted` configuration for renovate rebase
 strategy (#31770)

---
 .github/renovate.json5 | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 968c77cac..8a1067628 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -7,6 +7,7 @@
     ':prConcurrentLimitNone', // Remove limit for open PRs at any time.
     ':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
   ],
+  rebaseWhen: 'conflicted',
   minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it
   // packageRules order is important, they are applied from top to bottom and are merged,
   // meaning the most important ones must be at the bottom, for example grouping rules

From bc435c63bd99df757d02737674ca2f39aca49438 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 5 Sep 2024 16:57:53 +0200
Subject: [PATCH 25/91] Change width of columns in advanced web UI (#31762)

---
 app/javascript/styles/mastodon/components.scss | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 5936ef8a6..0b7c9ac90 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2789,7 +2789,7 @@ $ui-header-logo-wordmark-width: 99px;
 }
 
 .column {
-  width: 350px;
+  width: 400px;
   position: relative;
   box-sizing: border-box;
   display: flex;
@@ -2822,7 +2822,7 @@ $ui-header-logo-wordmark-width: 99px;
 }
 
 .drawer {
-  width: 300px;
+  width: 350px;
   box-sizing: border-box;
   display: flex;
   flex-direction: column;

From 5acec087caed4a2fdf0fd8ed11f891222496f321 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 5 Sep 2024 11:36:05 -0400
Subject: [PATCH 26/91] Simplify basic presence validations (#29664)

---
 spec/models/announcement_spec.rb          |  9 +-----
 spec/models/block_spec.rb                 | 13 ++-------
 spec/models/custom_emoji_category_spec.rb |  7 +----
 spec/models/custom_filter_spec.rb         | 15 ++--------
 spec/models/domain_allow_spec.rb          |  6 +---
 spec/models/domain_block_spec.rb          |  6 +---
 spec/models/follow_spec.rb                | 13 ++-------
 spec/models/import_spec.rb                | 17 ++---------
 spec/models/ip_block_spec.rb              | 15 ++--------
 spec/models/marker_spec.rb                | 11 ++-----
 spec/models/media_attachment_spec.rb      |  7 +----
 spec/models/mention_spec.rb               | 13 ++-------
 spec/models/poll_spec.rb                  |  7 ++---
 spec/models/user_spec.rb                  |  6 +---
 spec/models/webauthn_credential_spec.rb   | 35 +++--------------------
 spec/models/webhook_spec.rb               |  7 +----
 16 files changed, 28 insertions(+), 159 deletions(-)

diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb
index e3865e6fc..8bd1e74b2 100644
--- a/spec/models/announcement_spec.rb
+++ b/spec/models/announcement_spec.rb
@@ -64,14 +64,7 @@ RSpec.describe Announcement do
   end
 
   describe 'Validations' do
-    describe 'text' do
-      it 'validates presence of attribute' do
-        record = Fabricate.build(:announcement, text: nil)
-
-        expect(record).to_not be_valid
-        expect(record.errors[:text]).to be_present
-      end
-    end
+    it { is_expected.to validate_presence_of(:text) }
 
     describe 'ends_at' do
       it 'validates presence when starts_at is present' do
diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb
index 8249503c5..84f0f318f 100644
--- a/spec/models/block_spec.rb
+++ b/spec/models/block_spec.rb
@@ -4,17 +4,8 @@ require 'rails_helper'
 
 RSpec.describe Block do
   describe 'validations' do
-    it 'is invalid without an account' do
-      block = Fabricate.build(:block, account: nil)
-      block.valid?
-      expect(block).to model_have_error_on_field(:account)
-    end
-
-    it 'is invalid without a target_account' do
-      block = Fabricate.build(:block, target_account: nil)
-      block.valid?
-      expect(block).to model_have_error_on_field(:target_account)
-    end
+    it { is_expected.to belong_to(:account).required }
+    it { is_expected.to belong_to(:target_account).required }
   end
 
   it 'removes blocking cache after creation' do
diff --git a/spec/models/custom_emoji_category_spec.rb b/spec/models/custom_emoji_category_spec.rb
index 3da77344e..2b414a66e 100644
--- a/spec/models/custom_emoji_category_spec.rb
+++ b/spec/models/custom_emoji_category_spec.rb
@@ -4,11 +4,6 @@ require 'rails_helper'
 
 RSpec.describe CustomEmojiCategory do
   describe 'validations' do
-    it 'validates name presence' do
-      record = described_class.new(name: nil)
-
-      expect(record).to_not be_valid
-      expect(record).to model_have_error_on_field(:name)
-    end
+    it { is_expected.to validate_presence_of(:name) }
   end
 end
diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb
index 5bb615bb3..afbc42024 100644
--- a/spec/models/custom_filter_spec.rb
+++ b/spec/models/custom_filter_spec.rb
@@ -4,19 +4,8 @@ require 'rails_helper'
 
 RSpec.describe CustomFilter do
   describe 'Validations' do
-    it 'requires presence of title' do
-      record = described_class.new(title: '')
-      record.valid?
-
-      expect(record).to model_have_error_on_field(:title)
-    end
-
-    it 'requires presence of context' do
-      record = described_class.new(context: nil)
-      record.valid?
-
-      expect(record).to model_have_error_on_field(:context)
-    end
+    it { is_expected.to validate_presence_of(:title) }
+    it { is_expected.to validate_presence_of(:context) }
 
     it 'requires non-empty of context' do
       record = described_class.new(context: [])
diff --git a/spec/models/domain_allow_spec.rb b/spec/models/domain_allow_spec.rb
index 92f1ef8cc..d8f438f07 100644
--- a/spec/models/domain_allow_spec.rb
+++ b/spec/models/domain_allow_spec.rb
@@ -4,11 +4,7 @@ require 'rails_helper'
 
 RSpec.describe DomainAllow do
   describe 'Validations' do
-    it 'is invalid without a domain' do
-      domain_allow = Fabricate.build(:domain_allow, domain: nil)
-      domain_allow.valid?
-      expect(domain_allow).to model_have_error_on_field(:domain)
-    end
+    it { is_expected.to validate_presence_of(:domain) }
 
     it 'is invalid if the same normalized domain already exists' do
       _domain_allow = Fabricate(:domain_allow, domain: 'にゃん')
diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb
index d595441fd..8278454cd 100644
--- a/spec/models/domain_block_spec.rb
+++ b/spec/models/domain_block_spec.rb
@@ -4,11 +4,7 @@ require 'rails_helper'
 
 RSpec.describe DomainBlock do
   describe 'validations' do
-    it 'is invalid without a domain' do
-      domain_block = Fabricate.build(:domain_block, domain: nil)
-      domain_block.valid?
-      expect(domain_block).to model_have_error_on_field(:domain)
-    end
+    it { is_expected.to validate_presence_of(:domain) }
 
     it 'is invalid if the same normalized domain already exists' do
       _domain_block = Fabricate(:domain_block, domain: 'にゃん')
diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb
index 9aa172b2f..f22bd6ea8 100644
--- a/spec/models/follow_spec.rb
+++ b/spec/models/follow_spec.rb
@@ -9,17 +9,8 @@ RSpec.describe Follow do
   describe 'validations' do
     subject { described_class.new(account: alice, target_account: bob, rate_limit: true) }
 
-    it 'is invalid without an account' do
-      follow = Fabricate.build(:follow, account: nil)
-      follow.valid?
-      expect(follow).to model_have_error_on_field(:account)
-    end
-
-    it 'is invalid without a target_account' do
-      follow = Fabricate.build(:follow, target_account: nil)
-      follow.valid?
-      expect(follow).to model_have_error_on_field(:target_account)
-    end
+    it { is_expected.to belong_to(:account).required }
+    it { is_expected.to belong_to(:target_account).required }
 
     it 'is invalid if account already follows too many people' do
       alice.update(following_count: FollowLimitValidator::LIMIT)
diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb
index 10df5f8c0..587e0a9d2 100644
--- a/spec/models/import_spec.rb
+++ b/spec/models/import_spec.rb
@@ -3,19 +3,8 @@
 require 'rails_helper'
 
 RSpec.describe Import do
-  let(:account) { Fabricate(:account) }
-  let(:type) { 'following' }
-  let(:data) { attachment_fixture('imports.txt') }
-
-  describe 'validations' do
-    it 'is invalid without an type' do
-      import = described_class.create(account: account, data: data)
-      expect(import).to model_have_error_on_field(:type)
-    end
-
-    it 'is invalid without a data' do
-      import = described_class.create(account: account, type: type)
-      expect(import).to model_have_error_on_field(:data)
-    end
+  describe 'Validations' do
+    it { is_expected.to validate_presence_of(:type) }
+    it { is_expected.to validate_presence_of(:data) }
   end
 end
diff --git a/spec/models/ip_block_spec.rb b/spec/models/ip_block_spec.rb
index 6f1eb3842..ac9f5db60 100644
--- a/spec/models/ip_block_spec.rb
+++ b/spec/models/ip_block_spec.rb
@@ -4,19 +4,8 @@ require 'rails_helper'
 
 RSpec.describe IpBlock do
   describe 'validations' do
-    it 'validates ip presence', :aggregate_failures do
-      ip_block = described_class.new(ip: nil, severity: :no_access)
-
-      expect(ip_block).to_not be_valid
-      expect(ip_block).to model_have_error_on_field(:ip)
-    end
-
-    it 'validates severity presence', :aggregate_failures do
-      ip_block = described_class.new(ip: '127.0.0.1', severity: nil)
-
-      expect(ip_block).to_not be_valid
-      expect(ip_block).to model_have_error_on_field(:severity)
-    end
+    it { is_expected.to validate_presence_of(:ip) }
+    it { is_expected.to validate_presence_of(:severity) }
 
     it 'validates ip uniqueness', :aggregate_failures do
       described_class.create!(ip: '127.0.0.1', severity: :no_access)
diff --git a/spec/models/marker_spec.rb b/spec/models/marker_spec.rb
index 8339f8e25..1da8eb6f4 100644
--- a/spec/models/marker_spec.rb
+++ b/spec/models/marker_spec.rb
@@ -3,14 +3,7 @@
 require 'rails_helper'
 
 RSpec.describe Marker do
-  describe 'validations' do
-    describe 'timeline' do
-      it 'must be included in valid list' do
-        record = described_class.new(timeline: 'not real timeline')
-
-        expect(record).to_not be_valid
-        expect(record).to model_have_error_on_field(:timeline)
-      end
-    end
+  describe 'Validations' do
+    it { is_expected.to validate_inclusion_of(:timeline).in_array(described_class::TIMELINES) }
   end
 end
diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index 50f8d00a5..5f91ae096 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -257,12 +257,7 @@ RSpec.describe MediaAttachment, :attachment_processing do
     end
   end
 
-  it 'is invalid without file' do
-    media = described_class.new
-
-    expect(media.valid?).to be false
-    expect(media).to model_have_error_on_field(:file)
-  end
+  it { is_expected.to validate_presence_of(:file) }
 
   describe 'size limit validation' do
     it 'rejects video files that are too large' do
diff --git a/spec/models/mention_spec.rb b/spec/models/mention_spec.rb
index b241049a5..3a9b9fddf 100644
--- a/spec/models/mention_spec.rb
+++ b/spec/models/mention_spec.rb
@@ -4,16 +4,7 @@ require 'rails_helper'
 
 RSpec.describe Mention do
   describe 'validations' do
-    it 'is invalid without an account' do
-      mention = Fabricate.build(:mention, account: nil)
-      mention.valid?
-      expect(mention).to model_have_error_on_field(:account)
-    end
-
-    it 'is invalid without a status' do
-      mention = Fabricate.build(:mention, status: nil)
-      mention.valid?
-      expect(mention).to model_have_error_on_field(:status)
-    end
+    it { is_expected.to belong_to(:account).required }
+    it { is_expected.to belong_to(:status).required }
   end
 end
diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb
index 740ef63d8..736f3615d 100644
--- a/spec/models/poll_spec.rb
+++ b/spec/models/poll_spec.rb
@@ -32,12 +32,9 @@ RSpec.describe Poll do
 
   describe 'validations' do
     context 'when not valid' do
-      let(:poll) { Fabricate.build(:poll, expires_at: nil) }
+      subject { Fabricate.build(:poll) }
 
-      it 'is invalid without an expire date' do
-        poll.valid?
-        expect(poll).to model_have_error_on_field(:expires_at)
-      end
+      it { is_expected.to validate_presence_of(:expires_at) }
     end
   end
 end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 5c2af4dc3..fcff4c0d3 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -32,11 +32,7 @@ RSpec.describe User do
   end
 
   describe 'validations' do
-    it 'is invalid without an account' do
-      user = Fabricate.build(:user, account: nil)
-      user.valid?
-      expect(user).to model_have_error_on_field(:account)
-    end
+    it { is_expected.to belong_to(:account).required }
 
     it 'is invalid without a valid email' do
       user = Fabricate.build(:user, email: 'john@')
diff --git a/spec/models/webauthn_credential_spec.rb b/spec/models/webauthn_credential_spec.rb
index 23f0229a6..c4105d915 100644
--- a/spec/models/webauthn_credential_spec.rb
+++ b/spec/models/webauthn_credential_spec.rb
@@ -4,37 +4,10 @@ require 'rails_helper'
 
 RSpec.describe WebauthnCredential do
   describe 'validations' do
-    it 'is invalid without an external id' do
-      webauthn_credential = Fabricate.build(:webauthn_credential, external_id: nil)
-
-      webauthn_credential.valid?
-
-      expect(webauthn_credential).to model_have_error_on_field(:external_id)
-    end
-
-    it 'is invalid without a public key' do
-      webauthn_credential = Fabricate.build(:webauthn_credential, public_key: nil)
-
-      webauthn_credential.valid?
-
-      expect(webauthn_credential).to model_have_error_on_field(:public_key)
-    end
-
-    it 'is invalid without a nickname' do
-      webauthn_credential = Fabricate.build(:webauthn_credential, nickname: nil)
-
-      webauthn_credential.valid?
-
-      expect(webauthn_credential).to model_have_error_on_field(:nickname)
-    end
-
-    it 'is invalid without a sign_count' do
-      webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: nil)
-
-      webauthn_credential.valid?
-
-      expect(webauthn_credential).to model_have_error_on_field(:sign_count)
-    end
+    it { is_expected.to validate_presence_of(:external_id) }
+    it { is_expected.to validate_presence_of(:public_key) }
+    it { is_expected.to validate_presence_of(:nickname) }
+    it { is_expected.to validate_presence_of(:sign_count) }
 
     it 'is invalid if already exist a webauthn credential with the same external id' do
       Fabricate(:webauthn_credential, external_id: '_Typ0ygudDnk9YUVWLQayw')
diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb
index 1b2d803bd..03ef1dcc6 100644
--- a/spec/models/webhook_spec.rb
+++ b/spec/models/webhook_spec.rb
@@ -6,12 +6,7 @@ RSpec.describe Webhook do
   let(:webhook) { Fabricate(:webhook) }
 
   describe 'Validations' do
-    it 'requires presence of events' do
-      record = described_class.new(events: nil)
-      record.valid?
-
-      expect(record).to model_have_error_on_field(:events)
-    end
+    it { is_expected.to validate_presence_of(:events) }
 
     it 'requires non-empty events value' do
       record = described_class.new(events: [])

From 09017dd8f063926738b253fe964a6b12faaa744f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 5 Sep 2024 15:51:17 -0400
Subject: [PATCH 27/91] Add worker spec for annual report worker (#31778)

---
 .../generate_annual_report_worker_spec.rb     | 27 +++++++++++++++++++
 1 file changed, 27 insertions(+)
 create mode 100644 spec/workers/generate_annual_report_worker_spec.rb

diff --git a/spec/workers/generate_annual_report_worker_spec.rb b/spec/workers/generate_annual_report_worker_spec.rb
new file mode 100644
index 000000000..69b0d4424
--- /dev/null
+++ b/spec/workers/generate_annual_report_worker_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe GenerateAnnualReportWorker do
+  let(:worker) { described_class.new }
+  let(:account) { Fabricate :account }
+
+  describe '#perform' do
+    it 'generates new report for the account' do
+      expect { worker.perform(account.id, Date.current.year) }
+        .to change(account_reports, :count).by(1)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, Date.current.year)
+
+      expect(result).to be(true)
+    end
+
+    def account_reports
+      GeneratedAnnualReport
+        .where(account: account)
+        .where(year: Date.current.year)
+    end
+  end
+end

From 7efe0bde9dc363de57fc350dbcdb0b099bd1329a Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 5 Sep 2024 16:05:38 -0400
Subject: [PATCH 28/91] Add `have_http_link_header` matcher and set header
 values as strings (#31010)

---
 .../concerns/account_controller_concern.rb    |  2 +-
 app/controllers/concerns/api/pagination.rb    |  2 +-
 app/controllers/statuses_controller.rb        |  4 ++-
 .../account_controller_concern_spec.rb        | 13 +++-----
 spec/requests/accounts_spec.rb                |  3 +-
 spec/requests/api/v1/timelines/home_spec.rb   |  5 +--
 spec/requests/api/v1/timelines/list_spec.rb   |  5 +--
 spec/requests/link_headers_spec.rb            | 24 +++-----------
 spec/support/matchers/api_pagination.rb       |  2 +-
 spec/support/matchers/http_link_header.rb     | 33 +++++++++++++++++++
 10 files changed, 54 insertions(+), 39 deletions(-)
 create mode 100644 spec/support/matchers/http_link_header.rb

diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb
index d63bcc85c..b75f3e358 100644
--- a/app/controllers/concerns/account_controller_concern.rb
+++ b/app/controllers/concerns/account_controller_concern.rb
@@ -20,7 +20,7 @@ module AccountControllerConcern
         webfinger_account_link,
         actor_url_link,
       ]
-    )
+    ).to_s
   end
 
   def webfinger_account_link
diff --git a/app/controllers/concerns/api/pagination.rb b/app/controllers/concerns/api/pagination.rb
index 7f06dc020..b0b4ae460 100644
--- a/app/controllers/concerns/api/pagination.rb
+++ b/app/controllers/concerns/api/pagination.rb
@@ -19,7 +19,7 @@ module Api::Pagination
     links = []
     links << [next_path, [%w(rel next)]] if next_path
     links << [prev_path, [%w(rel prev)]] if prev_path
-    response.headers['Link'] = LinkHeader.new(links) unless links.empty?
+    response.headers['Link'] = LinkHeader.new(links).to_s unless links.empty?
   end
 
   def require_valid_pagination_options!
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index db7eddd78..a0885b469 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -56,7 +56,9 @@ class StatusesController < ApplicationController
   end
 
   def set_link_headers
-    response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]])
+    response.headers['Link'] = LinkHeader.new(
+      [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
+    ).to_s
   end
 
   def set_status
diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb
index 3eee46d7b..384406a0e 100644
--- a/spec/controllers/concerns/account_controller_concern_spec.rb
+++ b/spec/controllers/concerns/account_controller_concern_spec.rb
@@ -54,17 +54,12 @@ RSpec.describe AccountControllerConcern do
     it 'Prepares the account, returns success, and sets link headers' do
       get 'success', params: { account_username: account.username }
 
-      expect(response).to have_http_status(200)
-      expect(response.headers['Link'].to_s).to eq(expected_link_headers)
+      expect(response)
+        .to have_http_status(200)
+        .and have_http_link_header('http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io').for(rel: 'lrdd', type: 'application/jrd+json')
+        .and have_http_link_header('https://cb6e6126.ngrok.io/users/username').for(rel: 'alternate', type: 'application/activity+json')
       expect(response.body)
         .to include(account.username)
     end
-
-    def expected_link_headers
-      [
-        '<http://test.host/.well-known/webfinger?resource=acct%3Ausername%40cb6e6126.ngrok.io>; rel="lrdd"; type="application/jrd+json"',
-        '<https://cb6e6126.ngrok.io/users/username>; rel="alternate"; type="application/activity+json"',
-      ].join(', ')
-    end
   end
 end
diff --git a/spec/requests/accounts_spec.rb b/spec/requests/accounts_spec.rb
index d53816eff..3615868d7 100644
--- a/spec/requests/accounts_spec.rb
+++ b/spec/requests/accounts_spec.rb
@@ -69,8 +69,7 @@ RSpec.describe 'Accounts show response' do
             expect(response)
               .to have_http_status(200)
               .and render_template(:show)
-
-            expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account)
+              .and have_http_link_header(ActivityPub::TagManager.instance.uri_for(account)).for(rel: 'alternate')
           end
         end
 
diff --git a/spec/requests/api/v1/timelines/home_spec.rb b/spec/requests/api/v1/timelines/home_spec.rb
index d158e0801..9dd102fcb 100644
--- a/spec/requests/api/v1/timelines/home_spec.rb
+++ b/spec/requests/api/v1/timelines/home_spec.rb
@@ -94,8 +94,9 @@ RSpec.describe 'Home', :inline_jobs do
       it 'returns http unprocessable entity', :aggregate_failures do
         subject
 
-        expect(response).to have_http_status(422)
-        expect(response.headers['Link']).to be_nil
+        expect(response)
+          .to have_http_status(422)
+          .and not_have_http_link_header
       end
     end
   end
diff --git a/spec/requests/api/v1/timelines/list_spec.rb b/spec/requests/api/v1/timelines/list_spec.rb
index 753c78486..eb4395d1f 100644
--- a/spec/requests/api/v1/timelines/list_spec.rb
+++ b/spec/requests/api/v1/timelines/list_spec.rb
@@ -47,8 +47,9 @@ RSpec.describe 'API V1 Timelines List' do
       it 'returns http unprocessable entity' do
         get "/api/v1/timelines/list/#{list.id}", headers: headers
 
-        expect(response).to have_http_status(422)
-        expect(response.headers['Link']).to be_nil
+        expect(response)
+          .to have_http_status(422)
+          .and not_have_http_link_header
       end
     end
   end
diff --git a/spec/requests/link_headers_spec.rb b/spec/requests/link_headers_spec.rb
index 3116a54d6..e20f5e79e 100644
--- a/spec/requests/link_headers_spec.rb
+++ b/spec/requests/link_headers_spec.rb
@@ -6,28 +6,12 @@ RSpec.describe 'Link headers' do
   describe 'on the account show page' do
     let(:account) { Fabricate(:account, username: 'test') }
 
-    before do
+    it 'contains webfinger and activitypub urls in link header' do
       get short_account_path(username: account)
-    end
 
-    it 'contains webfinger url in link header' do
-      link_header = link_header_with_type('application/jrd+json')
-
-      expect(link_header.href).to eq 'https://cb6e6126.ngrok.io/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io'
-      expect(link_header.attr_pairs.first).to eq %w(rel lrdd)
-    end
-
-    it 'contains activitypub url in link header' do
-      link_header = link_header_with_type('application/activity+json')
-
-      expect(link_header.href).to eq 'https://cb6e6126.ngrok.io/users/test'
-      expect(link_header.attr_pairs.first).to eq %w(rel alternate)
-    end
-
-    def link_header_with_type(type)
-      LinkHeader.parse(response.headers['Link'].to_s).links.find do |link|
-        link.attr_pairs.any?(['type', type])
-      end
+      expect(response)
+        .to have_http_link_header('https://cb6e6126.ngrok.io/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io').for(rel: 'lrdd', type: 'application/jrd+json')
+        .and have_http_link_header('https://cb6e6126.ngrok.io/users/test').for(rel: 'alternate', type: 'application/activity+json')
     end
   end
 end
diff --git a/spec/support/matchers/api_pagination.rb b/spec/support/matchers/api_pagination.rb
index f7d552b24..1a4f53a84 100644
--- a/spec/support/matchers/api_pagination.rb
+++ b/spec/support/matchers/api_pagination.rb
@@ -3,7 +3,7 @@
 RSpec::Matchers.define :include_pagination_headers do |links|
   match do |response|
     links.map do |key, value|
-      response.headers['Link'].find_link(['rel', key.to_s]).href == value
+      expect(response).to have_http_link_header(value).for(rel: key.to_s)
     end.all?
   end
 
diff --git a/spec/support/matchers/http_link_header.rb b/spec/support/matchers/http_link_header.rb
new file mode 100644
index 000000000..3e658071c
--- /dev/null
+++ b/spec/support/matchers/http_link_header.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :have_http_link_header do |href|
+  match do |response|
+    @response = response
+
+    header_link&.href == href
+  end
+
+  match_when_negated do |response|
+    response.headers['Link'].blank?
+  end
+
+  chain :for do |attributes|
+    @attributes = attributes
+  end
+
+  failure_message do |response|
+    "Expected `#{response.headers['Link']}` to include `href` value of `#{href}` for `#{@attributes}` but it did not."
+  end
+
+  failure_message_when_negated do
+    "Expected response not to have a `Link` header but `#{response.headers['Link']}` is present."
+  end
+
+  def header_link
+    LinkHeader
+      .parse(@response.headers['Link'])
+      .find_link(*@attributes.stringify_keys)
+  end
+end
+
+RSpec::Matchers.define_negated_matcher :not_have_http_link_header, :have_http_link_header # Allow chaining

From 60182db0ca49f863d8f81f50f31b22386f734c90 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 6 Sep 2024 09:30:53 +0200
Subject: [PATCH 29/91] Update dependency tzinfo-data to v1.2024.2 (#31780)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index c70823d05..8577a5269 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -857,7 +857,7 @@ GEM
       unf (~> 0.1.0)
     tzinfo (2.0.6)
       concurrent-ruby (~> 1.0)
-    tzinfo-data (1.2024.1)
+    tzinfo-data (1.2024.2)
       tzinfo (>= 1.0.0)
     unf (0.1.4)
       unf_ext

From cc4865193a4454c8650bd6877c30dfec68d69d55 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 6 Sep 2024 07:38:08 +0000
Subject: [PATCH 30/91] New Crowdin Translations (automated) (#31781)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/fi.json |  6 +++---
 app/javascript/mastodon/locales/ga.json |  4 ++++
 app/javascript/mastodon/locales/ru.json |  2 +-
 config/locales/fi.yml                   | 14 +++++++-------
 config/locales/fr-CA.yml                |  2 ++
 config/locales/fr.yml                   |  2 ++
 config/locales/fy.yml                   |  1 +
 config/locales/ga.yml                   |  1 +
 config/locales/gd.yml                   |  1 +
 config/locales/pt-BR.yml                | 10 +++++++++-
 config/locales/simple_form.fr-CA.yml    |  1 +
 config/locales/simple_form.fr.yml       |  1 +
 config/locales/zh-CN.yml                |  1 +
 13 files changed, 34 insertions(+), 12 deletions(-)

diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 39df3e010..79ca95a58 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -1,6 +1,6 @@
 {
   "about.blocks": "Moderoidut palvelimet",
-  "about.contact": "Yhteydenotto:",
+  "about.contact": "Yhteystiedot:",
   "about.disclaimer": "Mastodon on vapaa avoimen lähdekoodin ohjelmisto ja Mastodon gGmbH:n tavaramerkki.",
   "about.domain_blocks.no_reason_available": "Syy ei ole tiedossa",
   "about.domain_blocks.preamble": "Mastodonin avulla voi yleensä tarkastella minkä tahansa fediversumiin kuuluvan palvelimen sisältöä ja olla yhteyksissä eri palvelinten käyttäjien kanssa. Nämä poikkeukset koskevat yksin tätä palvelinta.",
@@ -304,7 +304,7 @@
   "filter_modal.select_filter.title": "Suodata tämä julkaisu",
   "filter_modal.title.status": "Suodata julkaisu",
   "filter_warning.matches_filter": "Vastaa suodatinta ”{title}”",
-  "filtered_notifications_banner.pending_requests": "{count, plural, =0 {Ei keneltäkään, jonka} one {1 käyttäjältä, jonka} other {# käyttäjältä, jotka}} saatat tuntea",
+  "filtered_notifications_banner.pending_requests": "{count, plural, =0 {Ei keneltäkään, jonka} one {Yhdeltä käyttäjältä, jonka} other {# käyttäjältä, jotka}} saatat tuntea",
   "filtered_notifications_banner.title": "Suodatetut ilmoitukset",
   "firehose.all": "Kaikki",
   "firehose.local": "Tämä palvelin",
@@ -370,7 +370,7 @@
   "home.show_announcements": "Näytä tiedotteet",
   "ignore_notifications_modal.disclaimer": "Mastodon ei voi ilmoittaa käyttäjille, että olet sivuuttanut heidän ilmoituksensa. Ilmoitusten sivuuttaminen ei lopeta itse viestien lähetystä.",
   "ignore_notifications_modal.filter_instead": "Suodata sen sijaan",
-  "ignore_notifications_modal.filter_to_act_users": "Voit silti hyväksyä, hylätä tai raportoida käyttäjiä",
+  "ignore_notifications_modal.filter_to_act_users": "Voit kuitenkin yhä hyväksyä, hylätä tai raportoida käyttäjiä",
   "ignore_notifications_modal.filter_to_avoid_confusion": "Suodatus auttaa välttämään mahdollisia sekaannuksia",
   "ignore_notifications_modal.filter_to_review_separately": "Voit käydä suodatettuja ilmoituksia läpi erikseen",
   "ignore_notifications_modal.ignore": "Sivuuta ilmoitukset",
diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json
index 71577be95..b51d4adf6 100644
--- a/app/javascript/mastodon/locales/ga.json
+++ b/app/javascript/mastodon/locales/ga.json
@@ -97,6 +97,8 @@
   "block_modal.title": "An bhfuil fonn ort an t-úsáideoir a bhlocáil?",
   "block_modal.you_wont_see_mentions": "Ní fheicfidh tú postálacha a luann iad.",
   "boost_modal.combo": "Is féidir leat {combo} a bhrú chun é seo a scipeáil an chéad uair eile",
+  "boost_modal.reblog": "An post a threisiú?",
+  "boost_modal.undo_reblog": "An deireadh a chur le postáil?",
   "bundle_column_error.copy_stacktrace": "Cóipeáil tuairisc earráide",
   "bundle_column_error.error.body": "Ní féidir an leathanach a iarradh a sholáthar. Seans gurb amhlaidh mar gheall ar fhabht sa chód, nó mar gheall ar mhíréireacht leis an mbrabhsálaí.",
   "bundle_column_error.error.title": "Ó, níl sé sin go maith!",
@@ -467,6 +469,7 @@
   "mute_modal.you_wont_see_mentions": "Ní fheicfidh tú postálacha a luann iad.",
   "mute_modal.you_wont_see_posts": "Is féidir leo do phoist a fheiceáil go fóill, ach ní fheicfidh tú a gcuid postanna.",
   "navigation_bar.about": "Maidir le",
+  "navigation_bar.administration": "Riarachán",
   "navigation_bar.advanced_interface": "Oscail i gcomhéadan gréasáin chun cinn",
   "navigation_bar.blocks": "Cuntais bhactha",
   "navigation_bar.bookmarks": "Leabharmharcanna",
@@ -483,6 +486,7 @@
   "navigation_bar.follows_and_followers": "Ag leanúint agus do do leanúint",
   "navigation_bar.lists": "Liostaí",
   "navigation_bar.logout": "Logáil Amach",
+  "navigation_bar.moderation": "Measarthacht",
   "navigation_bar.mutes": "Úsáideoirí balbhaithe",
   "navigation_bar.opened_in_classic_interface": "Osclaítear poist, cuntais agus leathanaigh shonracha eile de réir réamhshocraithe sa chomhéadan gréasáin clasaiceach.",
   "navigation_bar.personal": "Pearsanta",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index cd64e8b8e..4439b8dc0 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -48,7 +48,7 @@
   "account.moved_to": "У {name} теперь новый аккаунт:",
   "account.mute": "Игнорировать @{name}",
   "account.mute_notifications_short": "Отключить уведомления",
-  "account.mute_short": "Немой",
+  "account.mute_short": "Глохни!",
   "account.muted": "Игнорируется",
   "account.mutual": "Взаимно",
   "account.no_bio": "Описание не предоставлено.",
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 42d197766..8f0c80f1d 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -442,9 +442,9 @@ fi:
         create: Lisää verkkotunnus
         resolve: Selvitä verkkotunnus
         title: Estä uusi sähköpostiverkkotunnus
-      no_email_domain_block_selected: Sähköpostin verkkotunnuksia ei muutettu, koska yhtään ei ollut valittuna
+      no_email_domain_block_selected: Sähköpostiverkkotunnusten estoja ei muutettu; yhtäkään ei ollut valittu
       not_permitted: Ei sallittu
-      resolved_dns_records_hint_html: Verkkotunnuksen nimi määräytyy seuraaviin MX-verkkotunnuksiin, jotka ovat viime kädessä vastuussa sähköpostin vastaanottamisesta. MX-verkkotunnuksen estäminen estää rekisteröitymisen mistä tahansa sähköpostiosoitteesta, joka käyttää samaa MX-verkkotunnusta, vaikka näkyvä verkkotunnuksen nimi olisikin erilainen. <strong>Varo estämästä suuria sähköpostin palveluntarjoajia.</strong>
+      resolved_dns_records_hint_html: Verkkotunnusnimi kytkeytyy seuraaviin MX-verkkotunnuksiin, jotka ovat viime kädessä vastuussa sähköpostin vastaanottamisesta. MX-verkkotunnuksen estäminen estää rekisteröitymisen mistä tahansa sähköpostiosoitteesta, joka käyttää samaa MX-verkkotunnusta, vaikka näkyvä verkkotunnuksen nimi olisikin erilainen. <strong>Varo estämästä suuria sähköpostipalvelujen tarjoajia.</strong>
       resolved_through_html: Ratkaistu verkkotunnuksen %{domain} kautta
       title: Estetyt sähköpostiverkkotunnukset
     export_domain_allows:
@@ -600,7 +600,7 @@ fi:
         resolve_description_html: Ilmoitettua tiliä kohtaan ei ryhdytä toimiin, varoitusta ei kirjata ja raportti suljetaan.
         silence_description_html: Tili näkyy vain niille, jotka jo seuraavat sitä tai etsivät sen manuaalisesti, mikä rajoittaa merkittävästi sen tavoitettavuutta. Voidaan perua milloin vain. Sulkee kaikki tiliin kohdistuvat raportit.
         suspend_description_html: Tili ja mikään sen sisältö eivät ole käytettävissä, ja lopulta ne poistetaan ja vuorovaikutus tilin kanssa on mahdotonta. Peruttavissa 30 päivän ajan. Sulkee kaikki tiliin kohdistuvat raportit.
-      actions_description_html: Päätä, mihin toimiin ryhdyt tämän raportin ratkaisemiseksi. Jos ryhdyt rangaistustoimeen ilmoitettua tiliä kohtaan, hänelle lähetetään sähköposti-ilmoitus, paitsi jos <strong>Roskaposti</strong>-luokka on valittuna.
+      actions_description_html: Päätä, mihin toimiin ryhdyt tämän raportin ratkaisemiseksi. Jos ryhdyt rangaistustoimeen ilmoitettua tiliä kohtaan, hänelle lähetetään sähköpostitse ilmoitus asiasta, paitsi jos valittuna on <strong>Roskaposti</strong>-luokka.
       actions_description_remote_html: Päätä, mihin toimiin ryhdyt tämän raportin ratkaisemiseksi. Tämä vaikuttaa vain siihen, miten <strong>sinun</strong> palvelimesi viestii tämän etätilin kanssa ja käsittelee sen sisältöä.
       add_to_report: Lisää raporttiin
       already_suspended_badges:
@@ -977,7 +977,7 @@ fi:
         used_by_over_week:
           one: Käyttänyt yksi käyttäjä viimeisen viikon aikana
           other: Käyttänyt %{count} käyttäjää viimeisen viikon aikana
-      title: Suositukset ja trendit
+      title: Suositukset ja suuntaukset
       trending: Trendaus
     warning_presets:
       add_new: Lisää uusi
@@ -1135,7 +1135,7 @@ fi:
     security: Turvallisuus
     set_new_password: Aseta uusi salasana
     setup:
-      email_below_hint_html: Tarkista roskapostikansiosi tai pyydä uusi viesti. Voit korjata sähköpostiosoitteesi, jos se oli väärin.
+      email_below_hint_html: Tarkista roskapostikansiosi tai pyydä uusi viesti. Voit myös korjata sähköpostiosoitteesi tarvittaessa.
       email_settings_hint_html: Napsauta lähettämäämme linkkiä vahvistaaksesi osoitteen %{email}. Odotamme täällä.
       link_not_received: Etkö saanut linkkiä?
       new_confirmation_instructions_sent: Saat pian uuden vahvistuslinkin sisältävän sähköpostiviestin!
@@ -1195,8 +1195,8 @@ fi:
       caches: Muiden palvelinten välimuistiinsa tallentamaa sisältöä voi säilyä
       data_removal: Julkaisusi ja muut tietosi poistetaan pysyvästi
       email_change_html: Voit <a href="%{path}">muuttaa sähköpostiosoitettasi</a> poistamatta tiliäsi
-      email_contact_html: Jos ei saavu perille, voit pyytää apua sähköpostilla <a href="mailto:%{email}">%{email}</a>
-      email_reconfirmation_html: Jos et saa vahvistuksen sähköpostia, niin voit <a href="%{path}">pyytää sitä uudelleen</a>
+      email_contact_html: Mikäli viesti ei vieläkään saavu perille, voit pyytää apua sähköpostitse osoitteella <a href="mailto:%{email}">%{email}</a>
+      email_reconfirmation_html: Jos et saa vahvistussähköpostiviestiä, voit <a href="%{path}">pyytää sitä uudelleen</a>
       irreversible: Et voi palauttaa tiliäsi etkä aktivoida sitä uudelleen
       more_details_html: Tarkempia tietoja saat <a href="%{terms_path}">tietosuojakäytännöstämme</a>.
       username_available: Käyttäjänimesi tulee saataville uudelleen
diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml
index 45c6baece..140bc9434 100644
--- a/config/locales/fr-CA.yml
+++ b/config/locales/fr-CA.yml
@@ -132,6 +132,7 @@ fr-CA:
       resubscribe: Se réabonner
       role: Rôle
       search: Rechercher
+      search_same_email_domain: Autres utilisateurs avec le même domaine de courriel
       search_same_ip: Autres utilisateur·rice·s avec la même IP
       security: Sécurité
       security_measures:
@@ -1428,6 +1429,7 @@ fr-CA:
   media_attachments:
     validations:
       images_and_video: Impossible de joindre une vidéo à un message contenant déjà des images
+      not_found: Média %{ids} introuvable ou déjà attaché à un autre message
       not_ready: Impossible de joindre les fichiers en cours de traitement. Réessayez dans un instant !
       too_many: Impossible de joindre plus de 4 fichiers
   migrations:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 4434a1653..5646877d2 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -132,6 +132,7 @@ fr:
       resubscribe: Se réabonner
       role: Rôle
       search: Rechercher
+      search_same_email_domain: Autres utilisateurs avec le même domaine de courriel
       search_same_ip: Autres utilisateur·rice·s avec la même IP
       security: Sécurité
       security_measures:
@@ -1428,6 +1429,7 @@ fr:
   media_attachments:
     validations:
       images_and_video: Impossible de joindre une vidéo à un message contenant déjà des images
+      not_found: Média %{ids} introuvable ou déjà attaché à un autre message
       not_ready: Impossible de joindre les fichiers en cours de traitement. Réessayez dans un instant !
       too_many: Impossible de joindre plus de 4 fichiers
   migrations:
diff --git a/config/locales/fy.yml b/config/locales/fy.yml
index d0127f77b..de7495dd5 100644
--- a/config/locales/fy.yml
+++ b/config/locales/fy.yml
@@ -1454,6 +1454,7 @@ fy:
   media_attachments:
     validations:
       images_and_video: In fideo kin net oan in berjocht mei ôfbyldingen keppele wurde
+      not_found: Media %{ids} net fûn of al tafoege oan in oar berjocht
       not_ready: Kin gjin bestannen tafoegje dy’t noch net ferwurke binne. Probearje it letter opnij!
       too_many: Der kinne net mear as 4 ôfbyldingen tafoege wurde
   migrations:
diff --git a/config/locales/ga.yml b/config/locales/ga.yml
index 27237bf3a..ad9ee21d3 100644
--- a/config/locales/ga.yml
+++ b/config/locales/ga.yml
@@ -1532,6 +1532,7 @@ ga:
   media_attachments:
     validations:
       images_and_video: Ní féidir físeán a cheangal le postáil a bhfuil íomhánna ann cheana féin
+      not_found: Meán %{ids} gan aimsiú nó ceangailte le postáil eile cheana
       not_ready: Ní féidir comhaid nach bhfuil próiseáil críochnaithe acu a cheangal. Bain triail eile as i gceann nóiméad!
       too_many: Ní féidir níos mó ná 4 chomhad a cheangal
   migrations:
diff --git a/config/locales/gd.yml b/config/locales/gd.yml
index ef4add838..2af647ab9 100644
--- a/config/locales/gd.yml
+++ b/config/locales/gd.yml
@@ -1506,6 +1506,7 @@ gd:
   media_attachments:
     validations:
       images_and_video: Chan urrainn dhut video a cheangal ri post sa bheil dealbh mu thràth
+      not_found: Cha deach na meadhanan %{ids} a lorg no chaidh an ceangal ri post eile mu thràth
       not_ready: Chan urrainn dhuinn faidhlichean a cheangal ris nach eil air am pròiseasadh fhathast. Feuch ris a-rithist an ceann greis!
       too_many: Chan urrainn dhut barrachd air 4 faidhlichean a ceangal ris
   migrations:
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 579dbd967..18496b9e1 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -203,7 +203,7 @@ pt-BR:
         disable_sign_in_token_auth_user: Desativar autenticação via Token de Email para Usuário
         disable_user: Desativar usuário
         enable_custom_emoji: Ativar emoji personalizado
-        enable_sign_in_token_auth_user: Desativar autenticação via token por e-mail para o usuário
+        enable_sign_in_token_auth_user: Ativar autenticação via Token de Email para Usuário
         enable_user: Ativar usuário
         memorialize_account: Converter conta em memorial
         promote_user: Promover usuário
@@ -442,6 +442,7 @@ pt-BR:
         create: Adicionar domínio
         resolve: Resolver domínio
         title: Bloquear novo domínio de e-mail
+      no_email_domain_block_selected: Nenhum bloco de domínio de email foi alterado, pois, nenhum foi selecionado
       not_permitted: Não permitido
       resolved_dns_records_hint_html: O nome de domínio é associado aos seguintes domínios MX, que são responsáveis por aceitar e-mails. Ao bloquear um domínio MX, você bloqueará as inscrições de qualquer endereço de e-mail que use o mesmo domínio MX, mesmo que o nome de domínio visível seja diferente. <strong>Tenha cuidado para não bloquear os principais provedores de e-mail.</strong>
       resolved_through_html: Resolvido através de %{domain}
@@ -1440,6 +1441,13 @@ pt-BR:
       action: Sim, cancelar subscrição
       complete: Desinscrito
       confirmation_html: Tem certeza que deseja cancelar a assinatura de %{type} para Mastodon no %{domain} para o seu endereço de e-mail %{email}? Você sempre pode se inscrever novamente nas <a href="%{settings_path}">configurações de notificação de email</a>.
+      emails:
+        notification_emails:
+          favourite: emails de notificação favoritos
+          follow: seguir emails de notificação
+          follow_request: emails de seguidores pendentes
+          mention: emails de notificação de menções
+          reblog: emails de notificação de boosts
       resubscribe_html: Se você cancelou sua inscrição por engano, você pode se inscrever novamente em suas <a href="%{settings_path}"> configurações de notificações por e-mail</a>.
       success_html: Você não mais receberá %{type} no Mastodon em %{domain} ao seu endereço de e-mail %{email}.
       title: Cancelar inscrição
diff --git a/config/locales/simple_form.fr-CA.yml b/config/locales/simple_form.fr-CA.yml
index e8aafa4b1..640d7e344 100644
--- a/config/locales/simple_form.fr-CA.yml
+++ b/config/locales/simple_form.fr-CA.yml
@@ -81,6 +81,7 @@ fr-CA:
         bootstrap_timeline_accounts: Ces comptes seront épinglés en tête de liste des recommandations pour les nouveaux utilisateurs.
         closed_registrations_message: Affiché lorsque les inscriptions sont fermées
         custom_css: Vous pouvez appliquer des styles personnalisés sur la version Web de Mastodon.
+        favicon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
         mascot: Remplace l'illustration dans l'interface Web avancée.
         media_cache_retention_period: Les fichiers médias des messages publiés par des utilisateurs distants sont mis en cache sur votre serveur. Lorsque cette valeur est positive, les médias sont supprimés au terme du nombre de jours spécifié. Si les données des médias sont demandées après leur suppression, elles seront téléchargées à nouveau, dans la mesure où le contenu source est toujours disponible. En raison des restrictions concernant la fréquence à laquelle les cartes de prévisualisation des liens interrogent des sites tiers, il est recommandé de fixer cette valeur à au moins 14 jours, faute de quoi les cartes de prévisualisation des liens ne seront pas mises à jour à la demande avant cette échéance.
         peers_api_enabled: Une liste de noms de domaine que ce serveur a rencontrés dans le fédiverse. Aucune donnée indiquant si vous vous fédérez ou non avec un serveur particulier n'est incluse ici, seulement l'information que votre serveur connaît un autre serveur. Cette option est utilisée par les services qui collectent des statistiques sur la fédération en général.
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index 315e22c5f..04d48573a 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -81,6 +81,7 @@ fr:
         bootstrap_timeline_accounts: Ces comptes seront épinglés en tête de liste des recommandations pour les nouveaux utilisateurs.
         closed_registrations_message: Affiché lorsque les inscriptions sont fermées
         custom_css: Vous pouvez appliquer des styles personnalisés sur la version Web de Mastodon.
+        favicon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
         mascot: Remplace l'illustration dans l'interface Web avancée.
         media_cache_retention_period: Les fichiers médias des messages publiés par des utilisateurs distants sont mis en cache sur votre serveur. Lorsque cette valeur est positive, les médias sont supprimés au terme du nombre de jours spécifié. Si les données des médias sont demandées après leur suppression, elles seront téléchargées à nouveau, dans la mesure où le contenu source est toujours disponible. En raison des restrictions concernant la fréquence à laquelle les cartes de prévisualisation des liens interrogent des sites tiers, il est recommandé de fixer cette valeur à au moins 14 jours, faute de quoi les cartes de prévisualisation des liens ne seront pas mises à jour à la demande avant cette échéance.
         peers_api_enabled: Une liste de noms de domaine que ce serveur a rencontrés dans le fédiverse. Aucune donnée indiquant si vous vous fédérez ou non avec un serveur particulier n'est incluse ici, seulement l'information que votre serveur connaît un autre serveur. Cette option est utilisée par les services qui collectent des statistiques sur la fédération en général.
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index c31a68926..a8d401c7b 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -1428,6 +1428,7 @@ zh-CN:
   media_attachments:
     validations:
       images_and_video: 无法在嘟文中同时插入视频和图片
+      not_found: 未发现媒体%{ids} 或已附在另一条嘟文中
       not_ready: 不能附加还在处理中的文件。请稍后再试!
       too_many: 最多只能添加 4 张图片
   migrations:

From be77a1098bef771dffe85d94e8ed4ddeb53cc768 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 6 Sep 2024 03:49:38 -0400
Subject: [PATCH 31/91] Extract `Account::AUTOMATED_ACTOR_TYPES` for "bot"
 actor_type values (#31772)

---
 app/models/account.rb        | 7 +++++--
 lib/mastodon/cli/accounts.rb | 2 +-
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/app/models/account.rb b/app/models/account.rb
index c20b72658..d773d3344 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -75,6 +75,8 @@ class Account < ApplicationRecord
   DISPLAY_NAME_LENGTH_LIMIT = 30
   NOTE_LENGTH_LIMIT = 500
 
+  AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze
+
   include Attachmentable # Load prior to Avatar & Header concerns
 
   include Account::Associations
@@ -127,7 +129,8 @@ class Account < ApplicationRecord
   scope :without_silenced, -> { where(silenced_at: nil) }
   scope :without_instance_actor, -> { where.not(id: INSTANCE_ACTOR_ID) }
   scope :recent, -> { reorder(id: :desc) }
-  scope :bots, -> { where(actor_type: %w(Application Service)) }
+  scope :bots, -> { where(actor_type: AUTOMATED_ACTOR_TYPES) }
+  scope :non_automated, -> { where.not(actor_type: AUTOMATED_ACTOR_TYPES) }
   scope :groups, -> { where(actor_type: 'Group') }
   scope :alphabetic, -> { order(domain: :asc, username: :asc) }
   scope :matches_uri_prefix, ->(value) { where(arel_table[:uri].matches("#{sanitize_sql_like(value)}/%", false, true)).or(where(uri: value)) }
@@ -183,7 +186,7 @@ class Account < ApplicationRecord
   end
 
   def bot?
-    %w(Application Service).include? actor_type
+    AUTOMATED_ACTOR_TYPES.include?(actor_type)
   end
 
   def instance_actor?
diff --git a/lib/mastodon/cli/accounts.rb b/lib/mastodon/cli/accounts.rb
index 0cdf68158..8a138323d 100644
--- a/lib/mastodon/cli/accounts.rb
+++ b/lib/mastodon/cli/accounts.rb
@@ -502,7 +502,7 @@ module Mastodon::CLI
       - not muted/blocked by us
     LONG_DESC
     def prune
-      query = Account.remote.where.not(actor_type: %i(Application Service))
+      query = Account.remote.non_automated
       query = query.where('NOT EXISTS (SELECT 1 FROM mentions WHERE account_id = accounts.id)')
       query = query.where('NOT EXISTS (SELECT 1 FROM favourites WHERE account_id = accounts.id)')
       query = query.where('NOT EXISTS (SELECT 1 FROM statuses WHERE account_id = accounts.id)')

From 6b6a80b407e03c31326deb9838bc9c199bab39ea Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 6 Sep 2024 05:58:46 -0400
Subject: [PATCH 32/91] Remove `body_as_json` in favor of built-in
 `response.parsed_body` for JSON response specs (#31749)

---
 .../collections_controller_spec.rb            |  8 ++--
 ...lowers_synchronizations_controller_spec.rb |  3 +-
 .../activitypub/outboxes_controller_spec.rb   | 41 +++++++++++--------
 .../activitypub/replies_controller_spec.rb    |  2 +-
 .../auth/sessions_controller_spec.rb          |  2 +-
 .../follower_accounts_controller_spec.rb      | 37 +++++++++--------
 .../following_accounts_controller_spec.rb     | 37 +++++++++--------
 spec/controllers/statuses_controller_spec.rb  | 14 +++----
 spec/requests/accounts_spec.rb                |  8 ++--
 .../api/v1/accounts/credentials_spec.rb       |  6 +--
 .../v1/accounts/familiar_followers_spec.rb    |  2 +-
 .../api/v1/accounts/featured_tags_spec.rb     |  4 +-
 .../api/v1/accounts/follower_accounts_spec.rb | 14 +++----
 .../v1/accounts/following_accounts_spec.rb    | 14 +++----
 .../api/v1/accounts/relationships_spec.rb     | 14 +++----
 .../requests/api/v1/accounts/statuses_spec.rb |  6 +--
 spec/requests/api/v1/accounts_spec.rb         | 20 ++++-----
 spec/requests/api/v1/admin/accounts_spec.rb   |  6 +--
 .../v1/admin/canonical_email_blocks_spec.rb   | 22 +++++-----
 spec/requests/api/v1/admin/dimensions_spec.rb |  2 +-
 .../api/v1/admin/domain_allows_spec.rb        | 12 +++---
 .../api/v1/admin/domain_blocks_spec.rb        | 40 +++++++++---------
 .../api/v1/admin/email_domain_blocks_spec.rb  | 16 ++++----
 spec/requests/api/v1/admin/ip_blocks_spec.rb  | 14 +++----
 spec/requests/api/v1/admin/measures_spec.rb   |  2 +-
 spec/requests/api/v1/admin/reports_spec.rb    | 16 ++++----
 spec/requests/api/v1/admin/retention_spec.rb  |  2 +-
 spec/requests/api/v1/admin/tags_spec.rb       | 14 +++----
 .../api/v1/admin/trends/links/links_spec.rb   |  4 +-
 spec/requests/api/v1/annual_reports_spec.rb   |  2 +-
 spec/requests/api/v1/apps/credentials_spec.rb | 12 +++---
 spec/requests/api/v1/apps_spec.rb             | 10 ++---
 spec/requests/api/v1/blocks_spec.rb           |  8 ++--
 spec/requests/api/v1/bookmarks_spec.rb        |  4 +-
 spec/requests/api/v1/conversations_spec.rb    |  8 ++--
 spec/requests/api/v1/custom_emojis_spec.rb    |  4 +-
 spec/requests/api/v1/directories_spec.rb      | 20 ++++-----
 spec/requests/api/v1/domain_blocks_spec.rb    |  4 +-
 .../api/v1/emails/confirmations_spec.rb       |  8 ++--
 spec/requests/api/v1/endorsements_spec.rb     |  4 +-
 spec/requests/api/v1/favourites_spec.rb       |  4 +-
 .../api/v1/featured_tags/suggestions_spec.rb  |  2 +-
 spec/requests/api/v1/featured_tags_spec.rb    |  8 ++--
 spec/requests/api/v1/filters_spec.rb          |  2 +-
 spec/requests/api/v1/follow_requests_spec.rb  |  8 ++--
 spec/requests/api/v1/followed_tags_spec.rb    |  4 +-
 spec/requests/api/v1/instance_spec.rb         |  4 +-
 .../api/v1/instances/activity_spec.rb         |  2 +-
 .../api/v1/instances/domain_blocks_spec.rb    |  2 +-
 .../instances/extended_descriptions_spec.rb   |  2 +-
 .../api/v1/instances/languages_spec.rb        |  2 +-
 spec/requests/api/v1/instances/peers_spec.rb  |  2 +-
 .../api/v1/instances/privacy_policies_spec.rb |  2 +-
 spec/requests/api/v1/instances/rules_spec.rb  |  2 +-
 .../instances/translation_languages_spec.rb   |  4 +-
 spec/requests/api/v1/lists/accounts_spec.rb   |  4 +-
 spec/requests/api/v1/lists_spec.rb            |  8 ++--
 spec/requests/api/v1/markers_spec.rb          |  4 +-
 spec/requests/api/v1/media_spec.rb            |  4 +-
 spec/requests/api/v1/mutes_spec.rb            |  8 ++--
 .../api/v1/notifications/policies_spec.rb     |  4 +-
 .../api/v1/notifications/requests_spec.rb     |  4 +-
 spec/requests/api/v1/notifications_spec.rb    | 28 ++++++-------
 spec/requests/api/v1/peers/search_spec.rb     |  8 ++--
 spec/requests/api/v1/polls_spec.rb            |  2 +-
 spec/requests/api/v1/preferences_spec.rb      |  2 +-
 .../api/v1/push/subscriptions_spec.rb         |  4 +-
 spec/requests/api/v1/reports_spec.rb          |  2 +-
 spec/requests/api/v1/scheduled_status_spec.rb |  4 +-
 .../api/v1/statuses/bookmarks_spec.rb         |  6 +--
 .../statuses/favourited_by_accounts_spec.rb   |  8 ++--
 .../api/v1/statuses/favourites_spec.rb        |  6 +--
 .../api/v1/statuses/histories_spec.rb         |  2 +-
 spec/requests/api/v1/statuses/pins_spec.rb    |  4 +-
 .../v1/statuses/reblogged_by_accounts_spec.rb |  8 ++--
 spec/requests/api/v1/statuses/reblogs_spec.rb |  6 +--
 spec/requests/api/v1/statuses/sources_spec.rb |  4 +-
 spec/requests/api/v1/statuses_spec.rb         | 10 ++---
 spec/requests/api/v1/suggestions_spec.rb      |  4 +-
 spec/requests/api/v1/tags_spec.rb             |  2 +-
 spec/requests/api/v1/timelines/home_spec.rb   |  4 +-
 spec/requests/api/v1/timelines/link_spec.rb   |  4 +-
 spec/requests/api/v1/timelines/public_spec.rb |  4 +-
 spec/requests/api/v1/timelines/tag_spec.rb    |  4 +-
 spec/requests/api/v2/admin/accounts_spec.rb   |  2 +-
 spec/requests/api/v2/filters/keywords_spec.rb |  6 +--
 spec/requests/api/v2/filters/statuses_spec.rb |  6 +--
 spec/requests/api/v2/filters_spec.rb          |  9 ++--
 spec/requests/api/v2/instance_spec.rb         |  4 +-
 spec/requests/api/v2/media_spec.rb            |  8 ++--
 .../api/v2/notifications/policies_spec.rb     |  4 +-
 spec/requests/api/v2/search_spec.rb           |  4 +-
 spec/requests/api/v2/suggestions_spec.rb      |  2 +-
 .../v2_alpha/notifications/accounts_spec.rb   |  4 +-
 .../api/v2_alpha/notifications_spec.rb        | 34 +++++++--------
 spec/requests/api/web/embeds_spec.rb          |  6 +--
 spec/requests/emojis_spec.rb                  |  2 +-
 spec/requests/instance_actor_spec.rb          |  2 +-
 spec/requests/invite_spec.rb                  |  2 +-
 spec/requests/log_out_spec.rb                 |  2 +-
 spec/requests/manifest_spec.rb                |  2 +-
 spec/requests/oauth/token_spec.rb             |  6 +--
 spec/requests/signature_verification_spec.rb  | 30 +++++++-------
 spec/requests/well_known/node_info_spec.rb    |  4 +-
 .../well_known/oauth_metadata_spec.rb         |  2 +-
 spec/requests/well_known/webfinger_spec.rb    | 12 +++---
 spec/spec_helper.rb                           |  4 --
 107 files changed, 422 insertions(+), 413 deletions(-)

diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb
index 088027385..408e0dd2f 100644
--- a/spec/controllers/activitypub/collections_controller_spec.rb
+++ b/spec/controllers/activitypub/collections_controller_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe ActivityPub::CollectionsController do
             .and have_cacheable_headers
           expect(response.media_type).to eq 'application/activity+json'
 
-          expect(body_as_json[:orderedItems])
+          expect(response.parsed_body[:orderedItems])
             .to be_an(Array)
             .and have_attributes(size: 3)
             .and include(ActivityPub::TagManager.instance.uri_for(private_pinned))
@@ -71,7 +71,7 @@ RSpec.describe ActivityPub::CollectionsController do
 
             expect(response.media_type).to eq 'application/activity+json'
 
-            expect(body_as_json[:orderedItems])
+            expect(response.parsed_body[:orderedItems])
               .to be_an(Array)
               .and have_attributes(size: 3)
               .and include(ActivityPub::TagManager.instance.uri_for(private_pinned))
@@ -94,7 +94,7 @@ RSpec.describe ActivityPub::CollectionsController do
               expect(response.media_type).to eq 'application/activity+json'
               expect(response.headers['Cache-Control']).to include 'private'
 
-              expect(body_as_json[:orderedItems])
+              expect(response.parsed_body[:orderedItems])
                 .to be_an(Array)
                 .and be_empty
             end
@@ -110,7 +110,7 @@ RSpec.describe ActivityPub::CollectionsController do
               expect(response.media_type).to eq 'application/activity+json'
               expect(response.headers['Cache-Control']).to include 'private'
 
-              expect(body_as_json[:orderedItems])
+              expect(response.parsed_body[:orderedItems])
                 .to be_an(Array)
                 .and be_empty
             end
diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
index c030078d4..cbd982f18 100644
--- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
+++ b/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb
@@ -34,7 +34,6 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do
     context 'with signature from example.com' do
       subject(:response) { get :show, params: { account_username: account.username } }
 
-      let(:body) { body_as_json }
       let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') }
 
       it 'returns http success and cache control and activity json types and correct items' do
@@ -42,7 +41,7 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do
         expect(response.headers['Cache-Control']).to eq 'max-age=0, private'
         expect(response.media_type).to eq 'application/activity+json'
 
-        expect(body[:orderedItems])
+        expect(response.parsed_body[:orderedItems])
           .to be_an(Array)
           .and contain_exactly(
             follower_example_com_instance_actor.uri,
diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb
index 26a52bad9..7ae28e8e0 100644
--- a/spec/controllers/activitypub/outboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/outboxes_controller_spec.rb
@@ -19,7 +19,6 @@ RSpec.describe ActivityPub::OutboxesController do
     context 'without signature' do
       subject(:response) { get :show, params: { account_username: account.username, page: page } }
 
-      let(:body) { body_as_json }
       let(:remote_account) { nil }
 
       context 'with page not requested' do
@@ -32,7 +31,7 @@ RSpec.describe ActivityPub::OutboxesController do
 
           expect(response.media_type).to eq 'application/activity+json'
           expect(response.headers['Vary']).to be_nil
-          expect(body[:totalItems]).to eq 4
+          expect(response.parsed_body[:totalItems]).to eq 4
         end
 
         context 'when account is permanently suspended' do
@@ -68,9 +67,11 @@ RSpec.describe ActivityPub::OutboxesController do
           expect(response.media_type).to eq 'application/activity+json'
           expect(response.headers['Vary']).to include 'Signature'
 
-          expect(body[:orderedItems]).to be_an Array
-          expect(body[:orderedItems].size).to eq 2
-          expect(body[:orderedItems].all? { |item| targets_public_collection?(item) }).to be true
+          expect(response.parsed_body)
+            .to include(
+              orderedItems: be_an(Array).and(have_attributes(size: 2))
+            )
+          expect(response.parsed_body[:orderedItems].all? { |item| targets_public_collection?(item) }).to be true
         end
 
         context 'when account is permanently suspended' do
@@ -110,9 +111,11 @@ RSpec.describe ActivityPub::OutboxesController do
           expect(response.media_type).to eq 'application/activity+json'
           expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
 
-          expect(body_as_json[:orderedItems]).to be_an Array
-          expect(body_as_json[:orderedItems].size).to eq 2
-          expect(body_as_json[:orderedItems].all? { |item| targets_public_collection?(item) }).to be true
+          expect(response.parsed_body)
+            .to include(
+              orderedItems: be_an(Array).and(have_attributes(size: 2))
+            )
+          expect(response.parsed_body[:orderedItems].all? { |item| targets_public_collection?(item) }).to be true
         end
       end
 
@@ -127,9 +130,11 @@ RSpec.describe ActivityPub::OutboxesController do
           expect(response.media_type).to eq 'application/activity+json'
           expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
 
-          expect(body_as_json[:orderedItems]).to be_an Array
-          expect(body_as_json[:orderedItems].size).to eq 3
-          expect(body_as_json[:orderedItems].all? { |item| targets_public_collection?(item) || targets_followers_collection?(item, account) }).to be true
+          expect(response.parsed_body)
+            .to include(
+              orderedItems: be_an(Array).and(have_attributes(size: 3))
+            )
+          expect(response.parsed_body[:orderedItems].all? { |item| targets_public_collection?(item) || targets_followers_collection?(item, account) }).to be true
         end
       end
 
@@ -144,9 +149,10 @@ RSpec.describe ActivityPub::OutboxesController do
           expect(response.media_type).to eq 'application/activity+json'
           expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
 
-          expect(body_as_json[:orderedItems])
-            .to be_an(Array)
-            .and be_empty
+          expect(response.parsed_body)
+            .to include(
+              orderedItems: be_an(Array).and(be_empty)
+            )
         end
       end
 
@@ -161,9 +167,10 @@ RSpec.describe ActivityPub::OutboxesController do
           expect(response.media_type).to eq 'application/activity+json'
           expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
 
-          expect(body_as_json[:orderedItems])
-            .to be_an(Array)
-            .and be_empty
+          expect(response.parsed_body)
+            .to include(
+              orderedItems: be_an(Array).and(be_empty)
+            )
         end
       end
     end
diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb
index c10c782c9..27821b0d4 100644
--- a/spec/controllers/activitypub/replies_controller_spec.rb
+++ b/spec/controllers/activitypub/replies_controller_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe ActivityPub::RepliesController do
 
     context 'when status is public' do
       let(:parent_visibility) { :public }
-      let(:page_json) { body_as_json[:first] }
+      let(:page_json) { response.parsed_body[:first] }
 
       it 'returns http success and correct media type' do
         expect(response)
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index 9a94e5e1a..3cc346071 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -402,7 +402,7 @@ RSpec.describe Auth::SessionsController do
           end
 
           it 'instructs the browser to redirect to home, logs the user in, and updates the sign count' do
-            expect(body_as_json[:redirect_path]).to eq(root_path)
+            expect(response.parsed_body[:redirect_path]).to eq(root_path)
 
             expect(controller.current_user).to eq user
 
diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb
index e84528d13..e14ed00e6 100644
--- a/spec/controllers/follower_accounts_controller_spec.rb
+++ b/spec/controllers/follower_accounts_controller_spec.rb
@@ -39,8 +39,6 @@ RSpec.describe FollowerAccountsController do
     end
 
     context 'when format is json' do
-      subject(:body) { response.parsed_body }
-
       let(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } }
 
       context 'with page' do
@@ -48,15 +46,15 @@ RSpec.describe FollowerAccountsController do
 
         it 'returns followers' do
           expect(response).to have_http_status(200)
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to include(
               orderedItems: contain_exactly(
                 include(follow_from_bob.account.username),
                 include(follow_from_chris.account.username)
-              )
+              ),
+              totalItems: eq(2),
+              partOf: be_present
             )
-          expect(body['totalItems']).to eq 2
-          expect(body['partOf']).to be_present
         end
 
         context 'when account is permanently suspended' do
@@ -86,8 +84,11 @@ RSpec.describe FollowerAccountsController do
 
         it 'returns followers' do
           expect(response).to have_http_status(200)
-          expect(body['totalItems']).to eq 2
-          expect(body['partOf']).to be_blank
+          expect(response.parsed_body)
+            .to include(
+              totalItems: eq(2)
+            )
+            .and not_include(:partOf)
         end
 
         context 'when account hides their network' do
@@ -95,15 +96,17 @@ RSpec.describe FollowerAccountsController do
             alice.update(hide_collections: true)
           end
 
-          it 'returns followers count' do
-            expect(body['totalItems']).to eq 2
-          end
-
-          it 'does not return items' do
-            expect(body['items']).to be_blank
-            expect(body['orderedItems']).to be_blank
-            expect(body['first']).to be_blank
-            expect(body['last']).to be_blank
+          it 'returns followers count but not any items' do
+            expect(response.parsed_body)
+              .to include(
+                totalItems: eq(2)
+              )
+              .and not_include(
+                :items,
+                :orderedItems,
+                :first,
+                :last
+              )
           end
         end
 
diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb
index 1e01b9f49..fea4d4845 100644
--- a/spec/controllers/following_accounts_controller_spec.rb
+++ b/spec/controllers/following_accounts_controller_spec.rb
@@ -39,8 +39,6 @@ RSpec.describe FollowingAccountsController do
     end
 
     context 'when format is json' do
-      subject(:body) { response.parsed_body }
-
       let(:response) { get :index, params: { account_username: alice.username, page: page, format: :json } }
 
       context 'with page' do
@@ -48,15 +46,15 @@ RSpec.describe FollowingAccountsController do
 
         it 'returns followers' do
           expect(response).to have_http_status(200)
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to include(
               orderedItems: contain_exactly(
                 include(follow_of_bob.target_account.username),
                 include(follow_of_chris.target_account.username)
-              )
+              ),
+              totalItems: eq(2),
+              partOf: be_present
             )
-          expect(body['totalItems']).to eq 2
-          expect(body['partOf']).to be_present
         end
 
         context 'when account is permanently suspended' do
@@ -86,8 +84,11 @@ RSpec.describe FollowingAccountsController do
 
         it 'returns followers' do
           expect(response).to have_http_status(200)
-          expect(body['totalItems']).to eq 2
-          expect(body['partOf']).to be_blank
+          expect(response.parsed_body)
+            .to include(
+              totalItems: eq(2)
+            )
+            .and not_include(:partOf)
         end
 
         context 'when account hides their network' do
@@ -95,15 +96,17 @@ RSpec.describe FollowingAccountsController do
             alice.update(hide_collections: true)
           end
 
-          it 'returns followers count' do
-            expect(body['totalItems']).to eq 2
-          end
-
-          it 'does not return items' do
-            expect(body['items']).to be_blank
-            expect(body['orderedItems']).to be_blank
-            expect(body['first']).to be_blank
-            expect(body['last']).to be_blank
+          it 'returns followers count but not any items' do
+            expect(response.parsed_body)
+              .to include(
+                totalItems: eq(2)
+              )
+              .and not_include(
+                :items,
+                :orderedItems,
+                :first,
+                :last
+              )
           end
         end
 
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 289109a1f..2d5ff0135 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe StatusesController do
             'Content-Type' => include('application/activity+json'),
             'Link' => satisfy { |header| header.to_s.include?('activity+json') }
           )
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to include(content: include(status.text))
         end
       end
@@ -186,7 +186,7 @@ RSpec.describe StatusesController do
               'Content-Type' => include('application/activity+json'),
               'Link' => satisfy { |header| header.to_s.include?('activity+json') }
             )
-            expect(body_as_json)
+            expect(response.parsed_body)
               .to include(content: include(status.text))
           end
         end
@@ -230,7 +230,7 @@ RSpec.describe StatusesController do
                 'Content-Type' => include('application/activity+json'),
                 'Link' => satisfy { |header| header.to_s.include?('activity+json') }
               )
-              expect(body_as_json)
+              expect(response.parsed_body)
                 .to include(content: include(status.text))
             end
           end
@@ -296,7 +296,7 @@ RSpec.describe StatusesController do
                 'Content-Type' => include('application/activity+json'),
                 'Link' => satisfy { |header| header.to_s.include?('activity+json') }
               )
-              expect(body_as_json)
+              expect(response.parsed_body)
                 .to include(content: include(status.text))
             end
           end
@@ -387,7 +387,7 @@ RSpec.describe StatusesController do
               'Content-Type' => include('application/activity+json'),
               'Link' => satisfy { |header| header.to_s.include?('activity+json') }
             )
-            expect(body_as_json)
+            expect(response.parsed_body)
               .to include(content: include(status.text))
           end
         end
@@ -431,7 +431,7 @@ RSpec.describe StatusesController do
                 'Link' => satisfy { |header| header.to_s.include?('activity+json') }
               )
 
-              expect(body_as_json)
+              expect(response.parsed_body)
                 .to include(content: include(status.text))
             end
           end
@@ -497,7 +497,7 @@ RSpec.describe StatusesController do
                 'Content-Type' => include('application/activity+json'),
                 'Link' => satisfy { |header| header.to_s.include?('activity+json') }
               )
-              expect(body_as_json)
+              expect(response.parsed_body)
                 .to include(content: include(status.text))
             end
           end
diff --git a/spec/requests/accounts_spec.rb b/spec/requests/accounts_spec.rb
index 3615868d7..e657ae606 100644
--- a/spec/requests/accounts_spec.rb
+++ b/spec/requests/accounts_spec.rb
@@ -134,7 +134,7 @@ RSpec.describe 'Accounts show response' do
                 media_type: eq('application/activity+json')
               )
 
-            expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
+            expect(response.parsed_body).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
           end
 
           context 'with authorized fetch mode' do
@@ -163,7 +163,7 @@ RSpec.describe 'Accounts show response' do
 
             expect(response.headers['Cache-Control']).to include 'private'
 
-            expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
+            expect(response.parsed_body).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
           end
         end
 
@@ -182,7 +182,7 @@ RSpec.describe 'Accounts show response' do
                 media_type: eq('application/activity+json')
               )
 
-            expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
+            expect(response.parsed_body).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
           end
 
           context 'with authorized fetch mode' do
@@ -198,7 +198,7 @@ RSpec.describe 'Accounts show response' do
               expect(response.headers['Cache-Control']).to include 'private'
               expect(response.headers['Vary']).to include 'Signature'
 
-              expect(body_as_json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
+              expect(response.parsed_body).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
             end
           end
         end
diff --git a/spec/requests/api/v1/accounts/credentials_spec.rb b/spec/requests/api/v1/accounts/credentials_spec.rb
index a3f552cad..cc82e1892 100644
--- a/spec/requests/api/v1/accounts/credentials_spec.rb
+++ b/spec/requests/api/v1/accounts/credentials_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'credentials API' do
 
       expect(response)
         .to have_http_status(200)
-      expect(body_as_json).to include({
+      expect(response.parsed_body).to include({
         source: hash_including({
           discoverable: false,
           indexable: false,
@@ -37,7 +37,7 @@ RSpec.describe 'credentials API' do
 
         expect(response).to have_http_status(200)
 
-        expect(body_as_json).to include({
+        expect(response.parsed_body).to include({
           locked: true,
         })
       end
@@ -93,7 +93,7 @@ RSpec.describe 'credentials API' do
       expect(response)
         .to have_http_status(200)
 
-      expect(body_as_json).to include({
+      expect(response.parsed_body).to include({
         source: hash_including({
           discoverable: true,
           indexable: true,
diff --git a/spec/requests/api/v1/accounts/familiar_followers_spec.rb b/spec/requests/api/v1/accounts/familiar_followers_spec.rb
index 475f1b17e..8edfa4c88 100644
--- a/spec/requests/api/v1/accounts/familiar_followers_spec.rb
+++ b/spec/requests/api/v1/accounts/familiar_followers_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Accounts Familiar Followers API' do
         account_ids = [account_a, account_b, account_b, account_a, account_a].map { |a| a.id.to_s }
         get '/api/v1/accounts/familiar_followers', params: { id: account_ids }, headers: headers
 
-        expect(body_as_json.pluck(:id)).to contain_exactly(account_a.id.to_s, account_b.id.to_s)
+        expect(response.parsed_body.pluck(:id)).to contain_exactly(account_a.id.to_s, account_b.id.to_s)
       end
     end
   end
diff --git a/spec/requests/api/v1/accounts/featured_tags_spec.rb b/spec/requests/api/v1/accounts/featured_tags_spec.rb
index bae7d448b..f48ed01de 100644
--- a/spec/requests/api/v1/accounts/featured_tags_spec.rb
+++ b/spec/requests/api/v1/accounts/featured_tags_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'account featured tags API' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to contain_exactly(a_hash_including({
+      expect(response.parsed_body).to contain_exactly(a_hash_including({
         name: 'bar',
         url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/bar",
       }), a_hash_including({
@@ -37,7 +37,7 @@ RSpec.describe 'account featured tags API' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to contain_exactly(a_hash_including({
+        expect(response.parsed_body).to contain_exactly(a_hash_including({
           name: 'bar',
           url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/bar",
         }), a_hash_including({
diff --git a/spec/requests/api/v1/accounts/follower_accounts_spec.rb b/spec/requests/api/v1/accounts/follower_accounts_spec.rb
index 400b1c7af..267261539 100644
--- a/spec/requests/api/v1/accounts/follower_accounts_spec.rb
+++ b/spec/requests/api/v1/accounts/follower_accounts_spec.rb
@@ -21,8 +21,8 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do
       get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
 
       expect(response).to have_http_status(200)
-      expect(body_as_json.size).to eq 2
-      expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
+      expect(response.parsed_body.size).to eq 2
+      expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
     end
 
     it 'does not return blocked users', :aggregate_failures do
@@ -30,8 +30,8 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do
       get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
 
       expect(response).to have_http_status(200)
-      expect(body_as_json.size).to eq 1
-      expect(body_as_json[0][:id]).to eq alice.id.to_s
+      expect(response.parsed_body.size).to eq 1
+      expect(response.parsed_body[0][:id]).to eq alice.id.to_s
     end
 
     context 'when requesting user is blocked' do
@@ -41,7 +41,7 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do
 
       it 'hides results' do
         get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
-        expect(body_as_json.size).to eq 0
+        expect(response.parsed_body.size).to eq 0
       end
     end
 
@@ -52,8 +52,8 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do
         account.mute!(bob)
         get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
 
-        expect(body_as_json.size).to eq 2
-        expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
+        expect(response.parsed_body.size).to eq 2
+        expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
       end
     end
   end
diff --git a/spec/requests/api/v1/accounts/following_accounts_spec.rb b/spec/requests/api/v1/accounts/following_accounts_spec.rb
index b0bb5141c..19105ebf2 100644
--- a/spec/requests/api/v1/accounts/following_accounts_spec.rb
+++ b/spec/requests/api/v1/accounts/following_accounts_spec.rb
@@ -21,8 +21,8 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do
       get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
 
       expect(response).to have_http_status(200)
-      expect(body_as_json.size).to eq 2
-      expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
+      expect(response.parsed_body.size).to eq 2
+      expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
     end
 
     it 'does not return blocked users', :aggregate_failures do
@@ -30,8 +30,8 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do
       get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
 
       expect(response).to have_http_status(200)
-      expect(body_as_json.size).to eq 1
-      expect(body_as_json[0][:id]).to eq alice.id.to_s
+      expect(response.parsed_body.size).to eq 1
+      expect(response.parsed_body[0][:id]).to eq alice.id.to_s
     end
 
     context 'when requesting user is blocked' do
@@ -41,7 +41,7 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do
 
       it 'hides results' do
         get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
-        expect(body_as_json.size).to eq 0
+        expect(response.parsed_body.size).to eq 0
       end
     end
 
@@ -52,8 +52,8 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do
         account.mute!(bob)
         get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
 
-        expect(body_as_json.size).to eq 2
-        expect([body_as_json[0][:id], body_as_json[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
+        expect(response.parsed_body.size).to eq 2
+        expect([response.parsed_body[0][:id], response.parsed_body[1][:id]]).to contain_exactly(alice.id.to_s, bob.id.to_s)
       end
     end
   end
diff --git a/spec/requests/api/v1/accounts/relationships_spec.rb b/spec/requests/api/v1/accounts/relationships_spec.rb
index 76b1830bb..9570d1214 100644
--- a/spec/requests/api/v1/accounts/relationships_spec.rb
+++ b/spec/requests/api/v1/accounts/relationships_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'GET /api/v1/accounts/relationships' do
 
       expect(response)
         .to have_http_status(200)
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to be_an(Enumerable)
         .and contain_exactly(
           include(
@@ -50,7 +50,7 @@ RSpec.describe 'GET /api/v1/accounts/relationships' do
 
           expect(response)
             .to have_http_status(200)
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to be_an(Enumerable)
             .and have_attributes(
               size: 2
@@ -70,7 +70,7 @@ RSpec.describe 'GET /api/v1/accounts/relationships' do
 
           expect(response)
             .to have_http_status(200)
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to be_an(Enumerable)
             .and have_attributes(
               size: 3
@@ -89,7 +89,7 @@ RSpec.describe 'GET /api/v1/accounts/relationships' do
         it 'removes duplicate account IDs from params' do
           subject
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to be_an(Enumerable)
             .and have_attributes(
               size: 2
@@ -141,7 +141,7 @@ RSpec.describe 'GET /api/v1/accounts/relationships' do
     it 'returns JSON with correct data on previously cached requests' do
       # Initial request including multiple accounts in params
       get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id, lewis.id] }
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to have_attributes(size: 2)
 
       # Subsequent request with different id, should override cache from first request
@@ -150,7 +150,7 @@ RSpec.describe 'GET /api/v1/accounts/relationships' do
       expect(response)
         .to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to be_an(Enumerable)
         .and have_attributes(
           size: 1
@@ -172,7 +172,7 @@ RSpec.describe 'GET /api/v1/accounts/relationships' do
       expect(response)
         .to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to be_an(Enumerable)
         .and contain_exactly(
           include(
diff --git a/spec/requests/api/v1/accounts/statuses_spec.rb b/spec/requests/api/v1/accounts/statuses_spec.rb
index 4a4d9383d..e056a7890 100644
--- a/spec/requests/api/v1/accounts/statuses_spec.rb
+++ b/spec/requests/api/v1/accounts/statuses_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'API V1 Accounts Statuses' do
       it 'returns posts along with self replies', :aggregate_failures do
         expect(response)
           .to have_http_status(200)
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to have_attributes(size: 2)
           .and contain_exactly(
             include(id: status.id.to_s),
@@ -102,7 +102,7 @@ RSpec.describe 'API V1 Accounts Statuses' do
         it 'lists the public status only' do
           get "/api/v1/accounts/#{account.id}/statuses", params: { pinned: true }, headers: headers
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to contain_exactly(
               a_hash_including(id: status.id.to_s)
             )
@@ -117,7 +117,7 @@ RSpec.describe 'API V1 Accounts Statuses' do
         it 'lists both the public and the private statuses' do
           get "/api/v1/accounts/#{account.id}/statuses", params: { pinned: true }, headers: headers
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to contain_exactly(
               a_hash_including(id: status.id.to_s),
               a_hash_including(id: private_status.id.to_s)
diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb
index e31644352..2ebe56fa7 100644
--- a/spec/requests/api/v1/accounts_spec.rb
+++ b/spec/requests/api/v1/accounts_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe '/api/v1/accounts' do
       get '/api/v1/accounts', headers: headers, params: { id: [account.id, other_account.id, 123_123] }
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to contain_exactly(
+      expect(response.parsed_body).to contain_exactly(
         hash_including(id: account.id.to_s),
         hash_including(id: other_account.id.to_s)
       )
@@ -32,7 +32,7 @@ RSpec.describe '/api/v1/accounts' do
         get "/api/v1/accounts/#{account.id}"
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:id]).to eq(account.id.to_s)
+        expect(response.parsed_body[:id]).to eq(account.id.to_s)
       end
     end
 
@@ -41,7 +41,7 @@ RSpec.describe '/api/v1/accounts' do
         get '/api/v1/accounts/1'
 
         expect(response).to have_http_status(404)
-        expect(body_as_json[:error]).to eq('Record not found')
+        expect(response.parsed_body[:error]).to eq('Record not found')
       end
     end
 
@@ -57,7 +57,7 @@ RSpec.describe '/api/v1/accounts' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:id]).to eq(account.id.to_s)
+        expect(response.parsed_body[:id]).to eq(account.id.to_s)
       end
 
       it_behaves_like 'forbidden for wrong scope', 'write:statuses'
@@ -80,7 +80,7 @@ RSpec.describe '/api/v1/accounts' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:access_token]).to_not be_blank
+        expect(response.parsed_body[:access_token]).to_not be_blank
 
         user = User.find_by(email: 'hello@world.tld')
         expect(user).to_not be_nil
@@ -114,7 +114,7 @@ RSpec.describe '/api/v1/accounts' do
 
           expect(response).to have_http_status(200)
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to include(
               following: true,
               requested: false
@@ -134,7 +134,7 @@ RSpec.describe '/api/v1/accounts' do
 
           expect(response).to have_http_status(200)
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to include(
               following: false,
               requested: true
@@ -157,7 +157,7 @@ RSpec.describe '/api/v1/accounts' do
       it 'changes reblogs option' do
         post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { reblogs: true }
 
-        expect(body_as_json).to include({
+        expect(response.parsed_body).to include({
           following: true,
           showing_reblogs: true,
           notifying: false,
@@ -167,7 +167,7 @@ RSpec.describe '/api/v1/accounts' do
       it 'changes notify option' do
         post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { notify: true }
 
-        expect(body_as_json).to include({
+        expect(response.parsed_body).to include({
           following: true,
           showing_reblogs: false,
           notifying: true,
@@ -177,7 +177,7 @@ RSpec.describe '/api/v1/accounts' do
       it 'changes languages option' do
         post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { languages: %w(en es) }
 
-        expect(body_as_json).to include({
+        expect(response.parsed_body).to include({
           following: true,
           showing_reblogs: false,
           notifying: false,
diff --git a/spec/requests/api/v1/admin/accounts_spec.rb b/spec/requests/api/v1/admin/accounts_spec.rb
index 1615581f0..2dc45d5eb 100644
--- a/spec/requests/api/v1/admin/accounts_spec.rb
+++ b/spec/requests/api/v1/admin/accounts_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Accounts' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.pluck(:id)).to match_array(expected_results.map { |a| a.id.to_s })
+        expect(response.parsed_body.pluck(:id)).to match_array(expected_results.map { |a| a.id.to_s })
       end
     end
 
@@ -93,7 +93,7 @@ RSpec.describe 'Accounts' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
     end
   end
@@ -112,7 +112,7 @@ RSpec.describe 'Accounts' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match(
+      expect(response.parsed_body).to match(
         a_hash_including(id: account.id.to_s, username: account.username, email: account.user.email)
       )
     end
diff --git a/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb b/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb
index 0cddf2c69..dd7e11991 100644
--- a/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb
+++ b/spec/requests/api/v1/admin/canonical_email_blocks_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Canonical Email Blocks' do
       it 'returns an empty list' do
         subject
 
-        expect(body_as_json).to be_empty
+        expect(response.parsed_body).to be_empty
       end
     end
 
@@ -41,7 +41,7 @@ RSpec.describe 'Canonical Email Blocks' do
       it 'returns the correct canonical email hashes' do
         subject
 
-        expect(body_as_json.pluck(:canonical_email_hash)).to match_array(expected_email_hashes)
+        expect(response.parsed_body.pluck(:canonical_email_hash)).to match_array(expected_email_hashes)
       end
 
       context 'with limit param' do
@@ -50,7 +50,7 @@ RSpec.describe 'Canonical Email Blocks' do
         it 'returns only the requested number of canonical email blocks' do
           subject
 
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
       end
 
@@ -62,7 +62,7 @@ RSpec.describe 'Canonical Email Blocks' do
 
           canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
 
-          expect(body_as_json.pluck(:id)).to match_array(canonical_email_blocks_ids[2..])
+          expect(response.parsed_body.pluck(:id)).to match_array(canonical_email_blocks_ids[2..])
         end
       end
 
@@ -74,7 +74,7 @@ RSpec.describe 'Canonical Email Blocks' do
 
           canonical_email_blocks_ids = canonical_email_blocks.pluck(:id).map(&:to_s)
 
-          expect(body_as_json.pluck(:id)).to match_array(canonical_email_blocks_ids[..2])
+          expect(response.parsed_body.pluck(:id)).to match_array(canonical_email_blocks_ids[..2])
         end
       end
     end
@@ -96,7 +96,7 @@ RSpec.describe 'Canonical Email Blocks' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to include(
             id: eq(canonical_email_block.id.to_s),
             canonical_email_hash: eq(canonical_email_block.canonical_email_hash)
@@ -142,7 +142,7 @@ RSpec.describe 'Canonical Email Blocks' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[0][:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
+          expect(response.parsed_body.first[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
         end
       end
 
@@ -151,7 +151,7 @@ RSpec.describe 'Canonical Email Blocks' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json).to be_empty
+          expect(response.parsed_body).to be_empty
         end
       end
     end
@@ -173,7 +173,7 @@ RSpec.describe 'Canonical Email Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
+      expect(response.parsed_body[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
     end
 
     context 'when the required email param is not provided' do
@@ -193,7 +193,7 @@ RSpec.describe 'Canonical Email Blocks' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:canonical_email_hash]).to eq(params[:canonical_email_hash])
+        expect(response.parsed_body[:canonical_email_hash]).to eq(params[:canonical_email_hash])
       end
     end
 
@@ -204,7 +204,7 @@ RSpec.describe 'Canonical Email Blocks' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
+        expect(response.parsed_body[:canonical_email_hash]).to eq(canonical_email_block.canonical_email_hash)
       end
     end
 
diff --git a/spec/requests/api/v1/admin/dimensions_spec.rb b/spec/requests/api/v1/admin/dimensions_spec.rb
index 43e2db00c..a28c2a9e3 100644
--- a/spec/requests/api/v1/admin/dimensions_spec.rb
+++ b/spec/requests/api/v1/admin/dimensions_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Admin Dimensions' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_an(Array)
       end
     end
diff --git a/spec/requests/api/v1/admin/domain_allows_spec.rb b/spec/requests/api/v1/admin/domain_allows_spec.rb
index b8f0b0055..26c962b34 100644
--- a/spec/requests/api/v1/admin/domain_allows_spec.rb
+++ b/spec/requests/api/v1/admin/domain_allows_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Domain Allows' do
       it 'returns an empty body' do
         subject
 
-        expect(body_as_json).to be_empty
+        expect(response.parsed_body).to be_empty
       end
     end
 
@@ -49,7 +49,7 @@ RSpec.describe 'Domain Allows' do
       it 'returns the correct allowed domains' do
         subject
 
-        expect(body_as_json).to match_array(expected_response)
+        expect(response.parsed_body).to match_array(expected_response)
       end
 
       context 'with limit param' do
@@ -58,7 +58,7 @@ RSpec.describe 'Domain Allows' do
         it 'returns only the requested number of allowed domains' do
           subject
 
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
       end
     end
@@ -79,7 +79,7 @@ RSpec.describe 'Domain Allows' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json[:domain]).to eq domain_allow.domain
+      expect(response.parsed_body[:domain]).to eq domain_allow.domain
     end
 
     context 'when the requested allowed domain does not exist' do
@@ -107,7 +107,7 @@ RSpec.describe 'Domain Allows' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:domain]).to eq 'foo.bar.com'
+        expect(response.parsed_body[:domain]).to eq 'foo.bar.com'
         expect(DomainAllow.find_by(domain: 'foo.bar.com')).to be_present
       end
     end
@@ -140,7 +140,7 @@ RSpec.describe 'Domain Allows' do
       it 'returns the existing allowed domain name' do
         subject
 
-        expect(body_as_json[:domain]).to eq(params[:domain])
+        expect(response.parsed_body[:domain]).to eq(params[:domain])
       end
     end
   end
diff --git a/spec/requests/api/v1/admin/domain_blocks_spec.rb b/spec/requests/api/v1/admin/domain_blocks_spec.rb
index 7f7b9aa48..3f2cbbf11 100644
--- a/spec/requests/api/v1/admin/domain_blocks_spec.rb
+++ b/spec/requests/api/v1/admin/domain_blocks_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Domain Blocks' do
       it 'returns an empty list' do
         subject
 
-        expect(body_as_json).to be_empty
+        expect(response.parsed_body).to be_empty
       end
     end
 
@@ -64,7 +64,7 @@ RSpec.describe 'Domain Blocks' do
       it 'returns the expected domain blocks' do
         subject
 
-        expect(body_as_json).to match_array(expected_responde)
+        expect(response.parsed_body).to match_array(expected_responde)
       end
 
       context 'with limit param' do
@@ -73,7 +73,7 @@ RSpec.describe 'Domain Blocks' do
         it 'returns only the requested number of domain blocks' do
           subject
 
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
       end
     end
@@ -94,19 +94,17 @@ RSpec.describe 'Domain Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match(
-        {
-          id: domain_block.id.to_s,
-          domain: domain_block.domain,
-          digest: domain_block.domain_digest,
-          created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
-          severity: domain_block.severity.to_s,
-          reject_media: domain_block.reject_media,
-          reject_reports: domain_block.reject_reports,
-          private_comment: domain_block.private_comment,
-          public_comment: domain_block.public_comment,
-          obfuscate: domain_block.obfuscate,
-        }
+      expect(response.parsed_body).to match(
+        id: domain_block.id.to_s,
+        domain: domain_block.domain,
+        digest: domain_block.domain_digest,
+        created_at: domain_block.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
+        severity: domain_block.severity.to_s,
+        reject_media: domain_block.reject_media,
+        reject_reports: domain_block.reject_reports,
+        private_comment: domain_block.private_comment,
+        public_comment: domain_block.public_comment,
+        obfuscate: domain_block.obfuscate
       )
     end
 
@@ -134,7 +132,7 @@ RSpec.describe 'Domain Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match a_hash_including(
+      expect(response.parsed_body).to match a_hash_including(
         {
           domain: 'foo.bar.com',
           severity: 'silence',
@@ -155,7 +153,7 @@ RSpec.describe 'Domain Blocks' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match a_hash_including(
+        expect(response.parsed_body).to match a_hash_including(
           {
             domain: 'foo.bar.com',
             severity: 'suspend',
@@ -175,7 +173,7 @@ RSpec.describe 'Domain Blocks' do
         subject
 
         expect(response).to have_http_status(422)
-        expect(body_as_json[:existing_domain_block][:domain]).to eq('foo.bar.com')
+        expect(response.parsed_body[:existing_domain_block][:domain]).to eq('foo.bar.com')
       end
     end
 
@@ -188,7 +186,7 @@ RSpec.describe 'Domain Blocks' do
         subject
 
         expect(response).to have_http_status(422)
-        expect(body_as_json[:existing_domain_block][:domain]).to eq('bar.com')
+        expect(response.parsed_body[:existing_domain_block][:domain]).to eq('bar.com')
       end
     end
 
@@ -219,7 +217,7 @@ RSpec.describe 'Domain Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match a_hash_including(
+      expect(response.parsed_body).to match a_hash_including(
         {
           id: domain_block.id.to_s,
           domain: domain_block.domain,
diff --git a/spec/requests/api/v1/admin/email_domain_blocks_spec.rb b/spec/requests/api/v1/admin/email_domain_blocks_spec.rb
index 16656e020..aa3073341 100644
--- a/spec/requests/api/v1/admin/email_domain_blocks_spec.rb
+++ b/spec/requests/api/v1/admin/email_domain_blocks_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'Email Domain Blocks' do
       it 'returns an empty list' do
         subject
 
-        expect(body_as_json).to be_empty
+        expect(response.parsed_body).to be_empty
       end
     end
 
@@ -42,7 +42,7 @@ RSpec.describe 'Email Domain Blocks' do
       it 'return the correct blocked email domains' do
         subject
 
-        expect(body_as_json.pluck(:domain)).to match_array(blocked_email_domains)
+        expect(response.parsed_body.pluck(:domain)).to match_array(blocked_email_domains)
       end
 
       context 'with limit param' do
@@ -51,7 +51,7 @@ RSpec.describe 'Email Domain Blocks' do
         it 'returns only the requested number of email domain blocks' do
           subject
 
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
       end
 
@@ -63,7 +63,7 @@ RSpec.describe 'Email Domain Blocks' do
 
           email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s)
 
-          expect(body_as_json.pluck(:id)).to match_array(email_domain_blocks_ids[2..])
+          expect(response.parsed_body.pluck(:id)).to match_array(email_domain_blocks_ids[2..])
         end
       end
 
@@ -75,7 +75,7 @@ RSpec.describe 'Email Domain Blocks' do
 
           email_domain_blocks_ids = email_domain_blocks.pluck(:id).map(&:to_s)
 
-          expect(body_as_json.pluck(:id)).to match_array(email_domain_blocks_ids[..2])
+          expect(response.parsed_body.pluck(:id)).to match_array(email_domain_blocks_ids[..2])
         end
       end
     end
@@ -97,7 +97,7 @@ RSpec.describe 'Email Domain Blocks' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:domain]).to eq(email_domain_block.domain)
+        expect(response.parsed_body[:domain]).to eq(email_domain_block.domain)
       end
     end
 
@@ -125,7 +125,7 @@ RSpec.describe 'Email Domain Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json[:domain]).to eq(params[:domain])
+      expect(response.parsed_body[:domain]).to eq(params[:domain])
     end
 
     context 'when domain param is not provided' do
@@ -176,7 +176,7 @@ RSpec.describe 'Email Domain Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to be_empty
+      expect(response.parsed_body).to be_empty
       expect(EmailDomainBlock.find_by(id: email_domain_block.id)).to be_nil
     end
 
diff --git a/spec/requests/api/v1/admin/ip_blocks_spec.rb b/spec/requests/api/v1/admin/ip_blocks_spec.rb
index bd4015b2d..b18f8f885 100644
--- a/spec/requests/api/v1/admin/ip_blocks_spec.rb
+++ b/spec/requests/api/v1/admin/ip_blocks_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'IP Blocks' do
       it 'returns an empty body' do
         subject
 
-        expect(body_as_json).to be_empty
+        expect(response.parsed_body).to be_empty
       end
     end
 
@@ -58,7 +58,7 @@ RSpec.describe 'IP Blocks' do
       it 'returns the correct blocked ips' do
         subject
 
-        expect(body_as_json).to match_array(expected_response)
+        expect(response.parsed_body).to match_array(expected_response)
       end
 
       context 'with limit param' do
@@ -67,7 +67,7 @@ RSpec.describe 'IP Blocks' do
         it 'returns only the requested number of ip blocks' do
           subject
 
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
       end
     end
@@ -89,7 +89,7 @@ RSpec.describe 'IP Blocks' do
 
       expect(response).to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           ip: eq("#{ip_block.ip}/#{ip_block.ip.prefix}"),
           severity: eq(ip_block.severity.to_s)
@@ -120,7 +120,7 @@ RSpec.describe 'IP Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           ip: eq("#{params[:ip]}/32"),
           severity: eq(params[:severity]),
@@ -185,7 +185,7 @@ RSpec.describe 'IP Blocks' do
         .and change_comment_value
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match(hash_including({
+      expect(response.parsed_body).to match(hash_including({
         ip: "#{ip_block.ip}/#{ip_block.ip.prefix}",
         severity: 'sign_up_requires_approval',
         comment: 'Decreasing severity',
@@ -220,7 +220,7 @@ RSpec.describe 'IP Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to be_empty
+      expect(response.parsed_body).to be_empty
       expect(IpBlock.find_by(id: ip_block.id)).to be_nil
     end
 
diff --git a/spec/requests/api/v1/admin/measures_spec.rb b/spec/requests/api/v1/admin/measures_spec.rb
index 56a2c1eae..de359a5cc 100644
--- a/spec/requests/api/v1/admin/measures_spec.rb
+++ b/spec/requests/api/v1/admin/measures_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'Admin Measures' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_an(Array)
       end
     end
diff --git a/spec/requests/api/v1/admin/reports_spec.rb b/spec/requests/api/v1/admin/reports_spec.rb
index 4b0b7e171..2c40f56dc 100644
--- a/spec/requests/api/v1/admin/reports_spec.rb
+++ b/spec/requests/api/v1/admin/reports_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Reports' do
       it 'returns an empty list' do
         subject
 
-        expect(body_as_json).to be_empty
+        expect(response.parsed_body).to be_empty
       end
     end
 
@@ -64,7 +64,7 @@ RSpec.describe 'Reports' do
       it 'returns all unresolved reports' do
         subject
 
-        expect(body_as_json).to match_array(expected_response)
+        expect(response.parsed_body).to match_array(expected_response)
       end
 
       context 'with resolved param' do
@@ -74,7 +74,7 @@ RSpec.describe 'Reports' do
         it 'returns only the resolved reports' do
           subject
 
-          expect(body_as_json).to match_array(expected_response)
+          expect(response.parsed_body).to match_array(expected_response)
         end
       end
 
@@ -85,7 +85,7 @@ RSpec.describe 'Reports' do
         it 'returns all unresolved reports filed by the specified account' do
           subject
 
-          expect(body_as_json).to match_array(expected_response)
+          expect(response.parsed_body).to match_array(expected_response)
         end
       end
 
@@ -96,7 +96,7 @@ RSpec.describe 'Reports' do
         it 'returns all unresolved reports targeting the specified account' do
           subject
 
-          expect(body_as_json).to match_array(expected_response)
+          expect(response.parsed_body).to match_array(expected_response)
         end
       end
 
@@ -106,7 +106,7 @@ RSpec.describe 'Reports' do
         it 'returns only the requested number of reports' do
           subject
 
-          expect(body_as_json.size).to eq(1)
+          expect(response.parsed_body.size).to eq(1)
         end
       end
     end
@@ -126,7 +126,7 @@ RSpec.describe 'Reports' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to include(
+      expect(response.parsed_body).to include(
         {
           id: report.id.to_s,
           action_taken: report.action_taken?,
@@ -159,7 +159,7 @@ RSpec.describe 'Reports' do
 
       report.reload
 
-      expect(body_as_json).to include(
+      expect(response.parsed_body).to include(
         {
           id: report.id.to_s,
           action_taken: report.action_taken?,
diff --git a/spec/requests/api/v1/admin/retention_spec.rb b/spec/requests/api/v1/admin/retention_spec.rb
index 138959a0a..c28fa6de8 100644
--- a/spec/requests/api/v1/admin/retention_spec.rb
+++ b/spec/requests/api/v1/admin/retention_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Admin Retention' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_an(Array)
       end
     end
diff --git a/spec/requests/api/v1/admin/tags_spec.rb b/spec/requests/api/v1/admin/tags_spec.rb
index 031be17f5..2f730cdeb 100644
--- a/spec/requests/api/v1/admin/tags_spec.rb
+++ b/spec/requests/api/v1/admin/tags_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Tags' do
       it 'returns an empty list' do
         subject
 
-        expect(body_as_json).to be_empty
+        expect(response.parsed_body).to be_empty
       end
     end
 
@@ -47,7 +47,7 @@ RSpec.describe 'Tags' do
       it 'returns the expected tags' do
         subject
         tags.each do |tag|
-          expect(body_as_json.find { |item| item[:id] == tag.id.to_s && item[:name] == tag.name }).to_not be_nil
+          expect(response.parsed_body.find { |item| item[:id] == tag.id.to_s && item[:name] == tag.name }).to_not be_nil
         end
       end
 
@@ -57,7 +57,7 @@ RSpec.describe 'Tags' do
         it 'returns only the requested number of tags' do
           subject
 
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
       end
     end
@@ -82,8 +82,8 @@ RSpec.describe 'Tags' do
     it 'returns expected tag content' do
       subject
 
-      expect(body_as_json[:id].to_i).to eq(tag.id)
-      expect(body_as_json[:name]).to eq(tag.name)
+      expect(response.parsed_body[:id].to_i).to eq(tag.id)
+      expect(response.parsed_body[:name]).to eq(tag.name)
     end
 
     context 'when the requested tag does not exist' do
@@ -116,8 +116,8 @@ RSpec.describe 'Tags' do
     it 'returns updated tag' do
       subject
 
-      expect(body_as_json[:id].to_i).to eq(tag.id)
-      expect(body_as_json[:name]).to eq(tag.name.upcase)
+      expect(response.parsed_body[:id].to_i).to eq(tag.id)
+      expect(response.parsed_body[:name]).to eq(tag.name.upcase)
     end
 
     context 'when the updated display name is invalid' do
diff --git a/spec/requests/api/v1/admin/trends/links/links_spec.rb b/spec/requests/api/v1/admin/trends/links/links_spec.rb
index 082af785a..c436b7081 100644
--- a/spec/requests/api/v1/admin/trends/links/links_spec.rb
+++ b/spec/requests/api/v1/admin/trends/links/links_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe 'Links' do
     end
 
     def expects_correct_link_data
-      expect(body_as_json).to match(
+      expect(response.parsed_body).to match(
         a_hash_including(
           url: preview_card.url,
           title: preview_card.title,
@@ -98,7 +98,7 @@ RSpec.describe 'Links' do
     it 'returns the link data' do
       subject
 
-      expect(body_as_json).to match(
+      expect(response.parsed_body).to match(
         a_hash_including(
           url: preview_card.url,
           title: preview_card.title,
diff --git a/spec/requests/api/v1/annual_reports_spec.rb b/spec/requests/api/v1/annual_reports_spec.rb
index bab184787..8051a6548 100644
--- a/spec/requests/api/v1/annual_reports_spec.rb
+++ b/spec/requests/api/v1/annual_reports_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'API V1 Annual Reports' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
       end
     end
diff --git a/spec/requests/api/v1/apps/credentials_spec.rb b/spec/requests/api/v1/apps/credentials_spec.rb
index b89999964..1cd6a4178 100644
--- a/spec/requests/api/v1/apps/credentials_spec.rb
+++ b/spec/requests/api/v1/apps/credentials_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Credentials' do
 
         expect(response).to have_http_status(200)
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(
             id: token.application.id.to_s,
             name: token.application.name,
@@ -37,8 +37,8 @@ RSpec.describe 'Credentials' do
 
         expect(response).to have_http_status(200)
 
-        expect(body_as_json[:client_id]).to_not be_present
-        expect(body_as_json[:client_secret]).to_not be_present
+        expect(response.parsed_body[:client_id]).to_not be_present
+        expect(response.parsed_body[:client_secret]).to_not be_present
       end
     end
 
@@ -56,7 +56,7 @@ RSpec.describe 'Credentials' do
       it 'returns the app information correctly' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(
             id: token.application.id.to_s,
             name: token.application.name,
@@ -95,7 +95,7 @@ RSpec.describe 'Credentials' do
       it 'returns the error in the json response' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(
             error: 'The access token was revoked'
           )
@@ -117,7 +117,7 @@ RSpec.describe 'Credentials' do
       it 'returns the error in the json response' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(
             error: 'The access token is invalid'
           )
diff --git a/spec/requests/api/v1/apps_spec.rb b/spec/requests/api/v1/apps_spec.rb
index 81d6c6812..51a0c3fd0 100644
--- a/spec/requests/api/v1/apps_spec.rb
+++ b/spec/requests/api/v1/apps_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe 'Apps' do
         expect(app.scopes.to_s).to eq scopes
         expect(app.redirect_uris).to eq redirect_uris
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(
             id: app.id.to_s,
             client_id: app.uid,
@@ -61,7 +61,7 @@ RSpec.describe 'Apps' do
         expect(response).to have_http_status(200)
         expect(Doorkeeper::Application.find_by(name: client_name)).to be_present
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to include(
             scopes: Doorkeeper.config.default_scopes.to_a
           )
@@ -82,7 +82,7 @@ RSpec.describe 'Apps' do
         expect(app).to be_present
         expect(app.scopes.to_s).to eq 'read'
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to include(
             scopes: %w(read)
           )
@@ -165,7 +165,7 @@ RSpec.describe 'Apps' do
         expect(app.redirect_uri).to eq redirect_uris
         expect(app.redirect_uris).to eq redirect_uris.split
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to include(
             redirect_uri: redirect_uris,
             redirect_uris: redirect_uris.split
@@ -187,7 +187,7 @@ RSpec.describe 'Apps' do
         expect(app.redirect_uri).to eq redirect_uris.join "\n"
         expect(app.redirect_uris).to eq redirect_uris
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to include(
             redirect_uri: redirect_uris.join("\n"),
             redirect_uris: redirect_uris
diff --git a/spec/requests/api/v1/blocks_spec.rb b/spec/requests/api/v1/blocks_spec.rb
index 06d2c4d99..d2f1c46a5 100644
--- a/spec/requests/api/v1/blocks_spec.rb
+++ b/spec/requests/api/v1/blocks_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match_array(expected_response)
+      expect(response.parsed_body).to match_array(expected_response)
     end
 
     context 'with limit param' do
@@ -35,7 +35,7 @@ RSpec.describe 'Blocks' do
       it 'returns only the requested number of blocked accounts' do
         subject
 
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
 
       it 'sets correct link header pagination' do
@@ -55,7 +55,7 @@ RSpec.describe 'Blocks' do
       it 'queries the blocks in range according to max_id', :aggregate_failures do
         subject
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to contain_exactly(include(id: blocks.first.target_account.id.to_s))
       end
     end
@@ -66,7 +66,7 @@ RSpec.describe 'Blocks' do
       it 'queries the blocks in range according to since_id', :aggregate_failures do
         subject
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to contain_exactly(include(id: blocks[2].target_account.id.to_s))
       end
     end
diff --git a/spec/requests/api/v1/bookmarks_spec.rb b/spec/requests/api/v1/bookmarks_spec.rb
index dc32820c8..95a71abca 100644
--- a/spec/requests/api/v1/bookmarks_spec.rb
+++ b/spec/requests/api/v1/bookmarks_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Bookmarks' do
     it 'returns the bookmarked statuses' do
       subject
 
-      expect(body_as_json).to match_array(expected_response)
+      expect(response.parsed_body).to match_array(expected_response)
     end
 
     context 'with limit param' do
@@ -42,7 +42,7 @@ RSpec.describe 'Bookmarks' do
       it 'paginates correctly', :aggregate_failures do
         subject
 
-        expect(body_as_json.size)
+        expect(response.parsed_body.size)
           .to eq(params[:limit])
 
         expect(response)
diff --git a/spec/requests/api/v1/conversations_spec.rb b/spec/requests/api/v1/conversations_spec.rb
index f136e1f4e..bd3cbfd0e 100644
--- a/spec/requests/api/v1/conversations_spec.rb
+++ b/spec/requests/api/v1/conversations_spec.rb
@@ -31,8 +31,8 @@ RSpec.describe 'API V1 Conversations' do
     it 'returns conversations', :aggregate_failures do
       get '/api/v1/conversations', headers: headers
 
-      expect(body_as_json.size).to eq 2
-      expect(body_as_json[0][:accounts].size).to eq 1
+      expect(response.parsed_body.size).to eq 2
+      expect(response.parsed_body.first[:accounts].size).to eq 1
     end
 
     context 'with since_id' do
@@ -40,7 +40,7 @@ RSpec.describe 'API V1 Conversations' do
         it 'returns conversations' do
           get '/api/v1/conversations', params: { since_id: Mastodon::Snowflake.id_at(1.hour.ago, with_random: false) }, headers: headers
 
-          expect(body_as_json.size).to eq 2
+          expect(response.parsed_body.size).to eq 2
         end
       end
 
@@ -48,7 +48,7 @@ RSpec.describe 'API V1 Conversations' do
         it 'returns no conversation' do
           get '/api/v1/conversations', params: { since_id: Mastodon::Snowflake.id_at(1.hour.from_now, with_random: false) }, headers: headers
 
-          expect(body_as_json.size).to eq 0
+          expect(response.parsed_body.size).to eq 0
         end
       end
     end
diff --git a/spec/requests/api/v1/custom_emojis_spec.rb b/spec/requests/api/v1/custom_emojis_spec.rb
index 798d8e29e..0942734ff 100644
--- a/spec/requests/api/v1/custom_emojis_spec.rb
+++ b/spec/requests/api/v1/custom_emojis_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Custom Emojis' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
           .and have_attributes(
             first: include(shortcode: 'coolcat')
@@ -34,7 +34,7 @@ RSpec.describe 'Custom Emojis' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
           .and have_attributes(
             first: include(shortcode: 'coolcat')
diff --git a/spec/requests/api/v1/directories_spec.rb b/spec/requests/api/v1/directories_spec.rb
index 94306c06e..aa602a71c 100644
--- a/spec/requests/api/v1/directories_spec.rb
+++ b/spec/requests/api/v1/directories_spec.rb
@@ -82,8 +82,8 @@ RSpec.describe 'Directories API' do
         get '/api/v1/directory', headers: headers
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to eq(2)
-        expect(body_as_json.pluck(:id)).to contain_exactly(eligible_remote_account.id.to_s, local_discoverable_account.id.to_s)
+        expect(response.parsed_body.size).to eq(2)
+        expect(response.parsed_body.pluck(:id)).to contain_exactly(eligible_remote_account.id.to_s, local_discoverable_account.id.to_s)
       end
     end
 
@@ -101,8 +101,8 @@ RSpec.describe 'Directories API' do
         get '/api/v1/directory', headers: headers, params: { local: '1' }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to eq(1)
-        expect(body_as_json.first[:id]).to include(local_account.id.to_s)
+        expect(response.parsed_body.size).to eq(1)
+        expect(response.parsed_body.first[:id]).to include(local_account.id.to_s)
         expect(response.body).to_not include(remote_account.id.to_s)
       end
     end
@@ -115,9 +115,9 @@ RSpec.describe 'Directories API' do
         get '/api/v1/directory', headers: headers, params: { order: 'active' }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to eq(2)
-        expect(body_as_json.first[:id]).to include(new_stat.account_id.to_s)
-        expect(body_as_json.second[:id]).to include(old_stat.account_id.to_s)
+        expect(response.parsed_body.size).to eq(2)
+        expect(response.parsed_body.first[:id]).to include(new_stat.account_id.to_s)
+        expect(response.parsed_body.second[:id]).to include(old_stat.account_id.to_s)
       end
     end
 
@@ -130,9 +130,9 @@ RSpec.describe 'Directories API' do
         get '/api/v1/directory', headers: headers, params: { order: 'new' }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to eq(2)
-        expect(body_as_json.first[:id]).to include(account_new.id.to_s)
-        expect(body_as_json.second[:id]).to include(account_old.id.to_s)
+        expect(response.parsed_body.size).to eq(2)
+        expect(response.parsed_body.first[:id]).to include(account_new.id.to_s)
+        expect(response.parsed_body.second[:id]).to include(account_old.id.to_s)
       end
     end
   end
diff --git a/spec/requests/api/v1/domain_blocks_spec.rb b/spec/requests/api/v1/domain_blocks_spec.rb
index 954497ebe..8184c26be 100644
--- a/spec/requests/api/v1/domain_blocks_spec.rb
+++ b/spec/requests/api/v1/domain_blocks_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Domain blocks' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match_array(blocked_domains)
+      expect(response.parsed_body).to match_array(blocked_domains)
     end
 
     context 'with limit param' do
@@ -35,7 +35,7 @@ RSpec.describe 'Domain blocks' do
       it 'returns only the requested number of blocked domains' do
         subject
 
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
     end
   end
diff --git a/spec/requests/api/v1/emails/confirmations_spec.rb b/spec/requests/api/v1/emails/confirmations_spec.rb
index 8f5171ee7..0a419a10c 100644
--- a/spec/requests/api/v1/emails/confirmations_spec.rb
+++ b/spec/requests/api/v1/emails/confirmations_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe 'Confirmations' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json).to be false
+          expect(response.parsed_body).to be false
         end
       end
 
@@ -122,7 +122,7 @@ RSpec.describe 'Confirmations' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json).to be true
+          expect(response.parsed_body).to be true
         end
       end
     end
@@ -139,7 +139,7 @@ RSpec.describe 'Confirmations' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json).to be false
+          expect(response.parsed_body).to be false
         end
       end
 
@@ -150,7 +150,7 @@ RSpec.describe 'Confirmations' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json).to be true
+          expect(response.parsed_body).to be true
         end
       end
     end
diff --git a/spec/requests/api/v1/endorsements_spec.rb b/spec/requests/api/v1/endorsements_spec.rb
index 255211a40..25917f527 100644
--- a/spec/requests/api/v1/endorsements_spec.rb
+++ b/spec/requests/api/v1/endorsements_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe 'Endorsements' do
           expect(response)
             .to have_http_status(200)
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to be_present
             .and have_attributes(
               first: include(acct: account_pin.target_account.acct)
@@ -52,7 +52,7 @@ RSpec.describe 'Endorsements' do
           expect(response)
             .to have_http_status(200)
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to_not be_present
         end
       end
diff --git a/spec/requests/api/v1/favourites_spec.rb b/spec/requests/api/v1/favourites_spec.rb
index b988ac99d..78e9d6155 100644
--- a/spec/requests/api/v1/favourites_spec.rb
+++ b/spec/requests/api/v1/favourites_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Favourites' do
     it 'returns the favourites' do
       subject
 
-      expect(body_as_json).to match_array(expected_response)
+      expect(response.parsed_body).to match_array(expected_response)
     end
 
     context 'with limit param' do
@@ -42,7 +42,7 @@ RSpec.describe 'Favourites' do
       it 'returns only the requested number of favourites' do
         subject
 
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
 
       it 'sets the correct pagination headers' do
diff --git a/spec/requests/api/v1/featured_tags/suggestions_spec.rb b/spec/requests/api/v1/featured_tags/suggestions_spec.rb
index 0a7bfe5cd..8815c65cf 100644
--- a/spec/requests/api/v1/featured_tags/suggestions_spec.rb
+++ b/spec/requests/api/v1/featured_tags/suggestions_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Featured Tags Suggestions API' do
 
       expect(response)
         .to have_http_status(200)
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to contain_exactly(
           include(name: used_tag.name)
         )
diff --git a/spec/requests/api/v1/featured_tags_spec.rb b/spec/requests/api/v1/featured_tags_spec.rb
index 81e99e015..423cc0c56 100644
--- a/spec/requests/api/v1/featured_tags_spec.rb
+++ b/spec/requests/api/v1/featured_tags_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe 'FeaturedTags' do
       it 'returns an empty body' do
         get '/api/v1/featured_tags', headers: headers
 
-        expect(body_as_json).to be_empty
+        expect(response.parsed_body).to be_empty
       end
     end
 
@@ -47,7 +47,7 @@ RSpec.describe 'FeaturedTags' do
       it 'returns only the featured tags belonging to the requesting user' do
         get '/api/v1/featured_tags', headers: headers
 
-        expect(body_as_json.pluck(:id))
+        expect(response.parsed_body.pluck(:id))
           .to match_array(
             user_featured_tags.pluck(:id).map(&:to_s)
           )
@@ -67,7 +67,7 @@ RSpec.describe 'FeaturedTags' do
     it 'returns the correct tag name' do
       post '/api/v1/featured_tags', headers: headers, params: params
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           name: params[:name]
         )
@@ -141,7 +141,7 @@ RSpec.describe 'FeaturedTags' do
     it 'returns an empty body' do
       delete "/api/v1/featured_tags/#{id}", headers: headers
 
-      expect(body_as_json).to be_empty
+      expect(response.parsed_body).to be_empty
     end
 
     it 'deletes the featured tag', :inline_jobs do
diff --git a/spec/requests/api/v1/filters_spec.rb b/spec/requests/api/v1/filters_spec.rb
index deb6e7421..93ed78b34 100644
--- a/spec/requests/api/v1/filters_spec.rb
+++ b/spec/requests/api/v1/filters_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'API V1 Filters' do
     it 'returns http success' do
       get '/api/v1/filters', headers: headers
       expect(response).to have_http_status(200)
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to contain_exactly(
           include(id: custom_filter_keyword.id.to_s)
         )
diff --git a/spec/requests/api/v1/follow_requests_spec.rb b/spec/requests/api/v1/follow_requests_spec.rb
index a8898ccb3..c143ccaec 100644
--- a/spec/requests/api/v1/follow_requests_spec.rb
+++ b/spec/requests/api/v1/follow_requests_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Follow requests' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match_array(expected_response)
+      expect(response.parsed_body).to match_array(expected_response)
     end
 
     context 'with limit param' do
@@ -45,7 +45,7 @@ RSpec.describe 'Follow requests' do
       it 'returns only the requested number of follow requests' do
         subject
 
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
     end
   end
@@ -66,7 +66,7 @@ RSpec.describe 'Follow requests' do
     it 'allows the requesting follower to follow', :aggregate_failures do
       expect { subject }.to change { follower.following?(user.account) }.from(false).to(true)
       expect(response).to have_http_status(200)
-      expect(body_as_json[:followed_by]).to be true
+      expect(response.parsed_body[:followed_by]).to be true
     end
   end
 
@@ -88,7 +88,7 @@ RSpec.describe 'Follow requests' do
 
       expect(response).to have_http_status(200)
       expect(FollowRequest.where(target_account: user.account, account: follower)).to_not exist
-      expect(body_as_json[:followed_by]).to be false
+      expect(response.parsed_body[:followed_by]).to be false
     end
   end
 end
diff --git a/spec/requests/api/v1/followed_tags_spec.rb b/spec/requests/api/v1/followed_tags_spec.rb
index 3d2d82d5d..f7787cb76 100644
--- a/spec/requests/api/v1/followed_tags_spec.rb
+++ b/spec/requests/api/v1/followed_tags_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe 'Followed tags' do
     it 'returns the followed tags correctly' do
       subject
 
-      expect(body_as_json).to match_array(expected_response)
+      expect(response.parsed_body).to match_array(expected_response)
     end
 
     context 'with limit param' do
@@ -46,7 +46,7 @@ RSpec.describe 'Followed tags' do
       it 'returns only the requested number of follow tags' do
         subject
 
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
 
       it 'sets the correct pagination headers' do
diff --git a/spec/requests/api/v1/instance_spec.rb b/spec/requests/api/v1/instance_spec.rb
index f0a4ceadb..8d6ba572e 100644
--- a/spec/requests/api/v1/instance_spec.rb
+++ b/spec/requests/api/v1/instance_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Instances' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
           .and include(title: 'Mastodon')
       end
@@ -28,7 +28,7 @@ RSpec.describe 'Instances' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
           .and include(title: 'Mastodon')
       end
diff --git a/spec/requests/api/v1/instances/activity_spec.rb b/spec/requests/api/v1/instances/activity_spec.rb
index 4f2bc91ad..72e3faeb6 100644
--- a/spec/requests/api/v1/instances/activity_spec.rb
+++ b/spec/requests/api/v1/instances/activity_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Activity' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
           .and(be_an(Array))
           .and(have_attributes(size: Api::V1::Instances::ActivityController::WEEKS_OF_ACTIVITY))
diff --git a/spec/requests/api/v1/instances/domain_blocks_spec.rb b/spec/requests/api/v1/instances/domain_blocks_spec.rb
index 397ecff08..460d33860 100644
--- a/spec/requests/api/v1/instances/domain_blocks_spec.rb
+++ b/spec/requests/api/v1/instances/domain_blocks_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Domain Blocks' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
           .and(be_an(Array))
           .and(have_attributes(size: 1))
diff --git a/spec/requests/api/v1/instances/extended_descriptions_spec.rb b/spec/requests/api/v1/instances/extended_descriptions_spec.rb
index 64982de68..bf6d58216 100644
--- a/spec/requests/api/v1/instances/extended_descriptions_spec.rb
+++ b/spec/requests/api/v1/instances/extended_descriptions_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Extended Descriptions' do
       expect(response)
         .to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to be_present
         .and include(:content)
     end
diff --git a/spec/requests/api/v1/instances/languages_spec.rb b/spec/requests/api/v1/instances/languages_spec.rb
index 8ab8bf99c..79ea62c59 100644
--- a/spec/requests/api/v1/instances/languages_spec.rb
+++ b/spec/requests/api/v1/instances/languages_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Languages' do
     end
 
     it 'returns the supported languages' do
-      expect(body_as_json.pluck(:code)).to match_array LanguagesHelper::SUPPORTED_LOCALES.keys.map(&:to_s)
+      expect(response.parsed_body.pluck(:code)).to match_array LanguagesHelper::SUPPORTED_LOCALES.keys.map(&:to_s)
     end
   end
 end
diff --git a/spec/requests/api/v1/instances/peers_spec.rb b/spec/requests/api/v1/instances/peers_spec.rb
index 1a7975f8b..1140612f0 100644
--- a/spec/requests/api/v1/instances/peers_spec.rb
+++ b/spec/requests/api/v1/instances/peers_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Peers' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_an(Array)
       end
     end
diff --git a/spec/requests/api/v1/instances/privacy_policies_spec.rb b/spec/requests/api/v1/instances/privacy_policies_spec.rb
index 24de98d88..93490542c 100644
--- a/spec/requests/api/v1/instances/privacy_policies_spec.rb
+++ b/spec/requests/api/v1/instances/privacy_policies_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Privacy Policy' do
       expect(response)
         .to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to be_present
         .and include(:content)
     end
diff --git a/spec/requests/api/v1/instances/rules_spec.rb b/spec/requests/api/v1/instances/rules_spec.rb
index 65b8d78c7..620c991ae 100644
--- a/spec/requests/api/v1/instances/rules_spec.rb
+++ b/spec/requests/api/v1/instances/rules_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Rules' do
       expect(response)
         .to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to be_an(Array)
     end
   end
diff --git a/spec/requests/api/v1/instances/translation_languages_spec.rb b/spec/requests/api/v1/instances/translation_languages_spec.rb
index e5a480c17..0de5ec3bc 100644
--- a/spec/requests/api/v1/instances/translation_languages_spec.rb
+++ b/spec/requests/api/v1/instances/translation_languages_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Translation Languages' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to eq({})
       end
     end
@@ -25,7 +25,7 @@ RSpec.describe 'Translation Languages' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to match({ und: %w(en de), en: ['de'] })
       end
 
diff --git a/spec/requests/api/v1/lists/accounts_spec.rb b/spec/requests/api/v1/lists/accounts_spec.rb
index de4998235..d147b21ee 100644
--- a/spec/requests/api/v1/lists/accounts_spec.rb
+++ b/spec/requests/api/v1/lists/accounts_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Accounts' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match_array(expected_response)
+      expect(response.parsed_body).to match_array(expected_response)
     end
 
     context 'with limit param' do
@@ -43,7 +43,7 @@ RSpec.describe 'Accounts' do
       it 'returns only the requested number of accounts' do
         subject
 
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
     end
   end
diff --git a/spec/requests/api/v1/lists_spec.rb b/spec/requests/api/v1/lists_spec.rb
index cf5ac28a5..2042a64d5 100644
--- a/spec/requests/api/v1/lists_spec.rb
+++ b/spec/requests/api/v1/lists_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Lists' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match_array(expected_response)
+      expect(response.parsed_body).to match_array(expected_response)
     end
   end
 
@@ -60,7 +60,7 @@ RSpec.describe 'Lists' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match({
+      expect(response.parsed_body).to match({
         id: list.id.to_s,
         title: list.title,
         replies_policy: list.replies_policy,
@@ -100,7 +100,7 @@ RSpec.describe 'Lists' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match(a_hash_including(title: 'my list', replies_policy: 'none', exclusive: true))
+      expect(response.parsed_body).to match(a_hash_including(title: 'my list', replies_policy: 'none', exclusive: true))
       expect(List.where(account: user.account).count).to eq(1)
     end
 
@@ -144,7 +144,7 @@ RSpec.describe 'Lists' do
       expect(response).to have_http_status(200)
       list.reload
 
-      expect(body_as_json).to match({
+      expect(response.parsed_body).to match({
         id: list.id.to_s,
         title: list.title,
         replies_policy: list.replies_policy,
diff --git a/spec/requests/api/v1/markers_spec.rb b/spec/requests/api/v1/markers_spec.rb
index 2dbb9d205..a10d2dc3e 100644
--- a/spec/requests/api/v1/markers_spec.rb
+++ b/spec/requests/api/v1/markers_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'API Markers' do
 
     it 'returns markers', :aggregate_failures do
       expect(response).to have_http_status(200)
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           home: include(last_read_id: '123'),
           notifications: include(last_read_id: '456')
@@ -61,7 +61,7 @@ RSpec.describe 'API Markers' do
       it 'returns error json' do
         expect(response)
           .to have_http_status(409)
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to include(error: /Conflict during update/)
       end
     end
diff --git a/spec/requests/api/v1/media_spec.rb b/spec/requests/api/v1/media_spec.rb
index c89c49afd..d0af33482 100644
--- a/spec/requests/api/v1/media_spec.rb
+++ b/spec/requests/api/v1/media_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Media' do
     it 'returns the media information' do
       subject
 
-      expect(body_as_json).to match(
+      expect(response.parsed_body).to match(
         a_hash_including(
           id: media.id.to_s,
           description: media.description,
@@ -83,7 +83,7 @@ RSpec.describe 'Media' do
         expect(MediaAttachment.first).to be_present
         expect(MediaAttachment.first).to have_attached_file(:file)
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(id: MediaAttachment.first.id.to_s, description: params[:description], type: media_type)
         )
       end
diff --git a/spec/requests/api/v1/mutes_spec.rb b/spec/requests/api/v1/mutes_spec.rb
index 988bb3c39..6402c908f 100644
--- a/spec/requests/api/v1/mutes_spec.rb
+++ b/spec/requests/api/v1/mutes_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Mutes' do
 
       muted_accounts = mutes.map(&:target_account)
 
-      expect(body_as_json.pluck(:id)).to match_array(muted_accounts.map { |account| account.id.to_s })
+      expect(response.parsed_body.pluck(:id)).to match_array(muted_accounts.map { |account| account.id.to_s })
     end
 
     context 'with limit param' do
@@ -38,7 +38,7 @@ RSpec.describe 'Mutes' do
       it 'returns only the requested number of muted accounts' do
         subject
 
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
 
       it 'sets the correct pagination headers', :aggregate_failures do
@@ -58,7 +58,7 @@ RSpec.describe 'Mutes' do
       it 'queries mutes in range according to max_id', :aggregate_failures do
         subject
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to contain_exactly(include(id: mutes.first.target_account_id.to_s))
       end
     end
@@ -69,7 +69,7 @@ RSpec.describe 'Mutes' do
       it 'queries mutes in range according to since_id', :aggregate_failures do
         subject
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to contain_exactly(include(id: mutes[1].target_account_id.to_s))
       end
     end
diff --git a/spec/requests/api/v1/notifications/policies_spec.rb b/spec/requests/api/v1/notifications/policies_spec.rb
index a73d4217b..8bafcad2f 100644
--- a/spec/requests/api/v1/notifications/policies_spec.rb
+++ b/spec/requests/api/v1/notifications/policies_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Policies' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to include(
+        expect(response.parsed_body).to include(
           filter_not_following: false,
           filter_not_followers: false,
           filter_new_accounts: false,
@@ -54,7 +54,7 @@ RSpec.describe 'Policies' do
         .to change { NotificationPolicy.find_or_initialize_by(account: user.account).for_not_following.to_sym }.from(:accept).to(:filter)
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to include(
+      expect(response.parsed_body).to include(
         filter_not_following: true,
         filter_not_followers: false,
         filter_new_accounts: false,
diff --git a/spec/requests/api/v1/notifications/requests_spec.rb b/spec/requests/api/v1/notifications/requests_spec.rb
index 45bb71adb..dc125bc7a 100644
--- a/spec/requests/api/v1/notifications/requests_spec.rb
+++ b/spec/requests/api/v1/notifications/requests_spec.rb
@@ -133,7 +133,7 @@ RSpec.describe 'Requests' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match({ merged: true })
+        expect(response.parsed_body).to match({ merged: true })
       end
     end
 
@@ -146,7 +146,7 @@ RSpec.describe 'Requests' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match({ merged: false })
+        expect(response.parsed_body).to match({ merged: false })
       end
     end
   end
diff --git a/spec/requests/api/v1/notifications_spec.rb b/spec/requests/api/v1/notifications_spec.rb
index 84e6db1e5..b74adb5df 100644
--- a/spec/requests/api/v1/notifications_spec.rb
+++ b/spec/requests/api/v1/notifications_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 5
+        expect(response.parsed_body[:count]).to eq 5
       end
     end
 
@@ -45,7 +45,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 2
+        expect(response.parsed_body[:count]).to eq 2
       end
     end
 
@@ -56,7 +56,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 4
+        expect(response.parsed_body[:count]).to eq 4
       end
     end
 
@@ -67,7 +67,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 2
+        expect(response.parsed_body[:count]).to eq 2
       end
     end
 
@@ -80,7 +80,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq Api::V1::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT
+        expect(response.parsed_body[:count]).to eq Api::V1::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT
       end
     end
   end
@@ -111,9 +111,9 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to eq 5
+        expect(response.parsed_body.size).to eq 5
         expect(body_json_types).to include('reblog', 'mention', 'favourite', 'follow')
-        expect(body_as_json.any? { |x| x[:filtered] }).to be false
+        expect(response.parsed_body.any? { |x| x[:filtered] }).to be false
       end
     end
 
@@ -124,9 +124,9 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to eq 6
+        expect(response.parsed_body.size).to eq 6
         expect(body_json_types).to include('reblog', 'mention', 'favourite', 'follow')
-        expect(body_as_json.any? { |x| x[:filtered] }).to be true
+        expect(response.parsed_body.any? { |x| x[:filtered] }).to be true
       end
     end
 
@@ -141,7 +141,7 @@ RSpec.describe 'Notifications' do
       end
 
       def body_json_account_ids
-        body_as_json.map { |x| x[:account][:id] }
+        response.parsed_body.map { |x| x[:account][:id] }
       end
     end
 
@@ -152,7 +152,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to eq 0
+        expect(response.parsed_body.size).to eq 0
       end
     end
 
@@ -163,7 +163,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to_not eq 0
+        expect(response.parsed_body.size).to_not eq 0
         expect(body_json_types.uniq).to_not include 'mention'
       end
     end
@@ -187,7 +187,7 @@ RSpec.describe 'Notifications' do
 
         notifications = user.account.notifications.browserable.order(id: :asc)
 
-        expect(body_as_json.size)
+        expect(response.parsed_body.size)
           .to eq(params[:limit])
 
         expect(response)
@@ -199,7 +199,7 @@ RSpec.describe 'Notifications' do
     end
 
     def body_json_types
-      body_as_json.pluck(:type)
+      response.parsed_body.pluck(:type)
     end
   end
 
diff --git a/spec/requests/api/v1/peers/search_spec.rb b/spec/requests/api/v1/peers/search_spec.rb
index 87b0dc4f6..dc5f550d0 100644
--- a/spec/requests/api/v1/peers/search_spec.rb
+++ b/spec/requests/api/v1/peers/search_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'API Peers Search' do
 
         expect(response)
           .to have_http_status(200)
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_blank
       end
     end
@@ -34,7 +34,7 @@ RSpec.describe 'API Peers Search' do
 
         expect(response)
           .to have_http_status(200)
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_blank
       end
     end
@@ -49,9 +49,9 @@ RSpec.describe 'API Peers Search' do
 
         expect(response)
           .to have_http_status(200)
-        expect(body_as_json.size)
+        expect(response.parsed_body.size)
           .to eq(1)
-        expect(body_as_json.first)
+        expect(response.parsed_body.first)
           .to eq(account.domain)
       end
     end
diff --git a/spec/requests/api/v1/polls_spec.rb b/spec/requests/api/v1/polls_spec.rb
index 1c8a818d5..138a37a73 100644
--- a/spec/requests/api/v1/polls_spec.rb
+++ b/spec/requests/api/v1/polls_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'Polls' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(
             id: poll.id.to_s,
             voted: false,
diff --git a/spec/requests/api/v1/preferences_spec.rb b/spec/requests/api/v1/preferences_spec.rb
index 6508b51c0..d6991ca90 100644
--- a/spec/requests/api/v1/preferences_spec.rb
+++ b/spec/requests/api/v1/preferences_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Preferences' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
       end
     end
diff --git a/spec/requests/api/v1/push/subscriptions_spec.rb b/spec/requests/api/v1/push/subscriptions_spec.rb
index 6674b048e..a9587f8d5 100644
--- a/spec/requests/api/v1/push/subscriptions_spec.rb
+++ b/spec/requests/api/v1/push/subscriptions_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe 'API V1 Push Subscriptions' do
           access_token_id: eq(token.id)
         )
 
-      expect(body_as_json.with_indifferent_access)
+      expect(response.parsed_body.with_indifferent_access)
         .to include(
           { endpoint: create_payload[:subscription][:endpoint], alerts: {}, policy: 'all' }
         )
@@ -124,7 +124,7 @@ RSpec.describe 'API V1 Push Subscriptions' do
         )
       end
 
-      expect(body_as_json.with_indifferent_access)
+      expect(response.parsed_body.with_indifferent_access)
         .to include(
           endpoint: create_payload[:subscription][:endpoint],
           alerts: alerts_payload[:data][:alerts],
diff --git a/spec/requests/api/v1/reports_spec.rb b/spec/requests/api/v1/reports_spec.rb
index a72d9bbcd..a176bd78a 100644
--- a/spec/requests/api/v1/reports_spec.rb
+++ b/spec/requests/api/v1/reports_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe 'Reports' do
       emails = capture_emails { subject }
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match(
+      expect(response.parsed_body).to match(
         a_hash_including(
           status_ids: [status.id.to_s],
           category: category,
diff --git a/spec/requests/api/v1/scheduled_status_spec.rb b/spec/requests/api/v1/scheduled_status_spec.rb
index b35d297a6..eb03827c9 100644
--- a/spec/requests/api/v1/scheduled_status_spec.rb
+++ b/spec/requests/api/v1/scheduled_status_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'Scheduled Statuses' do
           expect(response)
             .to have_http_status(200)
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to_not be_present
         end
       end
@@ -60,7 +60,7 @@ RSpec.describe 'Scheduled Statuses' do
           expect(response)
             .to have_http_status(200)
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to be_present
             .and have_attributes(
               first: include(id: scheduled_status.id.to_s)
diff --git a/spec/requests/api/v1/statuses/bookmarks_spec.rb b/spec/requests/api/v1/statuses/bookmarks_spec.rb
index d3007740a..f1bcfda0f 100644
--- a/spec/requests/api/v1/statuses/bookmarks_spec.rb
+++ b/spec/requests/api/v1/statuses/bookmarks_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe 'Bookmarks' do
       it 'returns json with updated attributes' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(id: status.id.to_s, bookmarked: true)
         )
       end
@@ -103,7 +103,7 @@ RSpec.describe 'Bookmarks' do
         it 'returns json with updated attributes' do
           subject
 
-          expect(body_as_json).to match(
+          expect(response.parsed_body).to match(
             a_hash_including(id: status.id.to_s, bookmarked: false)
           )
         end
@@ -127,7 +127,7 @@ RSpec.describe 'Bookmarks' do
         it 'returns json with updated attributes' do
           subject
 
-          expect(body_as_json).to match(
+          expect(response.parsed_body).to match(
             a_hash_including(id: status.id.to_s, bookmarked: false)
           )
         end
diff --git a/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb b/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb
index 2fd79f424..24bd03d34 100644
--- a/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb
+++ b/spec/requests/api/v1/statuses/favourited_by_accounts_spec.rb
@@ -34,9 +34,9 @@ RSpec.describe 'API V1 Statuses Favourited by Accounts' do
             next: api_v1_status_favourited_by_index_url(limit: 2, max_id: Favourite.first.id)
           )
 
-        expect(body_as_json.size)
+        expect(response.parsed_body.size)
           .to eq(2)
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to contain_exactly(
             include(id: alice.id.to_s),
             include(id: bob.id.to_s)
@@ -48,9 +48,9 @@ RSpec.describe 'API V1 Statuses Favourited by Accounts' do
 
         subject
 
-        expect(body_as_json.size)
+        expect(response.parsed_body.size)
           .to eq 1
-        expect(body_as_json.first[:id]).to eq(alice.id.to_s)
+        expect(response.parsed_body.first[:id]).to eq(alice.id.to_s)
       end
     end
   end
diff --git a/spec/requests/api/v1/statuses/favourites_spec.rb b/spec/requests/api/v1/statuses/favourites_spec.rb
index 22d0e4831..f9f0ff629 100644
--- a/spec/requests/api/v1/statuses/favourites_spec.rb
+++ b/spec/requests/api/v1/statuses/favourites_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe 'Favourites', :inline_jobs do
       it 'returns json with updated attributes' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(id: status.id.to_s, favourites_count: 1, favourited: true)
         )
       end
@@ -95,7 +95,7 @@ RSpec.describe 'Favourites', :inline_jobs do
       it 'returns json with updated attributes' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(id: status.id.to_s, favourites_count: 0, favourited: false)
         )
       end
@@ -118,7 +118,7 @@ RSpec.describe 'Favourites', :inline_jobs do
       it 'returns json with updated attributes' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(id: status.id.to_s, favourites_count: 0, favourited: false)
         )
       end
diff --git a/spec/requests/api/v1/statuses/histories_spec.rb b/spec/requests/api/v1/statuses/histories_spec.rb
index f13bf7986..4115a52fa 100644
--- a/spec/requests/api/v1/statuses/histories_spec.rb
+++ b/spec/requests/api/v1/statuses/histories_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'API V1 Statuses Histories' do
 
       it 'returns http success' do
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to_not be 0
+        expect(response.parsed_body.size).to_not be 0
       end
     end
   end
diff --git a/spec/requests/api/v1/statuses/pins_spec.rb b/spec/requests/api/v1/statuses/pins_spec.rb
index 3be1a16ee..56e60c6d3 100644
--- a/spec/requests/api/v1/statuses/pins_spec.rb
+++ b/spec/requests/api/v1/statuses/pins_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe 'Pins' do
       it 'return json with updated attributes' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(id: status.id.to_s, pinned: true)
         )
       end
@@ -96,7 +96,7 @@ RSpec.describe 'Pins' do
       it 'return json with updated attributes' do
         subject
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           a_hash_including(id: status.id.to_s, pinned: false)
         )
       end
diff --git a/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb b/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb
index 5fc54042f..bd26c22f0 100644
--- a/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb
+++ b/spec/requests/api/v1/statuses/reblogged_by_accounts_spec.rb
@@ -33,9 +33,9 @@ RSpec.describe 'API V1 Statuses Reblogged by Accounts' do
             next: api_v1_status_reblogged_by_index_url(limit: 2, max_id: alice.statuses.first.id)
           )
 
-        expect(body_as_json.size)
+        expect(response.parsed_body.size)
           .to eq(2)
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to contain_exactly(
             include(id: alice.id.to_s),
             include(id: bob.id.to_s)
@@ -47,9 +47,9 @@ RSpec.describe 'API V1 Statuses Reblogged by Accounts' do
 
         subject
 
-        expect(body_as_json.size)
+        expect(response.parsed_body.size)
           .to eq 1
-        expect(body_as_json.first[:id]).to eq(alice.id.to_s)
+        expect(response.parsed_body.first[:id]).to eq(alice.id.to_s)
       end
     end
   end
diff --git a/spec/requests/api/v1/statuses/reblogs_spec.rb b/spec/requests/api/v1/statuses/reblogs_spec.rb
index 0978c890a..8c7894d87 100644
--- a/spec/requests/api/v1/statuses/reblogs_spec.rb
+++ b/spec/requests/api/v1/statuses/reblogs_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'API V1 Statuses Reblogs' do
 
           expect(user.account.reblogged?(status)).to be true
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to include(
               reblog: include(
                 id: status.id.to_s,
@@ -60,7 +60,7 @@ RSpec.describe 'API V1 Statuses Reblogs' do
 
           expect(user.account.reblogged?(status)).to be false
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to include(
               id: status.id.to_s,
               reblogs_count: 0,
@@ -85,7 +85,7 @@ RSpec.describe 'API V1 Statuses Reblogs' do
 
           expect(user.account.reblogged?(status)).to be false
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to include(
               id: status.id.to_s,
               reblogs_count: 0,
diff --git a/spec/requests/api/v1/statuses/sources_spec.rb b/spec/requests/api/v1/statuses/sources_spec.rb
index c7b160382..eab19d64d 100644
--- a/spec/requests/api/v1/statuses/sources_spec.rb
+++ b/spec/requests/api/v1/statuses/sources_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'Sources' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match({
+        expect(response.parsed_body).to match({
           id: status.id.to_s,
           text: status.text,
           spoiler_text: status.spoiler_text,
@@ -51,7 +51,7 @@ RSpec.describe 'Sources' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match({
+        expect(response.parsed_body).to match({
           id: status.id.to_s,
           text: status.text,
           spoiler_text: status.spoiler_text,
diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb
index 1a211d14d..057800a26 100644
--- a/spec/requests/api/v1/statuses_spec.rb
+++ b/spec/requests/api/v1/statuses_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe '/api/v1/statuses' do
         get '/api/v1/statuses', headers: headers, params: { id: [status.id, other_status.id, 123_123] }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to contain_exactly(
+        expect(response.parsed_body).to contain_exactly(
           hash_including(id: status.id.to_s),
           hash_including(id: other_status.id.to_s)
         )
@@ -52,7 +52,7 @@ RSpec.describe '/api/v1/statuses' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[:filtered][0]).to include({
+          expect(response.parsed_body[:filtered][0]).to include({
             filter: a_hash_including({
               id: user.account.custom_filters.first.id.to_s,
               title: 'filter1',
@@ -75,7 +75,7 @@ RSpec.describe '/api/v1/statuses' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[:filtered][0]).to include({
+          expect(response.parsed_body[:filtered][0]).to include({
             filter: a_hash_including({
               id: user.account.custom_filters.first.id.to_s,
               title: 'filter1',
@@ -97,7 +97,7 @@ RSpec.describe '/api/v1/statuses' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[:reblog][:filtered][0]).to include({
+          expect(response.parsed_body[:reblog][:filtered][0]).to include({
             filter: a_hash_including({
               id: user.account.custom_filters.first.id.to_s,
               title: 'filter1',
@@ -154,7 +154,7 @@ RSpec.describe '/api/v1/statuses' do
           subject
 
           expect(response).to have_http_status(422)
-          expect(body_as_json[:unexpected_accounts].map { |a| a.slice(:id, :acct) }).to match [{ id: bob.id.to_s, acct: bob.acct }]
+          expect(response.parsed_body[:unexpected_accounts].map { |a| a.slice(:id, :acct) }).to match [{ id: bob.id.to_s, acct: bob.acct }]
         end
       end
 
diff --git a/spec/requests/api/v1/suggestions_spec.rb b/spec/requests/api/v1/suggestions_spec.rb
index b900c910d..8267bb92a 100644
--- a/spec/requests/api/v1/suggestions_spec.rb
+++ b/spec/requests/api/v1/suggestions_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Suggestions' do
     it 'returns accounts' do
       subject
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to contain_exactly(include(id: bob.id.to_s), include(id: jeff.id.to_s))
     end
 
@@ -42,7 +42,7 @@ RSpec.describe 'Suggestions' do
       it 'returns only the requested number of accounts' do
         subject
 
-        expect(body_as_json.size).to eq 1
+        expect(response.parsed_body.size).to eq 1
       end
     end
 
diff --git a/spec/requests/api/v1/tags_spec.rb b/spec/requests/api/v1/tags_spec.rb
index db74a6f03..9637823d4 100644
--- a/spec/requests/api/v1/tags_spec.rb
+++ b/spec/requests/api/v1/tags_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'Tags' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:name]).to eq(name)
+        expect(response.parsed_body[:name]).to eq(name)
       end
     end
 
diff --git a/spec/requests/api/v1/timelines/home_spec.rb b/spec/requests/api/v1/timelines/home_spec.rb
index 9dd102fcb..afad2988c 100644
--- a/spec/requests/api/v1/timelines/home_spec.rb
+++ b/spec/requests/api/v1/timelines/home_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe 'Home', :inline_jobs do
       it 'returns the statuses of followed users' do
         subject
 
-        expect(body_as_json.pluck(:id)).to match_array(home_statuses.map { |status| status.id.to_s })
+        expect(response.parsed_body.pluck(:id)).to match_array(home_statuses.map { |status| status.id.to_s })
       end
 
       context 'with limit param' do
@@ -49,7 +49,7 @@ RSpec.describe 'Home', :inline_jobs do
         it 'returns only the requested number of statuses' do
           subject
 
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
 
         it 'sets the correct pagination headers', :aggregate_failures do
diff --git a/spec/requests/api/v1/timelines/link_spec.rb b/spec/requests/api/v1/timelines/link_spec.rb
index 67d8bca02..899936470 100644
--- a/spec/requests/api/v1/timelines/link_spec.rb
+++ b/spec/requests/api/v1/timelines/link_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Link' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s })
+      expect(response.parsed_body.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s })
     end
   end
 
@@ -127,7 +127,7 @@ RSpec.describe 'Link' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
 
         it 'sets the correct pagination headers', :aggregate_failures do
diff --git a/spec/requests/api/v1/timelines/public_spec.rb b/spec/requests/api/v1/timelines/public_spec.rb
index 1fc62b393..759e236d0 100644
--- a/spec/requests/api/v1/timelines/public_spec.rb
+++ b/spec/requests/api/v1/timelines/public_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Public' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s })
+      expect(response.parsed_body.pluck(:id)).to match_array(expected_statuses.map { |status| status.id.to_s })
     end
   end
 
@@ -81,7 +81,7 @@ RSpec.describe 'Public' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json.size).to eq(params[:limit])
+          expect(response.parsed_body.size).to eq(params[:limit])
         end
 
         it 'sets the correct pagination headers', :aggregate_failures do
diff --git a/spec/requests/api/v1/timelines/tag_spec.rb b/spec/requests/api/v1/timelines/tag_spec.rb
index cfbfa0291..03d34e59f 100644
--- a/spec/requests/api/v1/timelines/tag_spec.rb
+++ b/spec/requests/api/v1/timelines/tag_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Tag' do
 
         expect(response)
           .to have_http_status(200)
-        expect(body_as_json.pluck(:id))
+        expect(response.parsed_body.pluck(:id))
           .to match_array(expected_statuses.map { |status| status.id.to_s })
           .and not_include(private_status.id)
       end
@@ -70,7 +70,7 @@ RSpec.describe 'Tag' do
       it 'returns only the requested number of statuses' do
         subject
 
-        expect(body_as_json.size).to eq(params[:limit])
+        expect(response.parsed_body.size).to eq(params[:limit])
       end
 
       it 'sets the correct pagination headers', :aggregate_failures do
diff --git a/spec/requests/api/v2/admin/accounts_spec.rb b/spec/requests/api/v2/admin/accounts_spec.rb
index 8f52c6a61..17c38e2e5 100644
--- a/spec/requests/api/v2/admin/accounts_spec.rb
+++ b/spec/requests/api/v2/admin/accounts_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe 'API V2 Admin Accounts' do
     end
 
     def body_json_ids
-      body_as_json.map { |a| a[:id].to_i }
+      response.parsed_body.map { |a| a[:id].to_i }
     end
 
     context 'with limit param' do
diff --git a/spec/requests/api/v2/filters/keywords_spec.rb b/spec/requests/api/v2/filters/keywords_spec.rb
index 69eff5a06..a31accaa5 100644
--- a/spec/requests/api/v2/filters/keywords_spec.rb
+++ b/spec/requests/api/v2/filters/keywords_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'API V2 Filters Keywords' do
     it 'returns http success' do
       get "/api/v2/filters/#{filter.id}/keywords", headers: headers
       expect(response).to have_http_status(200)
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to contain_exactly(
           include(id: keyword.id.to_s)
         )
@@ -42,7 +42,7 @@ RSpec.describe 'API V2 Filters Keywords' do
     it 'creates a filter', :aggregate_failures do
       expect(response).to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           keyword: 'magic',
           whole_word: false
@@ -73,7 +73,7 @@ RSpec.describe 'API V2 Filters Keywords' do
     it 'responds with the keyword', :aggregate_failures do
       expect(response).to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           keyword: 'foo',
           whole_word: false
diff --git a/spec/requests/api/v2/filters/statuses_spec.rb b/spec/requests/api/v2/filters/statuses_spec.rb
index 596932782..aed8934a5 100644
--- a/spec/requests/api/v2/filters/statuses_spec.rb
+++ b/spec/requests/api/v2/filters/statuses_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'API V2 Filters Statuses' do
     it 'returns http success' do
       get "/api/v2/filters/#{filter.id}/statuses", headers: headers
       expect(response).to have_http_status(200)
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to contain_exactly(
           include(id: status_filter.id.to_s)
         )
@@ -43,7 +43,7 @@ RSpec.describe 'API V2 Filters Statuses' do
     it 'creates a filter', :aggregate_failures do
       expect(response).to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           status_id: status.id.to_s
         )
@@ -73,7 +73,7 @@ RSpec.describe 'API V2 Filters Statuses' do
     it 'responds with the filter', :aggregate_failures do
       expect(response).to have_http_status(200)
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           status_id: status_filter.status.id.to_s
         )
diff --git a/spec/requests/api/v2/filters_spec.rb b/spec/requests/api/v2/filters_spec.rb
index 036a6a65a..850c773df 100644
--- a/spec/requests/api/v2/filters_spec.rb
+++ b/spec/requests/api/v2/filters_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Filters' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json.pluck(:id)).to match_array(filters.map { |filter| filter.id.to_s })
+      expect(response.parsed_body.pluck(:id)).to match_array(filters.map { |filter| filter.id.to_s })
     end
   end
 
@@ -58,7 +58,7 @@ RSpec.describe 'Filters' do
       it 'returns a filter with keywords', :aggregate_failures do
         subject
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to include(
             title: 'magic',
             filter_action: 'hide',
@@ -127,7 +127,10 @@ RSpec.describe 'Filters' do
       subject
 
       expect(response).to have_http_status(200)
-      expect(body_as_json[:id]).to eq(filter.id.to_s)
+      expect(response.parsed_body)
+        .to include(
+          id: filter.id.to_s
+        )
     end
 
     context 'when the filter belongs to someone else' do
diff --git a/spec/requests/api/v2/instance_spec.rb b/spec/requests/api/v2/instance_spec.rb
index 2f01db500..d484dc7c4 100644
--- a/spec/requests/api/v2/instance_spec.rb
+++ b/spec/requests/api/v2/instance_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Instances' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
           .and include(title: 'Mastodon')
           .and include_api_versions
@@ -30,7 +30,7 @@ RSpec.describe 'Instances' do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_present
           .and include(title: 'Mastodon')
           .and include_api_versions
diff --git a/spec/requests/api/v2/media_spec.rb b/spec/requests/api/v2/media_spec.rb
index 97540413f..06ce0053e 100644
--- a/spec/requests/api/v2/media_spec.rb
+++ b/spec/requests/api/v2/media_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'Media API', :attachment_processing do
         expect(response)
           .to have_http_status(200)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_a(Hash)
       end
     end
@@ -38,7 +38,7 @@ RSpec.describe 'Media API', :attachment_processing do
         expect(response)
           .to have_http_status(202)
 
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to be_a(Hash)
       end
     end
@@ -63,7 +63,7 @@ RSpec.describe 'Media API', :attachment_processing do
           expect(response)
             .to have_http_status(422)
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to be_a(Hash)
             .and include(error: /File type/)
         end
@@ -80,7 +80,7 @@ RSpec.describe 'Media API', :attachment_processing do
           expect(response)
             .to have_http_status(500)
 
-          expect(body_as_json)
+          expect(response.parsed_body)
             .to be_a(Hash)
             .and include(error: /processing/)
         end
diff --git a/spec/requests/api/v2/notifications/policies_spec.rb b/spec/requests/api/v2/notifications/policies_spec.rb
index f9860b5fb..dc205b6eb 100644
--- a/spec/requests/api/v2/notifications/policies_spec.rb
+++ b/spec/requests/api/v2/notifications/policies_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Policies' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to include(
+        expect(response.parsed_body).to include(
           for_not_following: 'accept',
           for_not_followers: 'accept',
           for_new_accounts: 'accept',
@@ -56,7 +56,7 @@ RSpec.describe 'Policies' do
         .and change { NotificationPolicy.find_or_initialize_by(account: user.account).for_limited_accounts.to_sym }.from(:filter).to(:drop)
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to include(
+      expect(response.parsed_body).to include(
         for_not_following: 'filter',
         for_not_followers: 'accept',
         for_new_accounts: 'accept',
diff --git a/spec/requests/api/v2/search_spec.rb b/spec/requests/api/v2/search_spec.rb
index 039e7513c..a59ec7ca6 100644
--- a/spec/requests/api/v2/search_spec.rb
+++ b/spec/requests/api/v2/search_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Search API' do
         it 'returns all matching accounts' do
           get '/api/v2/search', headers: headers, params: params
 
-          expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s)
+          expect(response.parsed_body[:accounts].pluck(:id)).to contain_exactly(bob.id.to_s, ana.id.to_s, tom.id.to_s)
         end
 
         context 'with truthy `resolve`' do
@@ -80,7 +80,7 @@ RSpec.describe 'Search API' do
           it 'returns only the followed accounts' do
             get '/api/v2/search', headers: headers, params: params
 
-            expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s)
+            expect(response.parsed_body[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s)
           end
         end
       end
diff --git a/spec/requests/api/v2/suggestions_spec.rb b/spec/requests/api/v2/suggestions_spec.rb
index 8895efd23..e92507ed6 100644
--- a/spec/requests/api/v2/suggestions_spec.rb
+++ b/spec/requests/api/v2/suggestions_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'Suggestions API' do
 
       expect(response).to have_http_status(200)
 
-      expect(body_as_json).to match_array(
+      expect(response.parsed_body).to match_array(
         [bob, jeff].map do |account|
           hash_including({
             source: 'staff',
diff --git a/spec/requests/api/v2_alpha/notifications/accounts_spec.rb b/spec/requests/api/v2_alpha/notifications/accounts_spec.rb
index 6a6ce043d..3c5bcd899 100644
--- a/spec/requests/api/v2_alpha/notifications/accounts_spec.rb
+++ b/spec/requests/api/v2_alpha/notifications/accounts_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Accounts in grouped notifications' do
 
       # The group we are interested in is only favorites
       notifications = user.account.notifications.where(type: 'favourite').reorder(id: :desc)
-      expect(body_as_json).to match(
+      expect(response.parsed_body).to match(
         [
           a_hash_including(
             id: notifications.first.from_account_id.to_s
@@ -58,7 +58,7 @@ RSpec.describe 'Accounts in grouped notifications' do
 
         # The group we are interested in is only favorites
         notifications = user.account.notifications.where(type: 'favourite').reorder(id: :desc)
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           [
             a_hash_including(
               id: notifications.first.from_account_id.to_s
diff --git a/spec/requests/api/v2_alpha/notifications_spec.rb b/spec/requests/api/v2_alpha/notifications_spec.rb
index 8009e7edc..b7821de56 100644
--- a/spec/requests/api/v2_alpha/notifications_spec.rb
+++ b/spec/requests/api/v2_alpha/notifications_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 4
+        expect(response.parsed_body[:count]).to eq 4
       end
     end
 
@@ -42,7 +42,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 5
+        expect(response.parsed_body[:count]).to eq 5
       end
     end
 
@@ -56,7 +56,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 2
+        expect(response.parsed_body[:count]).to eq 2
       end
     end
 
@@ -67,7 +67,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 3
+        expect(response.parsed_body[:count]).to eq 3
       end
     end
 
@@ -78,7 +78,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq 2
+        expect(response.parsed_body[:count]).to eq 2
       end
     end
 
@@ -91,7 +91,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:count]).to eq Api::V2Alpha::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT
+        expect(response.parsed_body[:count]).to eq Api::V2Alpha::NotificationsController::DEFAULT_NOTIFICATIONS_COUNT_LIMIT
       end
     end
   end
@@ -125,7 +125,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:notification_groups]).to eq []
+        expect(response.parsed_body[:notification_groups]).to eq []
       end
     end
 
@@ -145,7 +145,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:notification_groups]).to contain_exactly(
+        expect(response.parsed_body[:notification_groups]).to contain_exactly(
           a_hash_including(
             type: 'reblog',
             sample_account_ids: [bob.account_id.to_s]
@@ -177,7 +177,7 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json.size).to_not eq 0
+        expect(response.parsed_body.size).to_not eq 0
         expect(body_json_types.uniq).to_not include 'mention'
       end
     end
@@ -190,7 +190,7 @@ RSpec.describe 'Notifications' do
 
         expect(response).to have_http_status(200)
         expect(body_json_types.uniq).to eq ['mention']
-        expect(body_as_json.dig(:notification_groups, 0, :page_min_id)).to_not be_nil
+        expect(response.parsed_body.dig(:notification_groups, 0, :page_min_id)).to_not be_nil
       end
     end
 
@@ -201,7 +201,7 @@ RSpec.describe 'Notifications' do
       it 'returns the requested number of notifications paginated', :aggregate_failures do
         subject
 
-        expect(body_as_json[:notification_groups].size)
+        expect(response.parsed_body[:notification_groups].size)
           .to eq(params[:limit])
 
         expect(response)
@@ -221,7 +221,7 @@ RSpec.describe 'Notifications' do
       it 'returns the requested number of notifications paginated', :aggregate_failures do
         subject
 
-        expect(body_as_json[:notification_groups].size)
+        expect(response.parsed_body[:notification_groups].size)
           .to eq(2)
 
         expect(response)
@@ -247,10 +247,10 @@ RSpec.describe 'Notifications' do
         subject
 
         expect(response).to have_http_status(200)
-        expect(body_as_json[:partial_accounts].size).to be > 0
-        expect(body_as_json[:partial_accounts][0].keys.map(&:to_sym)).to contain_exactly(:acct, :avatar, :avatar_static, :bot, :id, :locked, :url)
-        expect(body_as_json[:partial_accounts].pluck(:id)).to_not include(recent_account.id.to_s)
-        expect(body_as_json[:accounts].pluck(:id)).to include(recent_account.id.to_s)
+        expect(response.parsed_body[:partial_accounts].size).to be > 0
+        expect(response.parsed_body[:partial_accounts][0].keys.map(&:to_sym)).to contain_exactly(:acct, :avatar, :avatar_static, :bot, :id, :locked, :url)
+        expect(response.parsed_body[:partial_accounts].pluck(:id)).to_not include(recent_account.id.to_s)
+        expect(response.parsed_body[:accounts].pluck(:id)).to include(recent_account.id.to_s)
       end
     end
 
@@ -265,7 +265,7 @@ RSpec.describe 'Notifications' do
     end
 
     def body_json_types
-      body_as_json[:notification_groups].pluck(:type)
+      response.parsed_body[:notification_groups].pluck(:type)
     end
   end
 
diff --git a/spec/requests/api/web/embeds_spec.rb b/spec/requests/api/web/embeds_spec.rb
index 0e6195204..2b2850283 100644
--- a/spec/requests/api/web/embeds_spec.rb
+++ b/spec/requests/api/web/embeds_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe '/api/web/embed' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[:html]).to be_present
+          expect(response.parsed_body[:html]).to be_present
         end
       end
 
@@ -71,7 +71,7 @@ RSpec.describe '/api/web/embed' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[:html]).to be_present
+          expect(response.parsed_body[:html]).to be_present
         end
 
         context 'when the requesting user is blocked' do
@@ -133,7 +133,7 @@ RSpec.describe '/api/web/embed' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[:html]).to be_present
+          expect(response.parsed_body[:html]).to be_present
         end
       end
 
diff --git a/spec/requests/emojis_spec.rb b/spec/requests/emojis_spec.rb
index b2e4702f2..644838dc6 100644
--- a/spec/requests/emojis_spec.rb
+++ b/spec/requests/emojis_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Emojis' do
 
       expect(response)
         .to have_http_status(200)
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           name: ':coolcat:',
           type: 'Emoji'
diff --git a/spec/requests/instance_actor_spec.rb b/spec/requests/instance_actor_spec.rb
index bb294b04a..b4a9b2ce6 100644
--- a/spec/requests/instance_actor_spec.rb
+++ b/spec/requests/instance_actor_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Instance actor endpoint' do
           .and have_cacheable_headers
         expect(response.content_type)
           .to start_with('application/activity+json')
-        expect(body_as_json)
+        expect(response.parsed_body)
           .to include(
             id: instance_actor_url,
             type: 'Application',
diff --git a/spec/requests/invite_spec.rb b/spec/requests/invite_spec.rb
index 4ce6c78e9..ba0464538 100644
--- a/spec/requests/invite_spec.rb
+++ b/spec/requests/invite_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'invites' do
       expect(response).to have_http_status(200)
       expect(response.media_type).to eq 'application/json'
 
-      expect(body_as_json[:invite_code]).to eq invite.code
+      expect(response.parsed_body[:invite_code]).to eq invite.code
     end
   end
 
diff --git a/spec/requests/log_out_spec.rb b/spec/requests/log_out_spec.rb
index 62ede0c10..25291fa79 100644
--- a/spec/requests/log_out_spec.rb
+++ b/spec/requests/log_out_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Log Out' do
       expect(response).to have_http_status(200)
       expect(response.media_type).to eq 'application/json'
 
-      expect(body_as_json[:redirect_to]).to eq '/auth/sign_in'
+      expect(response.parsed_body[:redirect_to]).to eq '/auth/sign_in'
     end
   end
 end
diff --git a/spec/requests/manifest_spec.rb b/spec/requests/manifest_spec.rb
index 69e308e3c..8133c90ee 100644
--- a/spec/requests/manifest_spec.rb
+++ b/spec/requests/manifest_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Manifest' do
         .and have_attributes(
           content_type: match('application/json')
         )
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           id: '/home',
           name: 'Mastodon'
diff --git a/spec/requests/oauth/token_spec.rb b/spec/requests/oauth/token_spec.rb
index 39ea9b35f..18d232e5a 100644
--- a/spec/requests/oauth/token_spec.rb
+++ b/spec/requests/oauth/token_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Obtaining OAuth Tokens' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[:scope]).to eq 'read write'
+          expect(response.parsed_body[:scope]).to eq 'read write'
         end
       end
 
@@ -76,7 +76,7 @@ RSpec.describe 'Obtaining OAuth Tokens' do
           subject
 
           expect(response).to have_http_status(200)
-          expect(body_as_json[:scope]).to eq('read')
+          expect(response.parsed_body[:scope]).to eq('read')
         end
       end
 
@@ -88,7 +88,7 @@ RSpec.describe 'Obtaining OAuth Tokens' do
             subject
 
             expect(response).to have_http_status(200)
-            expect(body_as_json[:scope]).to eq 'read write'
+            expect(response.parsed_body[:scope]).to eq 'read write'
           end
         end
 
diff --git a/spec/requests/signature_verification_spec.rb b/spec/requests/signature_verification_spec.rb
index 580d02833..128e7c078 100644
--- a/spec/requests/signature_verification_spec.rb
+++ b/spec/requests/signature_verification_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe 'signature verification concern' do
       get '/activitypub/success'
 
       expect(response).to have_http_status(200)
-      expect(body_as_json).to match(
+      expect(response.parsed_body).to match(
         signed_request: false,
         signature_actor_id: nil,
         error: 'Request not signed'
@@ -62,7 +62,7 @@ RSpec.describe 'signature verification concern' do
         get '/activitypub/signature_required'
 
         expect(response).to have_http_status(401)
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           error: 'Request not signed'
         )
       end
@@ -87,7 +87,7 @@ RSpec.describe 'signature verification concern' do
         }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: actor.id.to_s
         )
@@ -109,7 +109,7 @@ RSpec.describe 'signature verification concern' do
         }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: actor.id.to_s
         )
@@ -131,7 +131,7 @@ RSpec.describe 'signature verification concern' do
         }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: actor.id.to_s
         )
@@ -152,7 +152,7 @@ RSpec.describe 'signature verification concern' do
           'Signature' => signature_header,
         }
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: nil,
           error: anything
@@ -168,7 +168,7 @@ RSpec.describe 'signature verification concern' do
           'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
         }
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: nil,
           error: anything
@@ -184,7 +184,7 @@ RSpec.describe 'signature verification concern' do
           'Signature' => 'keyId="https://remote.domain/users/bob#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
         }
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: nil,
           error: anything
@@ -206,7 +206,7 @@ RSpec.describe 'signature verification concern' do
           'Signature' => signature_header,
         }
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: nil,
           error: 'Invalid Date header: not RFC 2616 compliant date: "wrong date"'
@@ -228,7 +228,7 @@ RSpec.describe 'signature verification concern' do
           'Signature' => signature_header,
         }
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: nil,
           error: 'Signed request date outside acceptable time window'
@@ -254,7 +254,7 @@ RSpec.describe 'signature verification concern' do
         }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: actor.id.to_s
         )
@@ -278,7 +278,7 @@ RSpec.describe 'signature verification concern' do
           'Signature' => signature_header,
         }
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: nil,
           error: 'Mastodon requires the Digest header to be signed when doing a POST request'
@@ -303,7 +303,7 @@ RSpec.describe 'signature verification concern' do
           'Signature' => signature_header,
         }
 
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: nil,
           error: 'Invalid Digest value. Computed SHA-256 digest: wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=; given: ZOyIygCyaOW6GjVnihtTFtIS9PNmskdyMlNKiuyjfzw='
@@ -321,7 +321,7 @@ RSpec.describe 'signature verification concern' do
         }
 
         expect(response).to have_http_status(200)
-        expect(body_as_json).to match(
+        expect(response.parsed_body).to match(
           signed_request: true,
           signature_actor_id: nil,
           error: anything
@@ -342,7 +342,7 @@ RSpec.describe 'signature verification concern' do
         'Signature' => 'keyId="https://remote.domain/users/alice#main-key",algorithm="rsa-sha256",headers="date host (request-target)",signature="Z8ilar3J7bOwqZkMp7sL8sRs4B1FT+UorbmvWoE+A5UeoOJ3KBcUmbsh+k3wQwbP5gMNUrra9rEWabpasZGphLsbDxfbsWL3Cf0PllAc7c1c7AFEwnewtExI83/qqgEkfWc2z7UDutXc2NfgAx89Ox8DXU/fA2GG0jILjB6UpFyNugkY9rg6oI31UnvfVi3R7sr3/x8Ea3I9thPvqI2byF6cojknSpDAwYzeKdngX3TAQEGzFHz3SDWwyp3jeMWfwvVVbM38FxhvAnSumw7YwWW4L7M7h4M68isLimoT3yfCn2ucBVL5Dz8koBpYf/40w7QidClAwCafZQFC29yDOg=="', # rubocop:disable Layout/LineLength
       }
 
-      expect(body_as_json).to match(
+      expect(response.parsed_body).to match(
         signed_request: true,
         signature_actor_id: nil,
         error: 'Unable to fetch key JSON at https://remote.domain/users/alice#main-key'
diff --git a/spec/requests/well_known/node_info_spec.rb b/spec/requests/well_known/node_info_spec.rb
index d02732c32..3d5afc7e2 100644
--- a/spec/requests/well_known/node_info_spec.rb
+++ b/spec/requests/well_known/node_info_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'The well-known node-info endpoints' do
           media_type: 'application/json'
         )
 
-      expect(body_as_json).to include(
+      expect(response.parsed_body).to include(
         links: be_an(Array).and(
           contain_exactly(
             include(
@@ -39,7 +39,7 @@ RSpec.describe 'The well-known node-info endpoints' do
       expect(non_matching_hash)
         .to_not match_json_schema('nodeinfo_2.0')
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to match_json_schema('nodeinfo_2.0')
         .and include(
           version: '2.0',
diff --git a/spec/requests/well_known/oauth_metadata_spec.rb b/spec/requests/well_known/oauth_metadata_spec.rb
index 378295b5a..9c86dbedf 100644
--- a/spec/requests/well_known/oauth_metadata_spec.rb
+++ b/spec/requests/well_known/oauth_metadata_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'The /.well-known/oauth-authorization-server request' do
     grant_types_supported = Doorkeeper.configuration.grant_flows.dup
     grant_types_supported << 'refresh_token' if Doorkeeper.configuration.refresh_token_enabled?
 
-    expect(body_as_json).to include(
+    expect(response.parsed_body).to include(
       issuer: root_url(protocol: protocol),
       service_documentation: 'https://docs.joinmastodon.org/',
       authorization_endpoint: oauth_authorization_url(protocol: protocol),
diff --git a/spec/requests/well_known/webfinger_spec.rb b/spec/requests/well_known/webfinger_spec.rb
index e5ce352d5..6880ba4b5 100644
--- a/spec/requests/well_known/webfinger_spec.rb
+++ b/spec/requests/well_known/webfinger_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
 
       expect(response.media_type).to eq 'application/jrd+json'
 
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           subject: eq('acct:alice@cb6e6126.ngrok.io'),
           aliases: include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
@@ -129,7 +129,7 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
     end
 
     it 'returns links for the internal account' do
-      expect(body_as_json)
+      expect(response.parsed_body)
         .to include(
           subject: 'acct:mastodon.internal@cb6e6126.ngrok.io',
           aliases: ['https://cb6e6126.ngrok.io/actor']
@@ -168,7 +168,7 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
     it 'returns avatar in response' do
       perform_request!
 
-      avatar_link = get_avatar_link(body_as_json)
+      avatar_link = get_avatar_link(response.parsed_body)
       expect(avatar_link).to_not be_nil
       expect(avatar_link[:type]).to eq alice.avatar.content_type
       expect(avatar_link[:href]).to eq Addressable::URI.new(host: Rails.configuration.x.local_domain, path: alice.avatar.to_s, scheme: 'https').to_s
@@ -182,7 +182,7 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
       it 'does not return avatar in response' do
         perform_request!
 
-        avatar_link = get_avatar_link(body_as_json)
+        avatar_link = get_avatar_link(response.parsed_body)
         expect(avatar_link).to be_nil
       end
     end
@@ -197,7 +197,7 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
       it 'does not return avatar in response' do
         perform_request!
 
-        avatar_link = get_avatar_link(body_as_json)
+        avatar_link = get_avatar_link(response.parsed_body)
         expect(avatar_link).to be_nil
       end
     end
@@ -212,7 +212,7 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
     end
 
     it 'does not return avatar in response' do
-      avatar_link = get_avatar_link(body_as_json)
+      avatar_link = get_avatar_link(response.parsed_body)
       expect(avatar_link).to be_nil
     end
   end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 60bec918e..2a2754440 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -34,10 +34,6 @@ RSpec.configure do |config|
   end
 end
 
-def body_as_json
-  response.parsed_body
-end
-
 def serialized_record_json(record, serializer, adapter: nil)
   options = { serializer: serializer }
   options[:adapter] = adapter if adapter.present?

From ebf09328d4b17ad199ecfe364b84aa9b3da3f9b3 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 6 Sep 2024 12:58:53 +0200
Subject: [PATCH 33/91] Disable codecov github annotations (#31783)

---
 .github/codecov.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/codecov.yml b/.github/codecov.yml
index 701ba3af8..d9b43b259 100644
--- a/.github/codecov.yml
+++ b/.github/codecov.yml
@@ -1,3 +1,4 @@
+annotations: false
 comment: false # Do not leave PR comments
 coverage:
   status:

From 1fed11cfa77c12135c68d5eff8c0d8760605b2b2 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 6 Sep 2024 14:33:38 +0200
Subject: [PATCH 34/91] Target firefox all the way back to Firefox 78 (#31782)

---
 .browserslistrc | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.browserslistrc b/.browserslistrc
index 0376af4bc..6367e4d35 100644
--- a/.browserslistrc
+++ b/.browserslistrc
@@ -1,6 +1,7 @@
 [production]
 defaults
 > 0.2%
+firefox >= 78
 ios >= 15.6
 not dead
 not OperaMini all

From fd7fc7bdc36ea559d70edfa8cc811bed18baefc9 Mon Sep 17 00:00:00 2001
From: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
Date: Fri, 6 Sep 2024 14:50:30 +0200
Subject: [PATCH 35/91] Disable actions on reports that have already been taken
 (#31773)

---
 app/models/admin/account_action.rb            |  8 ++++++++
 app/views/admin/account_actions/new.html.haml |  8 ++++++++
 app/views/admin/reports/_actions.html.haml    | 12 +++++++++---
 config/locales/en.yml                         |  3 +++
 4 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb
index 3700ce4cd..4be58ba85 100644
--- a/app/models/admin/account_action.rb
+++ b/app/models/admin/account_action.rb
@@ -73,6 +73,14 @@ class Admin::AccountAction
       end
     end
 
+    def disabled_types_for_account(account)
+      if account.suspended?
+        %w(silence suspend)
+      elsif account.silenced?
+        %w(silence)
+      end
+    end
+
     def i18n_scope
       :activerecord
     end
diff --git a/app/views/admin/account_actions/new.html.haml b/app/views/admin/account_actions/new.html.haml
index bce1c3176..5b98582d8 100644
--- a/app/views/admin/account_actions/new.html.haml
+++ b/app/views/admin/account_actions/new.html.haml
@@ -1,6 +1,13 @@
 - content_for :page_title do
   = t('admin.account_actions.title', acct: @account.pretty_acct)
 
+- if @account.suspended?
+  .flash-message.alert
+    = t('admin.account_actions.already_suspended')
+- elsif @account.silenced?
+  .flash-message.warn
+    = t('admin.account_actions.already_silenced')
+
 = simple_form_for @account_action, url: admin_account_action_path(@account.id) do |f|
   = f.input :report_id,
             as: :hidden
@@ -9,6 +16,7 @@
     = f.input :type,
               as: :radio_buttons,
               collection: Admin::AccountAction.types_for_account(@account),
+              disabled: Admin::AccountAction.disabled_types_for_account(@account),
               hint: t('simple_form.hints.admin_account_action.type_html', acct: @account.pretty_acct),
               include_blank: false,
               label_method: ->(type) { account_action_type_label(type) },
diff --git a/app/views/admin/reports/_actions.html.haml b/app/views/admin/reports/_actions.html.haml
index 5fb540931..7317d401e 100644
--- a/app/views/admin/reports/_actions.html.haml
+++ b/app/views/admin/reports/_actions.html.haml
@@ -17,21 +17,27 @@
       .report-actions__item__button
         = form.button t('admin.reports.delete_and_resolve'),
                       name: :delete,
-                      class: 'button button--destructive'
+                      class: 'button button--destructive',
+                      disabled: statuses.empty?,
+                      title: statuses.empty? ? t('admin.reports.actions_no_posts') : ''
       .report-actions__item__description
         = t('admin.reports.actions.delete_description_html')
     .report-actions__item
       .report-actions__item__button
         = form.button t('admin.accounts.silence'),
                       name: :silence,
-                      class: 'button button--destructive'
+                      class: 'button button--destructive',
+                      disabled: report.target_account.silenced? || report.target_account.suspended?,
+                      title: report.target_account.silenced? ? t('admin.account_actions.already_silenced') : ''
       .report-actions__item__description
         = t('admin.reports.actions.silence_description_html')
     .report-actions__item
       .report-actions__item__button
         = form.button t('admin.accounts.suspend'),
                       name: :suspend,
-                      class: 'button button--destructive'
+                      class: 'button button--destructive',
+                      disabled: report.target_account.suspended?,
+                      title: report.target_account.suspended? ? t('admin.account_actions.already_suspended') : ''
       .report-actions__item__description
         = t('admin.reports.actions.suspend_description_html')
     .report-actions__item
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 267e04618..217b1537f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -25,6 +25,8 @@ en:
   admin:
     account_actions:
       action: Perform action
+      already_silenced: This account has already been silenced.
+      already_suspended: This account has already been suspended.
       title: Perform moderation action on %{acct}
     account_moderation_notes:
       create: Leave note
@@ -602,6 +604,7 @@ en:
         suspend_description_html: The account and all its contents will be inaccessible and eventually deleted, and interacting with it will be impossible. Reversible within 30 days. Closes all reports against this account.
       actions_description_html: Decide which action to take to resolve this report. If you take a punitive action against the reported account, an email notification will be sent to them, except when the <strong>Spam</strong> category is selected.
       actions_description_remote_html: Decide which action to take to resolve this report. This will only affect how <strong>your</strong> server communicates with this remote account and handle its content.
+      actions_no_posts: This report doesn't have any associated posts to delete
       add_to_report: Add more to report
       already_suspended_badges:
         local: Already suspended on this server

From a9d0b48b6566f5e06337a84745cdd624a6b31426 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 6 Sep 2024 09:58:46 -0400
Subject: [PATCH 36/91] Set "admin" body class from `admin` nested layout
 (#31269)

---
 app/controllers/admin/base_controller.rb              |  5 -----
 app/controllers/auth/registrations_controller.rb      |  5 -----
 app/controllers/disputes/base_controller.rb           |  5 -----
 app/controllers/filters/statuses_controller.rb        |  5 -----
 app/controllers/filters_controller.rb                 |  5 -----
 app/controllers/invites_controller.rb                 |  5 -----
 .../oauth/authorized_applications_controller.rb       |  5 -----
 app/controllers/relationships_controller.rb           |  5 -----
 app/controllers/settings/base_controller.rb           |  5 -----
 app/controllers/severed_relationships_controller.rb   |  5 -----
 app/controllers/statuses_cleanup_controller.rb        |  5 -----
 app/helpers/application_helper.rb                     |  1 +
 app/views/layouts/admin.html.haml                     |  2 ++
 spec/helpers/application_helper_spec.rb               | 11 ++++++++++-
 14 files changed, 13 insertions(+), 56 deletions(-)

diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index 4b5afbe15..48685db17 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -7,17 +7,12 @@ module Admin
 
     layout 'admin'
 
-    before_action :set_body_classes
     before_action :set_cache_headers
 
     after_action :verify_authorized
 
     private
 
-    def set_body_classes
-      @body_classes = 'admin'
-    end
-
     def set_cache_headers
       response.cache_control.replace(private: true, no_store: true)
     end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index c12960934..4d94c8015 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -11,7 +11,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   before_action :configure_sign_up_params, only: [:create]
   before_action :set_sessions, only: [:edit, :update]
   before_action :set_strikes, only: [:edit, :update]
-  before_action :set_body_classes, only: [:new, :create, :edit, :update]
   before_action :require_not_suspended!, only: [:update]
   before_action :set_cache_headers, only: [:edit, :update]
   before_action :set_rules, only: :new
@@ -104,10 +103,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   private
 
-  def set_body_classes
-    @body_classes = 'admin' if %w(edit update).include?(action_name)
-  end
-
   def set_invite
     @invite = begin
       invite = Invite.find_by(code: invite_code) if invite_code.present?
diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb
index 1054f3db8..dd24a1b74 100644
--- a/app/controllers/disputes/base_controller.rb
+++ b/app/controllers/disputes/base_controller.rb
@@ -7,16 +7,11 @@ class Disputes::BaseController < ApplicationController
 
   skip_before_action :require_functional!
 
-  before_action :set_body_classes
   before_action :authenticate_user!
   before_action :set_cache_headers
 
   private
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def set_cache_headers
     response.cache_control.replace(private: true, no_store: true)
   end
diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb
index 94993f938..7ada13f68 100644
--- a/app/controllers/filters/statuses_controller.rb
+++ b/app/controllers/filters/statuses_controller.rb
@@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController
   before_action :authenticate_user!
   before_action :set_filter
   before_action :set_status_filters
-  before_action :set_body_classes
   before_action :set_cache_headers
 
   PER_PAGE = 20
@@ -42,10 +41,6 @@ class Filters::StatusesController < ApplicationController
     'remove' if params[:remove]
   end
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def set_cache_headers
     response.cache_control.replace(private: true, no_store: true)
   end
diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb
index bd9964426..8c4e867e9 100644
--- a/app/controllers/filters_controller.rb
+++ b/app/controllers/filters_controller.rb
@@ -5,7 +5,6 @@ class FiltersController < ApplicationController
 
   before_action :authenticate_user!
   before_action :set_filter, only: [:edit, :update, :destroy]
-  before_action :set_body_classes
   before_action :set_cache_headers
 
   def index
@@ -52,10 +51,6 @@ class FiltersController < ApplicationController
     params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
   end
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def set_cache_headers
     response.cache_control.replace(private: true, no_store: true)
   end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 9bc5164d5..070852695 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -6,7 +6,6 @@ class InvitesController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
-  before_action :set_body_classes
   before_action :set_cache_headers
 
   def index
@@ -47,10 +46,6 @@ class InvitesController < ApplicationController
     params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment)
   end
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def set_cache_headers
     response.cache_control.replace(private: true, no_store: true)
   end
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 7bb22453c..267409a9c 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -6,7 +6,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
   before_action :store_current_location
   before_action :authenticate_resource_owner!
   before_action :require_not_suspended!, only: :destroy
-  before_action :set_body_classes
   before_action :set_cache_headers
 
   before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
@@ -23,10 +22,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
 
   private
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def store_current_location
     store_location_for(:user, request.url)
   end
diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
index dd794f319..d351afcfb 100644
--- a/app/controllers/relationships_controller.rb
+++ b/app/controllers/relationships_controller.rb
@@ -6,7 +6,6 @@ class RelationshipsController < ApplicationController
   before_action :authenticate_user!
   before_action :set_accounts, only: :show
   before_action :set_relationships, only: :show
-  before_action :set_body_classes
   before_action :set_cache_headers
 
   helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
@@ -68,10 +67,6 @@ class RelationshipsController < ApplicationController
     end
   end
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def set_cache_headers
     response.cache_control.replace(private: true, no_store: true)
   end
diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb
index f15140aa2..188334ac2 100644
--- a/app/controllers/settings/base_controller.rb
+++ b/app/controllers/settings/base_controller.rb
@@ -4,15 +4,10 @@ class Settings::BaseController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
-  before_action :set_body_classes
   before_action :set_cache_headers
 
   private
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def set_cache_headers
     response.cache_control.replace(private: true, no_store: true)
   end
diff --git a/app/controllers/severed_relationships_controller.rb b/app/controllers/severed_relationships_controller.rb
index 168e85e3f..965753a26 100644
--- a/app/controllers/severed_relationships_controller.rb
+++ b/app/controllers/severed_relationships_controller.rb
@@ -4,7 +4,6 @@ class SeveredRelationshipsController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
-  before_action :set_body_classes
   before_action :set_cache_headers
 
   before_action :set_event, only: [:following, :followers]
@@ -51,10 +50,6 @@ class SeveredRelationshipsController < ApplicationController
     account.local? ? account.local_username_and_domain : account.acct
   end
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def set_cache_headers
     response.cache_control.replace(private: true, no_store: true)
   end
diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb
index 4a3fc10ca..e517bf3ae 100644
--- a/app/controllers/statuses_cleanup_controller.rb
+++ b/app/controllers/statuses_cleanup_controller.rb
@@ -5,7 +5,6 @@ class StatusesCleanupController < ApplicationController
 
   before_action :authenticate_user!
   before_action :set_policy
-  before_action :set_body_classes
   before_action :set_cache_headers
 
   def show; end
@@ -34,10 +33,6 @@ class StatusesCleanupController < ApplicationController
     params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs)
   end
 
-  def set_body_classes
-    @body_classes = 'admin'
-  end
-
   def set_cache_headers
     response.cache_control.replace(private: true, no_store: true)
   end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7c91df8d4..de00f76d3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -159,6 +159,7 @@ module ApplicationHelper
 
   def body_classes
     output = body_class_string.split
+    output << content_for(:body_classes)
     output << "theme-#{current_theme.parameterize}"
     output << 'system-font' if current_account&.user&.setting_system_font_ui
     output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index ebd236c62..3f7727cdf 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -3,6 +3,8 @@
   = javascript_pack_tag 'public', crossorigin: 'anonymous'
   = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
 
+- content_for :body_classes, 'admin'
+
 - content_for :content do
   .admin-wrapper
     .sidebar-wrapper
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index c2e618c7d..0f78dc82f 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -8,7 +8,16 @@ RSpec.describe ApplicationHelper do
       before { helper.extend controller_helpers }
 
       it 'uses the controller body classes in the result' do
-        expect(helper.body_classes).to match(/modal-layout compose-standalone/)
+        expect(helper.body_classes)
+          .to match(/modal-layout compose-standalone/)
+          .and match(/theme-default/)
+      end
+
+      it 'includes values set via content_for' do
+        helper.content_for(:body_classes) { 'admin' }
+
+        expect(helper.body_classes)
+          .to match(/admin/)
       end
 
       private

From c88ba523ee6eebb11413690639818ef1c926399c Mon Sep 17 00:00:00 2001
From: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
Date: Fri, 6 Sep 2024 16:58:36 +0200
Subject: [PATCH 37/91] Fix sort order of moderation notes on Reports and
 Accounts (#31528)

---
 .../account_moderation_notes_controller.rb    |  2 +-
 app/controllers/admin/accounts_controller.rb  |  2 +-
 .../admin/report_notes_controller.rb          |  2 +-
 app/controllers/admin/reports_controller.rb   |  2 +-
 app/models/account_moderation_note.rb         |  2 +-
 app/models/report_note.rb                     |  2 +-
 .../admin/accounts_controller_spec.rb         | 17 ++++++++++
 .../admin/reports_controller_spec.rb          | 18 +++++++++++
 .../account_moderation_note_fabricator.rb     |  2 +-
 spec/fabricators/report_note_fabricator.rb    |  2 +-
 spec/models/account_moderation_note_spec.rb   | 31 +++++++++++++++++++
 spec/models/report_note_spec.rb               | 31 +++++++++++++++++++
 12 files changed, 105 insertions(+), 8 deletions(-)
 create mode 100644 spec/models/account_moderation_note_spec.rb
 create mode 100644 spec/models/report_note_spec.rb

diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb
index 8b6c1a445..a3c4adf59 100644
--- a/app/controllers/admin/account_moderation_notes_controller.rb
+++ b/app/controllers/admin/account_moderation_notes_controller.rb
@@ -13,7 +13,7 @@ module Admin
         redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
       else
         @account          = @account_moderation_note.target_account
-        @moderation_notes = @account.targeted_moderation_notes.latest
+        @moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account)
         @warnings         = @account.strikes.custom.latest
 
         render 'admin/accounts/show'
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 9beb8fde6..7b169ba26 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -33,7 +33,7 @@ module Admin
 
       @deletion_request        = @account.deletion_request
       @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
-      @moderation_notes        = @account.targeted_moderation_notes.latest
+      @moderation_notes        = @account.targeted_moderation_notes.chronological.includes(:account)
       @warnings                = @account.strikes.includes(:target_account, :account, :appeal).latest
       @domain_block            = DomainBlock.rule_for(@account.domain)
     end
diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb
index b5f04a1ca..6b16c29fc 100644
--- a/app/controllers/admin/report_notes_controller.rb
+++ b/app/controllers/admin/report_notes_controller.rb
@@ -21,7 +21,7 @@ module Admin
 
         redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
       else
-        @report_notes = @report.notes.includes(:account).order(id: :desc)
+        @report_notes = @report.notes.chronological.includes(:account)
         @action_logs  = @report.history.includes(:target)
         @form         = Admin::StatusBatchAction.new
         @statuses     = @report.statuses.with_includes
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 00d200d7c..aa877f144 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -13,7 +13,7 @@ module Admin
       authorize @report, :show?
 
       @report_note  = @report.notes.new
-      @report_notes = @report.notes.includes(:account).order(id: :desc)
+      @report_notes = @report.notes.chronological.includes(:account)
       @action_logs  = @report.history.includes(:target)
       @form         = Admin::StatusBatchAction.new
       @statuses     = @report.statuses.with_includes
diff --git a/app/models/account_moderation_note.rb b/app/models/account_moderation_note.rb
index 79b8b4d25..ca7f8e3d5 100644
--- a/app/models/account_moderation_note.rb
+++ b/app/models/account_moderation_note.rb
@@ -18,7 +18,7 @@ class AccountModerationNote < ApplicationRecord
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
 
-  scope :latest, -> { reorder('created_at DESC') }
+  scope :chronological, -> { reorder(id: :asc) }
 
   validates :content, presence: true, length: { maximum: CONTENT_SIZE_LIMIT }
 end
diff --git a/app/models/report_note.rb b/app/models/report_note.rb
index 7361c97e6..9d3be5259 100644
--- a/app/models/report_note.rb
+++ b/app/models/report_note.rb
@@ -18,7 +18,7 @@ class ReportNote < ApplicationRecord
   belongs_to :account
   belongs_to :report, inverse_of: :notes, touch: true
 
-  scope :latest, -> { reorder(created_at: :desc) }
+  scope :chronological, -> { reorder(id: :asc) }
 
   validates :content, presence: true, length: { maximum: CONTENT_SIZE_LIMIT }
 end
diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index ca399fbd9..a18230010 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -55,6 +55,23 @@ RSpec.describe Admin::AccountsController do
   describe 'GET #show' do
     let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
 
+    describe 'account moderation notes' do
+      let(:account) { Fabricate(:account) }
+
+      it 'includes moderation notes' do
+        note1 = Fabricate(:account_moderation_note, target_account: account)
+        note2 = Fabricate(:account_moderation_note, target_account: account)
+
+        get :show, params: { id: account.id }
+        expect(response).to have_http_status(200)
+
+        moderation_notes = assigns(:moderation_notes).to_a
+
+        expect(moderation_notes.size).to be 2
+        expect(moderation_notes).to eq [note1, note2]
+      end
+    end
+
     context 'with a remote account' do
       let(:account) { Fabricate(:account, domain: 'example.com') }
 
diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb
index d07468a37..1252ceb1f 100644
--- a/spec/controllers/admin/reports_controller_spec.rb
+++ b/spec/controllers/admin/reports_controller_spec.rb
@@ -47,6 +47,24 @@ RSpec.describe Admin::ReportsController do
       expect(response.body)
         .to include(report.comment)
     end
+
+    describe 'account moderation notes' do
+      let(:report) { Fabricate(:report) }
+
+      it 'includes moderation notes' do
+        note1 = Fabricate(:report_note, report: report)
+        note2 = Fabricate(:report_note, report: report)
+
+        get :show, params: { id: report }
+
+        expect(response).to have_http_status(200)
+
+        report_notes = assigns(:report_notes).to_a
+
+        expect(report_notes.size).to be 2
+        expect(report_notes).to eq [note1, note2]
+      end
+    end
   end
 
   describe 'POST #resolve' do
diff --git a/spec/fabricators/account_moderation_note_fabricator.rb b/spec/fabricators/account_moderation_note_fabricator.rb
index 05a687bf4..1ded86263 100644
--- a/spec/fabricators/account_moderation_note_fabricator.rb
+++ b/spec/fabricators/account_moderation_note_fabricator.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 Fabricator(:account_moderation_note) do
-  content 'MyText'
+  content { Faker::Lorem.sentences }
   account { Fabricate.build(:account) }
   target_account { Fabricate.build(:account) }
 end
diff --git a/spec/fabricators/report_note_fabricator.rb b/spec/fabricators/report_note_fabricator.rb
index 080fad51a..a5e9cc900 100644
--- a/spec/fabricators/report_note_fabricator.rb
+++ b/spec/fabricators/report_note_fabricator.rb
@@ -3,5 +3,5 @@
 Fabricator(:report_note) do
   report { Fabricate.build(:report) }
   account { Fabricate.build(:account) }
-  content 'Test Content'
+  content { Faker::Lorem.sentences }
 end
diff --git a/spec/models/account_moderation_note_spec.rb b/spec/models/account_moderation_note_spec.rb
new file mode 100644
index 000000000..079774c49
--- /dev/null
+++ b/spec/models/account_moderation_note_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AccountModerationNote do
+  describe 'chronological scope' do
+    it 'returns account moderation notes oldest to newest' do
+      account = Fabricate(:account)
+      note1 = Fabricate(:account_moderation_note, target_account: account)
+      note2 = Fabricate(:account_moderation_note, target_account: account)
+
+      expect(account.targeted_moderation_notes.chronological).to eq [note1, note2]
+    end
+  end
+
+  describe 'validations' do
+    it 'is invalid if the content is empty' do
+      report = Fabricate.build(:account_moderation_note, content: '')
+      expect(report.valid?).to be false
+    end
+
+    it 'is invalid if content is longer than character limit' do
+      report = Fabricate.build(:account_moderation_note, content: comment_over_limit)
+      expect(report.valid?).to be false
+    end
+
+    def comment_over_limit
+      Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2)
+    end
+  end
+end
diff --git a/spec/models/report_note_spec.rb b/spec/models/report_note_spec.rb
new file mode 100644
index 000000000..417971c9a
--- /dev/null
+++ b/spec/models/report_note_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ReportNote do
+  describe 'chronological scope' do
+    it 'returns report notes oldest to newest' do
+      report = Fabricate(:report)
+      note1 = Fabricate(:report_note, report: report)
+      note2 = Fabricate(:report_note, report: report)
+
+      expect(report.notes.chronological).to eq [note1, note2]
+    end
+  end
+
+  describe 'validations' do
+    it 'is invalid if the content is empty' do
+      report = Fabricate.build(:report_note, content: '')
+      expect(report.valid?).to be false
+    end
+
+    it 'is invalid if content is longer than character limit' do
+      report = Fabricate.build(:report_note, content: comment_over_limit)
+      expect(report.valid?).to be false
+    end
+
+    def comment_over_limit
+      Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2)
+    end
+  end
+end

From b530fc5267e14b4eab0322d63af94525e999fd39 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 6 Sep 2024 11:22:35 -0400
Subject: [PATCH 38/91] Update rails to version 7.1.4 (#31563)

---
 Gemfile.lock                                  | 106 +++++++++---------
 .../initializers/active_record_encryption.rb  |   4 -
 2 files changed, 53 insertions(+), 57 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 8577a5269..a988ad3f2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -10,35 +10,35 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (7.1.3.4)
-      actionpack (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    actioncable (7.1.4)
+      actionpack (= 7.1.4)
+      activesupport (= 7.1.4)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
       zeitwerk (~> 2.6)
-    actionmailbox (7.1.3.4)
-      actionpack (= 7.1.3.4)
-      activejob (= 7.1.3.4)
-      activerecord (= 7.1.3.4)
-      activestorage (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    actionmailbox (7.1.4)
+      actionpack (= 7.1.4)
+      activejob (= 7.1.4)
+      activerecord (= 7.1.4)
+      activestorage (= 7.1.4)
+      activesupport (= 7.1.4)
       mail (>= 2.7.1)
       net-imap
       net-pop
       net-smtp
-    actionmailer (7.1.3.4)
-      actionpack (= 7.1.3.4)
-      actionview (= 7.1.3.4)
-      activejob (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    actionmailer (7.1.4)
+      actionpack (= 7.1.4)
+      actionview (= 7.1.4)
+      activejob (= 7.1.4)
+      activesupport (= 7.1.4)
       mail (~> 2.5, >= 2.5.4)
       net-imap
       net-pop
       net-smtp
       rails-dom-testing (~> 2.2)
-    actionpack (7.1.3.4)
-      actionview (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    actionpack (7.1.4)
+      actionview (= 7.1.4)
+      activesupport (= 7.1.4)
       nokogiri (>= 1.8.5)
       racc
       rack (>= 2.2.4)
@@ -46,15 +46,15 @@ GEM
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.2)
       rails-html-sanitizer (~> 1.6)
-    actiontext (7.1.3.4)
-      actionpack (= 7.1.3.4)
-      activerecord (= 7.1.3.4)
-      activestorage (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    actiontext (7.1.4)
+      actionpack (= 7.1.4)
+      activerecord (= 7.1.4)
+      activestorage (= 7.1.4)
+      activesupport (= 7.1.4)
       globalid (>= 0.6.0)
       nokogiri (>= 1.8.5)
-    actionview (7.1.3.4)
-      activesupport (= 7.1.3.4)
+    actionview (7.1.4)
+      activesupport (= 7.1.4)
       builder (~> 3.1)
       erubi (~> 1.11)
       rails-dom-testing (~> 2.2)
@@ -64,22 +64,22 @@ GEM
       activemodel (>= 4.1)
       case_transform (>= 0.2)
       jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
-    activejob (7.1.3.4)
-      activesupport (= 7.1.3.4)
+    activejob (7.1.4)
+      activesupport (= 7.1.4)
       globalid (>= 0.3.6)
-    activemodel (7.1.3.4)
-      activesupport (= 7.1.3.4)
-    activerecord (7.1.3.4)
-      activemodel (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    activemodel (7.1.4)
+      activesupport (= 7.1.4)
+    activerecord (7.1.4)
+      activemodel (= 7.1.4)
+      activesupport (= 7.1.4)
       timeout (>= 0.4.0)
-    activestorage (7.1.3.4)
-      actionpack (= 7.1.3.4)
-      activejob (= 7.1.3.4)
-      activerecord (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    activestorage (7.1.4)
+      actionpack (= 7.1.4)
+      activejob (= 7.1.4)
+      activerecord (= 7.1.4)
+      activesupport (= 7.1.4)
       marcel (~> 1.0)
-    activesupport (7.1.3.4)
+    activesupport (7.1.4)
       base64
       bigdecimal
       concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -638,20 +638,20 @@ GEM
     rackup (1.0.0)
       rack (< 3)
       webrick
-    rails (7.1.3.4)
-      actioncable (= 7.1.3.4)
-      actionmailbox (= 7.1.3.4)
-      actionmailer (= 7.1.3.4)
-      actionpack (= 7.1.3.4)
-      actiontext (= 7.1.3.4)
-      actionview (= 7.1.3.4)
-      activejob (= 7.1.3.4)
-      activemodel (= 7.1.3.4)
-      activerecord (= 7.1.3.4)
-      activestorage (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    rails (7.1.4)
+      actioncable (= 7.1.4)
+      actionmailbox (= 7.1.4)
+      actionmailer (= 7.1.4)
+      actionpack (= 7.1.4)
+      actiontext (= 7.1.4)
+      actionview (= 7.1.4)
+      activejob (= 7.1.4)
+      activemodel (= 7.1.4)
+      activerecord (= 7.1.4)
+      activestorage (= 7.1.4)
+      activesupport (= 7.1.4)
       bundler (>= 1.15.0)
-      railties (= 7.1.3.4)
+      railties (= 7.1.4)
     rails-controller-testing (1.0.5)
       actionpack (>= 5.0.1.rc1)
       actionview (>= 5.0.1.rc1)
@@ -666,9 +666,9 @@ GEM
     rails-i18n (7.0.9)
       i18n (>= 0.7, < 2)
       railties (>= 6.0.0, < 8)
-    railties (7.1.3.4)
-      actionpack (= 7.1.3.4)
-      activesupport (= 7.1.3.4)
+    railties (7.1.4)
+      actionpack (= 7.1.4)
+      activesupport (= 7.1.4)
       irb
       rackup (>= 1.0.0)
       rake (>= 12.2)
diff --git a/config/initializers/active_record_encryption.rb b/config/initializers/active_record_encryption.rb
index a83ca8076..b7a874e40 100644
--- a/config/initializers/active_record_encryption.rb
+++ b/config/initializers/active_record_encryption.rb
@@ -38,8 +38,4 @@ Rails.application.configure do
   config.active_record.encryption.key_derivation_salt = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT')
   config.active_record.encryption.primary_key = ENV.fetch('ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY')
   config.active_record.encryption.support_sha1_for_non_deterministic_encryption = true
-
-  # TODO: https://github.com/rails/rails/issues/50604#issuecomment-1880990392
-  # Remove after updating to Rails 7.1.4
-  ActiveRecord::Encryption.configure(**config.active_record.encryption)
 end

From 4f81ad249477ad25aea191b1987cddb4222e65ba Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 6 Sep 2024 12:46:25 -0400
Subject: [PATCH 39/91] Add coverage for `media#player`, move body class to
 view (#31790)

---
 app/controllers/media_controller.rb |  4 +---
 app/views/media/player.html.haml    |  2 ++
 spec/system/media_spec.rb           | 23 +++++++++++++++++++++++
 3 files changed, 26 insertions(+), 3 deletions(-)
 create mode 100644 spec/system/media_spec.rb

diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index 53eee4001..9d10468e6 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -19,9 +19,7 @@ class MediaController < ApplicationController
     redirect_to @media_attachment.file.url(:original)
   end
 
-  def player
-    @body_classes = 'player'
-  end
+  def player; end
 
   private
 
diff --git a/app/views/media/player.html.haml b/app/views/media/player.html.haml
index df02cc411..6b6e56673 100644
--- a/app/views/media/player.html.haml
+++ b/app/views/media/player.html.haml
@@ -2,6 +2,8 @@
   = render_initial_state
   = javascript_pack_tag 'public', crossorigin: 'anonymous'
 
+- content_for :body_classes, 'player'
+
 :ruby
   meta = @media_attachment.file.meta || {}
 
diff --git a/spec/system/media_spec.rb b/spec/system/media_spec.rb
new file mode 100644
index 000000000..d014c7e88
--- /dev/null
+++ b/spec/system/media_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Media' do
+  describe 'Player page' do
+    context 'when signed in' do
+      before { sign_in Fabricate(:user) }
+
+      it 'visits the media player page and renders the media' do
+        status = Fabricate :status
+        media = Fabricate :media_attachment, type: :video
+        status.media_attachments << media
+
+        visit medium_player_path(media)
+
+        expect(page)
+          .to have_css('body', class: 'player')
+          .and have_css('div[data-component="Video"]')
+      end
+    end
+  end
+end

From 0a433d08fb51e0bfdd9e5f512af529f6abafa7a6 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 6 Sep 2024 12:46:55 -0400
Subject: [PATCH 40/91] Move shares/modal body class to layout (#31789)

---
 app/controllers/shares_controller.rb | 7 -------
 app/views/layouts/modal.html.haml    | 2 ++
 2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb
index 6546b8497..1aa0ce5a0 100644
--- a/app/controllers/shares_controller.rb
+++ b/app/controllers/shares_controller.rb
@@ -4,13 +4,6 @@ class SharesController < ApplicationController
   layout 'modal'
 
   before_action :authenticate_user!
-  before_action :set_body_classes
 
   def show; end
-
-  private
-
-  def set_body_classes
-    @body_classes = 'modal-layout compose-standalone'
-  end
 end
diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml
index bbc9185f5..91bcb7c42 100644
--- a/app/views/layouts/modal.html.haml
+++ b/app/views/layouts/modal.html.haml
@@ -1,6 +1,8 @@
 - content_for :header_tags do
   = javascript_pack_tag 'public', crossorigin: 'anonymous'
 
+- content_for :body_classes, 'modal-layout compose-standalone'
+
 - content_for :content do
   - if user_signed_in? && !@hide_header
     .account-header

From 7335a43b6dac0e82c305ce4dec9db4da114c769e Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 6 Sep 2024 12:52:35 -0400
Subject: [PATCH 41/91] Use async count in admin dashboard (#30606)

---
 app/controllers/admin/dashboard_controller.rb | 8 ++++----
 app/views/admin/dashboard/index.html.haml     | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 3a6df662e..5b0867dcf 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -7,12 +7,12 @@ module Admin
     def index
       authorize :dashboard, :index?
 
+      @pending_appeals_count = Appeal.pending.async_count
+      @pending_reports_count = Report.unresolved.async_count
+      @pending_tags_count    = Tag.pending_review.async_count
+      @pending_users_count   = User.pending.async_count
       @system_checks         = Admin::SystemCheck.perform(current_user)
       @time_period           = (29.days.ago.to_date...Time.now.utc.to_date)
-      @pending_users_count   = User.pending.count
-      @pending_reports_count = Report.unresolved.count
-      @pending_tags_count    = Tag.pending_review.count
-      @pending_appeals_count = Appeal.pending.count
     end
   end
 end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 8430dd3c4..27d8f4790 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -56,19 +56,19 @@
 
   .dashboard__item
     = link_to admin_reports_path, class: 'dashboard__quick-access' do
-      %span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count)
+      %span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count.value)
       = material_symbol 'chevron_right'
 
     = link_to admin_accounts_path(status: 'pending'), class: 'dashboard__quick-access' do
-      %span= t('admin.dashboard.pending_users_html', count: @pending_users_count)
+      %span= t('admin.dashboard.pending_users_html', count: @pending_users_count.value)
       = material_symbol 'chevron_right'
 
     = link_to admin_trends_tags_path(status: 'pending_review'), class: 'dashboard__quick-access' do
-      %span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count)
+      %span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count.value)
       = material_symbol 'chevron_right'
 
     = link_to admin_disputes_appeals_path(status: 'pending'), class: 'dashboard__quick-access' do
-      %span= t('admin.dashboard.pending_appeals_html', count: @pending_appeals_count)
+      %span= t('admin.dashboard.pending_appeals_html', count: @pending_appeals_count.value)
       = material_symbol 'chevron_right'
   .dashboard__item
     = react_admin_component :dimension,

From b716248fc5bde4dc47b8104d092d092d87c50f1a Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 6 Sep 2024 19:21:49 +0200
Subject: [PATCH 42/91] Add link to `/admin/roles` in moderation interface when
 changing someone's role (#31791)

---
 app/views/admin/users/roles/show.html.haml | 3 ++-
 config/locales/en.yml                      | 1 +
 config/locales/simple_form.en.yml          | 2 +-
 3 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/app/views/admin/users/roles/show.html.haml b/app/views/admin/users/roles/show.html.haml
index f26640f2a..01b3830f4 100644
--- a/app/views/admin/users/roles/show.html.haml
+++ b/app/views/admin/users/roles/show.html.haml
@@ -7,7 +7,8 @@
                     collection: UserRole.assignable,
                     include_blank: I18n.t('admin.accounts.change_role.no_role'),
                     label_method: :name,
-                    wrapper: :with_block_label
+                    wrapper: :with_block_label,
+                    hint: safe_join([I18n.t('simple_form.hints.user.role'), ' ', link_to(I18n.t('admin.accounts.change_role.edit_roles'), admin_roles_path)])
 
   .actions
     = f.button :button,
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 217b1537f..e8c901048 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -48,6 +48,7 @@ en:
         title: Change email for %{username}
       change_role:
         changed_msg: Role successfully changed!
+        edit_roles: Manage user roles
         label: Change role
         no_role: No role
         title: Change role for %{username}
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index fee3a6151..c1fae7e83 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -130,7 +130,7 @@ en:
         name: You can only change the casing of the letters, for example, to make it more readable
       user:
         chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
-        role: The role controls which permissions the user has
+        role: The role controls which permissions the user has.
       user_role:
         color: Color to be used for the role throughout the UI, as RGB in hex format
         highlighted: This makes the role publicly visible

From 10143d053a99d69f5770f6a5478ab0f88a95ae5b Mon Sep 17 00:00:00 2001
From: Mike Dalessio <mike.dalessio@gmail.com>
Date: Sun, 8 Sep 2024 14:41:37 -0400
Subject: [PATCH 43/91] Change some instances of Nokogiri HTML4 parsing to
 HTML5 (#31812)

---
 app/helpers/admin/trends/statuses_helper.rb | 2 +-
 app/lib/emoji_formatter.rb                  | 8 ++++----
 app/lib/plain_text_formatter.rb             | 2 +-
 app/services/fetch_oembed_service.rb        | 2 +-
 app/services/fetch_resource_service.rb      | 2 +-
 app/services/translate_status_service.rb    | 2 +-
 lib/sanitize_ext/sanitize_config.rb         | 2 +-
 lib/tasks/emojis.rake                       | 2 +-
 8 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb
index 79fee44dc..c7a59660c 100644
--- a/app/helpers/admin/trends/statuses_helper.rb
+++ b/app/helpers/admin/trends/statuses_helper.rb
@@ -5,7 +5,7 @@ module Admin::Trends::StatusesHelper
     text = if status.local?
              status.text.split("\n").first
            else
-             Nokogiri::HTML(status.text).css('html > body > *').first&.text
+             Nokogiri::HTML5(status.text).css('html > body > *').first&.text
            end
 
     return '' if text.blank?
diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb
index 2a3683c49..5f1a4651f 100644
--- a/app/lib/emoji_formatter.rb
+++ b/app/lib/emoji_formatter.rb
@@ -24,7 +24,7 @@ class EmojiFormatter
   def to_s
     return html if custom_emojis.empty? || html.blank?
 
-    tree = Nokogiri::HTML.fragment(html)
+    tree = Nokogiri::HTML5.fragment(html)
     tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node|
       i                     = -1
       inside_shortname      = false
@@ -43,8 +43,8 @@ class EmojiFormatter
 
           next unless (char_after.nil? || !DISALLOWED_BOUNDING_REGEX.match?(char_after)) && (emoji = emoji_map[shortcode])
 
-          result << Nokogiri::XML::Text.new(text[last_index..shortname_start_index - 1], tree.document) if shortname_start_index.positive?
-          result << Nokogiri::HTML.fragment(tag_for_emoji(shortcode, emoji))
+          result << tree.document.create_text_node(text[last_index..shortname_start_index - 1]) if shortname_start_index.positive?
+          result << tree.document.fragment(tag_for_emoji(shortcode, emoji))
 
           last_index = i + 1
         elsif text[i] == ':' && (i.zero? || !DISALLOWED_BOUNDING_REGEX.match?(text[i - 1]))
@@ -53,7 +53,7 @@ class EmojiFormatter
         end
       end
 
-      result << Nokogiri::XML::Text.new(text[last_index..], tree.document)
+      result << tree.document.create_text_node(text[last_index..])
       node.replace(result)
     end
 
diff --git a/app/lib/plain_text_formatter.rb b/app/lib/plain_text_formatter.rb
index d1ff6808b..f960ba7ac 100644
--- a/app/lib/plain_text_formatter.rb
+++ b/app/lib/plain_text_formatter.rb
@@ -16,7 +16,7 @@ class PlainTextFormatter
     if local?
       text
     else
-      node = Nokogiri::HTML.fragment(insert_newlines)
+      node = Nokogiri::HTML5.fragment(insert_newlines)
       # Elements that are entirely removed with our Sanitize config
       node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
       node.text.chomp
diff --git a/app/services/fetch_oembed_service.rb b/app/services/fetch_oembed_service.rb
index dc84b16b6..c7d4f7e29 100644
--- a/app/services/fetch_oembed_service.rb
+++ b/app/services/fetch_oembed_service.rb
@@ -25,7 +25,7 @@ class FetchOEmbedService
     return if html.nil?
 
     @format = @options[:format]
-    page    = Nokogiri::HTML(html)
+    page    = Nokogiri::HTML5(html)
 
     if @format.nil? || @format == :json
       @endpoint_url ||= page.at_xpath('//link[@type="application/json+oembed"]|//link[@type="text/json+oembed"]')&.attribute('href')&.value
diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb
index 84c36f6a1..b69015a5e 100644
--- a/app/services/fetch_resource_service.rb
+++ b/app/services/fetch_resource_service.rb
@@ -73,7 +73,7 @@ class FetchResourceService < BaseService
   end
 
   def process_html(response)
-    page      = Nokogiri::HTML(response.body_with_limit)
+    page      = Nokogiri::HTML5(response.body_with_limit)
     json_link = page.xpath('//link[@rel="alternate"]').find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) }
 
     process(json_link['href'], terminal: true) unless json_link.nil?
diff --git a/app/services/translate_status_service.rb b/app/services/translate_status_service.rb
index 9ad146ae7..e2e076e21 100644
--- a/app/services/translate_status_service.rb
+++ b/app/services/translate_status_service.rb
@@ -100,7 +100,7 @@ class TranslateStatusService < BaseService
   end
 
   def unwrap_emoji_shortcodes(html)
-    fragment = Nokogiri::HTML.fragment(html)
+    fragment = Nokogiri::HTML5.fragment(html)
     fragment.css('span[translate="no"]').each do |element|
       element.remove_attribute('translate')
       element.replace(element.children) if element.attributes.empty?
diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb
index ad310b393..f0a7b6578 100644
--- a/lib/sanitize_ext/sanitize_config.rb
+++ b/lib/sanitize_ext/sanitize_config.rb
@@ -52,7 +52,7 @@ class Sanitize
                  :relative
                end
 
-      current_node.replace(Nokogiri::XML::Text.new(current_node.text, current_node.document)) unless LINK_PROTOCOLS.include?(scheme)
+      current_node.replace(current_node.document.create_text_node(current_node.text)) unless LINK_PROTOCOLS.include?(scheme)
     end
 
     UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env|
diff --git a/lib/tasks/emojis.rake b/lib/tasks/emojis.rake
index e9fea2dee..fb18f21cf 100644
--- a/lib/tasks/emojis.rake
+++ b/lib/tasks/emojis.rake
@@ -13,7 +13,7 @@ def gen_border(codepoint, color)
     view_box[3] += 4
     svg['viewBox'] = view_box.join(' ')
   end
-  g = Nokogiri::XML::Node.new 'g', doc
+  g = doc.create_element('g')
   doc.css('svg > *').each do |elem|
     border_elem = elem.dup
 

From afa2e257e481deb913ea04104966a613fc50a7f2 Mon Sep 17 00:00:00 2001
From: Mike Dalessio <mike.dalessio@gmail.com>
Date: Sun, 8 Sep 2024 14:50:22 -0400
Subject: [PATCH 44/91] Change verify link service to use CSS selectors instead
 of a complex XPath query (#31815)

---
 app/services/verify_link_service.rb       |  2 +-
 spec/services/verify_link_service_spec.rb | 30 +++++++++++++++++------
 2 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/app/services/verify_link_service.rb b/app/services/verify_link_service.rb
index b317fc31a..c4f4191e1 100644
--- a/app/services/verify_link_service.rb
+++ b/app/services/verify_link_service.rb
@@ -26,7 +26,7 @@ class VerifyLinkService < BaseService
   def link_back_present?
     return false if @body.blank?
 
-    links = Nokogiri::HTML5(@body).xpath('//a[contains(concat(" ", normalize-space(@rel), " "), " me ")]|//link[contains(concat(" ", normalize-space(@rel), " "), " me ")]')
+    links = Nokogiri::HTML5(@body).css("a[rel~='me'],link[rel~='me']")
 
     if links.any? { |link| link['href']&.downcase == @link_back.downcase }
       true
diff --git a/spec/services/verify_link_service_spec.rb b/spec/services/verify_link_service_spec.rb
index 0ce8c9a90..a4fd19751 100644
--- a/spec/services/verify_link_service_spec.rb
+++ b/spec/services/verify_link_service_spec.rb
@@ -11,13 +11,14 @@ RSpec.describe VerifyLinkService do
 
     before do
       stub_request(:head, 'https://redirect.me/abc').to_return(status: 301, headers: { 'Location' => ActivityPub::TagManager.instance.url_for(account) })
+      stub_request(:head, 'http://unrelated-site.com').to_return(status: 301)
       stub_request(:get, 'http://example.com').to_return(status: 200, body: html)
       subject.call(field)
     end
 
     context 'when a link contains an <a> back' do
       let(:html) do
-        <<-HTML
+        <<~HTML
           <!doctype html>
           <body>
             <a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me">Follow me on Mastodon</a>
@@ -30,9 +31,9 @@ RSpec.describe VerifyLinkService do
       end
     end
 
-    context 'when a link contains an <a rel="noopener noreferrer"> back' do
+    context 'when a link contains an <a rel="me noopener noreferrer"> back' do
       let(:html) do
-        <<-HTML
+        <<~HTML
           <!doctype html>
           <body>
             <a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me noopener noreferrer" target="_blank">Follow me on Mastodon</a>
@@ -47,7 +48,7 @@ RSpec.describe VerifyLinkService do
 
     context 'when a link contains a <link> back' do
       let(:html) do
-        <<-HTML
+        <<~HTML
           <!doctype html>
           <head>
             <link type="text/html" href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me" />
@@ -62,7 +63,7 @@ RSpec.describe VerifyLinkService do
 
     context 'when a link goes through a redirect back' do
       let(:html) do
-        <<-HTML
+        <<~HTML
           <!doctype html>
           <head>
             <link type="text/html" href="https://redirect.me/abc" rel="me" />
@@ -113,7 +114,7 @@ RSpec.describe VerifyLinkService do
 
     context 'when link has no `href` attribute' do
       let(:html) do
-        <<-HTML
+        <<~HTML
           <!doctype html>
           <head>
             <link type="text/html" rel="me" />
@@ -128,6 +129,21 @@ RSpec.describe VerifyLinkService do
         expect(field.verified?).to be false
       end
     end
+
+    context 'when a link contains a link to an unexpected URL' do
+      let(:html) do
+        <<~HTML
+          <!doctype html>
+          <body>
+            <a href="http://unrelated-site.com" rel="me">Follow me on Unrelated Site</a>
+          </body>
+        HTML
+      end
+
+      it 'does not mark the field as verified' do
+        expect(field.verified?).to be false
+      end
+    end
   end
 
   context 'when given a remote account' do
@@ -141,7 +157,7 @@ RSpec.describe VerifyLinkService do
 
     context 'when a link contains an <a> back' do
       let(:html) do
-        <<-HTML
+        <<~HTML
           <!doctype html>
           <body>
             <a href="https://profile.example.com/alice" rel="me">Follow me on Mastodon</a>

From 82161d8ae54b94be701cba600536d87957361cea Mon Sep 17 00:00:00 2001
From: Mike Dalessio <mike.dalessio@gmail.com>
Date: Sun, 8 Sep 2024 14:56:18 -0400
Subject: [PATCH 45/91] Change Account::Field parsing to use
 HTML5::DocumentFragment (#31813)

---
 app/models/account/field.rb | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/models/account/field.rb b/app/models/account/field.rb
index 2bada6954..bcd89015d 100644
--- a/app/models/account/field.rb
+++ b/app/models/account/field.rb
@@ -73,10 +73,10 @@ class Account::Field < ActiveModelSerializers::Model
   end
 
   def extract_url_from_html
-    doc = Nokogiri::HTML(value).at_xpath('//body')
+    doc = Nokogiri::HTML5.fragment(value)
 
     return if doc.nil?
-    return if doc.children.size > 1
+    return if doc.children.size != 1
 
     element = doc.children.first
 

From c6a0768fe564d342e6ac6b70aff9d7c23b8a9ffc Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 9 Sep 2024 04:01:26 -0400
Subject: [PATCH 46/91] Use shared system spec helper methods (#31784)

---
 spec/support/system_helpers.rb          |  4 ++++
 spec/system/admin/announcements_spec.rb | 10 +---------
 spec/system/admin/domain_blocks_spec.rb |  2 +-
 spec/system/filters_spec.rb             |  2 +-
 spec/system/invites_spec.rb             |  4 ----
 5 files changed, 7 insertions(+), 15 deletions(-)

diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb
index 05c9d3b12..4cc192870 100644
--- a/spec/support/system_helpers.rb
+++ b/spec/support/system_helpers.rb
@@ -16,4 +16,8 @@ module SystemHelpers
   def form_label(key)
     I18n.t key, scope: 'simple_form.labels'
   end
+
+  def css_id(record)
+    "##{dom_id(record)}"
+  end
 end
diff --git a/spec/system/admin/announcements_spec.rb b/spec/system/admin/announcements_spec.rb
index 1da569965..87b733263 100644
--- a/spec/system/admin/announcements_spec.rb
+++ b/spec/system/admin/announcements_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe 'Admin::Announcements' do
 
       fill_in text_label,
               with: 'Announcement text'
-      save_changes
+      click_on submit_button
 
       expect(page)
         .to have_content(I18n.t('admin.announcements.updated_msg'))
@@ -94,10 +94,6 @@ RSpec.describe 'Admin::Announcements' do
 
   private
 
-  def css_id(record)
-    "##{dom_id(record)}" # TODO: Extract to system spec helper?
-  end
-
   def publish_announcement(announcement)
     within css_id(announcement) do
       click_on I18n.t('admin.announcements.publish')
@@ -116,10 +112,6 @@ RSpec.describe 'Admin::Announcements' do
     end
   end
 
-  def save_changes
-    click_on I18n.t('generic.save_changes')
-  end
-
   def submit_form
     click_on I18n.t('admin.announcements.new.create')
   end
diff --git a/spec/system/admin/domain_blocks_spec.rb b/spec/system/admin/domain_blocks_spec.rb
index 9a39e2906..f00d65dfe 100644
--- a/spec/system/admin/domain_blocks_spec.rb
+++ b/spec/system/admin/domain_blocks_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe 'blocking domains through the moderation interface' do
       visit edit_admin_domain_block_path(domain_block)
 
       select I18n.t('admin.domain_blocks.new.severity.suspend'), from: 'domain_block_severity'
-      click_on I18n.t('generic.save_changes')
+      click_on submit_button
 
       # It doesn't immediately block but presents a confirmation screen
       expect(page).to have_title(I18n.t('admin.domain_blocks.confirm_suspension.title', domain: 'example.com'))
diff --git a/spec/system/filters_spec.rb b/spec/system/filters_spec.rb
index 052b5e173..64de384c0 100644
--- a/spec/system/filters_spec.rb
+++ b/spec/system/filters_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Filters' do
       click_on filter_title
 
       fill_in filter_title_field, with: new_title
-      click_on I18n.t('generic.save_changes')
+      click_on submit_button
 
       expect(page).to have_content(new_title)
     end
diff --git a/spec/system/invites_spec.rb b/spec/system/invites_spec.rb
index 648bbea82..c57de871c 100644
--- a/spec/system/invites_spec.rb
+++ b/spec/system/invites_spec.rb
@@ -56,10 +56,6 @@ RSpec.describe 'Invites' do
 
   private
 
-  def css_id(record)
-    "##{dom_id(record)}" # TODO: Extract to system spec helper?
-  end
-
   def copyable_field
     within '.input-copy' do
       find(:field, type: :text, readonly: true)

From 1f13b875671e203002207d9d794e26a17f41890e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 9 Sep 2024 10:31:13 +0200
Subject: [PATCH 47/91] Update dependency pg to v1.5.8 (#31795)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index a988ad3f2..48fd05b1a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -590,7 +590,7 @@ GEM
     parslet (2.0.0)
     pastel (0.8.0)
       tty-color (~> 0.5)
-    pg (1.5.7)
+    pg (1.5.8)
     pghero (3.6.0)
       activerecord (>= 6.1)
     premailer (1.27.0)

From e6969cf4e434abb7a03a1f1300e97f031b23a042 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 9 Sep 2024 04:33:51 -0400
Subject: [PATCH 48/91] Add method for media-referencing status in
 `AccountStatusCleanupPolicy` (#31798)

---
 app/models/account_statuses_cleanup_policy.rb | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/app/models/account_statuses_cleanup_policy.rb b/app/models/account_statuses_cleanup_policy.rb
index e2c035284..6e998e2dc 100644
--- a/app/models/account_statuses_cleanup_policy.rb
+++ b/app/models/account_statuses_cleanup_policy.rb
@@ -157,7 +157,7 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
   end
 
   def without_media_scope
-    Status.where('NOT EXISTS (SELECT 1 FROM media_attachments media WHERE media.status_id = statuses.id)')
+    Status.where.not(status_media_reference_exists)
   end
 
   def without_poll_scope
@@ -175,6 +175,14 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
     Status.where(account_id: account_id)
   end
 
+  def status_media_reference_exists
+    MediaAttachment
+      .where(MediaAttachment.arel_table[:status_id].eq Status.arel_table[:id])
+      .select(1)
+      .arel
+      .exists
+  end
+
   def self_status_reference_exists(model)
     model
       .where(model.arel_table[:account_id].eq Status.arel_table[:account_id])

From 9d9901cc5bf9472ff52df87ed2d1cd096182d571 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 9 Sep 2024 10:43:12 +0200
Subject: [PATCH 49/91] Update peter-evans/create-pull-request action to v7
 (#31818)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 .github/workflows/crowdin-download.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml
index 0faa7e493..f1817b3e9 100644
--- a/.github/workflows/crowdin-download.yml
+++ b/.github/workflows/crowdin-download.yml
@@ -52,7 +52,7 @@ jobs:
 
       # Create or update the pull request
       - name: Create Pull Request
-        uses: peter-evans/create-pull-request@v6.0.5
+        uses: peter-evans/create-pull-request@v7.0.1
         with:
           commit-message: 'New Crowdin translations'
           title: 'New Crowdin Translations (automated)'

From a0ea2fa3b01c61d4d390b5e4b6cddc8006b204ec Mon Sep 17 00:00:00 2001
From: Mike Dalessio <mike.dalessio@gmail.com>
Date: Mon, 9 Sep 2024 06:59:42 -0400
Subject: [PATCH 50/91] Change fetch link card service to parse as HTML5
 (#31814)

---
 app/lib/link_details_extractor.rb             | 36 +++++++++----------
 app/services/fetch_link_card_service.rb       |  4 +--
 spec/services/fetch_link_card_service_spec.rb |  4 +--
 3 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb
index bd78aef7a..dff57f74f 100644
--- a/app/lib/link_details_extractor.rb
+++ b/app/lib/link_details_extractor.rb
@@ -157,11 +157,11 @@ class LinkDetailsExtractor
   end
 
   def title
-    html_entities_decode(structured_data&.headline || opengraph_tag('og:title') || document.xpath('//title').map(&:content).first)&.strip
+    html_entities.decode(structured_data&.headline || opengraph_tag('og:title') || document.xpath('//title').map(&:content).first)&.strip
   end
 
   def description
-    html_entities_decode(structured_data&.description || opengraph_tag('og:description') || meta_tag('description'))
+    html_entities.decode(structured_data&.description || opengraph_tag('og:description') || meta_tag('description'))
   end
 
   def published_at
@@ -181,7 +181,7 @@ class LinkDetailsExtractor
   end
 
   def provider_name
-    html_entities_decode(structured_data&.publisher_name || opengraph_tag('og:site_name'))
+    html_entities.decode(structured_data&.publisher_name || opengraph_tag('og:site_name'))
   end
 
   def provider_url
@@ -189,7 +189,7 @@ class LinkDetailsExtractor
   end
 
   def author_name
-    html_entities_decode(structured_data&.author_name || opengraph_tag('og:author') || opengraph_tag('og:author:username'))
+    html_entities.decode(structured_data&.author_name || opengraph_tag('og:author') || opengraph_tag('og:author:username'))
   end
 
   def author_url
@@ -258,7 +258,7 @@ class LinkDetailsExtractor
 
       next if json_ld.blank?
 
-      structured_data = StructuredData.new(html_entities_decode(json_ld))
+      structured_data = StructuredData.new(html_entities.decode(json_ld))
 
       next unless structured_data.valid?
 
@@ -274,11 +274,20 @@ class LinkDetailsExtractor
   end
 
   def detect_encoding_and_parse_document
-    [detect_encoding, nil, header_encoding].uniq.each do |encoding|
-      document = Nokogiri::HTML(@html, nil, encoding)
-      return document if document.to_s.valid_encoding?
+    html = nil
+    encoding = nil
+
+    [detect_encoding, header_encoding].compact.each do |enc|
+      html = @html.dup.force_encoding(enc)
+      if html.valid_encoding?
+        encoding = enc
+        break
+      end
     end
-    Nokogiri::HTML(@html, nil, 'UTF-8')
+
+    html = @html unless encoding
+
+    Nokogiri::HTML5(html, nil, encoding)
   end
 
   def detect_encoding
@@ -299,15 +308,6 @@ class LinkDetailsExtractor
     end
   end
 
-  def html_entities_decode(string)
-    return if string.nil?
-
-    unicode_string = string.to_s.encode('UTF-8')
-    raise EncodingError, 'cannot convert string to valid UTF-8' unless unicode_string.valid_encoding?
-
-    html_entities.decode(unicode_string)
-  end
-
   def html_entities
     @html_entities ||= HTMLEntities.new(:expanded)
   end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 49b820557..36d5c490a 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -29,7 +29,7 @@ class FetchLinkCardService < BaseService
     end
 
     attach_card if @card&.persisted?
-  rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, EncodingError, ActiveRecord::RecordInvalid => e
+  rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, Encoding::UndefinedConversionError, ActiveRecord::RecordInvalid => e
     Rails.logger.debug { "Error fetching link #{@original_url}: #{e}" }
     nil
   end
@@ -80,7 +80,7 @@ class FetchLinkCardService < BaseService
     urls = if @status.local?
              @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
            else
-             document = Nokogiri::HTML(@status.text)
+             document = Nokogiri::HTML5(@status.text)
              links = document.css('a')
 
              links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index 2f64f4055..1d61e33c0 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -192,8 +192,8 @@ RSpec.describe FetchLinkCardService do
         context 'when encoding problems appear in title tag' do
           let(:status) { Fabricate(:status, text: 'Check out http://example.com/latin1_posing_as_utf8_broken') }
 
-          it 'does not create a preview card' do
-            expect(status.preview_card).to be_nil
+          it 'creates a preview card anyway that replaces invalid bytes with U+FFFD (replacement char)' do
+            expect(status.preview_card.title).to eq("Tofu � l'orange")
           end
         end
       end

From 1d03570080252ee70859f07638ab724fc4fadfc2 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 9 Sep 2024 13:16:09 +0200
Subject: [PATCH 51/91] Update dependency postcss-preset-env to v10.0.3
 (#31821)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 1b3a7f1e4..332f6d012 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6701,10 +6701,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cssdb@npm:^8.1.0":
-  version: 8.1.0
-  resolution: "cssdb@npm:8.1.0"
-  checksum: 10c0/1fa1f1566c7e9964f5c71e443583eaba16a90933a3ef6803815c4281d084b75da948c415bade33d7085894fe0929c082fcb3135bf4400048cfff40d227ebd5dd
+"cssdb@npm:^8.1.1":
+  version: 8.1.1
+  resolution: "cssdb@npm:8.1.1"
+  checksum: 10c0/d60facfad3bca70e21100fc35b9205cb9d3d0ac642f44f0a687e54bf787f21c43d28ce2d17fcd405f67950fb4709516108fe1f3cb15df570eff1007b5fbbc787
   languageName: node
   linkType: hard
 
@@ -13802,12 +13802,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-opacity-percentage@npm:^2.0.0":
-  version: 2.0.0
-  resolution: "postcss-opacity-percentage@npm:2.0.0"
+"postcss-opacity-percentage@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "postcss-opacity-percentage@npm:3.0.0"
   peerDependencies:
-    postcss: ^8.2
-  checksum: 10c0/f031f3281060c4c0ede8f9a5832f65a3d8c2a1896ff324c41de42016e092635f0e0abee07545b01db93dc430a9741674a1d09c377c6c01cd8c2f4be65f889161
+    postcss: ^8.4
+  checksum: 10c0/15c7d66036fa966d265c8737196646b3f93deb83d4eea0b17ed5033460599afc31d3a989345e4d7c472963b2a2bb75c83d06979d5d30d6a60fcc7f74cb6d8d40
   languageName: node
   linkType: hard
 
@@ -13855,8 +13855,8 @@ __metadata:
   linkType: hard
 
 "postcss-preset-env@npm:^10.0.0":
-  version: 10.0.2
-  resolution: "postcss-preset-env@npm:10.0.2"
+  version: 10.0.3
+  resolution: "postcss-preset-env@npm:10.0.3"
   dependencies:
     "@csstools/postcss-cascade-layers": "npm:^5.0.0"
     "@csstools/postcss-color-function": "npm:^4.0.2"
@@ -13893,7 +13893,7 @@ __metadata:
     css-blank-pseudo: "npm:^7.0.0"
     css-has-pseudo: "npm:^7.0.0"
     css-prefers-color-scheme: "npm:^10.0.0"
-    cssdb: "npm:^8.1.0"
+    cssdb: "npm:^8.1.1"
     postcss-attribute-case-insensitive: "npm:^7.0.0"
     postcss-clamp: "npm:^4.1.0"
     postcss-color-functional-notation: "npm:^7.0.2"
@@ -13912,7 +13912,7 @@ __metadata:
     postcss-lab-function: "npm:^7.0.2"
     postcss-logical: "npm:^8.0.0"
     postcss-nesting: "npm:^13.0.0"
-    postcss-opacity-percentage: "npm:^2.0.0"
+    postcss-opacity-percentage: "npm:^3.0.0"
     postcss-overflow-shorthand: "npm:^6.0.0"
     postcss-page-break: "npm:^3.0.4"
     postcss-place: "npm:^10.0.0"
@@ -13921,7 +13921,7 @@ __metadata:
     postcss-selector-not: "npm:^8.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/51eb19994dfdbb041e87082833bedf8976053c182ae8dafab6b3ee1bf2cfd309ab9c186644ed032518011e44fbcc9f0552083bc431535779a80a919bbe78d10d
+  checksum: 10c0/da42caa2aab4d825fddfde00ebe2416d338c7b9a6f79a68840297888a8384f85991991c3fa10cf2d359fb230c885375f5cebd7bd63972725cd2a596d218f8b6a
   languageName: node
   linkType: hard
 

From 2caa3f365ded73be17b623177967110e66f14061 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 9 Sep 2024 14:38:43 +0200
Subject: [PATCH 52/91] New Crowdin Translations (automated) (#31800)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/ar.json    |   6 +-
 app/javascript/mastodon/locales/et.json    |  42 +++++++
 app/javascript/mastodon/locales/fa.json    |  99 ++++++++++++++++
 app/javascript/mastodon/locales/ia.json    |  10 ++
 app/javascript/mastodon/locales/la.json    |  18 +++
 app/javascript/mastodon/locales/ms.json    |   2 +
 app/javascript/mastodon/locales/ru.json    |   1 +
 app/javascript/mastodon/locales/zh-CN.json |  20 ++--
 app/javascript/mastodon/locales/zh-TW.json |   2 +-
 config/locales/activerecord.zh-CN.yml      |   2 +-
 config/locales/ar.yml                      |   2 +-
 config/locales/ca.yml                      |   4 +
 config/locales/cy.yml                      |   5 +
 config/locales/da.yml                      |   4 +
 config/locales/de.yml                      |   6 +-
 config/locales/devise.zh-CN.yml            |  10 +-
 config/locales/doorkeeper.fa.yml           |   3 +
 config/locales/es-AR.yml                   |   4 +
 config/locales/es-MX.yml                   |   1 +
 config/locales/es.yml                      |   1 +
 config/locales/et.yml                      |  47 +++++++-
 config/locales/fi.yml                      |  12 +-
 config/locales/fo.yml                      |   4 +
 config/locales/fr-CA.yml                   |   7 ++
 config/locales/fr.yml                      |   7 ++
 config/locales/ga.yml                      |   4 +
 config/locales/gd.yml                      |   4 +
 config/locales/gl.yml                      |   4 +
 config/locales/hu.yml                      |   4 +
 config/locales/ia.yml                      |   4 +
 config/locales/is.yml                      |   4 +
 config/locales/it.yml                      |   4 +
 config/locales/ja.yml                      |   1 +
 config/locales/ko.yml                      |   4 +
 config/locales/lad.yml                     |   1 +
 config/locales/lt.yml                      |   3 +-
 config/locales/nl.yml                      |   4 +
 config/locales/pl.yml                      |   4 +
 config/locales/simple_form.an.yml          |   1 -
 config/locales/simple_form.ar.yml          |   1 -
 config/locales/simple_form.be.yml          |   1 -
 config/locales/simple_form.bg.yml          |   1 -
 config/locales/simple_form.ca.yml          |   2 +-
 config/locales/simple_form.cs.yml          |   1 -
 config/locales/simple_form.cy.yml          |   2 +-
 config/locales/simple_form.da.yml          |   2 +-
 config/locales/simple_form.de.yml          |   2 +-
 config/locales/simple_form.el.yml          |   1 -
 config/locales/simple_form.en-GB.yml       |   1 -
 config/locales/simple_form.eo.yml          |   1 -
 config/locales/simple_form.es-AR.yml       |   2 +-
 config/locales/simple_form.es-MX.yml       |   1 -
 config/locales/simple_form.es.yml          |   1 -
 config/locales/simple_form.et.yml          |   2 +-
 config/locales/simple_form.eu.yml          |   1 -
 config/locales/simple_form.fa.yml          |   4 +
 config/locales/simple_form.fi.yml          |   2 +-
 config/locales/simple_form.fo.yml          |   2 +-
 config/locales/simple_form.fr-CA.yml       |   2 +-
 config/locales/simple_form.fr.yml          |   2 +-
 config/locales/simple_form.fy.yml          |   1 -
 config/locales/simple_form.ga.yml          |   2 +-
 config/locales/simple_form.gd.yml          |   2 +-
 config/locales/simple_form.gl.yml          |   2 +-
 config/locales/simple_form.he.yml          |   1 -
 config/locales/simple_form.hu.yml          |   2 +-
 config/locales/simple_form.ia.yml          |   1 -
 config/locales/simple_form.id.yml          |   1 -
 config/locales/simple_form.ie.yml          |   1 -
 config/locales/simple_form.io.yml          |   1 -
 config/locales/simple_form.is.yml          |   2 +-
 config/locales/simple_form.it.yml          |   2 +-
 config/locales/simple_form.ja.yml          |   1 -
 config/locales/simple_form.ko.yml          |   2 +-
 config/locales/simple_form.ku.yml          |   1 -
 config/locales/simple_form.lad.yml         |   2 +-
 config/locales/simple_form.lt.yml          |   1 -
 config/locales/simple_form.lv.yml          |   1 -
 config/locales/simple_form.ms.yml          |   1 -
 config/locales/simple_form.my.yml          |   1 -
 config/locales/simple_form.nl.yml          |   2 +-
 config/locales/simple_form.nn.yml          |   1 -
 config/locales/simple_form.no.yml          |   1 -
 config/locales/simple_form.pl.yml          |   1 -
 config/locales/simple_form.pt-BR.yml       |   1 -
 config/locales/simple_form.pt-PT.yml       |   1 -
 config/locales/simple_form.ru.yml          |   1 -
 config/locales/simple_form.sco.yml         |   1 -
 config/locales/simple_form.sl.yml          |   1 -
 config/locales/simple_form.sq.yml          |   2 +-
 config/locales/simple_form.sr-Latn.yml     |   1 -
 config/locales/simple_form.sr.yml          |   1 -
 config/locales/simple_form.sv.yml          |   2 +-
 config/locales/simple_form.tr.yml          |   1 -
 config/locales/simple_form.uk.yml          |   2 +-
 config/locales/simple_form.vi.yml          |   2 +-
 config/locales/simple_form.zh-CN.yml       |  16 +--
 config/locales/simple_form.zh-HK.yml       |   1 -
 config/locales/simple_form.zh-TW.yml       |   4 +-
 config/locales/sq.yml                      |   4 +
 config/locales/sv.yml                      |   1 +
 config/locales/th.yml                      |   5 +
 config/locales/uk.yml                      |   2 +
 config/locales/vi.yml                      |   4 +
 config/locales/zh-CN.yml                   | 124 +++++++++++----------
 config/locales/zh-TW.yml                   |   4 +
 106 files changed, 456 insertions(+), 157 deletions(-)

diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index d83a42a6c..722f2bc98 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -229,8 +229,8 @@
   "domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.",
   "domain_pill.username": "اسم المستخدم",
   "domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟",
-  "domain_pill.who_they_are": "بما أن المعرفات تقول من هو الشخص ومكان وجوده، يمكنك التفاعل مع الناس عبر الويب الاجتماعي لل <button>منصات التي تعمل ب ActivityPub</button>.",
-  "domain_pill.who_you_are": "بما أن معرفك يقول من أنت ومكان وجوده، يمكن للناس التفاعل معك عبر الويب الاجتماعي لل <button>منصات التي تعمل ب ActivityPub</button>.",
+  "domain_pill.who_they_are": "بما أن المُعرّفات تحدد هوية الشخص ومكان وجوده، فبإمكانك التفاعل مع الأشخاص عبر الويب الاجتماعي لـ <button>المنصات التي تعمل بواسطة أكتيفيتي بوب</button>.",
+  "domain_pill.who_you_are": "بما أن مُعرّفك يحدد هويتك ومكان وجوده، فبإمكانك الآخرين التفاعل معك عبر الويب الاجتماعي لـ <button>المنصات التي تعمل بواسطة أكتيفيتي بوب</button>.",
   "domain_pill.your_handle": "عنوانك الكامل:",
   "domain_pill.your_server": "موطِنك الرقمي، حيث توجد فيه كافة منشوراتك. ألا يعجبك المكان؟ يمكنك الانتقال بين الخوادم في أي وقت واصطحاب متابعيك أيضاً.",
   "domain_pill.your_username": "معرفك الفريد على هذا الخادم. من الممكن العثور على مستخدمين بنفس إسم المستخدم على خوادم مختلفة.",
@@ -352,6 +352,7 @@
   "hashtags.and_other": "…و {count, plural, zero {} one {# واحد آخر} two {# اثنان آخران} few {# آخرون} many {# آخَرًا}other {# آخرون}}",
   "hints.profiles.see_more_followers": "عرض المزيد من المتابعين على {domain}",
   "hints.profiles.see_more_posts": "عرض المزيد من المنشورات من {domain}",
+  "hints.threads.replies_may_be_missing": "قد تكون الردود الواردة من الخوادم الأخرى غائبة.",
   "hints.threads.see_more": "اطلع على المزيد من الردود على {domain}",
   "home.column_settings.show_reblogs": "اعرض المعاد نشرها",
   "home.column_settings.show_replies": "اعرض الردود",
@@ -517,6 +518,7 @@
   "notification_requests.edit_selection": "تعديل",
   "notification_requests.exit_selection": "تمّ",
   "notification_requests.explainer_for_limited_account": "تم تصفية الإشعارات من هذا الحساب لأن الحساب تم تقييده من قبل مشرف.",
+  "notification_requests.minimize_banner": "تصغير شريط الإشعارات المُصفاة",
   "notification_requests.notifications_from": "إشعارات من {name}",
   "notification_requests.title": "الإشعارات المصفاة",
   "notification_requests.view": "عرض الإشعارات",
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
index 4838f7015..d0fc80e60 100644
--- a/app/javascript/mastodon/locales/et.json
+++ b/app/javascript/mastodon/locales/et.json
@@ -353,6 +353,14 @@
   "hashtag.follow": "Jälgi silti",
   "hashtag.unfollow": "Lõpeta sildi jälgimine",
   "hashtags.and_other": "…ja {count, plural, one {}other {# veel}}",
+  "hints.profiles.followers_may_be_missing": "Selle profiili jälgijaid võib olla puudu.",
+  "hints.profiles.follows_may_be_missing": "Selle profiili poolt jälgitavaid võib olla puudu.",
+  "hints.profiles.posts_may_be_missing": "Mõned selle profiili postitused võivad olla puudu.",
+  "hints.profiles.see_more_followers": "Vaata rohkem jälgijaid kohas {domain}",
+  "hints.profiles.see_more_follows": "Vaata rohkem jälgitavaid kohas {domain}",
+  "hints.profiles.see_more_posts": "Vaata rohkem postitusi kohas {domain}",
+  "hints.threads.replies_may_be_missing": "Vastuseid teistest serveritest võib olla puudu.",
+  "hints.threads.see_more": "Vaata rohkem vastuseid kohas {domain}",
   "home.column_settings.show_reblogs": "Näita jagamisi",
   "home.column_settings.show_replies": "Näita vastuseid",
   "home.hide_announcements": "Peida teadaanded",
@@ -360,6 +368,17 @@
   "home.pending_critical_update.link": "Vaata uuendusi",
   "home.pending_critical_update.title": "Saadaval kriitiline turvauuendus!",
   "home.show_announcements": "Kuva teadaandeid",
+  "ignore_notifications_modal.disclaimer": "Mastodon ei saa teavitada kasutajaid, et ignoreerisid nende teavitusi. Teavituste ignoreerimine ei peata sõnumite endi saatmist.",
+  "ignore_notifications_modal.filter_instead": "Selle asemel filtreeri",
+  "ignore_notifications_modal.filter_to_act_users": "Saad endiselt kasutajaid vastu võtta, tagasi lükata või neist teatada",
+  "ignore_notifications_modal.filter_to_avoid_confusion": "Filtreerimine aitab vältida võimalikke segaminiajamisi",
+  "ignore_notifications_modal.filter_to_review_separately": "Saad filtreeritud teateid eraldi vaadata",
+  "ignore_notifications_modal.ignore": "Ignoreeri teavitusi",
+  "ignore_notifications_modal.limited_accounts_title": "Ignoreeri modereeritud kontode teavitusi?",
+  "ignore_notifications_modal.new_accounts_title": "Ignoreeri uute kontode teavitusi?",
+  "ignore_notifications_modal.not_followers_title": "Ignoreeri inimeste teavitusi, kes sind ei jälgi?",
+  "ignore_notifications_modal.not_following_title": "Ignoreeri inimeste teavitusi, keda sa ei jälgi?",
+  "ignore_notifications_modal.private_mentions_title": "Ignoreeri soovimatute eraviisiliste mainimiste teateid?",
   "interaction_modal.description.favourite": "Mastodoni kontoga saad postituse lemmikuks märkida, et autor teaks, et sa hindad seda, ja jätta see hiljemaks alles.",
   "interaction_modal.description.follow": "Mastodoni kontoga saad jälgida kasutajat {name}, et tema postitusi oma koduvoos näha.",
   "interaction_modal.description.reblog": "Mastodoni kontoga saad seda postitust levitada, jagades seda oma jälgijatele.",
@@ -483,9 +502,13 @@
   "notification.admin.report_statuses": "{name} raporteeris {target} kategooriast {category}",
   "notification.admin.report_statuses_other": "{name} raporteeris kohast {target}",
   "notification.admin.sign_up": "{name} registreerus",
+  "notification.admin.sign_up.name_and_others": "{name} ja {count, plural, one {# veel} other {# teist}} liitus",
   "notification.favourite": "{name} märkis su postituse lemmikuks",
+  "notification.favourite.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# teist}}</a> märkis su postituse lemmikuks",
   "notification.follow": "{name} alustas su jälgimist",
+  "notification.follow.name_and_others": "{name} ja {count, plural, one {# veel} other {# teist}} hakkas sind jälgima",
   "notification.follow_request": "{name} soovib sind jälgida",
+  "notification.follow_request.name_and_others": "{name} ja {count, plural, one {# veel} other {# teist}} taotles sinu jälgimist",
   "notification.label.mention": "Mainimine",
   "notification.label.private_mention": "Privaatne mainimine",
   "notification.label.private_reply": "Privaatne vastus",
@@ -503,6 +526,7 @@
   "notification.own_poll": "Su küsitlus on lõppenud",
   "notification.poll": "Hääletus, millel osalesid, on lõppenud",
   "notification.reblog": "{name} jagas edasi postitust",
+  "notification.reblog.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# teist}}</a> jagas su postitust",
   "notification.relationships_severance_event": "Kadunud ühendus kasutajaga {name}",
   "notification.relationships_severance_event.account_suspension": "{from} admin on kustutanud {target}, mis tähendab, et sa ei saa enam neilt uuendusi või suhelda nendega.",
   "notification.relationships_severance_event.domain_block": "{from} admin on blokeerinud {target}, sealhulgas {followersCount} sinu jälgijat ja {followingCount, plural, one  {# konto} other {# kontot}}, mida jälgid.",
@@ -511,13 +535,24 @@
   "notification.status": "{name} just postitas",
   "notification.update": "{name} muutis postitust",
   "notification_requests.accept": "Nõus",
+  "notification_requests.accept_multiple": "{count, plural, one {Nõustu # taotlusega…} other {Nõustu # taotlusega…}}",
+  "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Nõustu taotlusega} other {Nõustu taotlustega}}",
+  "notification_requests.confirm_accept_multiple.message": "Oled nõustumas {count, plural, one {ühe teavituse taotlusega} other {# teavituse taotlusega}}. Oled kindel, et soovid jätkata?",
+  "notification_requests.confirm_accept_multiple.title": "Nõustuda teavituste taotlustega?",
+  "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Loobu taotlusest} other {Loobu taotlustest}}",
+  "notification_requests.confirm_dismiss_multiple.message": "Oled loobumas {count, plural, one {ühest teavituse taotlusest} other {# teavituse taotlusest}}. {count, plural, one {Sellele} other {Neile}} pole hiljem lihtne ligi pääseda. Oled kindel, et soovid jätkata?",
+  "notification_requests.confirm_dismiss_multiple.title": "Hüljata teavituse taotlused?",
   "notification_requests.dismiss": "Hülga",
+  "notification_requests.dismiss_multiple": "{count, plural, one {Loobuda # taotlusest…} other {Loobuda # taotlusest…}}",
+  "notification_requests.edit_selection": "Muuda",
+  "notification_requests.exit_selection": "Valmis",
   "notification_requests.explainer_for_limited_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot piiranud.",
   "notification_requests.explainer_for_limited_remote_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot või serverit piiranud.",
   "notification_requests.maximize": "Maksimeeri",
   "notification_requests.minimize_banner": "Minimeeri filtreeritud teavituste bänner",
   "notification_requests.notifications_from": "Teavitus kasutajalt {name}",
   "notification_requests.title": "Filtreeritud teavitused",
+  "notification_requests.view": "Vaata teavitusi",
   "notifications.clear": "Puhasta teated",
   "notifications.clear_confirmation": "Oled kindel, et soovid püsivalt kõik oma teated eemaldada?",
   "notifications.clear_title": "Tühjenda teavitus?",
@@ -554,6 +589,12 @@
   "notifications.permission_denied": "Töölauamärguanded pole saadaval, kuna eelnevalt keelduti lehitsejale teavituste luba andmast",
   "notifications.permission_denied_alert": "Töölaua märguandeid ei saa lubada, kuna brauseri luba on varem keeldutud",
   "notifications.permission_required": "Töölaua märguanded ei ole saadaval, kuna vajalik luba pole antud.",
+  "notifications.policy.accept": "Nõustun",
+  "notifications.policy.accept_hint": "Näita teavitustes",
+  "notifications.policy.drop": "Ignoreeri",
+  "notifications.policy.drop_hint": "Saada tühjusse, mitte kunagi seda enam näha",
+  "notifications.policy.filter": "Filter",
+  "notifications.policy.filter_hint": "Saadetud filtreeritud teavituste sisendkasti",
   "notifications.policy.filter_limited_accounts_hint": "Piiratud serveri moderaatorite poolt",
   "notifications.policy.filter_limited_accounts_title": "Modereeritud kontod",
   "notifications.policy.filter_new_accounts.hint": "Loodud viimase {days, plural, one {ühe päeva} other {# päeva}} jooksul",
@@ -564,6 +605,7 @@
   "notifications.policy.filter_not_following_title": "Inimesed, keda sa ei jälgi",
   "notifications.policy.filter_private_mentions_hint": "Filtreeritud, kui see pole vastus sinupoolt mainimisele või kui jälgid saatjat",
   "notifications.policy.filter_private_mentions_title": "Soovimatud privaatsed mainimised",
+  "notifications.policy.title": "Halda teavitusi kohast…",
   "notifications_permission_banner.enable": "Luba töölaua märguanded",
   "notifications_permission_banner.how_to_control": "Et saada teateid, ajal mil Mastodon pole avatud, luba töölauamärguanded. Saad täpselt määrata, mis tüüpi tegevused tekitavad märguandeid, kasutates peale teadaannete sisse lülitamist üleval olevat nuppu {icon}.",
   "notifications_permission_banner.title": "Ära jää millestki ilma",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 169c325ad..50c376b3b 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -11,6 +11,7 @@
   "about.not_available": "این اطّلاعات روی این کارساز موجود نشده.",
   "about.powered_by": "رسانهٔ اجتماعی نامتمرکز قدرت گرفته از {mastodon}",
   "about.rules": "قوانین کارساز",
+  "account.account_note_header": "یادداشت شخصی",
   "account.add_or_remove_from_list": "افزودن یا برداشتن از سیاهه‌ها",
   "account.badges.bot": "خودکار",
   "account.badges.group": "گروه",
@@ -33,7 +34,9 @@
   "account.follow_back": "دنبال کردن متقابل",
   "account.followers": "پی‌گیرندگان",
   "account.followers.empty": "هنوز کسی پی‌گیر این کاربر نیست.",
+  "account.followers_counter": "{count, plural, one {{counter} پی‌گیرنده} other {{counter} پی‌گیرنده}}",
   "account.following": "پی می‌گیرید",
+  "account.following_counter": "{count, plural, one {{counter} پی‌گرفته} other {{counter} پی‌گرفته}}",
   "account.follows.empty": "این کاربر هنوز پی‌گیر کسی نیست.",
   "account.go_to_profile": "رفتن به نمایه",
   "account.hide_reblogs": "نهفتن تقویت‌های ‎@{name}",
@@ -59,6 +62,7 @@
   "account.requested_follow": "{name} درخواست پی‌گیریتان را داد",
   "account.share": "هم‌رسانی نمایهٔ ‎@{name}",
   "account.show_reblogs": "نمایش تقویت‌های ‎@{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",
   "account.unblock": "رفع مسدودیت ‎@{name}",
   "account.unblock_domain": "رفع مسدودیت دامنهٔ {domain}",
   "account.unblock_short": "رفع مسدودیت",
@@ -86,9 +90,14 @@
   "audio.hide": "نهفتن صدا",
   "block_modal.show_less": "نمایش کم‌تر",
   "block_modal.show_more": "نمایش بیش‌تر",
+  "block_modal.they_cant_mention": "نمی‌توانند نامتان را برده یا پی‌تان بگیرند.",
+  "block_modal.they_cant_see_posts": "نمی‌توانند فرسته‌هایتان را دیده و فرسته‌هایشان را نمی‌بینید.",
+  "block_modal.they_will_know": "می‌توانند ببینند که مسدود شده‌اند.",
   "block_modal.title": "انسداد کاربر؟",
   "block_modal.you_wont_see_mentions": "فرسته‌هایی که از اون نام برده را نخواهید دید.",
   "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
+  "boost_modal.reblog": "تقویت فرسته؟",
+  "boost_modal.undo_reblog": "ناتقویت فرسته؟",
   "bundle_column_error.copy_stacktrace": "رونوشت از گزارش خطا",
   "bundle_column_error.error.body": "صفحهٔ درخواستی نتوانست پرداخت شود. ممکن است به خاطر اشکالی در کدمان یا مشکل سازگاری مرورگر باشد.",
   "bundle_column_error.error.title": "وای، نه!",
@@ -184,6 +193,8 @@
   "confirmations.unfollow.confirm": "پی‌نگرفتن",
   "confirmations.unfollow.message": "مطمئنید که می‌خواهید به پی‌گیری از {name} پایان دهید؟",
   "confirmations.unfollow.title": "ناپی‌گیری کاربر؟",
+  "content_warning.hide": "نهفتن فرسته",
+  "content_warning.show": "نمایش به هر روی",
   "conversation.delete": "حذف گفتگو",
   "conversation.mark_as_read": "علامت‌گذاری به عنوان خوانده شده",
   "conversation.open": "دیدن گفتگو",
@@ -204,9 +215,24 @@
   "dismissable_banner.explore_tags": "هم‌اکنون این برچسب‌ها بین افراد این کارساز و دیگر کارسازهای شبکهٔ نامتمرکز داغ شده‌اند.",
   "dismissable_banner.public_timeline": "این‌ها جدیدترین فرسته‌های عمومی از افرادی روی وب اجتماعیند که اعضای {domain} پی می‌گیرندشان.",
   "domain_block_modal.block": "انسداد کارساز",
+  "domain_block_modal.block_account_instead": "انسداد @{name} به جایش",
+  "domain_block_modal.they_can_interact_with_old_posts": "افزارد روی این کراساز می‌توانند با فرسته‌های قدیمیتان تعامل داشته باشند.",
+  "domain_block_modal.they_cant_follow": "هیچ‌کسی از این کارساز نمی‌تواند پیتان بگیرد.",
+  "domain_block_modal.they_wont_know": "نخواهند دانست که مسدود شده‌اند.",
   "domain_block_modal.title": "انسداد دامنه؟",
+  "domain_block_modal.you_will_lose_followers": "همهٔ پی‌گیرندگانتان از این کارساز برداشته خواهند شد.",
+  "domain_block_modal.you_wont_see_posts": "فرسته‌ها یا آگاهی‌ها از کاربران روی این کارساز را نخواهید دید.",
   "domain_pill.server": "کارساز",
+  "domain_pill.their_handle": "شناسه‌اش:",
+  "domain_pill.their_server": "خانهٔ رقمیش. جایی که همهٔ فرسته‌هایش می‌زیند.",
+  "domain_pill.their_username": "شناسهٔ یکتایش در کارسازش. ممکن است کاربرانی با نام کاربری مشابه روی کارسازهای مختلف باشند.",
   "domain_pill.username": "نام کاربری",
+  "domain_pill.whats_in_a_handle": "شناسه چیست؟",
+  "domain_pill.who_they_are": "از آن‌جا که شناسه‌ها کیستی و کجایی افراد را می‌گویند، می‌توانید با افرادی در سراسر وب اجتماعی <button>بن‌سازه‌های قدرت گرفته از اکتیویتی پاپ</button> تعامل داشته باشید.",
+  "domain_pill.who_you_are": "از آن‌جا که شناسه‌ها کیستی و کجاییتان را می‌گویند، افراد می‌توانند از سراسر وب اجتماعی <button>بن‌سازه‌های قدرت گرفته از اکتیویتی پاپ</button> با شما تعامل داشته باشند.",
+  "domain_pill.your_handle": "شناسه‌تان:",
+  "domain_pill.your_server": "خانهٔ رقمیتان. جایی که همهٔ فرسته‌هایتان می‌زیند. دوستش ندارید؟ در هر زمان کارسازتان را جابه‌جا کرده و پی‌گیرندگانتان را نیز بیاورید.",
+  "domain_pill.your_username": "شناسهٔ یکتایتان روی این کارساز. ممکن است کاربرانی با نام کاربری مشابه روی کارسازهای دیگر باشند.",
   "embed.instructions": "جاسازی این فرسته روی پایگاهتان با رونوشت کردن کد زیر.",
   "embed.preview": "این گونه دیده خواهد شد:",
   "emoji_button.activity": "فعالیت",
@@ -273,6 +299,9 @@
   "filter_modal.select_filter.subtitle": "استفاده از یک دستهً موجود یا ایجاد دسته‌ای جدید",
   "filter_modal.select_filter.title": "پالایش این فرسته",
   "filter_modal.title.status": "پالایش یک فرسته",
+  "filter_warning.matches_filter": "مطابق با پالایهٔ «{title}»",
+  "filtered_notifications_banner.pending_requests": "از {count, plural, =0 {هیچ‌کسی} one {فردی} other {# نفر}} که ممکن است بشناسید",
+  "filtered_notifications_banner.title": "آگاهی‌های پالوده",
   "firehose.all": "همه",
   "firehose.local": "این کارساز",
   "firehose.remote": "دیگر کارسازها",
@@ -281,6 +310,8 @@
   "follow_requests.unlocked_explanation": "با این که حسابتان قفل نیست، کارکنان {domain} فکر کردند که ممکن است بخواهید درخواست‌ها از این حساب‌ها را به صورت دستی بازبینی کنید.",
   "follow_suggestions.curated_suggestion": "گزینش سردبیر",
   "follow_suggestions.dismiss": "دیگر نشان داده نشود",
+  "follow_suggestions.featured_longer": "دست‌چین شده به دست گروه {domain}",
+  "follow_suggestions.friends_of_friends_longer": "محبوب بین کسانی که پی‌گرفته‌اید",
   "follow_suggestions.hints.featured": "این نمایه به دست گروه {domain} دستچین شده.",
   "follow_suggestions.hints.friends_of_friends": "این نمایه بین کسانی که پی می‌گیرید محبوب است.",
   "follow_suggestions.hints.most_followed": "این نمایه روی {domain} بسیار پی‌گرفته شده.",
@@ -288,6 +319,8 @@
   "follow_suggestions.hints.similar_to_recently_followed": "این نمایه شبیه نمایه‌هاییست که اخیراً پی‌گرفته‌اید.",
   "follow_suggestions.personalized_suggestion": "پیشنهاد شخصی",
   "follow_suggestions.popular_suggestion": "پیشنهاد محبوب",
+  "follow_suggestions.popular_suggestion_longer": "محبوب روی {domain}",
+  "follow_suggestions.similar_to_recently_followed_longer": "شبیه نمایه‌هایی که اخیراً پی گرفته‌اید",
   "follow_suggestions.view_all": "دیدن همه",
   "follow_suggestions.who_to_follow": "افرادی برای پی‌گیری",
   "followed_tags": "برچسب‌های پی‌گرفته",
@@ -316,6 +349,14 @@
   "hashtag.follow": "پی‌گرفتن برچسب",
   "hashtag.unfollow": "پی‌نگرفتن برچسب",
   "hashtags.and_other": "…و {count, plural, other {# بیش‌تر}}",
+  "hints.profiles.followers_may_be_missing": "شاید پی‌گیرندگان این نمایه نباشند.",
+  "hints.profiles.follows_may_be_missing": "شاید پی‌گرفته‌های این نمایه نباشند.",
+  "hints.profiles.posts_may_be_missing": "شاید فرسته‌هایی از این نمایه نباشند.",
+  "hints.profiles.see_more_followers": "دیدن پی‌گیرندگان بیش‌تر روی {domain}",
+  "hints.profiles.see_more_follows": "دیدن پی‌گرفته‌های بیش‌تر روی {domain}",
+  "hints.profiles.see_more_posts": "دیدن فرسته‌های بیش‌تر روی {domain}",
+  "hints.threads.replies_may_be_missing": "شاید پاسخ‌ها از دیگر کارسازها نباشند.",
+  "hints.threads.see_more": "دیدن پاسخ‌های بیش‌تر روی {domain}",
   "home.column_settings.show_reblogs": "نمایش تقویت‌ها",
   "home.column_settings.show_replies": "نمایش پاسخ‌ها",
   "home.hide_announcements": "نهفتن اعلامیه‌ها",
@@ -323,6 +364,11 @@
   "home.pending_critical_update.link": "دیدن به‌روز رسانی‌ها",
   "home.pending_critical_update.title": "به‌روز رسانی امنیتی بحرانی موجود است!",
   "home.show_announcements": "نمایش اعلامیه‌ها",
+  "ignore_notifications_modal.ignore": "چشم‌پوشی از آگاهی‌ها",
+  "ignore_notifications_modal.limited_accounts_title": "چشم‌پوشی از آگاهی‌های حساب‌های نظارت شده؟",
+  "ignore_notifications_modal.new_accounts_title": "چشم‌پوشی از آگاهی‌های حساب‌های جدید؟",
+  "ignore_notifications_modal.not_followers_title": "چشم‌پوشی از آگاهی‌های افرادی که پیتان نمی‌گیرند؟",
+  "ignore_notifications_modal.not_following_title": "چشم‌پوشی از آگاهی‌های افرادی که پیشان نمی‌گیرید؟",
   "interaction_modal.description.favourite": "با حسابی روی ماستودون می‌توانید این فرسته را برگزیده تا نگارنده بداند قدردانش هستید و برای آینده ذخیره‌اش می‌کنید.",
   "interaction_modal.description.follow": "با حسابی روی ماستودون می‌توانید {name} را برای دریافت فرسته‌هایش در خوراک خانگیتان دنبال کنید.",
   "interaction_modal.description.reblog": "با حسابی روی ماستودون می‌توانید این فرسته را با پی‌گیران خودتان هم‌رسانی کنید.",
@@ -383,6 +429,8 @@
   "limited_account_hint.action": "به هر روی نمایه نشان داده شود",
   "limited_account_hint.title": "این نمایه از سوی ناظم‌های {domain} پنهان شده.",
   "link_preview.author": "از {name}",
+  "link_preview.more_from_author": "بیش‌تر از {name}",
+  "link_preview.shares": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",
   "lists.account.add": "افزودن به سیاهه",
   "lists.account.remove": "برداشتن از سیاهه",
   "lists.delete": "حذف سیاهه",
@@ -401,9 +449,16 @@
   "loading_indicator.label": "در حال بارگذاری…",
   "media_gallery.toggle_visible": "{number, plural, one {نهفتن تصویر} other {نهفتن تصاویر}}",
   "moved_to_account_banner.text": "حسابتان {disabledAccount} اکنون از کار افتاده؛ چرا که به {movedToAccount} منتقل شدید.",
+  "mute_modal.hide_from_notifications": "نهفتن از آگاهی‌ها",
+  "mute_modal.hide_options": "گزینه‌های نهفتن",
+  "mute_modal.indefinite": "تا وقتی ناخموشش کنم",
   "mute_modal.show_options": "نمایش گزینه‌ها",
+  "mute_modal.they_wont_know": "نخواهند دانست که خموش شده‌اند.",
   "mute_modal.title": "خموشی کاربر؟",
+  "mute_modal.you_wont_see_mentions": "فرسته‌هایی که به او اشاره کرده‌اند را نخواهید دید.",
+  "mute_modal.you_wont_see_posts": "هنوز می‌توانند فرسته‌هایتان را ببینند، ولی فرسته‌هایشان را نمی‌بینید.",
   "navigation_bar.about": "درباره",
+  "navigation_bar.administration": "مدیریت",
   "navigation_bar.advanced_interface": "بازکردن در رابط کاربری وب پیشرفته",
   "navigation_bar.blocks": "کاربران مسدود شده",
   "navigation_bar.bookmarks": "نشانک‌ها",
@@ -420,6 +475,7 @@
   "navigation_bar.follows_and_followers": "پی‌گرفتگان و پی‌گیرندگان",
   "navigation_bar.lists": "سیاهه‌ها",
   "navigation_bar.logout": "خروج",
+  "navigation_bar.moderation": "نظارت",
   "navigation_bar.mutes": "کاربران خموشانده",
   "navigation_bar.opened_in_classic_interface": "فرسته‌ها، حساب‌ها و دیگر صفحه‌های خاص به طور پیش‌گزیده در میانای وب کلاسیک گشوده می‌شوند.",
   "navigation_bar.personal": "شخصی",
@@ -430,26 +486,54 @@
   "navigation_bar.security": "امنیت",
   "not_signed_in_indicator.not_signed_in": "برای دسترسی به این منبع باید وارد شوید.",
   "notification.admin.report": "{name}، {target} را گزارش داد",
+  "notification.admin.report_statuses_other": "{name}، {target} را گزارش داد",
   "notification.admin.sign_up": "{name} ثبت نام کرد",
   "notification.favourite": "{name} فرسته‌تان را برگزید",
   "notification.follow": "‫{name}‬ پی‌گیرتان شد",
   "notification.follow_request": "{name} درخواست پی‌گیریتان را داد",
+  "notification.label.mention": "اشاره",
+  "notification.label.private_mention": "اشارهٔ خصوصی",
+  "notification.label.private_reply": "پاسخ خصوصی",
+  "notification.label.reply": "پاسخ",
+  "notification.mention": "اشاره",
   "notification.moderation-warning.learn_more": "بیشتر بدانید",
+  "notification.moderation_warning": "هشداری مدیریتی گرفته‌اید",
+  "notification.moderation_warning.action_delete_statuses": "برخی از فرسته‌هایتان برداشته شدند.",
+  "notification.moderation_warning.action_disable": "حسابتان از کار افتاد.",
+  "notification.moderation_warning.action_mark_statuses_as_sensitive": "برخی از فرسته‌هایتان به عنوان حسّاس علامت خوردند.",
+  "notification.moderation_warning.action_none": "حسابتان هشداری مدیریتی گرفت.",
+  "notification.moderation_warning.action_sensitive": "فرسته‌هایتان از اکنون به عنوان حسّاس علامت خواهند خورد.",
+  "notification.moderation_warning.action_silence": "حسابتان محدود شده.",
+  "notification.moderation_warning.action_suspend": "حسابتان معلّق شده.",
   "notification.own_poll": "نظرسنجیتان پایان یافت",
+  "notification.poll": "نظرسنجی‌ای که در آن رأی دادید به پایان رسید",
   "notification.reblog": "‫{name}‬ فرسته‌تان را تقویت کرد",
+  "notification.relationships_severance_event": "قطع ارتباط با {name}",
   "notification.relationships_severance_event.learn_more": "بیشتر بدانید",
   "notification.status": "{name} چیزی فرستاد",
   "notification.update": "{name} فرسته‌ای را ویرایش کرد",
   "notification_requests.accept": "پذیرش",
+  "notification_requests.confirm_accept_multiple.title": "پذیرش درخواست‌های آگاهی؟",
+  "notification_requests.confirm_dismiss_multiple.title": "رد کردن درخواست‌های آگاهی؟",
   "notification_requests.dismiss": "دورانداختن",
+  "notification_requests.edit_selection": "ویرایش",
+  "notification_requests.exit_selection": "انجام شد",
   "notification_requests.maximize": "بیشنه",
+  "notification_requests.minimize_banner": "کمینه کردن بیرق آگاهی‌های پالوده",
+  "notification_requests.notifications_from": "آگاهی‌ها از {name}",
+  "notification_requests.title": "آگاهی‌های پالوده",
+  "notification_requests.view": "دیدن آگاهی‌ها",
   "notifications.clear": "پاک‌سازی آگاهی‌ها",
   "notifications.clear_confirmation": "مطمئنید می‌خواهید همهٔ آگاهی‌هایتان را برای همیشه پاک کنید؟",
+  "notifications.clear_title": "پاک‌سازی آگاهی‌ها؟",
   "notifications.column_settings.admin.report": "گزارش‌های جدید:",
   "notifications.column_settings.admin.sign_up": "ثبت نام‌های جدید:",
   "notifications.column_settings.alert": "آگاهی‌های میزکار",
   "notifications.column_settings.beta.category": "ویژگی‌های آزمایشی",
+  "notifications.column_settings.beta.grouping": "گروه‌بندی آگاهی‌ها",
   "notifications.column_settings.favourite": "برگزیده‌ها:",
+  "notifications.column_settings.filter_bar.advanced": "نمایش همۀ دسته‌ها",
+  "notifications.column_settings.filter_bar.category": "نوار پالایش سریع",
   "notifications.column_settings.follow": "پی‌گیرندگان جدید:",
   "notifications.column_settings.follow_request": "درخواست‌های جدید پی‌گیری:",
   "notifications.column_settings.mention": "اشاره‌ها:",
@@ -475,8 +559,19 @@
   "notifications.permission_denied": "آگاهی‌های میزکار به دلیل رد کردن درخواست اجازهٔ پیشین مرورگر، در دسترس نیستند",
   "notifications.permission_denied_alert": "از آن‌جا که پیش از این اجازهٔ مرورگر رد شده است، آگاهی‌های میزکار نمی‌توانند به کار بیفتند",
   "notifications.permission_required": "آگاهی‌های میزکار در دسترس نیستند زیرا اجازه‌های لازم، اعطا نشده.",
+  "notifications.policy.accept": "پذیرش",
+  "notifications.policy.accept_hint": "نمایش در آگاهی‌ها",
+  "notifications.policy.drop": "چشم‌پوشی",
+  "notifications.policy.drop_hint": "فرستادن به هیچ. دیگر هرگز دیده نخواهند شد",
+  "notifications.policy.filter": "پالایش",
+  "notifications.policy.filter_hint": "فرستادن به صندوق آگاهی‌های پالوده",
+  "notifications.policy.filter_limited_accounts_hint": "محدود شده به دست ناظم‌های کارساز",
+  "notifications.policy.filter_limited_accounts_title": "حساب‌های مدیریت شده",
+  "notifications.policy.filter_new_accounts_title": "حساب‌های جدید",
   "notifications.policy.filter_not_followers_title": "کسانی که شما را دنبال میکنند",
   "notifications.policy.filter_not_following_hint": "",
+  "notifications.policy.filter_not_following_title": "کسانی که پی نمی‌گیرید",
+  "notifications.policy.title": "مدیریت آگاهی‌ها از…",
   "notifications_permission_banner.enable": "به کار انداختن آگاهی‌های میزکار",
   "notifications_permission_banner.how_to_control": "برای دریافت آگاهی‌ها هنگام باز نبودن ماستودون، آگاهی‌های میزکار را به کار بیندازید. پس از به کار افتادنشان می‌توانید گونه‌های دقیق برهم‌کنش‌هایی که آگاهی‌های میزکار تولید می‌کنند را از {icon} بالا واپایید.",
   "notifications_permission_banner.title": "هرگز چیزی را از دست ندهید",
@@ -609,6 +704,7 @@
   "report_notification.categories.spam": "هرزنامه",
   "report_notification.categories.spam_sentence": "هرزنامه",
   "report_notification.categories.violation": "تخطّی از قانون",
+  "report_notification.categories.violation_sentence": "تخطّی از قانون",
   "report_notification.open": "گشودن گزارش",
   "search.no_recent_searches": "جست‌وجوی اخیری نیست",
   "search.placeholder": "جست‌وجو",
@@ -653,9 +749,11 @@
   "status.direct": "اشارهٔ خصوصی به ‪@{name}‬",
   "status.direct_indicator": "اشارهٔ خصوصی",
   "status.edit": "ویرایش",
+  "status.edited": "آخرین ویرایش {date}",
   "status.edited_x_times": "{count, plural, one {{count} مرتبه} other {{count} مرتبه}} ویرایش شد",
   "status.embed": "جاسازی",
   "status.favourite": "برگزیده‌",
+  "status.favourites": "{count, plural, one {برگزیده} other {برگزیده}}",
   "status.filter": "پالایش این فرسته",
   "status.history.created": "توسط {name} در {date} ایجاد شد",
   "status.history.edited": "توسط {name} در {date} ویرایش شد",
@@ -674,6 +772,7 @@
   "status.reblog": "تقویت",
   "status.reblog_private": "تقویت برای مخاطبان نخستین",
   "status.reblogged_by": "‫{name}‬ تقویت کرد",
+  "status.reblogs": "{count, plural, one {تقویت} other {تقویت}}",
   "status.reblogs.empty": "هنوز هیچ کسی این فرسته را تقویت نکرده است. وقتی کسی چنین کاری کند، این‌جا نمایش داده خواهد شد.",
   "status.redraft": "حذف و بازنویسی",
   "status.remove_bookmark": "برداشتن نشانک",
diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json
index f575c005d..8a448920c 100644
--- a/app/javascript/mastodon/locales/ia.json
+++ b/app/javascript/mastodon/locales/ia.json
@@ -180,6 +180,7 @@
   "confirmations.discard_edit_media.message": "Tu ha cambiamentos non salvate in le description o previsualisation del objecto multimedial. Abandonar los?",
   "confirmations.edit.confirm": "Modificar",
   "confirmations.edit.message": "Si tu modifica isto ora, le message in curso de composition essera perdite. Es tu secur de voler continuar?",
+  "confirmations.edit.title": "Superscriber le message?",
   "confirmations.logout.confirm": "Clauder session",
   "confirmations.logout.message": "Es tu secur que tu vole clauder le session?",
   "confirmations.logout.title": "Clauder session?",
@@ -189,6 +190,7 @@
   "confirmations.redraft.title": "Deler e rescriber le message?",
   "confirmations.reply.confirm": "Responder",
   "confirmations.reply.message": "Si tu responde ora, le message in curso de composition essera perdite. Es tu secur de voler continuar?",
+  "confirmations.reply.title": "Superscriber le message?",
   "confirmations.unfollow.confirm": "Non plus sequer",
   "confirmations.unfollow.message": "Es tu secur que tu vole cessar de sequer {name}?",
   "confirmations.unfollow.title": "Cessar de sequer le usator?",
@@ -361,6 +363,12 @@
   "home.pending_critical_update.link": "Vider actualisationes",
   "home.pending_critical_update.title": "Actualisation de securitate critic disponibile!",
   "home.show_announcements": "Monstrar annuncios",
+  "ignore_notifications_modal.ignore": "Ignorar le notificationes",
+  "ignore_notifications_modal.limited_accounts_title": "Ignorar le notificationes de contos moderate?",
+  "ignore_notifications_modal.new_accounts_title": "Ignorar le notificationes de nove contos?",
+  "ignore_notifications_modal.not_followers_title": "Ignorar notificationes de personas qui non te seque?",
+  "ignore_notifications_modal.not_following_title": "Ignorar notificationes de personas que tu non seque?",
+  "ignore_notifications_modal.private_mentions_title": "Ignorar notificationes de mentiones private non requestate?",
   "interaction_modal.description.favourite": "Con un conto sur Mastodon, tu pote marcar iste message como favorite pro informar le autor que tu lo apprecia e lo salva pro plus tarde.",
   "interaction_modal.description.follow": "Con un conto sur Mastodon, tu pote sequer {name} e reciper su messages in tu fluxo de initio.",
   "interaction_modal.description.reblog": "Con un conto sur Mastodon, tu pote impulsar iste message pro condivider lo con tu proprie sequitores.",
@@ -479,6 +487,8 @@
   "navigation_bar.security": "Securitate",
   "not_signed_in_indicator.not_signed_in": "Es necessari aperir session pro acceder a iste ressource.",
   "notification.admin.report": "{name} ha reportate {target}",
+  "notification.admin.report_account": "{name} ha reportate {count, plural, one {un message} other {# messages}} de {target} per {category}",
+  "notification.admin.report_account_other": "{name} ha reportate {count, plural, one {un message} other {# messages}} de {target}",
   "notification.admin.report_statuses": "{name} ha reportate {target} pro {category}",
   "notification.admin.report_statuses_other": "{name} ha reportate {target}",
   "notification.admin.sign_up": "{name} se ha inscribite",
diff --git a/app/javascript/mastodon/locales/la.json b/app/javascript/mastodon/locales/la.json
index 5ef238a2b..dc0796144 100644
--- a/app/javascript/mastodon/locales/la.json
+++ b/app/javascript/mastodon/locales/la.json
@@ -24,6 +24,7 @@
   "announcement.announcement": "Proclamatio",
   "attachments_list.unprocessed": "(immūtātus)",
   "block_modal.you_wont_see_mentions": "Nuntios quibus eos commemorant non videbis.",
+  "boost_modal.combo": "Potes premēre {combo} ut hoc iterum transilīre",
   "bundle_column_error.error.title": "Eheu!",
   "bundle_column_error.retry": "Retemptare",
   "bundle_column_error.routing.title": "CCCCIIII",
@@ -72,7 +73,10 @@
   "empty_column.account_timeline": "Hic nulla contributa!",
   "empty_column.account_unavailable": "Notio non impetrabilis",
   "empty_column.blocks": "Nondum quemquam usorem obsēcāvisti.",
+  "empty_column.bookmarked_statuses": "Nūllae adhuc postēs notātī habēs. Ubi unum notāverīs, hic apparebit.",
   "empty_column.direct": "Nōn habēs adhūc ullo mentionēs prīvātās. Cum ūnam mīseris aut accipis, hīc apparēbit.",
+  "empty_column.favourited_statuses": "Nūllae adhuc postēs praeferendī habēs. Ubi unum praeferās, hic apparebit.",
+  "empty_column.follow_requests": "Nūllae adhuc petitionēs sequendi habēs. Ubi unum accipīs, hic apparebit.",
   "empty_column.followed_tags": "Nōn adhūc aliquem hastāginem secūtus es. Cum id fēceris, hic ostendētur.",
   "empty_column.home": "Tua linea temporum domesticus vacua est! Sequere plures personas ut eam compleas.",
   "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
@@ -84,12 +88,19 @@
   "firehose.all": "Omnis",
   "footer.about": "De",
   "generic.saved": "Servavit",
+  "hashtag.column_header.tag_mode.none": "sine {additional}",
   "hashtag.column_settings.tag_mode.all": "Haec omnia",
   "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
   "hashtag.counter_by_accounts": "{count, plural, one {{counter} particeps} other {{counter} participēs}}",
   "hashtag.counter_by_uses": "{count, plural, one {{counter} nuntius} other {{counter} nuntii}}",
   "hashtag.counter_by_uses_today": "{count, plural, one {{counter} nuntius} other {{counter} nuntii}} hodie",
   "hashtags.and_other": "…et {count, plural, other {# plus}}",
+  "ignore_notifications_modal.filter_to_act_users": "Adhuc poteris accipere, reicere, vel referre usores",
+  "ignore_notifications_modal.filter_to_review_separately": "Percolantur notificatiōnes separātim recensere potes",
+  "interaction_modal.description.favourite": "Cum accūntū in Mastodon, hanc postem praeferre potes ut auctōrī indicēs tē eam aestimāre et ad posterius servēs.",
+  "interaction_modal.description.follow": "Cum accūntū in Mastodon, {name} sequī potes ut eōrum postēs in tēlā domī tuā recipiās.",
+  "interaction_modal.description.reply": "Mastodon de Ratione, huic nuntio respondere potes.",
+  "interaction_modal.sign_in": "Ad hōc servientem nōn dēlūxī. Ubi accūntum tuum hospitātum est?",
   "intervals.full.days": "{number, plural, one {# die} other {# dies}}",
   "intervals.full.hours": "{number, plural, one {# hora} other {# horae}}",
   "intervals.full.minutes": "{number, plural, one {# minutum} other {# minuta}}",
@@ -155,6 +166,8 @@
   "notification.status": "{name} nuper publicavit",
   "notification.update": "{name} nuntium correxit",
   "notification_requests.accept": "Accipe",
+  "notification_requests.confirm_accept_multiple.message": "Tu es accepturus {count, plural, one {una notitia petitionem} other {# notitia petitiones}}. Certus esne procedere vis?",
+  "notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {Illa} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?",
   "notifications.filter.all": "Omnia",
   "notifications.filter.polls": "Eventus electionis",
   "notifications.group": "Notificātiōnēs",
@@ -163,6 +176,8 @@
   "onboarding.follows.lead": "Tua domus feed est principalis via Mastodon experīrī. Quō plūrēs persōnas sequeris, eō actīvior et interessantior erit. Ad tē incipiendum, ecce quaedam suāsiones:",
   "onboarding.follows.title": "Popular on Mastodon",
   "onboarding.profile.display_name_hint": "Tuum nomen completum aut tuum nomen ludens…",
+  "onboarding.profile.lead": "Hoc semper postea in ratiōnibus complērī potest, ubi etiam plūrēs optiōnēs personalizātiōnis praesto sunt.",
+  "onboarding.profile.note_hint": "Alios hominēs vel #hashtags @nōmināre potes…",
   "onboarding.start.lead": "Nunc pars es Mastodonis, singularis, socialis medii platformae decentralis ubi—non algorismus—tuam ipsius experientiam curas. Incipiāmus in nova hac socialis regione:",
   "onboarding.start.skip": "Want to skip right ahead?",
   "onboarding.start.title": "Perfecisti eam!",
@@ -206,8 +221,11 @@
   "report.mute_explanation": "Non videbis eōrum nuntiōs. Possunt adhuc tē sequī et tuōs nuntiōs vidēre, nec sciēbunt sē tacitōs esse.",
   "report.next": "Secundum",
   "report.placeholder": "Commentāriī adiūnctī",
+  "report.reasons.legal_description": "Putās id legem tuae aut servientis patriae violāre.",
+  "report.reasons.violation_description": "Scis quod certa praecepta frangit",
   "report.submit": "Mittere",
   "report.target": "Report {target}",
+  "report.unfollow_explanation": "Tu hanc rationem secutus es. Non videre stationes suas in domo tua amplius pascere, eas sequere.",
   "report_notification.attached_statuses": "{count, plural, one {{count} nuntius} other {{count} nuntii}} attachiatus",
   "report_notification.categories.other": "Altera",
   "search.placeholder": "Quaerere",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index c9f8b7a27..4a32b1d82 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -11,6 +11,7 @@
   "about.not_available": "Maklumat ini belum tersedia pada pelayan ini.",
   "about.powered_by": "Media sosial terpencar yang dikuasakan oleh {mastodon}",
   "about.rules": "Peraturan pelayan",
+  "account.account_note_header": "Personal note",
   "account.add_or_remove_from_list": "Tambah atau Buang dari senarai",
   "account.badges.bot": "Bot",
   "account.badges.group": "Kumpulan",
@@ -33,6 +34,7 @@
   "account.follow_back": "Ikut balik",
   "account.followers": "Pengikut",
   "account.followers.empty": "Belum ada yang mengikuti pengguna ini.",
+  "account.followers_counter": "{count, plural, one {{counter} Diikuti} other {{counter} Diikuti}}",
   "account.following": "Mengikuti",
   "account.follows.empty": "Pengguna ini belum mengikuti sesiapa.",
   "account.go_to_profile": "Pergi ke profil",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 4439b8dc0..2c55da90b 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -190,6 +190,7 @@
   "confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
   "confirmations.unfollow.title": "Отписаться?",
   "content_warning.hide": "Скрыть пост",
+  "content_warning.show": "Всё равно показать",
   "conversation.delete": "Удалить беседу",
   "conversation.mark_as_read": "Отметить как прочитанное",
   "conversation.open": "Просмотр беседы",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index fded16b19..ea4ebcda4 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -94,7 +94,7 @@
   "block_modal.they_cant_mention": "他们不能提及或关注你。",
   "block_modal.they_cant_see_posts": "他们看不到你的嘟文,你也看不到他们的嘟文。",
   "block_modal.they_will_know": "他们将能看到他们被屏蔽。",
-  "block_modal.title": "屏蔽该用户?",
+  "block_modal.title": "是否屏蔽该用户?",
   "block_modal.you_wont_see_mentions": "你将无法看到提及他们的嘟文。",
   "boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
   "boost_modal.reblog": "是否转嘟?",
@@ -317,10 +317,10 @@
   "follow_suggestions.featured_longer": "由 {domain} 管理团队精选",
   "follow_suggestions.friends_of_friends_longer": "在你关注的人中很受欢迎",
   "follow_suggestions.hints.featured": "该用户已被 {domain} 管理团队精选。",
-  "follow_suggestions.hints.friends_of_friends": "该用户在您关注的人中很受欢迎。",
+  "follow_suggestions.hints.friends_of_friends": "该用户在你关注的人中很受欢迎。",
   "follow_suggestions.hints.most_followed": "该用户是 {domain} 上关注度最高的用户之一。",
   "follow_suggestions.hints.most_interactions": "该用户最近在 {domain} 上获得了很多关注。",
-  "follow_suggestions.hints.similar_to_recently_followed": "该用户与您最近关注的用户类似。",
+  "follow_suggestions.hints.similar_to_recently_followed": "该用户与你最近关注的用户类似。",
   "follow_suggestions.personalized_suggestion": "个性化建议",
   "follow_suggestions.popular_suggestion": "热门建议",
   "follow_suggestions.popular_suggestion_longer": "在 {domain} 上很受欢迎",
@@ -364,7 +364,7 @@
   "home.column_settings.show_reblogs": "显示转嘟",
   "home.column_settings.show_replies": "显示回复",
   "home.hide_announcements": "隐藏公告",
-  "home.pending_critical_update.body": "请尽快更新您的 Mastodon 服务器!",
+  "home.pending_critical_update.body": "请尽快更新你的 Mastodon 服务器!",
   "home.pending_critical_update.link": "查看更新",
   "home.pending_critical_update.title": "紧急安全更新可用!",
   "home.show_announcements": "显示公告",
@@ -618,12 +618,12 @@
   "onboarding.follows.lead": "你管理你自己的家庭饲料。你关注的人越多,它将越活跃和有趣。 这些配置文件可能是一个很好的起点——你可以随时取消关注它们!",
   "onboarding.follows.title": "定制您的主页动态",
   "onboarding.profile.discoverable": "让我的资料卡可被他人发现",
-  "onboarding.profile.discoverable_hint": "当您选择在 Mastodon 上启用发现功能时,你的嘟文可能会出现在搜索结果和热门中,你的账户可能会被推荐给与你兴趣相似的人。",
+  "onboarding.profile.discoverable_hint": "当你选择在 Mastodon 上启用发现功能时,你的嘟文可能会出现在搜索结果和热门中,你的账户可能会被推荐给与你兴趣相似的人。",
   "onboarding.profile.display_name": "昵称",
-  "onboarding.profile.display_name_hint": "您的全名或昵称…",
-  "onboarding.profile.lead": "您可以稍后在设置中完成此操作,设置中有更多的自定义选项。",
+  "onboarding.profile.display_name_hint": "你的全名或昵称…",
+  "onboarding.profile.lead": "你可以稍后在设置中完成此操作,设置中有更多的自定义选项。",
   "onboarding.profile.note": "简介",
-  "onboarding.profile.note_hint": "您可以提及 @其他人 或 #标签…",
+  "onboarding.profile.note_hint": "你可以提及 @其他人 或 #标签…",
   "onboarding.profile.save_and_continue": "保存并继续",
   "onboarding.profile.title": "设置个人资料",
   "onboarding.profile.upload_avatar": "上传头像",
@@ -632,7 +632,7 @@
   "onboarding.share.message": "我是来自 #Mastodon 的 {username}!请在 {url} 关注我。",
   "onboarding.share.next_steps": "可能的下一步:",
   "onboarding.share.title": "分享你的个人资料",
-  "onboarding.start.lead": "您新的 Mastodon 帐户已准备好。下面是如何最大限度地利用它:",
+  "onboarding.start.lead": "你的新 Mastodon 帐户已准备好。下面是如何最大限度地利用它:",
   "onboarding.start.skip": "想要在前面跳过吗?",
   "onboarding.start.title": "你已经成功了!",
   "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
@@ -644,7 +644,7 @@
   "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
   "onboarding.steps.share_profile.title": "分享你的个人资料",
   "onboarding.tips.2fa": "<strong>你知道吗?</strong>你可以在账户设置中配置双因素认证来保护账户安全。可以使用你选择的任何 TOTP 应用,无需电话号码!",
-  "onboarding.tips.accounts_from_other_servers": "<strong>您知道吗?</strong> 既然Mastodon是去中心化的,您所看到的一些账户将被托管在您以外的服务器上。 但你可以无缝地与他们交互!他们的服务器在他们的用户名的后半部分!",
+  "onboarding.tips.accounts_from_other_servers": "<strong>你知道吗?</strong> 既然Mastodon是去中心化的,你所看到的一些账户将被托管在你以外的服务器上。 但你可以无缝地与他们交互!他们的服务器在他们的用户名的后半部分!",
   "onboarding.tips.migration": "<strong>您知道吗?</strong> 如果你觉得你喜欢 {domain} 不是您未来的一个伟大的服务器选择。 您可以移动到另一个 Mastodon 服务器而不失去您的关注者。 您甚至可以主持您自己的服务器!",
   "onboarding.tips.verification": "<strong>您知道吗?</strong> 您可以通过在自己的网站上放置一个链接到您的 Mastodon 个人资料并将网站添加到您的个人资料来验证您的帐户。 无需收费或文书工作!",
   "password_confirmation.exceeds_maxlength": "密码确认超过最大密码长度",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index df754f6d6..257bec016 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -21,7 +21,7 @@
   "account.blocked": "已封鎖",
   "account.cancel_follow_request": "收回跟隨請求",
   "account.copy": "複製個人檔案連結",
-  "account.direct": "私訊 @{name}",
+  "account.direct": " @{name}",
   "account.disable_notifications": "取消來自 @{name} 嘟文的通知",
   "account.domain_blocked": "已封鎖網域",
   "account.edit_profile": "編輯個人檔案",
diff --git a/config/locales/activerecord.zh-CN.yml b/config/locales/activerecord.zh-CN.yml
index c510a58d1..1b661266c 100644
--- a/config/locales/activerecord.zh-CN.yml
+++ b/config/locales/activerecord.zh-CN.yml
@@ -56,4 +56,4 @@ zh-CN:
         webhook:
           attributes:
             events:
-              invalid_permissions: 不能包含您没有权限的事件
+              invalid_permissions: 不能包含你没有权限的事件
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 7ab1b4f07..ee05684b6 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -1824,7 +1824,7 @@ ar:
     keep_pinned: الاحتفاظ بالمنشورات المثبتة
     keep_pinned_hint: لن تحذف أي من منشوراتك المثبتة
     keep_polls: الاحتفاظ باستطلاعات الرأي
-    keep_polls_hint: لم تقم بحذف أي من استطلاعاتك
+    keep_polls_hint: لن يتم حذف أي من استطلاعات الرأي الخاصة بك
     keep_self_bookmark: احتفظ بالمنشورات التي أدرجتها في الفواصل المرجعية
     keep_self_bookmark_hint: لن تحذف منشوراتك الخاصة إذا قمت بوضع علامة مرجعية عليها
     keep_self_fav: احتفظ بالمنشورات التي أدرجتها في المفضلة
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 9a8f32117..8918228f4 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -25,6 +25,8 @@ ca:
   admin:
     account_actions:
       action: Realitza l'acció
+      already_silenced: Aquest compte ja s'ha silenciat.
+      already_suspended: Aquest compte ja s'ha suspès.
       title: Fer l'acció de moderació a %{acct}
     account_moderation_notes:
       create: Crea nota
@@ -46,6 +48,7 @@ ca:
         title: Canvia l'adreça electrònica de %{username}
       change_role:
         changed_msg: Els privilegis del compte s'han canviat correctament!
+        edit_roles: Gestió de rols d'usuari
         label: Canvia rol
         no_role: Sense rol
         title: Canvia el rol per a %{username}
@@ -585,6 +588,7 @@ ca:
         silence_description_html: El compte només serà visible a qui ja el seguia o l'ha cercat manualment, limitant-ne fortament l'abast. Sempre es pot revertir. Es tancaran tots els informes contra aquest compte.
         suspend_description_html: Aquest compte i tots els seus continguts seran inaccessibles i finalment eliminats, i interaccionar amb ell no serà possible. Reversible en 30 dies. Tanca tots els informes contra aquest compte.
       actions_description_remote_html: Decideix quina acció prendre per a resoldre aquest informe. Això només afectarà com <strong>el teu</strong> servidor es comunica amb aquest compte remot i en gestiona el contingut.
+      actions_no_posts: Aquest informe no té associada cap publicació a esborrar
       add_to_report: Afegir més al informe
       already_suspended_badges:
         local: Ja és suspès en aquest servidor
diff --git a/config/locales/cy.yml b/config/locales/cy.yml
index 492eb8af7..4a01967e2 100644
--- a/config/locales/cy.yml
+++ b/config/locales/cy.yml
@@ -33,6 +33,8 @@ cy:
   admin:
     account_actions:
       action: Cyflawni gweithred
+      already_silenced: Mae'r cyfrif hwn eisoes wedi'i dewi.
+      already_suspended: Mae'r cyfrif hwn eisoes wedi'i atal.
       title: Cyflawni gweithred cymedroli ar %{acct}
     account_moderation_notes:
       create: Gadael nodyn
@@ -54,6 +56,7 @@ cy:
         title: Newid e-bost i %{username}
       change_role:
         changed_msg: Rôl wedi ei newid yn llwyddiannus!
+        edit_roles: Rheoli rolau defnyddwyr
         label: Newid rôl
         no_role: Dim rôl
         title: Newid rôl %{username}
@@ -650,6 +653,7 @@ cy:
         suspend_description_html: Bydd y cyfrif a'i holl gynnwys yn anhygyrch ac yn cael ei ddileu yn y pen draw, a bydd rhyngweithio ag ef yn amhosibl. Yn gildroadwy o fewn 30 diwrnod. Yn cau pob adroddiad yn erbyn y cyfrif hwn.
       actions_description_html: Penderfynwch pa gamau i'w cymryd i delio gyda'r adroddiad hwn. Os cymerwch gamau cosbi yn erbyn y cyfrif a adroddwyd, bydd hysbysiad e-bost yn cael ei anfon atyn nhw, ac eithrio pan fydd y categori <strong>Sbam</strong> yn cael ei ddewis.
       actions_description_remote_html: Penderfynwch pa gamau i'w cymryd i ddatrys yr adroddiad hwn. Bydd hyn ond yn effeithio ar sut <strong>mae'ch</strong> gweinydd yn cyfathrebu â'r cyfrif hwn o bell ac yn trin ei gynnwys.
+      actions_no_posts: Nid oes gan yr adroddiad hwn unrhyw bostiadau cysylltiedig i'w dileu
       add_to_report: Ychwanegu rhagor i adroddiad
       already_suspended_badges:
         local: Wedi atal dros dro ar y gweinydd hwn yn barod
@@ -1558,6 +1562,7 @@ cy:
   media_attachments:
     validations:
       images_and_video: Methu atodi fideo i bostiad sydd eisoes yn cynnwys delweddau
+      not_found: Cyfryngau %{ids} heb eu canfod neu wedi'u hatodi i bostiad arall yn barod
       not_ready: Methu atodi ffeiliau nad ydynt wedi gorffen prosesu. Ceisiwch eto, cyn hir!
       too_many: Methu atodi mwy na 4 ffeil
   migrations:
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 1366370eb..e3f834345 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -25,6 +25,8 @@ da:
   admin:
     account_actions:
       action: Udfør handling
+      already_silenced: Denne konto er allerede gjort tavs.
+      already_suspended: Denne konto er allerede suspenderet.
       title: Udfør moderatorhandling på %{acct}
     account_moderation_notes:
       create: Skriv notat
@@ -46,6 +48,7 @@ da:
         title: Skift e-mail for %{username}
       change_role:
         changed_msg: Rolle ændret!
+        edit_roles: Håndtér brugerroller
         label: Ændr rolle
         no_role: Ingen rolle
         title: Ændr rolle for %{username}
@@ -602,6 +605,7 @@ da:
         suspend_description_html: Kontoen inkl. alt indhold utilgængeliggøres og interaktion umuliggøres, og den slettes på et tidspunkt. Kan omgøres inden for 30 dage. Lukker alle indrapporteringer af kontoen.
       actions_description_html: Afgør, hvilke foranstaltning, der skal træffes for at løse denne anmeldelse. Ved en straffende foranstaltning mod den anmeldte konto, fremsendes en e-mailnotifikation, undtagen når kategorien <strong>Spam</strong> er valgt.
       actions_description_remote_html: Fastslå en nødvendig handling mhp. at løse denne anmeldelse. Dette vil kun påvirke <strong>din</strong> servers kommunikation med, og indholdshåndtering for, fjernkontoen.
+      actions_no_posts: Denne anmeldelse har ingen tilknyttede indlæg at slette
       add_to_report: Føj mere til anmeldelse
       already_suspended_badges:
         local: Allerede suspenderet på denne server
diff --git a/config/locales/de.yml b/config/locales/de.yml
index e5e3f37a3..2952d22d7 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -25,6 +25,8 @@ de:
   admin:
     account_actions:
       action: Aktion ausführen
+      already_silenced: Dieses Konto wurde bereits stummgeschaltet.
+      already_suspended: Dieses Konto wurde bereits gesperrt.
       title: "@%{acct} moderieren"
     account_moderation_notes:
       create: Notiz abspeichern
@@ -46,6 +48,7 @@ de:
         title: E-Mail-Adresse für %{username} ändern
       change_role:
         changed_msg: Rolle erfolgreich geändert!
+        edit_roles: Rollen verwalten
         label: Rolle ändern
         no_role: Keine Rolle
         title: Rolle für %{username} ändern
@@ -602,6 +605,7 @@ de:
         suspend_description_html: Das Konto und alle Inhalte werden unzugänglich und ggf. gelöscht. Eine Interaktion mit dem Konto wird unmöglich. Dies kann innerhalb von 30 Tagen rückgängig gemacht werden. Alle Meldungen zu diesem Konto werden geschlossen.
       actions_description_html: Entscheide, welche Maßnahmen du zum Klären dieser Meldung ergreifen möchtest. Wenn du eine Strafmaßnahme gegen das gemeldete Konto ergreifst, wird eine E-Mail-Benachrichtigung an dieses gesendet, außer wenn die <strong>Spam</strong>-Kategorie ausgewählt ist.
       actions_description_remote_html: Entscheide, welche Maßnahmen du zum Klären dieser Meldung ergreifen möchtest. Dies wirkt sich lediglich darauf aus, wie <strong>dein</strong> Server mit diesem externen Konto kommuniziert und dessen Inhalt handhabt.
+      actions_no_posts: Diese Meldung enthält keine zu löschenden Beiträge
       add_to_report: Meldung ergänzen
       already_suspended_badges:
         local: Auf diesem Server bereits gesperrt
@@ -1454,7 +1458,7 @@ de:
   media_attachments:
     validations:
       images_and_video: Es kann kein Video an einen Beitrag angehängt werden, der bereits Bilder enthält
-      not_found: Medien %{ids} nicht verfügbar oder bereits an einen anderen Beitrag angehängt
+      not_found: Medieninhalt(e) %{ids} nicht gefunden oder bereits an einen anderen Beitrag angehängt
       not_ready: Dateien, die noch nicht verarbeitet wurden, können nicht angehängt werden. Versuche es gleich noch einmal!
       too_many: Mehr als vier Dateien können nicht angehängt werden
   migrations:
diff --git a/config/locales/devise.zh-CN.yml b/config/locales/devise.zh-CN.yml
index 2a50c131d..86e78c1b1 100644
--- a/config/locales/devise.zh-CN.yml
+++ b/config/locales/devise.zh-CN.yml
@@ -50,12 +50,12 @@ zh-CN:
       two_factor_disabled:
         explanation: 目前只能通过邮箱地址和密码登录。
         subject: Mastodon:双因素认证已禁用
-        subtitle: 您账户的双因素认证已被停用。
+        subtitle: 你账户的双因素认证已被停用。
         title: 双因素认证已停用
       two_factor_enabled:
         explanation: 登录时需要输入先前配对好的TOTP应用生成的令牌。
         subject: Mastodon:双因素认证已启用
-        subtitle: 您账户的双因素认证已被启用。
+        subtitle: 你账户的双因素认证已被启用。
         title: 双因素认证已启用
       two_factor_recovery_codes_changed:
         explanation: 新恢复码已生成,同时旧恢复码已失效。
@@ -74,13 +74,13 @@ zh-CN:
           subject: Mastodon:安全密钥已删除
           title: 你的安全密钥之一已被删除
       webauthn_disabled:
-        explanation: 您账户的安全密钥身份认证已被停用。
+        explanation: 你账户的安全密钥身份认证已被停用。
         extra: 目前只能用先前配对的TOTP应用生成的令牌登录。
         subject: Mastodon:安全密钥认证已禁用
         title: 安全密钥已禁用
       webauthn_enabled:
-        explanation: 您账户的安全密钥身份认证已被启用。
-        extra: 您的安全密钥现在可用于登录。
+        explanation: 你账户的安全密钥身份认证已被启用。
+        extra: 你的安全密钥现在可用于登录。
         subject: Mastodon:安全密钥认证已启用
         title: 已启用安全密钥
     omniauth_callbacks:
diff --git a/config/locales/doorkeeper.fa.yml b/config/locales/doorkeeper.fa.yml
index c56e76e34..0ce7a9591 100644
--- a/config/locales/doorkeeper.fa.yml
+++ b/config/locales/doorkeeper.fa.yml
@@ -83,6 +83,7 @@ fa:
         access_denied: صاحب منبع یا کارساز تأیید هویت، درخواست را رد کردند.
         credential_flow_not_configured: جریان اعتبارنامهٔ گذرواژهٔ مالک منبع به دلیل پیکربندی نشده بودن Doorkeeper.configure.resource_owner_from_credentials شکست خورد.
         invalid_client: تأیید هویت کارخواه به دلیل کارخواه ناشناخته، عدم وجود تأیید هویت کاره یا روش تأیید هویت پشتیبانی‌نشده شکست خورد.
+        invalid_code_challenge_method: روش چالش کدی باید S256 باشد. متن خام پشتیبانی نمی‌شود.
         invalid_grant: اعطای دسترسی فراهم ‌شده نامعتبر، منقضی یا نامطابق با نشانی بازگشت استفاده‌شده در درخواست تأیید هویت بوده و یا برای کارخواهی دیگر صادر شده است.
         invalid_redirect_uri: نشانی بازگشت موجود، معتبر نیست.
         invalid_request:
@@ -135,6 +136,7 @@ fa:
         media: پیوست‌های رسانه‌ای
         mutes: خموش‌ها
         notifications: آگاهی‌ها
+        profile: نمایهٔ ماستودونتان
         push: آگاهی‌های ارسالی
         reports: گزارش‌ها
         search: جست‌وجو
@@ -165,6 +167,7 @@ fa:
       admin:write:reports: انجام کنش مدیریتی روی گزارش‌ها
       crypto: از رمزگذاری سرتاسر استفاده کنید
       follow: پیگیری، مسدودسازی، لغو مسدودسازی، و لغو پیگیری حساب‌ها
+      profile: فقط خواندن اطّلاعات نمایهٔ حسابتان
       push: دریافت آگاهی‌ای ارسالیتان
       read: خواندن اطلاعات حساب شما
       read:accounts: دیدن اطّلاعات حساب
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index ab0dcb58f..520211e2e 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -25,6 +25,8 @@ es-AR:
   admin:
     account_actions:
       action: Ejecutar acción
+      already_silenced: Esta cuenta ya ha sido limitada.
+      already_suspended: Esta cuenta ya ha sido suspendida.
       title: Ejecutar acción de moderación en %{acct}
     account_moderation_notes:
       create: Dejar nota
@@ -46,6 +48,7 @@ es-AR:
         title: Cambiar correo electrónico para %{username}
       change_role:
         changed_msg: "¡Rol cambiado exitosamente!"
+        edit_roles: Administrar roles de usuario
         label: Cambiar rol
         no_role: Sin rol
         title: Cambiar rol para %{username}
@@ -602,6 +605,7 @@ es-AR:
         suspend_description_html: La cuenta y todos sus contenidos serán inaccesibles y finalmente eliminados, e interactuar con ella será imposible. Revertible en 30 días. Esto cierra todas las denuncias contra esta cuenta.
       actions_description_html: Decidí qué medidas tomar para resolver esta denuncia. Si tomás una acción punitiva contra la cuenta denunciada, se le enviará a dicha cuenta una notificación por correo electrónico, excepto cuando se seleccione la categoría <strong>Spam</strong>.
       actions_description_remote_html: Decidí qué medidas tomar para resolver esta denuncia. Esto sólo afectará la forma en que <strong>tu servidor</strong> se comunica con esta cuenta remota y maneja su contenido.
+      actions_no_posts: Esta denuncia no tiene ningún mensaje asociado para eliminar
       add_to_report: Agregar más a la denuncia
       already_suspended_badges:
         local: Ya suspendido en este servidor
diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml
index 9df6a784d..52e440ffe 100644
--- a/config/locales/es-MX.yml
+++ b/config/locales/es-MX.yml
@@ -1454,6 +1454,7 @@ es-MX:
   media_attachments:
     validations:
       images_and_video: No se puede adjuntar un video a un estado que ya contenga imágenes
+      not_found: Archivos multimedia %{ids} no encontrados, o ya se encuentran adjuntos a otra publicación
       not_ready: No se pueden adjuntar archivos que no se han terminado de procesar. ¡Inténtalo de nuevo en un momento!
       too_many: No se pueden adjuntar más de 4 archivos
   migrations:
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 0bbad767e..21b900192 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -1454,6 +1454,7 @@ es:
   media_attachments:
     validations:
       images_and_video: No se puede adjuntar un video a unapublicación que ya contenga imágenes
+      not_found: Archivos multimedia %{ids} no encontrados, o ya se encuentran adjuntos a otra publicación
       not_ready: No se pueden adjuntar archivos que no se han terminado de procesar. ¡Inténtalo de nuevo en un momento!
       too_many: No se pueden adjuntar más de 4 archivos
   migrations:
diff --git a/config/locales/et.yml b/config/locales/et.yml
index 17fd41a8e..bbd1b4ab2 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -25,12 +25,15 @@ et:
   admin:
     account_actions:
       action: Täida tegevus
+      already_silenced: See konto on juba vaigistatud.
+      already_suspended: See konto on juba peatatud.
       title: Rakenda moderaatori tegevus kasutajale %{acct}
     account_moderation_notes:
       create: Jäta teade
       created_msg: Modereerimisteade edukalt koostatud!
       destroyed_msg: Modereerimisteade edukalt kustutatud!
     accounts:
+      add_email_domain_block: Blokeeri e-posti domeen
       approve: Võta vastu
       approved_msg: Kasutaja %{username} liitumisavaldus rahuldatud
       are_you_sure: Oled kindel?
@@ -45,6 +48,7 @@ et:
         title: Muuda e-postiaadressi kasutajale %{username}
       change_role:
         changed_msg: Roll on muudetud!
+        edit_roles: Halda kasutaja rolle
         label: Muuda rolli
         no_role: Roll puudub
         title: "%{username} rolli muutmine"
@@ -57,6 +61,7 @@ et:
       demote: Alanda
       destroyed_msg: "%{username} andmed on nüüd lõpliku kustutamise ootel"
       disable: Lukusta
+      disable_sign_in_token_auth: E-posti võtme abil autentimise väljalülitamine
       disable_two_factor_authentication: Keela 2FA
       disabled: Keelatud
       display_name: Kuvanimi
@@ -65,6 +70,7 @@ et:
       email: E-post
       email_status: E-posti olek
       enable: Luba
+      enable_sign_in_token_auth: Luba e-posti võtme abil autentimine
       enabled: Lubatud
       enabled_msg: Kasutaja %{username} konto taastatud
       followers: Jälgijad
@@ -191,8 +197,10 @@ et:
         destroy_user_role: Rolli kustutamine
         disable_2fa_user: Keela 2FA
         disable_custom_emoji: Keelas kohandatud emotikoni
+        disable_sign_in_token_auth_user: Keela e-posti võtme abil autentimine kasutajale
         disable_user: Keelas kasutaja
         enable_custom_emoji: Lubas kohandatud emotikoni
+        enable_sign_in_token_auth_user: Luba e-posti võtme abil autentimine kasutajale
         enable_user: Lubas kasutaja
         memorialize_account: Igaveselt lahkunuks märkimine
         promote_user: Edendas kasutaja
@@ -243,8 +251,10 @@ et:
         destroy_user_role_html: "%{name} kustutas %{target} rolli"
         disable_2fa_user_html: "%{name} eemaldas kasutaja %{target} kahe etapise nõude"
         disable_custom_emoji_html: "%{name} keelas emotikooni %{target}"
+        disable_sign_in_token_auth_user_html: "%{name} keelas e-posti võtme abil autentimise %{target} jaoks"
         disable_user_html: "%{name} keelas %{target} sisenemise"
         enable_custom_emoji_html: "%{name} lubas emotikooni %{target}"
+        enable_sign_in_token_auth_user_html: "%{name} lubas e-posti võtme abil autentimise %{target} jaoks"
         enable_user_html: "%{name} lubas %{target} sisenemise"
         memorialize_account_html: "%{name} märkis %{target} igaveselt lahkunuks"
         promote_user_html: "%{name} ülendas kasutajat %{target}"
@@ -576,6 +586,7 @@ et:
         silence_description_html: Konto saab olema nähtav ainult senistele jälgijatele või otsestele pöördujatele, mõjutates avalikku levi. On tagasipööratav. Sulgeb kõik konto suhtes esitatud raportid.
         suspend_description_html: See konto ja kogu selle sisu muutub kättesaamatuks ning kustub lõpuks ja igasugune suhtlus sellega muutub võimatuks. Tagasipööratav 30 päeva jooksul. Lõpetab kõik selle konto kohta esitatud kaebused.
       actions_description_remote_html: Otsusta, mida teha selle raporti lahendamiseks. See mõjutab ainult seda, kuidas <strong>Sinu</strong> server selle kaugkontoga suhtleb ning selle sisu käsitleb.
+      actions_no_posts: Selle raportiga pole seotud ühtegi postitust, mida kustutada
       add_to_report: Lisa raportile juurde
       already_suspended_badges:
         local: Juba kustutamisel selles serveris
@@ -769,6 +780,7 @@ et:
       destroyed_msg: Üleslaetud fail edukalt kustutatud!
     software_updates:
       critical_update: Kriitiline — uuenda kiiresti
+      description: Soovitatav on hoida oma Mastodoni paigaldus ajakohasena, et saada kasu viimastest parandustest ja funktsioonidest. Lisaks sellele on mõnikord oluline Mastodoni õigeaegne uuendamine, et vältida turvaprobleeme. Neil põhjustel kontrollib Mastodon uuendusi iga 30 minuti järel ja teavitab vastavalt sinu e-posti teavitamise eelistustele.
       documentation_link: Vaata lisa
       release_notes: Väljalaskemärkused
       title: Saadaval uuendused
@@ -878,10 +890,16 @@ et:
     trends:
       allow: Luba
       approved: Kinnitatud
+      confirm_allow: Oled kindel, et soovid valitud sildid lubada?
+      confirm_disallow: Oled kindel, et soovid valitud sildid keelata?
       disallow: Keela
       links:
         allow: Luba viit
         allow_provider: Luba autor
+        confirm_allow: Oled kindel, et soovid valitud lingid lubada?
+        confirm_allow_provider: Oled kindel, et soovid valitud teenusepakkujad lubada?
+        confirm_disallow: Oled kindel, et soovid valitud lingid keelata?
+        confirm_disallow_provider: Oled kindel, et soovid valitud teenusepakkujad keelata?
         description_html: Need on lingid, mida jagavad praegu paljud kontod, mille postitusi server näeb. See võib aidata kasutajatel teada saada, mis maailmas toimub. Ühtegi linki ei kuvata avalikult enne, kui avaldaja on heakskiidetud. Samuti saab üksikuid linke lubada või tagasi lükata.
         disallow: Keela viit
         disallow_provider: Keela autor
@@ -905,6 +923,10 @@ et:
       statuses:
         allow: Luba postitada
         allow_account: Luba autor
+        confirm_allow: Oled kindel, et soovid valitud olekud lubada?
+        confirm_allow_account: Oled kindel, et soovid valitud kontod lubada?
+        confirm_disallow: Oled kindel, et soovid valitud olekud lubada?
+        confirm_disallow_account: Oled kindel, et soovid valitud kontod keelata?
         description_html: Need on postitused, millest server teab ja mida praegu jagatakse ja mis on hetkel paljude lemmikud. See võib aidata uutel ja naasvatel kasutajatel leida rohkem inimesi, keda jälgida. Ühtegi postitust ei kuvata avalikult enne, kui autor on heaks kiidetud ja autor lubab oma kontot teistele soovitada. Samuti saab üksikuid postitusi lubada või tagasi lükata.
         disallow: Ära luba postitada
         disallow_account: Keela autor
@@ -937,6 +959,7 @@ et:
         used_by_over_week:
           one: Kasutatud ühe kasutaja pool viimase nädala jooksul
           other: Kasutatud %{count} kasutaja poolt viimase nädala jooksul
+      title: Soovitused ja trendid
       trending: Trendid
     warning_presets:
       add_new: Lisa uus
@@ -1021,7 +1044,9 @@ et:
       guide_link_text: Panustada võib igaüks!
     sensitive_content: Tundlik sisu
   application_mailer:
+    notification_preferences: Muuda e-posti eelistusi
     salutation: "%{name}!"
+    settings: 'Muuda e-posti eelistusi: %{link}'
     unsubscribe: Loobu tellimisest
     view: 'Vaade:'
     view_profile: Vaata profiili
@@ -1030,10 +1055,10 @@ et:
     created: Rakenduse loomine õnnestus
     destroyed: Rakenduse kustutamine õnnestus
     logout: Logi välja
-    regenerate_token: Loo uus access token
-    token_regenerated: Access tokeni loomine õnnestus
+    regenerate_token: Loo uus ligipääsuvõti
+    token_regenerated: Ligipääsuvõtme loomine õnnestus
     warning: Ole nende andmetega ettevaatlikud. Ära jaga neid kellegagi!
-    your_token: Su juurdepääsutunnus
+    your_token: Su juurdepääsuvõti
   auth:
     apply_for_account: Konto taotluse esitamine
     captcha_confirmation:
@@ -1041,6 +1066,7 @@ et:
       hint_html: Üks asi veel! Me peame veenduma, et oled inimene (et me saaksime spämmi väljaspoole jätta!). Lahenda allpool olev CAPTCHA ja klõpsa "Jätka".
       title: Turvalisuse kontroll
     confirmations:
+      awaiting_review: Sinu e-posti aadress on kinnitatud! %{domain} meeskond vaatab praegu sinu registreeringut läbi. Saad e-kirja, kui nad konto heaks kiidavad!
       awaiting_review_title: Su registreeringut vaadatakse läbi
       clicking_this_link: klõpsates seda linki
       login_link: logi sisse
@@ -1048,6 +1074,7 @@ et:
       redirect_to_app_html: Sind oleks pidanud suunatama rakendusse <strong>%{app_name}</strong>. Kui seda ei juhtunud, proovi %{clicking_this_link} või naase käsitsi rakendusse.
       registration_complete: Sinu registreering domeenil %{domain} on nüüd valmis!
       welcome_title: Tere tulemast, %{name}!
+      wrong_email_hint: Kui see e-postiaadress pole korrektne, saad seda muuta konto seadetes.
     delete_account: Konto kustutamine
     delete_account_html: Kui soovid oma konto kustutada, siis <a href="%{path}">jätka siit</a>. Pead kustutamise eraldi kinnitama.
     description:
@@ -1068,6 +1095,7 @@ et:
     or_log_in_with: Või logi sisse koos
     privacy_policy_agreement_html: Olen tutvunud <a href="%{privacy_policy_path}" target="_blank">isikuandmete kaitse põhimõtetega</a> ja nõustun nendega
     progress:
+      confirm: E-posti kinnitamine
       details: Sinu üksikasjad
       review: Meie ülevaatamine
       rules: Nõustu reeglitega
@@ -1089,8 +1117,10 @@ et:
     security: Turvalisus
     set_new_password: Uue salasõna määramine
     setup:
+      email_below_hint_html: Kontrolli rämpsposti kausta või taotle uut. Saad oma e-posti aadressi parandada, kui see on vale.
       email_settings_hint_html: Klõpsa linki, mis saadeti sulle, et kinnitada %{email}. Seni me ootame.
       link_not_received: Kas ei saanud linki?
+      new_confirmation_instructions_sent: Saad mõne minuti pärast uue kinnituslingiga e-kirja!
       title: Kontrolli sisendkasti
     sign_in:
       preamble_html: Logi sisse oma <strong>%{domain}</strong> volitustega. Kui konto asub teises serveris, ei saa siin sisse logida.
@@ -1101,7 +1131,9 @@ et:
       title: Loo konto serverisse  %{domain}.
     status:
       account_status: Konto olek
+      confirming: E-posti kinnitamise ootamine.
       functional: Konto on täies mahus kasutatav.
+      pending: Sinu taotlus ootab meie meeskonna läbivaatamist. See võib võtta aega. Kui taotlus on heaks kiidetud, saadetakse sulle e-kiri.
       redirecting_to: See konto pole aktiivne, sest on suunatud aadressile %{acct}.
       self_destruct: Kuna %{domain} on sulgemisel, saad oma kontole vaid piiratud ligipääsu.
       view_strikes: Vaata enda eelnevaid juhtumeid
@@ -1144,6 +1176,9 @@ et:
       before: 'Veendu, et saad aru, mida toob plaanitav muudatus kaasa:'
       caches: Teiste serverite poolt talletatud sisu võib jääda kättesaadavaks
       data_removal: Sinu postitused ning kontoandmed kustutatakse jäädavalt
+      email_change_html: Saad <a href="%{path}">muuta oma e-postiaadressi</a> ilma oma kontot kustutamata
+      email_contact_html: Kui see ikkagi ei saabu, võid abi saamiseks kirjutada <a href="mailto:%{email}">%{email}</a>
+      email_reconfirmation_html: Kui sa ei saa kinnituskirja, saad <a href="%{path}">taotleda seda uuesti</a>
       irreversible: Kustutatud kontot ei saa taastada ega uuesti aktiveerida
       more_details_html: Konto kustutamise kohta loe täpsemalt <a href="%{terms_path}">isikuandmete kaitse põhimõtetest</a>.
       username_available: Kasutajanimi muutub uuesti kasutatavaks
@@ -1376,6 +1411,7 @@ et:
     authentication_methods:
       otp: kaheastmelise autentimise rakendus
       password: salasõna
+      sign_in_token: e-posti turvvakood
       webauthn: turvavõtmed
     description_html: Kui paistab tundmatuid tegevusi, tuleks vahetada salasõna ja aktiveerida kaheastmeline autentimine.
     empty: Autentimisajalugu pole saadaval
@@ -1390,6 +1426,7 @@ et:
   media_attachments:
     validations:
       images_and_video: Ei saa lisada video postitusele, milles on juba pildid
+      not_found: Meedia %{ids} pole leitav või juba teisele postitusele lisatud
       not_ready: Ei saa lisada faile, mida hetkel töödeldakse. Proovi uuesti mõne hetke pärast!
       too_many: Ei saa lisada rohkem, kui 4 faili
   migrations:
@@ -1466,6 +1503,8 @@ et:
     update:
       subject: "%{name} muutis postitust"
   notifications:
+    administration_emails: Admini e-postiteated
+    email_events: Sündmused e-postiteavituste jaoks
     email_events_hint: 'Vali sündmused, mille kohta soovid teavitusi:'
   number:
     human:
@@ -1624,6 +1663,7 @@ et:
     import: Impordi
     import_and_export: Import / eksport
     migrate: Konto kolimine
+    notifications: E-postiteated
     preferences: Eelistused
     profile: Profiil
     relationships: Jälgitud ja jälgijad
@@ -1872,6 +1912,7 @@ et:
     invalid_otp_token: Vale kaheastmeline võti
     otp_lost_help_html: Kui kaotasid ligipääsu mõlemale, saad võtta ühendust %{email}-iga
     rate_limited: Liiga palju autentimise katseid, proovi hiljem uuesti.
+    seamless_external_login: Oled sisse logitud välise teenuse kaudu. Nii pole salasõna ja e-posti seaded saadaval.
     signed_in_as: 'Sisse logitud kasutajana:'
   verification:
     extra_instructions_html: <strong>Soovitus:</strong> Sinu kodulehel olev link võib olla nähtamatu. Oluline osa on <code>rel="me"</code>, mis väldib kasutaja loodud sisuga lehtedel libaisikustamist. Sa saad isegi kasutada lehe HEADER osas silti <code>link</code> sildi <code>a</code> asemel, kuid HTML peab olema kättesaadav ilma JavaScripti käivitamata.
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 8f0c80f1d..0c5d5ef98 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -25,6 +25,8 @@ fi:
   admin:
     account_actions:
       action: Suorita toimi
+      already_silenced: Tätä tiliä on jo rajoitettu.
+      already_suspended: Tämä tili on jo jäädytetty.
       title: Suorita moderointitoimi käyttäjälle %{acct}
     account_moderation_notes:
       create: Jätä muistiinpano
@@ -46,6 +48,7 @@ fi:
         title: Vaihda käyttäjän %{username} sähköposti-osoite
       change_role:
         changed_msg: Roolin vaihto onnistui!
+        edit_roles: Hallinnoi käyttäjien rooleja
         label: Vaihda rooli
         no_role: Ei roolia
         title: Vaihda käyttäjän %{username} rooli
@@ -442,7 +445,7 @@ fi:
         create: Lisää verkkotunnus
         resolve: Selvitä verkkotunnus
         title: Estä uusi sähköpostiverkkotunnus
-      no_email_domain_block_selected: Sähköpostiverkkotunnusten estoja ei muutettu; yhtäkään ei ollut valittu
+      no_email_domain_block_selected: Sähköpostiverkkotunnusten estoja ei muutettu, koska yhtäkään ei ollut valittuna
       not_permitted: Ei sallittu
       resolved_dns_records_hint_html: Verkkotunnusnimi kytkeytyy seuraaviin MX-verkkotunnuksiin, jotka ovat viime kädessä vastuussa sähköpostin vastaanottamisesta. MX-verkkotunnuksen estäminen estää rekisteröitymisen mistä tahansa sähköpostiosoitteesta, joka käyttää samaa MX-verkkotunnusta, vaikka näkyvä verkkotunnuksen nimi olisikin erilainen. <strong>Varo estämästä suuria sähköpostipalvelujen tarjoajia.</strong>
       resolved_through_html: Ratkaistu verkkotunnuksen %{domain} kautta
@@ -600,8 +603,9 @@ fi:
         resolve_description_html: Ilmoitettua tiliä kohtaan ei ryhdytä toimiin, varoitusta ei kirjata ja raportti suljetaan.
         silence_description_html: Tili näkyy vain niille, jotka jo seuraavat sitä tai etsivät sen manuaalisesti, mikä rajoittaa merkittävästi sen tavoitettavuutta. Voidaan perua milloin vain. Sulkee kaikki tiliin kohdistuvat raportit.
         suspend_description_html: Tili ja mikään sen sisältö eivät ole käytettävissä, ja lopulta ne poistetaan ja vuorovaikutus tilin kanssa on mahdotonta. Peruttavissa 30 päivän ajan. Sulkee kaikki tiliin kohdistuvat raportit.
-      actions_description_html: Päätä, mihin toimiin ryhdyt tämän raportin ratkaisemiseksi. Jos ryhdyt rangaistustoimeen ilmoitettua tiliä kohtaan, hänelle lähetetään sähköpostitse ilmoitus asiasta, paitsi jos valittuna on <strong>Roskaposti</strong>-luokka.
+      actions_description_html: Päätä, mihin toimiin ryhdyt tämän raportin ratkaisemiseksi. Jos ryhdyt rangaistustoimeen raportoitua tiliä kohtaan, hänelle lähetetään sähköpostitse ilmoitus asiasta, paitsi jos valittuna on <strong>Roskaposti</strong>-luokka.
       actions_description_remote_html: Päätä, mihin toimiin ryhdyt tämän raportin ratkaisemiseksi. Tämä vaikuttaa vain siihen, miten <strong>sinun</strong> palvelimesi viestii tämän etätilin kanssa ja käsittelee sen sisältöä.
+      actions_no_posts: Tähän raporttiin ei liity poistettavia julkaisuja
       add_to_report: Lisää raporttiin
       already_suspended_badges:
         local: Jäädytetty jo tällä palvelimella
@@ -1135,7 +1139,7 @@ fi:
     security: Turvallisuus
     set_new_password: Aseta uusi salasana
     setup:
-      email_below_hint_html: Tarkista roskapostikansiosi tai pyydä uusi viesti. Voit myös korjata sähköpostiosoitteesi tarvittaessa.
+      email_below_hint_html: Tarkista roskapostikansiosi tai pyydä uusi viesti. Voit korjata sähköpostiosoitteesi tarvittaessa.
       email_settings_hint_html: Napsauta lähettämäämme linkkiä vahvistaaksesi osoitteen %{email}. Odotamme täällä.
       link_not_received: Etkö saanut linkkiä?
       new_confirmation_instructions_sent: Saat pian uuden vahvistuslinkin sisältävän sähköpostiviestin!
@@ -1195,7 +1199,7 @@ fi:
       caches: Muiden palvelinten välimuistiinsa tallentamaa sisältöä voi säilyä
       data_removal: Julkaisusi ja muut tietosi poistetaan pysyvästi
       email_change_html: Voit <a href="%{path}">muuttaa sähköpostiosoitettasi</a> poistamatta tiliäsi
-      email_contact_html: Mikäli viesti ei vieläkään saavu perille, voit pyytää apua sähköpostitse osoitteella <a href="mailto:%{email}">%{email}</a>
+      email_contact_html: Jos viesti ei vieläkään saavu perille, voit pyytää apua sähköpostitse osoitteella <a href="mailto:%{email}">%{email}</a>
       email_reconfirmation_html: Jos et saa vahvistussähköpostiviestiä, voit <a href="%{path}">pyytää sitä uudelleen</a>
       irreversible: Et voi palauttaa tiliäsi etkä aktivoida sitä uudelleen
       more_details_html: Tarkempia tietoja saat <a href="%{terms_path}">tietosuojakäytännöstämme</a>.
diff --git a/config/locales/fo.yml b/config/locales/fo.yml
index 0c3620ad4..040e312d7 100644
--- a/config/locales/fo.yml
+++ b/config/locales/fo.yml
@@ -25,6 +25,8 @@ fo:
   admin:
     account_actions:
       action: Frem atgerð
+      already_silenced: Hendan kontan er longu gjørd kvirr.
+      already_suspended: Hendan kontan er longu ógildað.
       title: Frem umsjónaratgerð á %{acct}
     account_moderation_notes:
       create: Skriva umsjónarviðmerking
@@ -46,6 +48,7 @@ fo:
         title: Broyt teldupostin hjá %{username}
       change_role:
         changed_msg: Leiklutur broyttur!
+        edit_roles: Stýr brúkaraleiklutir
         label: Broyt leiklut
         no_role: Eingin leiklutur
         title: Broyt leiklut hjá %{username}
@@ -602,6 +605,7 @@ fo:
         suspend_description_html: Kontan og alt innihald hjá kontuni gerast óatkomulig og við tíðini strikaði, og tað verður ógjørligt at samvirka við henni. Kann angrast innan 30 dagar. Lukkar allar rapporteringar av hesi kontuni.
       actions_description_html: Ger av hvør atgerð skal takast fyri at avgreiða hesa meldingina. Revsitiltøk móti meldaðu kontuni føra við sær, at ein teldupostfráboðan verður send teimum, undantikið tá <strong>Ruskpostur</strong> verður valdur.
       actions_description_remote_html: Tak avgerð um hvat skal gerast fyri at avgreiða hesa rapporteringina. Hetta fer einans at ávirka, hvussu <strong>tín</strong> ambætari samskiftir við hesa fjarkontuna og hvussu hann handfer tilfar frá henni.
+      actions_no_posts: Hendan fráboðanin hevur ongar viðkomandi postar at strika
       add_to_report: Legg meira afturat meldingini
       already_suspended_badges:
         local: Longu gjørt óvirkin á hesum ambætaranum
diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml
index 140bc9434..dd1f73c45 100644
--- a/config/locales/fr-CA.yml
+++ b/config/locales/fr-CA.yml
@@ -25,6 +25,8 @@ fr-CA:
   admin:
     account_actions:
       action: Effectuer l'action
+      already_silenced: Ce compte est déjà limité.
+      already_suspended: Ce compte est déjà suspendu.
       title: Effectuer une action de modération sur %{acct}
     account_moderation_notes:
       create: Laisser une remarque
@@ -46,6 +48,7 @@ fr-CA:
         title: Modifier le courriel pour %{username}
       change_role:
         changed_msg: Rôle modifié avec succès !
+        edit_roles: Gérer les rôles d'utilisateur·ices
         label: Modifier le rôle
         no_role: Aucun rôle
         title: Modifier le rôle de %{username}
@@ -188,9 +191,11 @@ fr-CA:
         create_user_role: Créer le rôle
         demote_user: Rétrograder l’utilisateur·ice
         destroy_announcement: Supprimer l’annonce
+        destroy_canonical_email_block: Supprimer le blocage de courriel
         destroy_custom_emoji: Supprimer des émojis personnalisés
         destroy_domain_allow: Supprimer le domaine autorisé
         destroy_domain_block: Supprimer le blocage de domaine
+        destroy_email_domain_block: Supprimer le blocage de domaine de courriel
         destroy_instance: Purge du domaine
         destroy_ip_block: Supprimer la règle IP
         destroy_status: Supprimer le message
@@ -236,6 +241,7 @@ fr-CA:
         confirm_user_html: "%{name} a confirmé l'adresse e-mail de l'utilisateur %{target}"
         create_account_warning_html: "%{name} a envoyé un avertissement à %{target}"
         create_announcement_html: "%{name} a créé une nouvelle annonce %{target}"
+        create_canonical_email_block_html: "%{name} a bloqué l'adresse email avec le hachage %{target}"
         create_custom_emoji_html: "%{name} a téléversé un nouvel émoji %{target}"
         create_domain_allow_html: "%{name} a autorisé la fédération avec le domaine %{target}"
         create_domain_block_html: "%{name} a bloqué le domaine %{target}"
@@ -245,6 +251,7 @@ fr-CA:
         create_user_role_html: "%{name} a créé le rôle %{target}"
         demote_user_html: "%{name} a rétrogradé l'utilisateur·rice %{target}"
         destroy_announcement_html: "%{name} a supprimé l'annonce %{target}"
+        destroy_canonical_email_block_html: "%{name} a débloqué l'adresse email avec le hachage %{target}"
         destroy_custom_emoji_html: "%{name} a supprimé l'émoji %{target}"
         destroy_domain_allow_html: "%{name} a rejeté la fédération avec le domaine %{target}"
         destroy_domain_block_html: "%{name} a débloqué le domaine %{target}"
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 5646877d2..7e30b517a 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -25,6 +25,8 @@ fr:
   admin:
     account_actions:
       action: Effectuer l'action
+      already_silenced: Ce compte est déjà limité.
+      already_suspended: Ce compte est déjà suspendu.
       title: Effectuer une action de modération sur %{acct}
     account_moderation_notes:
       create: Laisser une remarque
@@ -46,6 +48,7 @@ fr:
         title: Modifier l’adresse de courriel pour %{username}
       change_role:
         changed_msg: Rôle modifié avec succès !
+        edit_roles: Gérer les rôles d'utilisateur·ices
         label: Modifier le rôle
         no_role: Aucun rôle
         title: Modifier le rôle de %{username}
@@ -188,9 +191,11 @@ fr:
         create_user_role: Créer le rôle
         demote_user: Rétrograder l’utilisateur·ice
         destroy_announcement: Supprimer l’annonce
+        destroy_canonical_email_block: Supprimer le blocage de courriel
         destroy_custom_emoji: Supprimer des émojis personnalisés
         destroy_domain_allow: Supprimer le domaine autorisé
         destroy_domain_block: Supprimer le blocage de domaine
+        destroy_email_domain_block: Supprimer le blocage de domaine de courriel
         destroy_instance: Purge du domaine
         destroy_ip_block: Supprimer la règle IP
         destroy_status: Supprimer le message
@@ -236,6 +241,7 @@ fr:
         confirm_user_html: "%{name} a confirmé l'adresse e-mail de l'utilisateur %{target}"
         create_account_warning_html: "%{name} a envoyé un avertissement à %{target}"
         create_announcement_html: "%{name} a créé une nouvelle annonce %{target}"
+        create_canonical_email_block_html: "%{name} a bloqué l'adresse email avec le hachage %{target}"
         create_custom_emoji_html: "%{name} a téléversé un nouvel émoji %{target}"
         create_domain_allow_html: "%{name} a autorisé la fédération avec le domaine %{target}"
         create_domain_block_html: "%{name} a bloqué le domaine %{target}"
@@ -245,6 +251,7 @@ fr:
         create_user_role_html: "%{name} a créé le rôle %{target}"
         demote_user_html: "%{name} a rétrogradé l'utilisateur·rice %{target}"
         destroy_announcement_html: "%{name} a supprimé l'annonce %{target}"
+        destroy_canonical_email_block_html: "%{name} a débloqué l'adresse email avec le hachage %{target}"
         destroy_custom_emoji_html: "%{name} a supprimé l'émoji %{target}"
         destroy_domain_allow_html: "%{name} a rejeté la fédération avec le domaine %{target}"
         destroy_domain_block_html: "%{name} a débloqué le domaine %{target}"
diff --git a/config/locales/ga.yml b/config/locales/ga.yml
index ad9ee21d3..4ea9cef73 100644
--- a/config/locales/ga.yml
+++ b/config/locales/ga.yml
@@ -31,6 +31,8 @@ ga:
   admin:
     account_actions:
       action: Déan gníomh
+      already_silenced: Tá an cuntas seo ina thost cheana féin.
+      already_suspended: Tá an cuntas seo curtha ar fionraí cheana féin.
       title: Dean gníomh modhnóireachta ar %{acct}
     account_moderation_notes:
       create: Fág nóta
@@ -52,6 +54,7 @@ ga:
         title: Athraigh ríomhphost do %{username}
       change_role:
         changed_msg: Athraíodh ról go rathúil!
+        edit_roles: Bainistigh róil úsáideora
         label: Athraigh ról
         no_role: Gan ról
         title: Athraigh ról do %{username}
@@ -638,6 +641,7 @@ ga:
         suspend_description_html: Beidh an cuntas agus a bhfuil ann go léir dorochtana agus scriosfar iad ar deireadh, agus beidh sé dodhéanta idirghníomhú leis. Inchúlaithe laistigh de 30 lá. Dúnann sé gach tuairisc i gcoinne an chuntais seo.
       actions_description_html: Déan cinneadh ar an ngníomh atá le déanamh chun an tuarascáil seo a réiteach. Má dhéanann tú beart pionósach in aghaidh an chuntais tuairiscithe, seolfar fógra ríomhphoist chucu, ach amháin nuair a roghnaítear an chatagóir <strong>Turscar</strong>.
       actions_description_remote_html: Déan cinneadh ar an ngníomh atá le déanamh chun an tuarascáil seo a réiteach. Ní bheidh tionchar aige seo ach ar an gcaoi a ndéanann <strong>do fhreastalaí</strong> cumarsáid leis an gcianchuntas seo agus a láimhseálann sé a ábhar.
+      actions_no_posts: Níl aon phostáil ghaolmhar ag an tuarascáil seo le scriosadh
       add_to_report: Cuir tuilleadh leis an tuairisc
       already_suspended_badges:
         local: Ar fionraí cheana féin ar an bhfreastalaí seo
diff --git a/config/locales/gd.yml b/config/locales/gd.yml
index 2af647ab9..f824e3081 100644
--- a/config/locales/gd.yml
+++ b/config/locales/gd.yml
@@ -29,6 +29,8 @@ gd:
   admin:
     account_actions:
       action: Gabh an gnìomh
+      already_silenced: Chaidh an cunntas seo a chuingeachadh mu thràth.
+      already_suspended: Chaidh an cunntas seo a chur à rèim mu thràth.
       title: Gabh gnìomh maorsainneachd air %{acct}
     account_moderation_notes:
       create: Fàg nòta
@@ -50,6 +52,7 @@ gd:
         title: Atharraich am post-d airson %{username}
       change_role:
         changed_msg: Chaidh an dreuchd atharrachadh!
+        edit_roles: Stiùirich dreuchdan nan cleachdaichean
         label: Atharraich an dreuchd
         no_role: Gun dreuchd
         title: Atharraich an dreuchd aig %{username}
@@ -626,6 +629,7 @@ gd:
         suspend_description_html: Cha ghabh an cunntas seo agus an t-susbaint gu leòr aige inntrigeadh gus an dèid a sguabadh às air deireadh na sgeòil agus cha ghabh eadar-ghabhail a dhèanamh leis. Gabhaidh seo a neo-dhèanamh am broinn 30 latha. Dùinidh seo gach gearan mun chunntas seo.
       actions_description_html: Cuir romhad dè nì thu airson an gearan seo fhuasgladh. Ma chuireas tu peanas air a’ chunntas le gearan air, gheibh iad brath air a’ phost-d mura tagh thu an roinn-seòrsa <strong>Spama</strong>.
       actions_description_remote_html: Cuir romhad dè an gnìomh a ghabhas tu airson an gearan seo fhuasgladh. Cha bheir seo buaidh ach air mar a làimhsicheas am frithealaiche <strong>agadsa</strong> an cunntas cèin seo is mar a nì e conaltradh leis.
+      actions_no_posts: Chan eil post ri sguabadh às ris a’ ghearan seo
       add_to_report: Cuir barrachd ris a’ ghearan
       already_suspended_badges:
         local: Chaidh an cur à rèim air an fhrithealaiche seo mu thràth
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index 86010b066..de4840dda 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -25,6 +25,8 @@ gl:
   admin:
     account_actions:
       action: Executar acción
+      already_silenced: Esta conta xa está silenciada.
+      already_suspended: Esta conta xa está suspendida.
       title: Executar acción de moderación a %{acct}
     account_moderation_notes:
       create: Deixar nota
@@ -46,6 +48,7 @@ gl:
         title: Mudar email de %{username}
       change_role:
         changed_msg: Rol mudado correctamente!
+        edit_roles: Xestionar roles de usuarias
         label: Cambiar rol
         no_role: Sen rol
         title: Cambiar o rol de %{username}
@@ -602,6 +605,7 @@ gl:
         suspend_description_html: A conta e todo o seu contido non serán accesible e finalmente eliminaranse, será imposible interactuar con ela. A decisión é reversible durante 30 días. Isto pecha tódalas denuncias sobre esta conta.
       actions_description_html: Decide a acción a tomar para resolver esta denuncia. Se tomas accións punitivas contra a conta denunciada enviaraselle un correo, excepto se está indicada a categoría <strong>Spam</strong>.
       actions_description_remote_html: Decide a acción a tomar para resolver a denuncia. Isto só lle afecta ao xeito en que o <strong>teu</strong> servidor se comunica con esta conta remota e xestiona o seu contido.
+      actions_no_posts: Esta denuncia non ten publicacións asociadas para eliminar
       add_to_report: Engadir máis á denuncia
       already_suspended_badges:
         local: Xa está suspendida neste servidor
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 2bf138d9b..60fb96a12 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -25,6 +25,8 @@ hu:
   admin:
     account_actions:
       action: Művelet végrehajtása
+      already_silenced: Ezt a fiókot már elnémították.
+      already_suspended: Ezt a fiókot már felfüggesztették.
       title: 'Moderálási művelet végrehajtása ezen: %{acct}'
     account_moderation_notes:
       create: Megjegyzés hagyása
@@ -46,6 +48,7 @@ hu:
         title: "%{username} e-mail-címének megváltoztatása"
       change_role:
         changed_msg: A szerep sikeresen megváltoztatva!
+        edit_roles: Felhasználói szerepkörök kezelése
         label: Szerep megváltoztatása
         no_role: Nincs szerep
         title: "%{username} szerepének megváltoztatása"
@@ -602,6 +605,7 @@ hu:
         suspend_description_html: A fiók és minden tartalma elérhetetlenné válik és végül törlésre kerül. A fiókkal kapcsolatbalépni lehetetlen lesz. Ez a művelet 30 napig visszafordítható. A fiók ellen indított minden bejelentést lezárunk.
       actions_description_html: Döntsd el, mit csináljunk, hogy megoldjuk ezt a bejelentést. Ha valamilyen büntető intézkedést hozol a bejelentett fiók ellen, küldünk neki egy figyelmeztetést e-mailben, kivéve ha a <strong>Spam</strong> kategóriát választod.
       actions_description_remote_html: Döntsd el, mit tegyünk a bejelentés lezárásának érdekében. Ez csak azt befolyásolja, hogy a <strong>saját</strong> kiszolgálód hogyan kommunikál ezzel a távoli fiókkal és hogyan kezeli annak tartalmait.
+      actions_no_posts: Ennek a bejelentésnek nincs egyetlen törölhető, társított bejegyzése sem
       add_to_report: Továbbiak hozzáadása a bejelentéshez
       already_suspended_badges:
         local: Már felfüggesztették ezen a szerveren
diff --git a/config/locales/ia.yml b/config/locales/ia.yml
index 5596aacf2..683edbe7c 100644
--- a/config/locales/ia.yml
+++ b/config/locales/ia.yml
@@ -25,6 +25,8 @@ ia:
   admin:
     account_actions:
       action: Exequer action
+      already_silenced: Iste conto jam ha essite silentiate.
+      already_suspended: Iste conto jam ha essite suspendite.
       title: Exequer action de moderation sur %{acct}
     account_moderation_notes:
       create: Lassar un nota
@@ -46,6 +48,7 @@ ia:
         title: Cambiar e-mail pro %{username}
       change_role:
         changed_msg: Rolo cambiate con successo!
+        edit_roles: Gerer le regulas de usator
         label: Cambiar rolo
         no_role: Necun rolo
         title: Cambiar rolo pro %{username}
@@ -964,6 +967,7 @@ ia:
         used_by_over_week:
           one: Usate per un persona in le ultime septimana
           other: Usate per %{count} personas in le ultime septimana
+      title: Recommendationes e tendentias
       trending: In tendentia
     warning_presets:
       add_new: Adder nove
diff --git a/config/locales/is.yml b/config/locales/is.yml
index 2c73dbae7..0854d8812 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -25,6 +25,8 @@ is:
   admin:
     account_actions:
       action: Framkvæma aðgerð
+      already_silenced: Þessi aðgangur hefur þegar verið þaggaður.
+      already_suspended: Þessi aðgangur hefur þegar verið settur í frysti.
       title: Framkvæma umsjónaraðgerð á %{acct}
     account_moderation_notes:
       create: Skilja eftir minnispunkt
@@ -46,6 +48,7 @@ is:
         title: Breyta tölvupóstfangi fyrir %{username}
       change_role:
         changed_msg: Tókst að breyta hlutverki!
+        edit_roles: Sýsla með hlutverk notenda
         label: Breyta hlutverki
         no_role: Ekkert hlutverk
         title: Breyta hlutverki fyrir %{username}
@@ -602,6 +605,7 @@ is:
         suspend_description_html: Notandaaðgangurinn og allt efni á honum mun verða óaðgengilegt og á endanum eytt út og samskipti við aðganginn verða ekki möguleg. Hægt að afturkalla innan 30 daga. Lokar öllum kærum gagnvart þessum aðgangi.
       actions_description_html: Ákveddu til hvaða aðgerða eigi að taka til að leysa þessa kæru. Ef þú ákveður að refsa kærða notandaaðgangnum, verður viðkomandi send tilkynning í tölvupósti, nema ef flokkurinn <strong>Ruslpóstur</strong> sé valinn.
       actions_description_remote_html: Ákveddu til hvaða aðgerða eigi að taka til að leysa þessa kæru. Þetta mun aðeins hafa áhrif á hvernig <strong>netþjónninn þinn</strong> meðhöndlar þennan fjartengda aðgang og efnið á honum.
+      actions_no_posts: Þessi kæra er ekki með neinar tengdar færslur til að eyða
       add_to_report: Bæta fleiru í kæru
       already_suspended_badges:
         local: Þegar frystur á þessum netþjóni
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 54e122fd7..66a462e61 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -25,6 +25,8 @@ it:
   admin:
     account_actions:
       action: Esegui azione
+      already_silenced: Questo account è già stato silenziato.
+      already_suspended: Questo account è già stato sospeso.
       title: Esegui l'azione di moderazione su %{acct}
     account_moderation_notes:
       create: Lascia una nota
@@ -46,6 +48,7 @@ it:
         title: Cambia l'email per %{username}
       change_role:
         changed_msg: Ruolo modificato correttamente!
+        edit_roles: Gestisci i ruoli utente
         label: Cambia il ruolo
         no_role: Nessun ruolo
         title: Cambia il ruolo per %{username}
@@ -602,6 +605,7 @@ it:
         suspend_description_html: L'account e tutti i suoi contenuti saranno inaccessibili ed eventualmente cancellati, e interagire con esso sarà impossibile. Reversibile entro 30 giorni. Chiude tutte le segnalazioni contro questo account.
       actions_description_html: Decidi quale azione intraprendere per risolvere questa segnalazione. Se intraprendi un'azione punitiva nei confronti dell'account segnalato, gli verrà inviata una notifica via e-mail, tranne quando è selezionata la categoria <strong>Spam</strong>.
       actions_description_remote_html: Decide quali azioni intraprendere per risolvere la relazione. Questo influenzerà solo come <strong>il tuo</strong> server comunica con questo account remoto e ne gestisce il contenuto.
+      actions_no_posts: Questa segnalazione non ha alcun post associato da eliminare
       add_to_report: Aggiungi altro al report
       already_suspended_badges:
         local: Già sospeso su questo server
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index b54707c7c..af9173cfc 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -1428,6 +1428,7 @@ ja:
   media_attachments:
     validations:
       images_and_video: 既に画像が追加されているため、動画を追加することはできません
+      not_found: メディア (%{ids}) が存在しないか、すでに添付して投稿されています
       not_ready: ファイルのアップロードに失敗しました。しばらくしてからもう一度お試しください!
       too_many: 追加できるファイルは4つまでです
   migrations:
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index ef3775d4d..9bec26c45 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -23,6 +23,8 @@ ko:
   admin:
     account_actions:
       action: 조치 취하기
+      already_silenced: 이 계정은 이미 침묵되었습니다.
+      already_suspended: 이 계정은 이미 정지되었습니다.
       title: "%{acct} 계정에 중재 취하기"
     account_moderation_notes:
       create: 참고사항 남기기
@@ -44,6 +46,7 @@ ko:
         title: "%{username}의 이메일 바꾸기"
       change_role:
         changed_msg: 역할이 성공적으로 변경되었습니다!
+        edit_roles: 사용자 역할 관리
         label: 역할 변경
         no_role: 역할 없음
         title: "%{username}의 역할 변경"
@@ -592,6 +595,7 @@ ko:
         suspend_description_html: 이 계정과 이 계정의 콘텐츠들은 접근 불가능해지고 삭제될 것이며, 상호작용은 불가능해집니다. 30일 이내에 되돌릴 수 있습니다. 이 계정에 대한 모든 신고를 닫습니다.
       actions_description_html: 이 신고를 해결하기 위해 취해야 할 조치를 지정해주세요. 신고된 계정에 대해 처벌 조치를 취하면, <strong>스팸</strong> 카테고리가 선택된 경우를 제외하고 해당 계정으로 이메일 알림이 전송됩니다.
       actions_description_remote_html: 이 신고를 해결하기 위해 실행할 행동을 결정하세요. 이 결정은 이 원격 계정과 그 콘텐츠를 다루는 방식에 대해 <strong>이 서버</strong>에서만 영향을 끼칩니다
+      actions_no_posts: 이 신고는 삭제할 관련 게시물이 없습니다
       add_to_report: 신고에 더 추가하기
       already_suspended_badges:
         local: 이 서버에서 이미 정지되었습니다
diff --git a/config/locales/lad.yml b/config/locales/lad.yml
index 0de73fd27..5d60e6e9a 100644
--- a/config/locales/lad.yml
+++ b/config/locales/lad.yml
@@ -25,6 +25,7 @@ lad:
   admin:
     account_actions:
       action: Realiza aksion
+      already_suspended: Este kuento ya tiene sido suspendido.
       title: Modera %{acct}
     account_moderation_notes:
       create: Kriya nota
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 8c3b8e2e7..0fd71f52e 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -1036,7 +1036,7 @@ lt:
       generic: Nežinoma naršyklė
     current_session: Dabartinis seansas
     date: Data
-    description: "%{browser} ant %{platform}"
+    description: "„%{browser}“ per „%{platform}“"
     explanation: Čia rodomos web naršyklės prijungtos prie Jūsų Mastodon paskyros.
     ip: IP
     platforms:
@@ -1116,6 +1116,7 @@ lt:
       unlisted_long: matyti gali visi, bet nėra išvardyti į viešąsias laiko skales
   statuses_cleanup:
     enabled_hint: Automatiškai ištrina įrašus, kai jie pasiekia nustatytą amžiaus ribą, nebent jie atitinka vieną iš toliau nurodytų išimčių
+    interaction_exceptions_explanation: Atkreipk dėmesį, kad negarantuojama, jog įrašai nebus ištrinti, jei jų mėgstamumo ar pasidalinimo riba bus žemesnė, nors vieną kartą ji jau buvo viršyta.
     keep_polls_hint: Neištrina jokių tavo apklausų
     keep_self_bookmark: Laikyti įrašus, kuriuos pažymėjai
     keep_self_bookmark_hint: Neištrina tavo pačių įrašų, jei esi juos pažymėjęs (-usi)
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 46ed41177..725d3915c 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -25,6 +25,8 @@ nl:
   admin:
     account_actions:
       action: Actie uitvoeren
+      already_silenced: Dit account is al beperkt.
+      already_suspended: Dit account is al geschorst.
       title: Moderatiemaatregel tegen %{acct} nemen
     account_moderation_notes:
       create: Laat een opmerking achter
@@ -46,6 +48,7 @@ nl:
         title: E-mailadres wijzigen voor %{username}
       change_role:
         changed_msg: Rol succesvol veranderd!
+        edit_roles: Gebruikersrollen beheren
         label: Rol veranderen
         no_role: Geen rol
         title: Rol van %{username} veranderen
@@ -602,6 +605,7 @@ nl:
         suspend_description_html: Het account en de inhoud hiervan is niet meer toegankelijk, en het is ook niet meer mogelijk om ermee interactie te hebben. Uiteindelijk wordt het account volledig verwijderd. Dit is omkeerbaar binnen 30 dagen. Dit sluit alle rapporten tegen dit account af.
       actions_description_html: Beslis welke maatregel moet worden genomen om deze rapportage op te lossen. Wanneer je een (straf)maatregel tegen het gerapporteerde account neemt, krijgt het account een e-mailmelding, behalve wanneer de <strong>spam</strong>-categorie is gekozen.
       actions_description_remote_html: Beslis welke actie moet worden ondernomen om deze rapportage op te lossen. Dit is alleen van invloed op hoe <strong>jouw</strong> server met dit externe account communiceert en de inhoud ervan beheert.
+      actions_no_posts: Dit rapport heeft geen bijbehorende berichten om te verwijderen
       add_to_report: Meer aan de rapportage toevoegen
       already_suspended_badges:
         local: Al geschorst op deze server
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 95746bd10..2a1a0c8d7 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -29,6 +29,8 @@ pl:
   admin:
     account_actions:
       action: Wykonaj działanie
+      already_silenced: To konto zostało już wyciszone.
+      already_suspended: To konto zostało już zawieszone.
       title: Wykonaj działanie moderacyjne na %{acct}
     account_moderation_notes:
       create: Pozostaw notatkę
@@ -50,6 +52,7 @@ pl:
         title: Zmień adres e-mail dla %{username}
       change_role:
         changed_msg: Pomyślnie zmieniono rolę!
+        edit_roles: Zarządzaj rolami użytkowników
         label: Zmień rolę
         no_role: Brak roli
         title: Zmień rolę dla %{username}
@@ -626,6 +629,7 @@ pl:
         suspend_description_html: Konto i cała jego zawartość będą niedostępne i ostatecznie usunięte, a interakcja z nim będzie niemożliwa. Możliwość odwrócenia w ciągu 30 dni. Zamyka wszelkie zgłoszenia dotyczące tego konta.
       actions_description_html: Zdecyduj, jakie działania należy podjąć, aby rozstrzygnąć niniejsze zgłoszenie. Jeśli podejmiesz działania karne przeciwko zgłoszonemu kontowi, zostanie do nich wysłane powiadomienie e-mail, chyba że wybrano kategorię <strong>Spam</strong>.
       actions_description_remote_html: Zdecyduj, jakie działanie należy podjąć, aby rozwiązać to zgłoszenie. Będzie to miało wpływ jedynie na sposób, w jaki <strong>Twój</strong> serwer komunikuje się z tym kontem zdalnym i obsługuje jego zawartość.
+      actions_no_posts: Ten raport nie ma żadnych powiązanych wpisów do usunięcia
       add_to_report: Dodaj więcej do zgłoszenia
       already_suspended_badges:
         local: Już zawieszono na tym serwerze
diff --git a/config/locales/simple_form.an.yml b/config/locales/simple_form.an.yml
index 7119aadba..c59391f55 100644
--- a/config/locales/simple_form.an.yml
+++ b/config/locales/simple_form.an.yml
@@ -108,7 +108,6 @@ an:
         name: Nomás se puede cambiar lo cajón d'as letras, per eixemplo, pa que sía mas leyible
       user:
         chosen_languages: Quan se marca, nomás s'amostrarán las publicacions en os idiomas triaus en as linias de tiempo publicas
-        role: Lo rol controla qué permisos tiene la usuaria
       user_role:
         color: Color que s'utilizará pa lo rol a lo largo d'a interficie d'usuario, como RGB en formato hexadecimal
         highlighted: Esto fa que lo rol sía publicament visible
diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml
index 81be19a44..0a665fb78 100644
--- a/config/locales/simple_form.ar.yml
+++ b/config/locales/simple_form.ar.yml
@@ -130,7 +130,6 @@ ar:
         name: يمكنك فقط تغيير غلاف الحروف ، على سبيل المثال ، لجعلها أكثر قابلية للقراءة
       user:
         chosen_languages: إن تم اختيارها، فلن تظهر على الخيوط العامة إلّا الرسائل المنشورة في تلك اللغات
-        role: الوظيفة تتحكم في الصلاحيات التي يملكها المستخدم
       user_role:
         color: اللون الذي سيتم استخدامه للوظيفه في جميع وحدات واجهة المستخدم، كـ RGB بتنسيق hex
         highlighted: وهذا يجعل الوظيفه مرئيا علنا
diff --git a/config/locales/simple_form.be.yml b/config/locales/simple_form.be.yml
index a50db2298..db6a94f8e 100644
--- a/config/locales/simple_form.be.yml
+++ b/config/locales/simple_form.be.yml
@@ -130,7 +130,6 @@ be:
         name: Вы можаце змяняць толькі рэгістр літар, напрыклад для таго, каб падвысіць чытабельнасць
       user:
         chosen_languages: У публічных стужках будуць паказвацца допісы толькі на тых мовах, якія вы пазначыце
-        role: Гэтая роля кантралюе дазволы, якія мае карыстальнік
       user_role:
         color: Колер, які будзе выкарыстоўвацца для гэтай ролі па ўсім UI, у фармаце RGB ці hex
         highlighted: Гэта робіць ролю публічна бачнай
diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml
index 333ab25c8..a2cf8e422 100644
--- a/config/locales/simple_form.bg.yml
+++ b/config/locales/simple_form.bg.yml
@@ -130,7 +130,6 @@ bg:
         name: Можете да смените само употребата на големи/малки букви, например, за да е по-четимо
       user:
         chosen_languages: Само публикации на отметнатите езици ще се показват в публичните часови оси
-        role: Ролите управляват какви права има потребителят
       user_role:
         color: Цветът, използван за ролите в потребителския интерфейс, като RGB в шестнадесетичен формат
         highlighted: Това прави ролята обществено видима
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index 4daf65723..c628bebaa 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -130,7 +130,7 @@ ca:
         name: Només pots canviar la caixa de les lletres, per exemple, per fer-la més llegible
       user:
         chosen_languages: Quan estigui marcat, només es mostraran els tuts de les llengües seleccionades en les línies de temps públiques
-        role: El rol controla quines permissions té l'usuari
+        role: El rol controla quins permisos té l'usuari.
       user_role:
         color: Color que s'usarà per al rol a tota la interfície d'usuari, com a RGB en format hexadecimal
         highlighted: Això fa el rol visible públicament
diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml
index 2fe08e698..6242b1ca6 100644
--- a/config/locales/simple_form.cs.yml
+++ b/config/locales/simple_form.cs.yml
@@ -130,7 +130,6 @@ cs:
         name: Můžete měnit pouze velikost písmen, například kvůli lepší čitelnosti
       user:
         chosen_languages: Po zaškrtnutí budou ve veřejných časových osách zobrazeny pouze příspěvky ve zvolených jazycích
-        role: Role určuje, která oprávnění má uživatel
       user_role:
         color: Barva, která má být použita pro roli v celém UI, jako RGB v hex formátu
         highlighted: Toto roli učiní veřejně viditelnou
diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml
index 22545a776..56586ecc9 100644
--- a/config/locales/simple_form.cy.yml
+++ b/config/locales/simple_form.cy.yml
@@ -130,7 +130,7 @@ cy:
         name: Dim ond er mwyn ei gwneud yn fwy darllenadwy y gallwch chi newid y llythrennau, er enghraifft
       user:
         chosen_languages: Wedi eu dewis, dim ond tŵtiau yn yr ieithoedd hyn bydd yn cael eu harddangos mewn ffrydiau cyhoeddus
-        role: Mae'r rôl yn rheoli pa ganiatâd sydd gan y defnyddiwr
+        role: Mae'r rôl yn rheoli pa ganiatâd sydd gan y defnyddiwr.
       user_role:
         color: Lliw i'w ddefnyddio ar gyfer y rôl drwy'r UI, fel RGB mewn fformat hecs
         highlighted: Mae hyn yn gwneud y rôl yn weladwy i'r cyhoedd
diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml
index 5763885ac..8bacdc46c 100644
--- a/config/locales/simple_form.da.yml
+++ b/config/locales/simple_form.da.yml
@@ -130,7 +130,7 @@ da:
         name: Kun bogstavtyper (store/små) kan ændres, eksempelvis for at gøre det mere læsbart
       user:
         chosen_languages: Når markeret, vil kun indlæg på de valgte sprog fremgå på offentlige tidslinjer
-        role: Rollen styrer, hvilke tilladelser brugeren har
+        role: Rollen styrer, hvilke tilladelser brugeren er tildelt.
       user_role:
         color: Farven, i RGB hex-format, der skal bruges til rollen i hele UI'en
         highlighted: Dette gør rollen offentligt synlig
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index fb7bda948..ddb6621aa 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -130,7 +130,7 @@ de:
         name: Du kannst nur die Groß- und Kleinschreibung der Buchstaben ändern, um es z. B. lesbarer zu machen
       user:
         chosen_languages: Wenn du hier eine oder mehrere Sprachen auswählst, werden ausschließlich Beiträge in diesen Sprachen in deinen öffentlichen Timelines angezeigt
-        role: Die Rolle legt fest, welche Berechtigungen das Konto hat
+        role: Die Rolle bestimmt, welche Berechtigungen das Konto hat.
       user_role:
         color: Farbe, die für diese Rolle in der gesamten Benutzerschnittstelle verwendet wird, als RGB im Hexadezimalsystem
         highlighted: Dies macht die Rolle öffentlich im Profil sichtbar
diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml
index af93f65fe..88c1c0e18 100644
--- a/config/locales/simple_form.el.yml
+++ b/config/locales/simple_form.el.yml
@@ -111,7 +111,6 @@ el:
         name: Μπορείς να αλλάξεις μόνο το πλαίσιο των χαρακτήρων, για παράδειγμα για να γίνει περισσότερο ευανάγνωστο
       user:
         chosen_languages: Όταν ενεργοποιηθεί, στη δημόσια ροή θα εμφανίζονται τουτ μόνο από τις επιλεγμένες γλώσσες
-        role: Ο ρόλος ελέγχει ποια δικαιώματα έχει ο χρήστης
       user_role:
         color: Το χρώμα που θα χρησιμοποιηθεί για το ρόλο σε ολόκληρη τη διεπαφή, ως RGB σε δεκαεξαδική μορφή
         highlighted: Αυτό καθιστά το ρόλο δημόσια ορατό
diff --git a/config/locales/simple_form.en-GB.yml b/config/locales/simple_form.en-GB.yml
index 2f40feb5e..18776d67d 100644
--- a/config/locales/simple_form.en-GB.yml
+++ b/config/locales/simple_form.en-GB.yml
@@ -130,7 +130,6 @@ en-GB:
         name: You can only change the casing of the letters, for example, to make it more readable
       user:
         chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
-        role: The role controls which permissions the user has
       user_role:
         color: Color to be used for the role throughout the UI, as RGB in hex format
         highlighted: This makes the role publicly visible
diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml
index 3b51c1590..053816ef8 100644
--- a/config/locales/simple_form.eo.yml
+++ b/config/locales/simple_form.eo.yml
@@ -122,7 +122,6 @@ eo:
         name: Vi povas ŝanĝi nur la majuskladon de la literoj, ekzemple, por igi ĝin pli legebla
       user:
         chosen_languages: Kun tio markita nur mesaĝoj en elektitaj lingvoj aperos en publikaj tempolinioj
-        role: La rolregiloj kies permesojn la uzanto havas
       user_role:
         color: Koloro uzita por la rolo sur la UI, kun RGB-formato
         highlighted: Ĉi tio igi la rolon publike videbla
diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml
index e26af5b0c..70573c75f 100644
--- a/config/locales/simple_form.es-AR.yml
+++ b/config/locales/simple_form.es-AR.yml
@@ -130,7 +130,7 @@ es-AR:
         name: Sólo podés cambiar la capitalización de las letras, por ejemplo, para que sea más legible
       user:
         chosen_languages: Cuando estén marcados, sólo se mostrarán los mensajes en los idiomas seleccionados en las líneas temporales públicas
-        role: El rol controla qué permisos tiene el usuario
+        role: El rol controla qué permisos tiene el usuario.
       user_role:
         color: Color que se utilizará para el rol a lo largo de la interface de usuario, como RGB en formato hexadecimal
         highlighted: Esto hace que el rol sea públicamente visible
diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml
index 733a5c276..ff66bcb59 100644
--- a/config/locales/simple_form.es-MX.yml
+++ b/config/locales/simple_form.es-MX.yml
@@ -130,7 +130,6 @@ es-MX:
         name: Sólo se puede cambiar el cajón de las letras, por ejemplo, para que sea más legible
       user:
         chosen_languages: Cuando se marca, solo se mostrarán los toots en los idiomas seleccionados en los timelines públicos
-        role: El rol controla qué permisos tiene el usuario
       user_role:
         color: Color que se utilizará para el rol a lo largo de la interfaz de usuario, como RGB en formato hexadecimal
         highlighted: Esto hace que el rol sea públicamente visible
diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml
index 02f0562d2..143c5d807 100644
--- a/config/locales/simple_form.es.yml
+++ b/config/locales/simple_form.es.yml
@@ -130,7 +130,6 @@ es:
         name: Sólo se puede cambiar el cajón de las letras, por ejemplo, para que sea más legible
       user:
         chosen_languages: Cuando se marca, solo se mostrarán las publicaciones en los idiomas seleccionados en las líneas de tiempo públicas
-        role: El rol controla qué permisos tiene el usuario
       user_role:
         color: Color que se utilizará para el rol a lo largo de la interfaz de usuario, como RGB en formato hexadecimal
         highlighted: Esto hace que el rol sea públicamente visible
diff --git a/config/locales/simple_form.et.yml b/config/locales/simple_form.et.yml
index 9fd9e238a..4a9245682 100644
--- a/config/locales/simple_form.et.yml
+++ b/config/locales/simple_form.et.yml
@@ -130,7 +130,7 @@ et:
         name: Saad muuta ainult tähtede suurtähelisust, näiteks selleks, et muuta seda loetavamaks
       user:
         chosen_languages: Keelte valimisel näidatakse avalikel ajajoontel ainult neis keeltes postitusi
-        role: See roll kontrollib, millised õigused kasutajal on
+        role: Rollid määravad, millised õigused kasutajal on.
       user_role:
         color: Rolli tähistamise värvus üle kasutajaliidese, RGB 16nd-formaadis
         highlighted: Teeb rolli avalikult nähtavaks
diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml
index c7e2667ca..7647e187e 100644
--- a/config/locales/simple_form.eu.yml
+++ b/config/locales/simple_form.eu.yml
@@ -130,7 +130,6 @@ eu:
         name: Letrak maiuskula/minuskulara aldatu ditzakezu besterik ez, adibidez irakurterrazago egiteko
       user:
         chosen_languages: Markatzean, hautatutako hizkuntzetan dauden tutak besterik ez dira erakutsiko.
-        role: Rolak erabiltzaileak dituen baimenak kontrolatzen ditu
       user_role:
         color: Rolarentzat erabiltzaile interfazean erabiliko den kolorea, formatu hamaseitarreko RGB bezala
         highlighted: Honek rola publikoki ikusgai jartzen du
diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml
index 0610f7fce..bbb0523b1 100644
--- a/config/locales/simple_form.fa.yml
+++ b/config/locales/simple_form.fa.yml
@@ -188,6 +188,7 @@ fa:
         setting_default_privacy: حریم خصوصی نوشته‌ها
         setting_default_sensitive: همیشه تصاویر را به عنوان حساس علامت بزن
         setting_delete_modal: نمایش پیغام تأیید پیش از پاک کردن یک نوشته
+        setting_disable_hover_cards: از کار انداختن پیش‌نمایش نمایه هنگام رفتن رویش
         setting_disable_swiping: از کار انداختن حرکت‌های کشیدنی
         setting_display_media: نمایش عکس و ویدیو
         setting_display_media_default: پیش‌فرض
@@ -219,6 +220,7 @@ fa:
           warn: نهفتن با هشدار
       form_admin_settings:
         activity_api_enabled: انتشار آمار تجمیعی دربارهٔ فعالیت کاربران در API
+        app_icon: نقشک کاره
         backups_retention_period: دورهٔ نگه‌داری بایگانی کاربری
         bootstrap_timeline_accounts: پیشنهاد همیشگی این حساب‌ها به کاربران جدید
         closed_registrations_message: پیام سفارشی هنگام در دسترس نبودن ثبت‌نام‌ها
@@ -278,6 +280,7 @@ fa:
           patch: آگاهی برای به‌روز رسانی‌های رفع اشکال
         trending_tag: روند جدیدی نیازمند بازبینی است
       rule:
+        hint: اطّلاعات اضافی
         text: قانون
       settings:
         indexable: بودن صفحهٔ نمایه در نتیجه‌های جست‌وجو
@@ -286,6 +289,7 @@ fa:
         listable: اجازه به این برچسب برای ظاهر شدن در جست‌وجوها و پیشنهادها
         name: برچسب
         trendable: بگذارید که این برچسب در موضوعات پرطرفدار دیده شود
+        usable: اجازه به فرسته‌ها برای استفتاده از این برچسب به صورت محلی
       user:
         role: نقش
         time_zone: منطقهٔ زمانی
diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml
index c20ff9fa5..d3f062863 100644
--- a/config/locales/simple_form.fi.yml
+++ b/config/locales/simple_form.fi.yml
@@ -130,7 +130,7 @@ fi:
         name: Voit esimerkiksi vaihtaa suur- ja pienaakkosten kesken helppolukuistaaksesi tekstiäsi
       user:
         chosen_languages: Jos valitset kieliä oheisesta luettelosta, vain niidenkieliset julkaisut näkyvät sinulle julkisilla aikajanoilla
-        role: Rooli vaikuttaa käyttäjän käyttöoikeuksiin
+        role: Rooli määrää, millaiset käyttöoikeudet käyttäjällä on.
       user_role:
         color: Väri, jota käytetään roolille kaikkialla käyttöliittymässä, RGB-heksadesimaalimuodossa
         highlighted: Tämä tekee roolista julkisesti näkyvän
diff --git a/config/locales/simple_form.fo.yml b/config/locales/simple_form.fo.yml
index ecd840268..35e42f6c7 100644
--- a/config/locales/simple_form.fo.yml
+++ b/config/locales/simple_form.fo.yml
@@ -130,7 +130,7 @@ fo:
         name: Tú kanst einans broyta millum stórar og smáar stavir, til dømis fyri at gera tað meira lesiligt
       user:
         chosen_languages: Tá hetta er valt, verða einans postar í valdum málum vístir á almennum tíðarlinjum
-        role: Leikluturin stýrir, hvørji loyvir brúkarin hevur
+        role: Leikluturin stýrir hvørji rættindi, brúkarin hevur.
       user_role:
         color: Litur, sum leikluturin hevur í øllum brúkaramarkamótinum, sum RGB og upplýst sum sekstandatal
         highlighted: Hetta ger, at leikluturin er alment sjónligur
diff --git a/config/locales/simple_form.fr-CA.yml b/config/locales/simple_form.fr-CA.yml
index 640d7e344..1128335f1 100644
--- a/config/locales/simple_form.fr-CA.yml
+++ b/config/locales/simple_form.fr-CA.yml
@@ -128,7 +128,7 @@ fr-CA:
         name: Vous ne pouvez modifier que la casse des lettres, par exemple, pour le rendre plus lisible
       user:
         chosen_languages: Lorsque coché, seuls les messages dans les langues sélectionnées seront affichés sur les fils publics
-        role: Le rôle définit quelles autorisations a l'utilisateur⋅rice
+        role: Le rôle définit quelles autorisations a l'utilisateur⋅rice.
       user_role:
         color: Couleur à attribuer au rôle dans l'interface, au format hexadécimal RVB
         highlighted: Cela rend le rôle visible publiquement
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index 04d48573a..c06453298 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -128,7 +128,7 @@ fr:
         name: Vous ne pouvez modifier que la casse des lettres, par exemple, pour le rendre plus lisible
       user:
         chosen_languages: Lorsque coché, seuls les messages dans les langues sélectionnées seront affichés sur les fils publics
-        role: Le rôle définit quelles autorisations a l'utilisateur⋅rice
+        role: Le rôle définit quelles autorisations a l'utilisateur⋅rice.
       user_role:
         color: Couleur à attribuer au rôle dans l'interface, au format hexadécimal RVB
         highlighted: Cela rend le rôle visible publiquement
diff --git a/config/locales/simple_form.fy.yml b/config/locales/simple_form.fy.yml
index e6c1b0ee8..e7deca946 100644
--- a/config/locales/simple_form.fy.yml
+++ b/config/locales/simple_form.fy.yml
@@ -130,7 +130,6 @@ fy:
         name: Jo kinne elk wurd mei in haadletter begjinne, om sa bygelyks de tekst mear lêsber te meitsjen
       user:
         chosen_languages: Allinnich berjochten yn de selektearre talen wurde op de iepenbiere tiidline toand
-        role: De rol bepaalt hokker rjochten in brûker hat
       user_role:
         color: Kleur dy’t brûkt wurdt foar de rol yn de UI, as RGB yn heksadesimaal formaat
         highlighted: Dit makket de rol iepenbier sichtber
diff --git a/config/locales/simple_form.ga.yml b/config/locales/simple_form.ga.yml
index 15ea3094b..7c125b165 100644
--- a/config/locales/simple_form.ga.yml
+++ b/config/locales/simple_form.ga.yml
@@ -130,7 +130,7 @@ ga:
         name: Ní féidir leat ach cásáil na litreacha a athrú, mar shampla, chun é a dhéanamh níos inléite
       user:
         chosen_languages: Nuair a dhéantar iad a sheiceáil, ní thaispeánfar ach postálacha i dteangacha roghnaithe in amlínte poiblí
-        role: Rialaíonn an ról na ceadanna atá ag an úsáideoir
+        role: Rialaíonn an ról na ceadanna atá ag an úsáideoir.
       user_role:
         color: Dath le húsáid don ról ar fud an Chomhéadain, mar RGB i bhformáid heicsidheachúlach
         highlighted: Déanann sé seo an ról le feiceáil go poiblí
diff --git a/config/locales/simple_form.gd.yml b/config/locales/simple_form.gd.yml
index 4375feb49..9b6c156de 100644
--- a/config/locales/simple_form.gd.yml
+++ b/config/locales/simple_form.gd.yml
@@ -130,7 +130,7 @@ gd:
         name: Mar eisimpleir, ’s urrainn dhut measgachadh de litrichean mòra ’s beaga a chleachdadh ach an gabh a leughadh nas fhasa
       user:
         chosen_languages: Nuair a bhios cromag ris, cha nochd ach postaichean sna cànain a thagh thu air loidhnichean-ama poblach
-        role: Stiùiridh an dreuchd dè na ceadan a bhios aig cleachdaiche
+        role: Stiùiridh an dreuchd dè na ceadan a bhios aig cleachdaiche.
       user_role:
         color: An datha a bhios air an dreuchd air feadh na h-eadar-aghaidh, ’na RGB san fhòrmat sia-dheicheach
         highlighted: Le seo, chithear an dreuchd gu poblach
diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml
index aff8a95f0..abb30fa48 100644
--- a/config/locales/simple_form.gl.yml
+++ b/config/locales/simple_form.gl.yml
@@ -130,7 +130,7 @@ gl:
         name: Só podes cambiar maiús/minúsculas, por exemplo, mellorar a lexibilidade
       user:
         chosen_languages: Se ten marca, só as publicacións nos idiomas seleccionados serán mostrados en cronoloxías públicas
-        role: O control dos roles adxudicados ás usuarias
+        role: Os roles establecen os permisos que ten a usuaria.
       user_role:
         color: Cor que se usará para o rol a través da IU, como RGB en formato hex
         highlighted: Isto fai o rol publicamente visible
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index 9e2becc52..f595a3199 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -130,7 +130,6 @@ he:
         name: ניתן רק להחליף בין אותיות קטנות וגדולות, למשל כדי לשפר את הקריאות
       user:
         chosen_languages: אם פעיל, רק הודעות בשפות הנבחרות יוצגו לפידים הפומביים
-        role: התפקיד שולט על אילו הרשאות יש למשתמש
       user_role:
         color: צבע לתפקיד בממשק המשתמש, כ RGB בפורמט הקסדצימלי
         highlighted: מאפשר נראות ציבורית של התפקיד
diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml
index 512e13d3a..545fd4a8e 100644
--- a/config/locales/simple_form.hu.yml
+++ b/config/locales/simple_form.hu.yml
@@ -130,7 +130,7 @@ hu:
         name: Csak a kis/nagybetűséget változtathatod meg, pl. hogy olvashatóbb legyen
       user:
         chosen_languages: Ha aktív, csak a kiválasztott nyelvű bejegyzések jelennek majd meg a nyilvános idővonalon
-        role: A szerep szabályozza, hogy a felhasználó milyen jogosultságokkal rendelkezik
+        role: A szerep szabályozza, hogy a felhasználó milyen jogosultságokkal rendelkezik.
       user_role:
         color: A szerephez használandó szín mindenhol a felhasználói felületen, hexa RGB formátumban
         highlighted: Ez nyilvánosan láthatóvá teszi a szerepet
diff --git a/config/locales/simple_form.ia.yml b/config/locales/simple_form.ia.yml
index 76990527a..85fa74f1e 100644
--- a/config/locales/simple_form.ia.yml
+++ b/config/locales/simple_form.ia.yml
@@ -130,7 +130,6 @@ ia:
         name: Tu pote solmente cambiar le litteras inter majusculas e minusculas, per exemplo, pro render lo plus legibile
       user:
         chosen_languages: Si marcate, solo le messages in le linguas seligite sera monstrate in chronologias public
-        role: Le rolo controla que permissos ha le usator
       user_role:
         color: Color a esser usate pro le rolo in omne parte del UI, como RGB in formato hexadecimal
         highlighted: Iste rende le rolo publicamente visibile
diff --git a/config/locales/simple_form.id.yml b/config/locales/simple_form.id.yml
index 99f4372cc..0bc98874e 100644
--- a/config/locales/simple_form.id.yml
+++ b/config/locales/simple_form.id.yml
@@ -107,7 +107,6 @@ id:
         name: Anda hanya dapat mengubahnya ke huruf kecil/besar, misalnya, agar lebih mudah dibaca
       user:
         chosen_languages: Ketika dicentang, hanya toot dalam bahasa yang dipilih yang akan ditampilkan di linimasa publik
-        role: Peran mengatur izin apa yang dimiliki pengguna
       user_role:
         color: Warna yang digunakan untuk peran di antarmuka pengguna, sebagai RGB dalam format hex
         highlighted: Ini membuat peran terlihat secara publik
diff --git a/config/locales/simple_form.ie.yml b/config/locales/simple_form.ie.yml
index 0828139a4..771e34161 100644
--- a/config/locales/simple_form.ie.yml
+++ b/config/locales/simple_form.ie.yml
@@ -130,7 +130,6 @@ ie:
         name: Tu posse changear solmen li minu/majusculitá del lítteres, por exemple, por far it plu leibil
       user:
         chosen_languages: Quande selectet, solmen postas in ti lingues va esser monstrat in public témpor-lineas
-        role: Permissiones de usator decidet per su rol
       user_role:
         color: Color a usar por li rol tra li UI, quam RGB (rubi-verdi-blu) in formate hex
         highlighted: Va far li rol publicmen visibil
diff --git a/config/locales/simple_form.io.yml b/config/locales/simple_form.io.yml
index 4de7475b2..fe8243b0e 100644
--- a/config/locales/simple_form.io.yml
+++ b/config/locales/simple_form.io.yml
@@ -122,7 +122,6 @@ io:
         name: Vu povas nur chanjar literkaso, por exemplo, por kauzigar lu divenar plu lektebla
       user:
         chosen_languages: Kande marketigesis, nur posti en selektesis lingui montresos en publika tempolinei
-        role: Rolo dominacas permisi quon uzanto havas
       user_role:
         color: Koloro quo uzesas por rolo en tota UI, quale RGB kun hexformato
         highlighted: Co kauzigas rolo divenar publike videbla
diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml
index a7e2083f4..d615e391a 100644
--- a/config/locales/simple_form.is.yml
+++ b/config/locales/simple_form.is.yml
@@ -130,7 +130,7 @@ is:
         name: Þú getur aðeins breytt stafstöði mill há-/lágstafa, til gæmis til að gera þetta læsilegra
       user:
         chosen_languages: Þegar merkt er við þetta, birtast einungis færslur á völdum tungumálum á opinberum tímalínum
-        role: Hlutverk stýrir hvaða heimildir notandinn hefur
+        role: Hlutverk stýrir hvaða heimildir notandinn hefur.
       user_role:
         color: Litur sem notaður er fyrir hlutverkið allsstaðar í viðmótinu, sem RGB-gildi á hex-sniði
         highlighted: Þetta gerir hlutverk sýnilegt opinberlega
diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml
index ea175e2bf..1068b2f92 100644
--- a/config/locales/simple_form.it.yml
+++ b/config/locales/simple_form.it.yml
@@ -130,7 +130,7 @@ it:
         name: Puoi cambiare solo il minuscolo/maiuscolo delle lettere, ad esempio, per renderlo più leggibile
       user:
         chosen_languages: Quando una o più lingue sono contrassegnate, nelle timeline pubbliche vengono mostrati solo i toot nelle lingue selezionate
-        role: Il ruolo controlla quali permessi ha l'utente
+        role: Il ruolo controlla quali permessi ha l'utente.
       user_role:
         color: Colore da usare per il ruolo in tutta l'UI, come RGB in formato esadecimale
         highlighted: Rende il ruolo visibile
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index 8815af993..2c1bd6a08 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -130,7 +130,6 @@ ja:
         name: 視認性向上などのためにアルファベット大文字小文字の変更のみ行うことができます
       user:
         chosen_languages: 選択すると、選択した言語の投稿のみが公開タイムラインに表示されるようになります
-        role: このロールはユーザーが持つ権限を管理します
       user_role:
         color: UI 全体でロールの表示に使用される色(16進数RGB形式)
         highlighted: これによりロールが公開されます。
diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml
index 5fa3aee6c..fee07fa5e 100644
--- a/config/locales/simple_form.ko.yml
+++ b/config/locales/simple_form.ko.yml
@@ -130,7 +130,7 @@ ko:
         name: 읽기 쉽게하기 위한 글자의 대소문자만 변경할 수 있습니다.
       user:
         chosen_languages: 체크하면, 선택 된 언어로 작성된 게시물들만 공개 타임라인에 보여집니다
-        role: 역할은 사용자가 어떤 권한을 가지게 될 지 결정합니다
+        role: 역할은 사용자가 어떤 권한을 가지게 될 지 결정합니다.
       user_role:
         color: 색상은 사용자 인터페이스에서 역할을 나타내기 위해 사용되며, RGB 16진수 형식입니다
         highlighted: 이 역할이 공개적으로 보이도록 설정합니다
diff --git a/config/locales/simple_form.ku.yml b/config/locales/simple_form.ku.yml
index 96e047d93..db50384c7 100644
--- a/config/locales/simple_form.ku.yml
+++ b/config/locales/simple_form.ku.yml
@@ -106,7 +106,6 @@ ku:
         name: Tu dikarî tenê mezinahiya tîpan biguherînî bo mînak, da ku ew bêtir were xwendin
       user:
         chosen_languages: Dema were nîşankirin, tenê parvekirinên bi zimanên hilbijartî dê di rêzikên giştî de werin nîşandan
-        role: Rola kîjan mafdayînên bikarhêner heye kontrol dike
       user_role:
         color: Renga ku were bikaranîn ji bo rola li seranserê navrûya bikarhêneriyê, wekî RGB di forma hex
         highlighted: Ev rola xwe ji raya giştî re xuya dike
diff --git a/config/locales/simple_form.lad.yml b/config/locales/simple_form.lad.yml
index f7093ff73..2a381534b 100644
--- a/config/locales/simple_form.lad.yml
+++ b/config/locales/simple_form.lad.yml
@@ -125,7 +125,7 @@ lad:
         name: Solo se puede trokar la kapitalizasyon de las letras, por enshemplo, para ke sea mas meldable
       user:
         chosen_languages: Kuando se marka, solo se amostraran las publikasyones en las linguas eskojidas en las linyas de tiempo publikas
-        role: El rolo kontrola kualos permisos tiene el utilizador
+        role: El rolo kontrola kualos permisos tiene el utilizador.
       user_role:
         color: Color ke se utilizara para el rolo a lo largo de la enterfaz de utilizador, komo RGB en formato heksadesimal
         highlighted: Esto faze ke el rolo sea publikamente visible
diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml
index 02f036093..ecbf50138 100644
--- a/config/locales/simple_form.lt.yml
+++ b/config/locales/simple_form.lt.yml
@@ -101,7 +101,6 @@ lt:
         show_application: Neatsižvelgiant į tai, visada galėsi matyti, kuri programėlė paskelbė tavo įrašą.
       user:
         chosen_languages: Kai pažymėta, viešose laiko skalėse bus rodomi tik įrašai pasirinktomis kalbomis.
-        role: Vaidmuo valdo, kokius leidimus naudotojas (-a) turi
     labels:
       account:
         discoverable: Rekomenduoti profilį ir įrašus į atradimo algoritmus
diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml
index eedae998e..d96f57058 100644
--- a/config/locales/simple_form.lv.yml
+++ b/config/locales/simple_form.lv.yml
@@ -128,7 +128,6 @@ lv:
         name: Tu vari mainīt tikai burtu lielumu, piemēram, lai tie būtu vieglāk lasāmi
       user:
         chosen_languages: Ja ieķeksēts, publiskos laika grafikos tiks parādītas tikai ziņas noteiktajās valodās
-        role: Loma kontrolē, kādas atļaujas ir lietotājam
       user_role:
         color: Krāsa, kas jāizmanto lomai visā lietotāja saskarnē, kā RGB hex formātā
         highlighted: Tas padara lomu publiski redzamu
diff --git a/config/locales/simple_form.ms.yml b/config/locales/simple_form.ms.yml
index da00e2dc2..ecc3588d6 100644
--- a/config/locales/simple_form.ms.yml
+++ b/config/locales/simple_form.ms.yml
@@ -122,7 +122,6 @@ ms:
         name: Anda hanya boleh menukar selongsong huruf, sebagai contoh, untuk menjadikannya lebih mudah dibaca
       user:
         chosen_languages: Apabila disemak, hanya siaran dalam bahasa terpilih akan dipaparkan dalam garis masa awam
-        role: Peranan mengawal kebenaran yang dimiliki oleh pengguna
       user_role:
         color: Warna yang akan digunakan untuk peranan ini dalam seluruh UI, sebagai RGB dalam format hex
         highlighted: Ini menjadikan peranan ini dipaparkan secara umum
diff --git a/config/locales/simple_form.my.yml b/config/locales/simple_form.my.yml
index a44635edd..abcb11bda 100644
--- a/config/locales/simple_form.my.yml
+++ b/config/locales/simple_form.my.yml
@@ -122,7 +122,6 @@ my:
         name: ဥပမာအားဖြင့် စာလုံးများကို ပိုမိုဖတ်ရှုနိုင်စေရန်မှာ သင်သာ ပြောင်းလဲနိုင်သည်။
       user:
         chosen_languages: အမှန်ခြစ် ရွေးချယ်ထားသော ဘာသာစကားများဖြင့်သာ ပို့စ်များကို အများမြင်စာမျက်နှာတွင် ပြသပါမည်
-        role: အသုံးပြုသူ၏ ခွင့်ပြုချက်ကဏ္ဍကို ထိန်းချုပ်ထားသည်
       user_role:
         color: hex ပုံစံ RGB အဖြစ် UI တစ်လျှောက်လုံး အခန်းကဏ္ဍအတွက် အသုံးပြုရမည့်အရောင်
         highlighted: ယင်းက အခန်းကဏ္ဍကို အများမြင်အောင် ဖွင့်ပေးထားသည်။
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
index 91ee9bc3e..066f6c2ac 100644
--- a/config/locales/simple_form.nl.yml
+++ b/config/locales/simple_form.nl.yml
@@ -130,7 +130,7 @@ nl:
         name: Je kunt elk woord met een hoofdletter beginnen, om zo bijvoorbeeld de tekst leesbaarder te maken
       user:
         chosen_languages: Alleen berichten in de aangevinkte talen worden op de openbare tijdlijnen getoond
-        role: De rol bepaalt welke rechten een gebruiker heeft
+        role: De rol bepaalt welke rechten de gebruiker heeft.
       user_role:
         color: Kleur die gebruikt wordt voor de rol in de UI, als RGB in hexadecimale formaat
         highlighted: Dit maakt de rol openbaar zichtbaar
diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml
index c4d4438b9..500a41c8c 100644
--- a/config/locales/simple_form.nn.yml
+++ b/config/locales/simple_form.nn.yml
@@ -130,7 +130,6 @@ nn:
         name: Du kan berre endra bruken av store/små bokstavar, t. d. for å gjera det meir leseleg
       user:
         chosen_languages: Når merka vil berre tuta på dei valde språka synast på offentlege tidsliner
-        role: Rolla kontrollerer kva tilgangar brukaren har
       user_role:
         color: Fargen som skal nyttast for denne rolla i heile brukargrensesnittet, som RGB i hex-format
         highlighted: Dette gjer rolla synleg offentleg
diff --git a/config/locales/simple_form.no.yml b/config/locales/simple_form.no.yml
index 82de0adb7..73ba17cd4 100644
--- a/config/locales/simple_form.no.yml
+++ b/config/locales/simple_form.no.yml
@@ -124,7 +124,6 @@
         name: Du kan bare forandre bruken av store/små bokstaver, f.eks. for å gjøre det mer lesbart
       user:
         chosen_languages: Hvis noen av dem er valgt, vil kun innlegg i de valgte språkene bli vist i de offentlige tidslinjene
-        role: Rollekontroller som bestemmer rettigheter brukeren har
       user_role:
         color: Farge som skal brukes for rollen gjennom hele UI, som RGB i hex-format
         highlighted: Dette gjør rollen offentlig synlig
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index 1a9051b95..741916989 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -130,7 +130,6 @@ pl:
         name: Możesz zmieniać tylko wielkość liter, np. aby były bardziej widoczne
       user:
         chosen_languages: Jeżeli zaznaczone, tylko wpisy w wybranych językach będą wyświetlane na publicznych osiach czasu
-        role: Rola kontroluje uprawnienia użytkownika
       user_role:
         color: Kolor używany dla roli w całym interfejsie użytkownika, wyrażony jako RGB w formacie szesnastkowym
         highlighted: To sprawia, że rola jest widoczna publicznie
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index 65d388369..6b0bad0f0 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -130,7 +130,6 @@ pt-BR:
         name: Você pode mudar a capitalização das letras, por exemplo, para torná-la mais legível
       user:
         chosen_languages: Apenas as publicações dos idiomas selecionados serão exibidas nas linhas públicas
-        role: O cargo controla quais permissões o usuário tem
       user_role:
         color: Cor a ser usada para o cargo em toda a interface do usuário, como RGB no formato hexadecimal
         highlighted: Isso torna o cargo publicamente visível
diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml
index 25348f277..971773b2d 100644
--- a/config/locales/simple_form.pt-PT.yml
+++ b/config/locales/simple_form.pt-PT.yml
@@ -130,7 +130,6 @@ pt-PT:
         name: Só pode alterar a capitalização das letras, por exemplo, para torná-las mais legíveis
       user:
         chosen_languages: Quando selecionado, só serão mostradas nas cronologias públicas as publicações nos idiomas escolhidos
-        role: A função controla que permissões o utilizador tem
       user_role:
         color: Cor a ser utilizada para a função em toda a interface de utilizador, como RGB no formato hexadecimal
         highlighted: Isto torna a função visível publicamente
diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml
index 0f830174c..b41457e86 100644
--- a/config/locales/simple_form.ru.yml
+++ b/config/locales/simple_form.ru.yml
@@ -125,7 +125,6 @@ ru:
         name: Вы можете изменить только регистр букв чтобы, например, сделать тег более читаемым
       user:
         chosen_languages: Если выбрано, то в публичных лентах будут показаны только посты на выбранных языках.
-        role: Роль определяет, какие разрешения есть у пользователя
       user_role:
         color: Цвет, который будет использоваться для роли в интерфейсе (UI), как RGB в формате HEX
         highlighted: Это действие сделает роль публичной
diff --git a/config/locales/simple_form.sco.yml b/config/locales/simple_form.sco.yml
index 9fc6fd57d..2bc4f6a45 100644
--- a/config/locales/simple_form.sco.yml
+++ b/config/locales/simple_form.sco.yml
@@ -104,7 +104,6 @@ sco:
         name: Ye kin ainly chynge the case o the letters, fir example, fir tae mak it mair readable
       user:
         chosen_languages: Whan ticked, ainly posts in selectit leids wull be displayit in public timelines
-        role: The role controls whit permissions the uiser haes
       user_role:
         color: Colour tae be uised fir the role throuoot the UI, as RGB in hex format
         highlighted: This maks the role visible publicly
diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml
index bb16e56a0..d1ae553c8 100644
--- a/config/locales/simple_form.sl.yml
+++ b/config/locales/simple_form.sl.yml
@@ -130,7 +130,6 @@ sl:
         name: Spremenite lahko le npr. velikost črk (velike/male), da je bolj berljivo
       user:
         chosen_languages: Ko je označeno, bodo v javnih časovnicah prikazane samo objave v izbranih jezikih
-        role: Vloga nadzira, katere pravice ima uporabnik
       user_role:
         color: Barva, uporabljena za vlogo po celem up. vmesniku, podana v šestnajstiškem zapisu RGB
         highlighted: S tem je vloga javno vidna
diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml
index 8b00751d9..3d8655728 100644
--- a/config/locales/simple_form.sq.yml
+++ b/config/locales/simple_form.sq.yml
@@ -130,7 +130,7 @@ sq:
         name: Mund të ndryshoni shkronjat vetëm nga të mëdha në të vogla ose anasjelltas, për shembull, për t’i bërë më të lexueshme
       user:
         chosen_languages: Në iu vëntë shenjë, te rrjedha kohore publike do të shfaqen vetëm mesazhe në gjuhët e përzgjedhura
-        role: Roli kontrollon cilat leje ka përdoruesi
+        role: Roli kontrollon cilat leje ka përdoruesi.
       user_role:
         color: Ngjyrë për t’u përdorur për rolin nëpër UI, si RGB në format gjashtëmbëdhjetësh
         highlighted: Kjo e bën rolin të dukshëm publikisht
diff --git a/config/locales/simple_form.sr-Latn.yml b/config/locales/simple_form.sr-Latn.yml
index 527c5e0a1..1dec90134 100644
--- a/config/locales/simple_form.sr-Latn.yml
+++ b/config/locales/simple_form.sr-Latn.yml
@@ -130,7 +130,6 @@ sr-Latn:
         name: Mogu se samo promeniti mala slova u velika ili obrnuto, na primer, da bi bilo čitljivije
       user:
         chosen_languages: Kada je označeno, objave u izabranim jezicima će biti prikazane na javnoj vremenskoj liniji
-        role: Uloga kontroliše koje dozvole korisnik ima
       user_role:
         color: Boja koja će se koristiti za ulogu u celom korisničkom okruženju, kao RGB u heksadecimalnom formatu
         highlighted: Ovo čini ulogu javno vidljivom
diff --git a/config/locales/simple_form.sr.yml b/config/locales/simple_form.sr.yml
index d541c0b49..9566e0947 100644
--- a/config/locales/simple_form.sr.yml
+++ b/config/locales/simple_form.sr.yml
@@ -130,7 +130,6 @@ sr:
         name: Могу се само променити мала слова у велика или обрнуто, на пример, да би било читљивије
       user:
         chosen_languages: Када је означено, објаве у изабраним језицима ће бити приказане на јавној временској линији
-        role: Улога контролише које дозволе корисник има
       user_role:
         color: Боја која ће се користити за улогу у целом корисничком окружењу, као RGB у хексадецималном формату
         highlighted: Ово чини улогу јавно видљивом
diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml
index 329cabf94..297e96a2b 100644
--- a/config/locales/simple_form.sv.yml
+++ b/config/locales/simple_form.sv.yml
@@ -130,7 +130,7 @@ sv:
         name: Du kan bara ändra skriftläget av bokstäverna, till exempel, för att göra det mer läsbart
       user:
         chosen_languages: Vid aktivering visas bara inlägg på dina valda språk i offentliga tidslinjer
-        role: Rollen bestämmer vilka behörigheter användaren har
+        role: Rollen styr vilka behörigheter användaren har.
       user_role:
         color: Färgen som ska användas för rollen i användargränssnittet, som RGB i hex-format
         highlighted: Detta gör rollen synlig offentligt
diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml
index c62be89b2..89fb1675f 100644
--- a/config/locales/simple_form.tr.yml
+++ b/config/locales/simple_form.tr.yml
@@ -130,7 +130,6 @@ tr:
         name: Harflerin, örneğin daha okunabilir yapmak için, sadece büyük/küçük harf durumlarını değiştirebilirsiniz
       user:
         chosen_languages: İşaretlendiğinde, yalnızca seçilen dillerdeki gönderiler genel zaman çizelgelerinde görüntülenir
-        role: Rol, kullanıcıların sahip olduğu izinleri denetler
       user_role:
         color: Arayüz boyunca rol için kullanılacak olan renk, hex biçiminde RGB
         highlighted: Bu rolü herkese açık hale getirir
diff --git a/config/locales/simple_form.uk.yml b/config/locales/simple_form.uk.yml
index c6305598b..e2a1562b5 100644
--- a/config/locales/simple_form.uk.yml
+++ b/config/locales/simple_form.uk.yml
@@ -130,7 +130,7 @@ uk:
         name: Тут ви можете лише змінювати регістр літер, щоб підвищити читабельність
       user:
         chosen_languages: У глобальних стрічках будуть показані дописи тільки вибраними мовами
-        role: Роль визначає права користувача
+        role: Роль визначає, які права має користувач.
       user_role:
         color: Колір, який буде використовуватися для ролі у всьому інтерфейсі, як RGB у форматі hex
         highlighted: Це робить роль видимою всім
diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml
index 7954bb1e6..4e7c8b0a9 100644
--- a/config/locales/simple_form.vi.yml
+++ b/config/locales/simple_form.vi.yml
@@ -130,7 +130,7 @@ vi:
         name: Bạn có thể thay đổi cách viết hoa các chữ cái để giúp nó dễ đọc hơn
       user:
         chosen_languages: Chỉ hiển thị những tút viết bằng các ngôn ngữ sau
-        role: Vai trò kiểm soát những quyền mà người dùng có
+        role: Vai trò kiểm soát những quyền mà người dùng có.
       user_role:
         color: Màu được sử dụng cho vai trò trong toàn bộ giao diện người dùng, dưới dạng RGB ở định dạng hex
         highlighted: Vai trò sẽ hiển thị công khai
diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml
index 3f7b8782e..cb4341b55 100644
--- a/config/locales/simple_form.zh-CN.yml
+++ b/config/locales/simple_form.zh-CN.yml
@@ -4,12 +4,12 @@ zh-CN:
     hints:
       account:
         discoverable: 您的公开嘟文和个人资料可能会在 Mastodon 的多个位置展示,您的个人资料可能会被推荐给其他用户。
-        display_name: 您的全名或昵称。
+        display_name: 你的全名或昵称。
         fields: 你的主页、人称代词、年龄,以及任何你想要添加的内容。
         indexable: 您的公开嘟文会出现在 Mastodon 的搜索结果中。无论是否勾选,与您的嘟文有过交互的人都可能通过搜索找到它们。
-        note: '您可以提及 @其他人 或 #标签 。'
+        note: '你可以提及 @其他人 或 #标签 。'
         show_collections: 人们将能够浏览您的关注和追随者。您关注的人会看到您关注他们。
-        unlocked: 人们将能够在不请求批准的情况下关注您。如果您希望审核关注请求并选择接受或拒绝新的粉丝,请取消勾选此项。
+        unlocked: 人们将能够在不请求批准的情况下关注你。如果你希望审核关注请求并选择接受或拒绝新的粉丝,请取消勾选此项。
       account_alias:
         acct: 指定你想要迁移过来的原账号:用户名@站点域名
       account_migration:
@@ -78,7 +78,7 @@ zh-CN:
       form_admin_settings:
         activity_api_enabled: 本站每周的嘟文数、活跃用户数和新注册用户数
         app_icon: WEBP、PNG、GIF 或 JPG。使用自定义图标覆盖移动设备上的默认应用图标。
-        backups_retention_period: 用户可以生成其嘟文存档以供之后下载。当该值被设为正值时,这些存档将在指定的天数后自动从您的存储中删除。
+        backups_retention_period: 用户可以生成其嘟文存档以供之后下载。当该值被设为正值时,这些存档将在指定的天数后自动从你的存储中删除。
         bootstrap_timeline_accounts: 这些账号将在新用户关注推荐中置顶。
         closed_registrations_message: 在关闭注册时显示
         content_cache_retention_period: 来自其它实例的所有嘟文(包括转嘟与回复)都将在指定天数后被删除,不论本实例用户是否与这些嘟文产生过交互。这包括被本实例用户喜欢和收藏的嘟文。实例间用户的私下提及也将丢失并无法恢复。此设置针对的是特殊用途的实例,用于一般用途时会打破许多用户的期望。
@@ -125,12 +125,12 @@ zh-CN:
         webauthn: 如果是 USB 密钥,请确保将其插入,如有必要,请点击它。
       settings:
         indexable: 您的个人资料页面可能会出现在Google、Bing等搜索结果中。
-        show_application: 无论如何,您始终可以看到是哪个应用发布了您的嘟文。
+        show_application: 无论如何,你始终可以看到是哪个应用发布了你的嘟文。
       tag:
         name: 你只能改变字母的大小写,让它更易读
       user:
         chosen_languages: 仅选中语言的嘟文会出现在公共时间轴上(全不选则显示所有语言的嘟文)
-        role: 角色用于控制用户拥有的权限
+        role: 角色用于控制用户拥有的权限。
       user_role:
         color: 在界面各处用于标记该角色的颜色,以十六进制 RGB 格式表示
         highlighted: 使角色公开可见
@@ -143,7 +143,7 @@ zh-CN:
         url: 事件将被发往的目的地
     labels:
       account:
-        discoverable: 在发现算法中展示您的个人资料和嘟文
+        discoverable: 在发现算法中展示你的个人资料和嘟文
         fields:
           name: 标签
           value: 内容
@@ -309,7 +309,7 @@ zh-CN:
         text: 规则
       settings:
         indexable: 允许搜索引擎索引个人资料页面
-        show_application: 显示您发嘟所用的应用
+        show_application: 显示你发嘟所用的应用
       tag:
         listable: 允许这个话题标签在用户目录中显示
         name: 话题标签
diff --git a/config/locales/simple_form.zh-HK.yml b/config/locales/simple_form.zh-HK.yml
index 6a7cd5a24..dd134a58f 100644
--- a/config/locales/simple_form.zh-HK.yml
+++ b/config/locales/simple_form.zh-HK.yml
@@ -128,7 +128,6 @@ zh-HK:
         name: 你只能變更大小寫(以使其更易讀)。
       user:
         chosen_languages: 只有被選擇的語言會在公開時間軸內顯示
-        role: 角色控制使用者擁有的權限
       user_role:
         color: 介面各處用於角色的顏色,是以十六進制 RGB 格式表示
         highlighted: 這使該角色公開可見
diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml
index 05692243a..8b4c44002 100644
--- a/config/locales/simple_form.zh-TW.yml
+++ b/config/locales/simple_form.zh-TW.yml
@@ -53,7 +53,7 @@ zh-TW:
         password: 使用至少 8 個字元
         phrase: 無論是嘟文的本文或是內容警告都會被過濾
         scopes: 允許使應用程式存取的 API。 若您選擇最高階範圍,則無須選擇個別項目。
-        setting_aggregate_reblogs: 請勿顯示最近已被轉嘟之嘟文的最新轉嘟(只影響最新收到的嘟文)
+        setting_aggregate_reblogs: 不顯示最近已被轉嘟之嘟文的最新轉嘟(只影響最新收到的嘟文)
         setting_always_send_emails: 一般情況下若您活躍使用 Mastodon ,我們不會寄送電子郵件通知
         setting_default_sensitive: 敏感內容媒體預設隱藏,且按一下即可重新顯示
         setting_display_media_default: 隱藏標為敏感內容的媒體
@@ -130,7 +130,7 @@ zh-TW:
         name: 您只能變更大小寫,例如,以使其更易讀。
       user:
         chosen_languages: 當選取時,只有選取語言之嘟文會於公開時間軸中顯示
-        role: 角色控制使用者有哪些權限
+        role: 角色控制使用者有哪些權限。
       user_role:
         color: 於整個使用者介面中用於角色的顏色,十六進位格式的 RGB
         highlighted: 這將使角色公開可見
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index b3d273024..0f43f4398 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -25,6 +25,8 @@ sq:
   admin:
     account_actions:
       action: Kryeje veprimin
+      already_silenced: Kjo llogari është heshtuar tashmë.
+      already_suspended: Kjo llogari është pezulluar tashmë.
       title: Kryeni veprim moderimi te %{acct}
     account_moderation_notes:
       create: Lini një shënim
@@ -46,6 +48,7 @@ sq:
         title: Ndrysho email-in për %{username}
       change_role:
         changed_msg: Roli u ndryshua me sukses!
+        edit_roles: Administroni role përdoruesish
         label: Ndryshoni rol
         no_role: Pa rol
         title: Ndryshoni rolin për %{username}
@@ -600,6 +603,7 @@ sq:
         suspend_description_html: Llogaria dhe krejt lënda e saj s’do të jenë të përdorshme dhe, së fundi, do të fshihen dhe ndërveprimi me te do të jetë i pamundur. E prapakthyeshme brenda 30 ditësh. Mbyll krejt raportimet kundër kësaj llogarie.
       actions_description_html: Vendosni cili veprim të kryhet për të zgjidhur këtë raportim. Nëse ndërmerrni një veprim ndëshkues kundër llogarisë së raportuar, atyre do t’u dërgohet një njoftim me email, hiq rastin kur përzgjidhet kategoria <strong>I padëshiruar</strong>.
       actions_description_remote_html: Vendosni cili veprim të ndërmerret për zgjidhjen e këtij raportimi. Kjo do të prekë vetëm mënyrën se si shërbyesi <strong>juaj</strong> komunikon me këtë llogari të largët dhe se si e trajtojnë lëndën e saj.
+      actions_no_posts: Ky raportim s’ka ndonjë postim të përshoqëruar, për fshirje
       add_to_report: Shtoni më tepër te raportimi
       already_suspended_badges:
         local: Tashmë i pezulluar në këtë shërbyes
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index e47f506e6..bcf1e3b81 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -46,6 +46,7 @@ sv:
         title: Byt e-postadress för %{username}
       change_role:
         changed_msg: Rollen har ändrats!
+        edit_roles: Hantera användarroller
         label: Ändra roll
         no_role: Ingen roll
         title: Ändra roll för %{username}
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 7f90c3bc9..f11910211 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -23,6 +23,8 @@ th:
   admin:
     account_actions:
       action: ทำการกระทำ
+      already_silenced: มีการทำให้บัญชีนี้เงียบไปแล้ว
+      already_suspended: มีการระงับบัญชีนี้ไปแล้ว
       title: ทำการกระทำการกลั่นกรองต่อ %{acct}
     account_moderation_notes:
       create: เขียนหมายเหตุ
@@ -44,6 +46,7 @@ th:
         title: เปลี่ยนอีเมลสำหรับ %{username}
       change_role:
         changed_msg: เปลี่ยนบทบาทสำเร็จ!
+        edit_roles: จัดการบทบาทผู้ใช้
         label: เปลี่ยนบทบาท
         no_role: ไม่มีบทบาท
         title: เปลี่ยนบทบาทสำหรับ %{username}
@@ -590,6 +593,7 @@ th:
         suspend_description_html: บัญชีและเนื้อหาของบัญชีทั้งหมดจะเข้าถึงไม่ได้และได้รับการลบในที่สุด และการโต้ตอบกับบัญชีจะเป็นไปไม่ได้ แปลงกลับได้ภายใน 30 วัน ปิดรายงานต่อบัญชีนี้ทั้งหมด
       actions_description_html: ตัดสินใจว่าการกระทำใดที่จะใช้เพื่อแก้ปัญหารายงานนี้ หากคุณใช้การกระทำที่เป็นการลงโทษต่อบัญชีที่รายงาน จะส่งการแจ้งเตือนอีเมลถึงเขา ยกเว้นเมื่อมีการเลือกหมวดหมู่ <strong>สแปม</strong>
       actions_description_remote_html: ตัดสินใจว่าการกระทำใดที่จะใช้เพื่อแก้ปัญหารายงานนี้ นี่จะมีผลต่อวิธีที่เซิร์ฟเวอร์ <strong>ของคุณ</strong> สื่อสารกับบัญชีระยะไกลนี้และจัดการเนื้อหาของบัญชีเท่านั้น
+      actions_no_posts: รายงานนี้ไม่มีโพสต์ที่เกี่ยวข้องใด ๆ ให้ลบ
       add_to_report: เพิ่มข้อมูลเพิ่มเติมไปยังรายงาน
       already_suspended_badges:
         local: ระงับในเซิร์ฟเวอร์นี้อยู่แล้ว
@@ -1428,6 +1432,7 @@ th:
   media_attachments:
     validations:
       images_and_video: ไม่สามารถแนบวิดีโอกับโพสต์ที่มีภาพอยู่แล้ว
+      not_found: ไม่พบสื่อ %{ids} หรือได้แนบกับโพสต์อื่นไปแล้ว
       not_ready: ไม่สามารถแนบไฟล์ที่ยังประมวลผลไม่เสร็จ ลองอีกครั้งในอีกสักครู่!
       too_many: ไม่สามารถแนบมากกว่า 4 ไฟล์
   migrations:
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 544e2671c..261c87cd7 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -29,6 +29,7 @@ uk:
   admin:
     account_actions:
       action: Виконати дію
+      already_suspended: Цей обліковий запис вже було призупинено.
       title: Здійснити модераційну дію над %{acct}
     account_moderation_notes:
       create: Залишити нотатку
@@ -50,6 +51,7 @@ uk:
         title: Змінити адресу електронної пошти для %{username}
       change_role:
         changed_msg: Роль успішно змінено!
+        edit_roles: Керування ролями
         label: Змінити роль
         no_role: Немає ролі
         title: Змінити роль для %{username}
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index 2f607d1ec..975df3024 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -23,6 +23,8 @@ vi:
   admin:
     account_actions:
       action: Thực hiện hành động
+      already_silenced: Tài khoản này đã bị hạn chế.
+      already_suspended: Tài khoản này đã bị vô hiệu hóa.
       title: Áp đặt kiểm duyệt với %{acct}
     account_moderation_notes:
       create: Thêm lưu ý
@@ -44,6 +46,7 @@ vi:
         title: Thay đổi email cho %{username}
       change_role:
         changed_msg: Vai trò đã thay đổi thành công!
+        edit_roles: Quản lý vai trò người dùng
         label: Đổi vai trò
         no_role: Chưa có vai trò
         title: Thay đổi vai trò %{username}
@@ -590,6 +593,7 @@ vi:
         suspend_description_html: Tài khoản và tất cả nội dung của nó sẽ không thể truy cập được và cuối cùng sẽ bị xóa, đồng thời không thể tương tác với tài khoản đó. Có thể đảo ngược trong vòng 30 ngày. Đóng tất cả các báo cáo đối với tài khoản này.
       actions_description_html: Nếu áp đặt kiểm duyệt, một email thông báo sẽ được gửi cho người này, ngoại trừ <strong>Spam</strong>.
       actions_description_remote_html: Chọn hành động cần thực hiện để xử lý báo cáo này. Điều này sẽ chỉ ảnh hưởng đến cách máy chủ <strong>của bạn</strong> giao tiếp với tài khoản này và xử lý nội dung của nó.
+      actions_no_posts: Báo cáo này không có tút liên quan để xóa
       add_to_report: Bổ sung báo cáo
       already_suspended_badges:
         local: Đã vô hiệu hóa trên máy chủ này
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index a8d401c7b..6b399d349 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -23,6 +23,8 @@ zh-CN:
   admin:
     account_actions:
       action: 执行操作
+      already_silenced: 此帐户已受限。
+      already_suspended: 此帐户已被封禁。
       title: 在 %{acct} 上执行管理操作
     account_moderation_notes:
       create: 新建记录
@@ -44,6 +46,7 @@ zh-CN:
         title: 更改 %{username} 的电子邮箱
       change_role:
         changed_msg: 已成功更改角色!
+        edit_roles: 管理用户角色
         label: 更改角色
         no_role: 没有角色
         title: 更改 %{username} 的角色
@@ -383,9 +386,9 @@ zh-CN:
         cancel: 取消
         confirm: 封禁
         permanent_action: 撤销暂停不会恢复任何数据或关系。
-        preamble_html: 您将要暂停 <strong>%{domain}</strong> 及其子域。
-        remove_all_data: 这将从您的实例上删除此域名下账户的所有内容、媒体和个人资料数据。
-        stop_communication: 您的实例将停止与这些实例的通信。
+        preamble_html: 你将要暂停 <strong>%{domain}</strong> 及其子域。
+        remove_all_data: 这将从你的实例上删除此域名下账户的所有内容、媒体和个人资料数据。
+        stop_communication: 你的实例将停止与这些实例的通信。
         title: 确认对 %{domain} 的封锁
         undo_relationships: 这将解除你的实例与这些实例上账户之间的任何关注。
       created_msg: 正在进行域名屏蔽
@@ -590,6 +593,7 @@ zh-CN:
         suspend_description_html: 该帐户及其所有内容将无法访问并最终被删除,且无法与该帐户进行互动。 在 30 天内可随时撤销。关闭针对此帐户的所有举报。
       actions_description_html: 决定采取何种措施处理此举报。如果对被举报账号采取惩罚性措施,将向其发送一封电子邮件通知。但若选中<strong>垃圾信息</strong>类别则不会发送通知。
       actions_description_remote_html: 决定采取何种行动来解决此举报。 这只会影响<strong>您的</strong>服务器如何与该远程帐户的通信并处理其内容。
+      actions_no_posts: 该举报没有相关嘟文可供删除
       add_to_report: 增加更多举报内容
       already_suspended_badges:
         local: 已经在此服务器上暂停
@@ -609,7 +613,7 @@ zh-CN:
       created_at: 举报时间
       delete_and_resolve: 删除嘟文
       forwarded: 已转发
-      forwarded_replies_explanation: 该举报来自外站用户,涉及外站内容。之所以转发给您,是因为被举报的内容是对您站点一位用户的回复。
+      forwarded_replies_explanation: 该举报来自外站用户,涉及外站内容。之所以转发给你,是因为被举报的内容是对你站点一位用户的回复。
       forwarded_to: 转发举报至 %{domain}
       mark_as_resolved: 标记为已处理
       mark_as_sensitive: 标记为敏感内容
@@ -764,7 +768,7 @@ zh-CN:
         disabled: 不对任何人
         users: 对本地已登录用户
       registrations:
-        moderation_recommandation: 在向所有人开放注册之前,请确保您拥有一个人手足够且反应迅速的管理团队!
+        moderation_recommandation: 在向所有人开放注册之前,请确保你拥有一个人手足够且反应迅速的管理团队!
         preamble: 控制谁可以在你的服务器上创建账号。
         title: 注册
       registrations_mode:
@@ -772,7 +776,7 @@ zh-CN:
           approved: 注册时需要批准
           none: 关闭注册
           open: 开放注册
-        warning_hint: 我们建议使用“注册必须经过批准”,除非您确信您的管理团队能够及时处理骚扰和恶意注册。
+        warning_hint: 我们建议使用“注册必须经过批准”,除非你确信你的管理团队能够及时处理骚扰和恶意注册。
       security:
         authorized_fetch: 需要跨站认证
         authorized_fetch_hint: 要求外站请求通过验证能够使用户级别与服务器级别的封锁更为严格。然而,这将带来额外的性能负担、减少回复触达范围、并可能导致与一些联邦宇宙服务的兼容性问题。此外,这并不能阻止他人针对性地获取公开嘟文与账户。
@@ -784,7 +788,7 @@ zh-CN:
       destroyed_msg: 站点上传的文件已经成功删除!
     software_updates:
       critical_update: 紧急 — 请尽快更新
-      description: 建议你及时更新Mastodon实例,以便获得最新修复和功能。此外,为避免安全问题,有时及时更新Mastodon是至关重要的。出于这些原因,Mastodon每30分钟检查一次更新,并根据您的邮件通知偏好向您发送通知。
+      description: 建议你及时更新Mastodon实例,以便获得最新修复和功能。此外,为避免安全问题,有时及时更新Mastodon是至关重要的。出于这些原因,Mastodon每30分钟检查一次更新,并根据你的邮件通知偏好向你发送通知。
       documentation_link: 详细了解
       release_notes: 发行说明
       title: 可用的更新
@@ -837,17 +841,17 @@ zh-CN:
       elasticsearch_health_red:
         message_html: Elasticsearch 集群状态不健康(红色),搜索功能不可用
       elasticsearch_health_yellow:
-        message_html: Elasticsearch 集群不健康(黄色状态),您可能想要调查原因
+        message_html: Elasticsearch 集群不健康(黄色状态),你可能想要调查原因
       elasticsearch_index_mismatch:
         message_html: Elasticsearch索引映射已过时。请运行<code>tootctl search deploy --only=%{value}</code>。
       elasticsearch_preset:
         action: 查看文档
-        message_html: 您的Elasticsearch集群有多个节点,但Mastodon未配置好使用它们。
+        message_html: 你的Elasticsearch集群有多个节点,但Mastodon未配置好使用它们。
       elasticsearch_preset_single_node:
         action: 查看文档
-        message_html: 您的Elasticsearch集群只有一个节点,<code>ES_PRESET</code>应该设置为<code>single_node_cluster</code>。
+        message_html: 你的Elasticsearch集群只有一个节点,<code>ES_PRESET</code>应该设置为<code>single_node_cluster</code>。
       elasticsearch_reset_chewy:
-        message_html: 您的Elasticsearch系统索引已过时,可能是由于设置更改导致的。请运行<code>tootctl search deploy --reset-chewy</code>命令来更新它。
+        message_html: 你的Elasticsearch系统索引已过时,可能是由于设置更改导致的。请运行<code>tootctl search deploy --reset-chewy</code>命令来更新它。
       elasticsearch_running_check:
         message_html: 无法连接到 Elasticsearch。请检查它是否正在运行,或禁用全文搜索
       elasticsearch_version_check:
@@ -989,7 +993,7 @@ zh-CN:
       webhook: Webhook
   admin_mailer:
     auto_close_registrations:
-      body: 由于近期缺乏管理员活动, %{instance} 上的注册已自动切换为需要手动审核,以防止 %{instance} 被潜在的不良行为者用作平台。您可以随时将其切换回开放注册。
+      body: 由于近期缺乏管理员活动, %{instance} 上的注册已自动切换为需要手动审核,以防止 %{instance} 被潜在的不良行为者用作平台。你可以随时将其切换回开放注册。
       subject: "%{instance} 的注册已自动切换为需要批准"
     new_appeal:
       actions:
@@ -1004,7 +1008,7 @@ zh-CN:
       next_steps: 你可以批准此申诉并撤销该审核结果,也可以忽略此申诉。
       subject: "%{username} 对 %{instance} 的审核结果提出了申诉"
     new_critical_software_updates:
-      body: 新的紧急更新版本Mastodon已经发布,您可能希望尽快更新!
+      body: 新的紧急更新版本Mastodon已经发布,你可能希望尽快更新!
       subject: 适用于 %{instance} 的Mastodon紧急更新已经可用。
     new_pending_account:
       body: 新账户的详细信息如下。你可以批准或拒绝此申请。
@@ -1014,7 +1018,7 @@ zh-CN:
       body_remote: 来自 %{domain} 的用户举报了用户 %{target}
       subject: 来自 %{instance} 的用户举报(#%{id})
     new_software_updates:
-      body: 新的 Mastodon 版本已发布,您可能想要更新!
+      body: 新的 Mastodon 版本已发布,你可能想要更新!
       subject: 适用于 %{instance} 的Mastodon版本更新已经可用!
     new_trends:
       body: 以下项目需要审核才能公开显示:
@@ -1062,18 +1066,18 @@ zh-CN:
   auth:
     apply_for_account: 申请账号
     captcha_confirmation:
-      help_html: 如果您在输入验证码时遇到问题,可以通过%{email} 与我们联系,我们将为您提供帮助。
-      hint_html: 只剩最后一件事了!我们需要确认您是一个人类(这样我们才能阻止恶意访问!)。请输入下面的验证码,然后点击“继续”。
+      help_html: 如果你在输入验证码时遇到问题,可以通过%{email} 与我们联系,我们将为你提供帮助。
+      hint_html: 只剩最后一件事了!我们需要确认你是一个人类(这样我们才能阻止恶意访问!)。请输入下面的验证码,然后点击“继续”。
       title: 安全检查
     confirmations:
       awaiting_review: 你的邮箱地址已确认!%{domain} 的工作人员正在审核你的注册信息。如果他们批准了你的账户,你将收到一封邮件通知!
-      awaiting_review_title: 您的注册申请正在审核中
+      awaiting_review_title: 你的注册申请正在审核中
       clicking_this_link: 点击此链接
       login_link: 登录
-      proceed_to_login_html: 现在您可以继续前往 %{login_link} 。
-      redirect_to_app_html: 您应该已被重定向到 <strong>%{app_name}</strong> 应用程序。如果没有,请尝试 %{clicking_this_link} 或手动返回应用程序。
-      registration_complete: 您在 %{domain} 上的注册现已完成!
-      welcome_title: 欢迎您,%{name}!
+      proceed_to_login_html: 现在你可以继续前往 %{login_link} 。
+      redirect_to_app_html: 你应该已被重定向到 <strong>%{app_name}</strong> 应用程序。如果没有,请尝试 %{clicking_this_link} 或手动返回应用程序。
+      registration_complete: 你在 %{domain} 上的注册现已完成!
+      welcome_title: 欢迎你,%{name}!
       wrong_email_hint: 如果该邮箱地址不正确,你可以在账户设置中进行更改。
     delete_account: 删除帐户
     delete_account_html: 如果你想删除你的帐户,请<a href="%{path}">点击这里继续</a>。你需要确认你的操作。
@@ -1111,7 +1115,7 @@ zh-CN:
       back: 返回
       invited_by: 你可以加入%{domain},这是由于你收到了他人的邀请,邀请来自:
       preamble: 这些由 %{domain} 监察员设置和执行。
-      preamble_invited: 在您继续之前,请考虑 %{domain} 的管理员设定的基本规则。
+      preamble_invited: 在你继续之前,请考虑 %{domain} 的管理员设定的基本规则。
       title: 一些基本规则。
       title_invited: 您已经被邀请。
     security: 账户安全
@@ -1325,26 +1329,26 @@ zh-CN:
       too_large: 文件过大
     failures: 失败
     imported: 已导入
-    mismatched_types_warning: 您似乎选择了导入错误的类型,请再次检查。
+    mismatched_types_warning: 你似乎选择了导入错误的类型,请再次检查。
     modes:
       merge: 合并
       merge_long: 保留现有记录并添加新的记录
       overwrite: 覆盖
       overwrite_long: 将当前记录替换为新记录
     overwrite_preambles:
-      blocking_html: 您即将使用来自<strong> %{filename} </strong>的最多<strong> %{total_items} 个账户</strong>替换您的屏蔽列表。
-      bookmarks_html: 您即将使用来自<strong> %{filename} </strong>的<strong> %{total_items} 篇嘟文</strong>替换您的书签。
-      domain_blocking_html: 您即将使用来自<strong> %{filename} </strong>的最多<strong> %{total_items} 个域名</strong>替换您的域名屏蔽列表。
-      following_html: 您即将从<strong> %{filename} </strong>关注<strong> %{total_items} 个账户</strong>,并停止关注其他任何人。
+      blocking_html: 你即将使用来自<strong> %{filename} </strong>的最多<strong> %{total_items} 个账户</strong>替换你的屏蔽列表。
+      bookmarks_html: 你即将使用来自<strong> %{filename} </strong>的<strong> %{total_items} 篇嘟文</strong>替换你的书签。
+      domain_blocking_html: 你即将使用来自<strong> %{filename} </strong>的最多<strong> %{total_items} 个域名</strong>替换你的域名屏蔽列表。
+      following_html: 你即将从<strong> %{filename} </strong>关注<strong> %{total_items} 个账户</strong>,并停止关注其他任何人。
       lists_html: 你即将用<strong> %{filename} </strong>的内容<strong>替换你的列表</strong>。新列表中将添加<strong> %{total_items} 个账户</strong>。
-      muting_html: 您即将使用来自<strong> %{filename} </strong>的最多<strong> %{total_items} 个账户</strong>替换您已隐藏的账户列表。
+      muting_html: 你即将使用来自<strong> %{filename} </strong>的最多<strong> %{total_items} 个账户</strong>替换你已隐藏的账户列表。
     preambles:
-      blocking_html: 您即将从<strong> %{filename} </strong>中<strong>封锁</strong>多达<strong> %{total_items} </strong>个账户。
-      bookmarks_html: 您即将把来自<strong> %{filename} </strong>的<strong> %{total_items} 篇嘟文</strong>添加到您的<strong>书签</strong>中。
-      domain_blocking_html: 您即将从<strong> %{filename} </strong>中<strong>屏蔽</strong><strong> %{total_items} 个域名</strong>。
-      following_html: 您即将从<strong> %{filename} </strong><strong>关注</strong>最多<strong> %{total_items} 个账户</strong>。
+      blocking_html: 你即将从<strong> %{filename} </strong>中<strong>封锁</strong>多达<strong> %{total_items} </strong>个账户。
+      bookmarks_html: 你即将把来自<strong> %{filename} </strong>的<strong> %{total_items} 篇嘟文</strong>添加到你的<strong>书签</strong>中。
+      domain_blocking_html: 你即将从<strong> %{filename} </strong>中<strong>屏蔽</strong><strong> %{total_items} 个域名</strong>。
+      following_html: 你即将从<strong> %{filename} </strong><strong>关注</strong>最多<strong> %{total_items} 个账户</strong>。
       lists_html: 你即将从<strong> %{filename} </strong>中添加最多<strong> %{total_items} 个账户</strong>到你的<strong>列表</strong>中。如果没有可用列表,将创建新的列表。
-      muting_html: 您即将从<strong> %{filename} </strong>中<strong>隐藏</strong><strong> %{total_items} 个账户</strong>。
+      muting_html: 你即将从<strong> %{filename} </strong>中<strong>隐藏</strong><strong> %{total_items} 个账户</strong>。
     preface: 你可以在此导入你在其他实例导出的数据,比如你所关注或屏蔽的用户列表。
     recent_imports: 最近导入
     states:
@@ -1414,7 +1418,7 @@ zh-CN:
     unsubscribe:
       action: 是,取消订阅
       complete: 已取消订阅
-      confirmation_html: 你确定要退订来自 %{domain} 上的 Mastodon 的 %{type} 到您的邮箱 %{email} 吗?您可以随时在<a href="%{settings_path}">邮件通知设置</a>中重新订阅。
+      confirmation_html: 你确定要退订来自 %{domain} 上的 Mastodon 的 %{type} 到你的邮箱 %{email} 吗?你可以随时在<a href="%{settings_path}">邮件通知设置</a>中重新订阅。
       emails:
         notification_emails:
           favourite: 嘟文被喜欢邮件通知
@@ -1428,7 +1432,7 @@ zh-CN:
   media_attachments:
     validations:
       images_and_video: 无法在嘟文中同时插入视频和图片
-      not_found: 未发现媒体%{ids} 或已附在另一条嘟文中
+      not_found: 媒体 %{ids} 未找到,有可能已随附于另一条嘟文。
       not_ready: 不能附加还在处理中的文件。请稍后再试!
       too_many: 最多只能添加 4 张图片
   migrations:
@@ -1541,7 +1545,7 @@ zh-CN:
       expired: 投票已经结束
       invalid_choice: 被选中的投票选项不存在
       over_character_limit: 每条不能超过 %{max} 个字符
-      self_vote: 您不能参与自己发起的投票
+      self_vote: 你不能参与自己发起的投票
       too_few_options: 至少需要两个选项
       too_many_options: 不能超过 %{max} 项
   preferences:
@@ -1549,7 +1553,7 @@ zh-CN:
     posting_defaults: 发布默认值
     public_timelines: 公共时间轴
   privacy:
-    hint_html: "<strong>自定义您希望如何找到您的个人资料和嘟文。</strong>启用Mastodon中的各种功能可以帮助您扩大受众范围。请花点时间查看这些设置,确保它们适合您的使用情况。"
+    hint_html: "<strong>自定义你希望如何找到你的个人资料和嘟文。</strong>启用Mastodon中的各种功能可以帮助你扩大受众范围。请花点时间查看这些设置,确保它们适合你的使用情况。"
     privacy: 隐私
     privacy_hint_html: 控制你愿意向他人透露多少信息。通过浏览他人的关注列表和查看他们发嘟所用的应用,人们可以发现有趣的用户和酷炫的应用,但你可能更喜欢将其隐藏起来。
     reach: 范围
@@ -1564,8 +1568,8 @@ zh-CN:
       limit_reached: 互动种类的限制
       unrecognized_emoji: 不是一个可识别的表情
   redirects:
-    prompt: 如果您信任此链接,请单击以继续跳转。
-    title: 您正在离开 %{instance} 。
+    prompt: 如果你信任此链接,请单击以继续跳转。
+    title: 你正在离开 %{instance} 。
   relationships:
     activity: 账号活动
     confirm_follow_selected_followers: 您确定想要关注所选的关注者吗?
@@ -1803,22 +1807,22 @@ zh-CN:
       action: 账户设置
       explanation: 你于 %{appeal_date} 对 %{strike_date} 在你账号上做出的处罚提出的申诉已被批准,你的账号已回到正常状态。
       subject: 你于 %{date} 提出的申诉已被批准
-      subtitle: 您的账户已再次回到良好状态。
+      subtitle: 你的账户已再次回到良好状态。
       title: 申诉已批准
     appeal_rejected:
       explanation: 你于 %{appeal_date} 对 %{strike_date} 在你账号上做出的处罚提出的申诉已被驳回。
       subject: 你于 %{date} 提出的申诉已被驳回
-      subtitle: 您的申诉已被驳回。
+      subtitle: 你的申诉已被驳回。
       title: 申诉已驳回
     backup_ready:
-      explanation: 您之前请求为您的 Mastodon 账户创建一份完整的备份。
+      explanation: 你之前请求为你的 Mastodon 账户创建一份完整的备份。
       extra: 现在它可以下载了!
       subject: 你的存档已经准备完毕
       title: 存档导出
     failed_2fa:
       details: 以下是该次登录尝试的详情:
-      explanation: 有人试图登录到您的账户,但提供了无效的辅助认证因子。
-      further_actions_html: 如果这不是您所为,您的密码可能已经泄露,建议您立即 %{action} 。
+      explanation: 有人试图登录到你的账户,但提供了无效的辅助认证因子。
+      further_actions_html: 如果这不是你所为,你的密码可能已经泄露,建议你立即 %{action} 。
       subject: 辅助认证失败
       title: 辅助认证失败
     suspicious_sign_in:
@@ -1864,24 +1868,24 @@ zh-CN:
       apps_ios_action: 从 App Store 下载
       apps_step: 下载我们的官方应用。
       apps_title: Mastodon应用
-      checklist_subtitle: 让我们带您开启这片社交新天地:
+      checklist_subtitle: 让我们带你开启这片社交新天地:
       checklist_title: 欢迎清单
       edit_profile_action: 个性化
-      edit_profile_step: 完善个人资料,提升您的互动体验。
-      edit_profile_title: 个性化您的个人资料
+      edit_profile_step: 完善个人资料,提升你的互动体验。
+      edit_profile_title: 个性化你的个人资料
       explanation: 下面是几个小贴士,希望它们能帮到你
       feature_action: 了解更多
-      feature_audience: Mastodon为您提供了无需中间商即可管理受众的独特可能。Mastodon可被部署在您自己的基础设施上,允许您关注其它任何Mastodon在线服务器的用户,或被任何其他在线 Mastodon 服务器的用户关注,并且不受您之外的任何人控制。
-      feature_audience_title: 放手去建立起您的受众
-      feature_control: 您最清楚您想在你自己的主页中看到什么动态。没有算法或广告浪费您的时间。您可以用一个账号关注任何 Mastodon 服务器上的任何人,并按时间顺序获得他们发布的嘟文,让您的互联网的角落更合自己的心意。
+      feature_audience: Mastodon为你提供了无需中间商即可管理受众的独特可能。Mastodon可被部署在你自己的基础设施上,允许你关注其它任何Mastodon在线服务器的用户,或被任何其他在线 Mastodon 服务器的用户关注,并且不受你之外的任何人控制。
+      feature_audience_title: 放手去建立起你的受众
+      feature_control: 你最清楚你想在你自己的主页中看到什么动态。没有算法或广告浪费你的时间。你可以用一个账号关注任何 Mastodon 服务器上的任何人,并按时间顺序获得他们发布的嘟文,让你的互联网的角落更合自己的心意。
       feature_control_title: 掌控自己的时间线
-      feature_creativity: Mastodon支持音频、视频和图片、无障碍描述、投票、内容警告, 动画头像、自定义表情包、缩略图裁剪控制等功能,帮助您在网上尽情表达自己。无论您是要发布您的艺术作品、音乐还是播客,Mastodon 都能为您服务。
+      feature_creativity: Mastodon支持音频、视频和图片、无障碍描述、投票、内容警告, 动画头像、自定义表情包、缩略图裁剪控制等功能,帮助你在网上尽情表达自己。无论你是要发布你的艺术作品、音乐还是播客,Mastodon 都能为你服务。
       feature_creativity_title: 无与伦比的创造力
-      feature_moderation: Mastodon将决策权交还给您。每个服务器都会创建自己的规则和条例,并在站点内施行,而不是像企业社交媒体那样居高临下,这使得它可以最灵活地响应不同人群的需求。加入一个您认同其规则的服务器,或托管您自己的服务器。
+      feature_moderation: Mastodon将决策权交还给你。每个服务器都会创建自己的规则和条例,并在站点内施行,而不是像企业社交媒体那样居高临下,这使得它可以最灵活地响应不同人群的需求。加入一个你认同其规则的服务器,或托管你自己的服务器。
       feature_moderation_title: 管理,本应如此
       follow_action: 关注
       follow_step: 关注有趣的人,这就是Mastodon的意义所在。
-      follow_title: 个性化您的首页动态
+      follow_title: 个性化你的首页动态
       follows_subtitle: 关注知名账户
       follows_title: 推荐关注
       follows_view_more: 查看更多可关注的人
@@ -1892,10 +1896,10 @@ zh-CN:
       hashtags_view_more: 查看更多热门话题标签
       post_action: 撰写
       post_step: 向世界打个招呼吧。
-      post_title: 发布您的第一条嘟文
+      post_title: 发布你的第一条嘟文
       share_action: 分享
-      share_step: 让您的朋友知道如何在Mastodon找到你。
-      share_title: 分享您的Mastodon个人资料
+      share_step: 让你的朋友知道如何在Mastodon找到你。
+      share_title: 分享你的Mastodon个人资料
       sign_in_action: 登录
       subject: 欢迎来到 Mastodon
       title: "%{name},欢迎你的加入!"
@@ -1908,12 +1912,12 @@ zh-CN:
     seamless_external_login: 你通过外部服务登录,因此密码和邮件设置不可用。
     signed_in_as: 当前登录的账户:
   verification:
-    extra_instructions_html: <strong>提示:</strong>您网站上的链接可能是不可见的。重要的部分是 <code>rel="me"</code>,它可以防止在具有用户生成内容的网站上冒充身份。您甚至可以在页面头部使用 <code>link</code> 标签而不是 <code>a</code>,但 HTML 必须能够在不执行 JavaScript 的情况下访问。
+    extra_instructions_html: <strong>提示:</strong>你网站上的链接可能是不可见的。重要的部分是 <code>rel="me"</code>,它可以防止在具有用户生成内容的网站上冒充身份。你甚至可以在页面头部使用 <code>link</code> 标签而不是 <code>a</code>,但 HTML 必须能够在不执行 JavaScript 的情况下访问。
     here_is_how: 具体方法如下:
-    hint_html: "<strong>在Mastodon上验证您的身份对每个人都是必要的。</strong>基于开放网络标准,现在和将来永远免费。您只需要一个被人们认可的个人网站。当您在个人资料中链接到这个网站时,我们会检查该网站是否回链到您的资料,并显示一个指示符号。"
-    instructions_html: 将下面的代码复制并粘贴到您网站的HTML中。然后在“编辑个人资料”选项卡中的附加字段之一添加您网站的地址,并保存更改。
+    hint_html: "<strong>在Mastodon上验证你的身份对每个人都是必要的。</strong>基于开放网络标准,现在和将来永远免费。你只需要一个被人们认可的个人网站。当你在个人资料中链接到这个网站时,我们会检查该网站是否回链到你的资料,并显示一个指示符号。"
+    instructions_html: 将下面的代码复制并粘贴到你网站的HTML中。然后在“编辑个人资料”选项卡中的附加字段之一添加你网站的地址,并保存更改。
     verification: 验证
-    verified_links: 您已验证的链接
+    verified_links: 你已验证的链接
   webauthn_credentials:
     add: 添加新的安全密钥
     create:
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 97a4399b2..8288e9bfa 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -23,6 +23,8 @@ zh-TW:
   admin:
     account_actions:
       action: 執行動作
+      already_silenced: 此帳號已被靜音。
+      already_suspended: 此帳號已被停權。
       title: 對 %{acct} 執行站務動作
     account_moderation_notes:
       create: 新增站務記錄
@@ -44,6 +46,7 @@ zh-TW:
         title: 為 %{username} 變更電子郵件地址
       change_role:
         changed_msg: 成功修改角色!
+        edit_roles: 管理使用者權限
         label: 變更角色
         no_role: 沒有角色
         title: 為 %{username} 變更角色
@@ -590,6 +593,7 @@ zh-TW:
         suspend_description_html: 此帳號及其所有內容將不可被存取並且最終被移除,並且無法與之進行互動。三十天內可以撤銷此動作。關閉所有對此帳號之檢舉報告。
       actions_description_html: 決定應對此報告採取何種行動。若您對檢舉之帳號採取懲罰措施,則將對他們發送 e-mail 通知,除非選擇了 <strong>垃圾郵件</strong> 類別。
       actions_description_remote_html: 決定將對此檢舉報告採取何種動作。這將僅作用於<strong>您的伺服器</strong>與此遠端帳號及其內容之通訊行為。
+      actions_no_posts: 此報告無任何需要刪除之相關嘟文
       add_to_report: 加入更多至報告
       already_suspended_badges:
         local: 已自此伺服器停權

From a021dee64214fcc662c0c36ad4e44dc1deaba65f Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 9 Sep 2024 17:28:54 +0200
Subject: [PATCH 53/91] Change labels on thread indicators in web UI (#31806)

---
 app/javascript/mastodon/components/status.jsx | 17 +++----
 .../components/status_thread_label.tsx        | 50 +++++++++++++++++++
 app/javascript/mastodon/locales/en.json       |  2 +
 .../styles/mastodon/components.scss           | 22 ++++++--
 4 files changed, 76 insertions(+), 15 deletions(-)
 create mode 100644 app/javascript/mastodon/components/status_thread_label.tsx

diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 7236c9633..6c32fd245 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -12,7 +12,6 @@ import { HotKeys } from 'react-hotkeys';
 import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
 import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
-import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
 import { ContentWarning } from 'mastodon/components/content_warning';
 import { FilterWarning } from 'mastodon/components/filter_warning';
 import { Icon }  from 'mastodon/components/icon';
@@ -34,6 +33,7 @@ import { getHashtagBarForStatus } from './hashtag_bar';
 import { RelativeTimestamp } from './relative_timestamp';
 import StatusActionBar from './status_action_bar';
 import StatusContent from './status_content';
+import { StatusThreadLabel } from './status_thread_label';
 import { VisibilityIcon } from './visibility_icon';
 
 const domParser = new DOMParser();
@@ -413,7 +413,7 @@ class Status extends ImmutablePureComponent {
     if (featured) {
       prepend = (
         <div className='status__prepend'>
-          <div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' icon={PushPinIcon} className='status__prepend-icon' /></div>
+          <div className='status__prepend__icon'><Icon id='thumb-tack' icon={PushPinIcon} /></div>
           <FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
         </div>
       );
@@ -422,7 +422,7 @@ class Status extends ImmutablePureComponent {
 
       prepend = (
         <div className='status__prepend'>
-          <div className='status__prepend-icon-wrapper'><Icon id='retweet' icon={RepeatIcon} className='status__prepend-icon' /></div>
+          <div className='status__prepend__icon'><Icon id='retweet' icon={RepeatIcon} /></div>
           <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
         </div>
       );
@@ -434,18 +434,13 @@ class Status extends ImmutablePureComponent {
     } else if (status.get('visibility') === 'direct') {
       prepend = (
         <div className='status__prepend'>
-          <div className='status__prepend-icon-wrapper'><Icon id='at' icon={AlternateEmailIcon} className='status__prepend-icon' /></div>
+          <div className='status__prepend__icon'><Icon id='at' icon={AlternateEmailIcon} /></div>
           <FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
         </div>
       );
-    } else if (showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
-      const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
-
+    } else if (showThread && status.get('in_reply_to_id')) {
       prepend = (
-        <div className='status__prepend'>
-          <div className='status__prepend-icon-wrapper'><Icon id='reply' icon={ReplyIcon} className='status__prepend-icon' /></div>
-          <FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
-        </div>
+        <StatusThreadLabel accountId={status.getIn(['account', 'id'])} inReplyToAccountId={status.get('in_reply_to_account_id')} />
       );
     }
 
diff --git a/app/javascript/mastodon/components/status_thread_label.tsx b/app/javascript/mastodon/components/status_thread_label.tsx
new file mode 100644
index 000000000..b18aca6dc
--- /dev/null
+++ b/app/javascript/mastodon/components/status_thread_label.tsx
@@ -0,0 +1,50 @@
+import { FormattedMessage } from 'react-intl';
+
+import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
+import { Icon } from 'mastodon/components/icon';
+import { DisplayedName } from 'mastodon/features/notifications_v2/components/displayed_name';
+import { useAppSelector } from 'mastodon/store';
+
+export const StatusThreadLabel: React.FC<{
+  accountId: string;
+  inReplyToAccountId: string;
+}> = ({ accountId, inReplyToAccountId }) => {
+  const inReplyToAccount = useAppSelector((state) =>
+    state.accounts.get(inReplyToAccountId),
+  );
+
+  let label;
+
+  if (accountId === inReplyToAccountId) {
+    label = (
+      <FormattedMessage
+        id='status.continued_thread'
+        defaultMessage='Continued thread'
+      />
+    );
+  } else if (inReplyToAccount) {
+    label = (
+      <FormattedMessage
+        id='status.replied_to'
+        defaultMessage='Replied to {name}'
+        values={{ name: <DisplayedName accountIds={[inReplyToAccountId]} /> }}
+      />
+    );
+  } else {
+    label = (
+      <FormattedMessage
+        id='status.replied_in_thread'
+        defaultMessage='Replied in thread'
+      />
+    );
+  }
+
+  return (
+    <div className='status__prepend'>
+      <div className='status__prepend__icon'>
+        <Icon id='reply' icon={ReplyIcon} />
+      </div>
+      {label}
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 4321acef4..7c1d7f126 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Bookmark",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
+  "status.continued_thread": "Continued thread",
   "status.copy": "Copy link to post",
   "status.delete": "Delete",
   "status.detailed_status": "Detailed conversation view",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
   "status.redraft": "Delete & re-draft",
   "status.remove_bookmark": "Remove bookmark",
+  "status.replied_in_thread": "Replied in thread",
   "status.replied_to": "Replied to {name}",
   "status.reply": "Reply",
   "status.replyAll": "Reply to thread",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 0b7c9ac90..92d203463 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1607,15 +1607,29 @@ body > [data-popper-placement] {
 .status__prepend {
   padding: 16px;
   padding-bottom: 0;
-  display: inline-flex;
-  gap: 10px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
   font-size: 15px;
   line-height: 22px;
   font-weight: 500;
   color: $dark-text-color;
 
-  .status__display-name strong {
-    color: $dark-text-color;
+  &__icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 0 0 auto;
+
+    .icon {
+      width: 16px;
+      height: 16px;
+    }
+  }
+
+  a {
+    color: inherit;
+    text-decoration: none;
   }
 
   > span {

From d0ab94c4d256d8239d0708c5a1e1d694607dae71 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 9 Sep 2024 15:57:19 -0400
Subject: [PATCH 54/91] Add `FeaturedTag` coverage, use `pick` in model
 (#31828)

---
 app/models/featured_tag.rb       |   4 +-
 spec/models/featured_tag_spec.rb | 130 +++++++++++++++++++++++++++++++
 2 files changed, 132 insertions(+), 2 deletions(-)

diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb
index cdd97205e..a4e7b7cf6 100644
--- a/app/models/featured_tag.rb
+++ b/app/models/featured_tag.rb
@@ -45,7 +45,7 @@ class FeaturedTag < ApplicationRecord
   end
 
   def decrement(deleted_status_id)
-    update(statuses_count: [0, statuses_count - 1].max, last_status_at: visible_tagged_account_statuses.where.not(id: deleted_status_id).select(:created_at).first&.created_at)
+    update(statuses_count: [0, statuses_count - 1].max, last_status_at: visible_tagged_account_statuses.where.not(id: deleted_status_id).pick(:created_at))
   end
 
   private
@@ -56,7 +56,7 @@ class FeaturedTag < ApplicationRecord
 
   def reset_data
     self.statuses_count = visible_tagged_account_statuses.count
-    self.last_status_at = visible_tagged_account_statuses.select(:created_at).first&.created_at
+    self.last_status_at = visible_tagged_account_statuses.pick(:created_at)
   end
 
   def validate_featured_tags_limit
diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb
index 6056e645e..0f5ead8f9 100644
--- a/spec/models/featured_tag_spec.rb
+++ b/spec/models/featured_tag_spec.rb
@@ -8,4 +8,134 @@ RSpec.describe FeaturedTag do
       it { is_expected.to normalize(:name).from('  #hashtag  ').to('hashtag') }
     end
   end
+
+  describe 'Validations' do
+    context 'when account already has a featured tag' do
+      subject { Fabricate.build :featured_tag, account: account }
+
+      before { Fabricate :featured_tag, account: account, name: 'Test' }
+
+      let(:account) { Fabricate :account }
+
+      it { is_expected.to_not allow_value('Test').for(:name) }
+
+      context 'when account has hit limit' do
+        before { stub_const 'FeaturedTag::LIMIT', 1 }
+
+        context 'with a local account' do
+          let(:account) { Fabricate :account, domain: nil }
+
+          it { is_expected.to_not allow_value(account).for(:account).against(:base).with_message(I18n.t('featured_tags.errors.limit')) }
+        end
+
+        context 'with a remote account' do
+          let(:account) { Fabricate :account, domain: 'host.example' }
+
+          it { is_expected.to allow_value(account).for(:account) }
+        end
+      end
+    end
+  end
+
+  describe 'Callback to set the tag' do
+    context 'with no matching tag' do
+      it 'creates a new tag' do
+        expect { Fabricate :featured_tag, name: 'tag' }
+          .to change(Tag, :count).by(1)
+      end
+    end
+
+    context 'with a matching tag' do
+      it 'creates a new tag' do
+        tag = Fabricate :tag, name: 'tag'
+
+        expect { Fabricate :featured_tag, name: 'tag' }
+          .to_not change(Tag, :count)
+
+        expect(described_class.last.tag)
+          .to eq(tag)
+      end
+    end
+  end
+
+  describe 'Callback to set the stats' do
+    context 'when no statuses are relevant' do
+      it 'sets values to nil' do
+        featured_tag = Fabricate :featured_tag
+
+        expect(featured_tag)
+          .to have_attributes(
+            statuses_count: 0,
+            last_status_at: be_nil
+          )
+      end
+    end
+
+    context 'when some statuses are relevant' do
+      it 'sets values to nil' do
+        tag = Fabricate :tag, name: 'test'
+        status = Fabricate :status, visibility: :public, created_at: 10.days.ago
+        status.tags << tag
+
+        featured_tag = Fabricate :featured_tag, name: 'test', account: status.account
+
+        expect(featured_tag)
+          .to have_attributes(
+            statuses_count: 1,
+            last_status_at: be_within(0.1).of(status.created_at)
+          )
+      end
+    end
+  end
+
+  describe '#sign?' do
+    it { is_expected.to be_sign }
+  end
+
+  describe '#display_name' do
+    subject { Fabricate.build :featured_tag, name: name, tag: tag }
+
+    context 'with a name value present' do
+      let(:name) { 'Test' }
+      let(:tag) { nil }
+
+      it 'uses name value' do
+        expect(subject.display_name).to eq('Test')
+      end
+    end
+
+    context 'with a missing name value but a present tag' do
+      let(:name) { nil }
+      let(:tag) { Fabricate.build :tag, name: 'Tester' }
+
+      it 'uses name value' do
+        expect(subject.display_name).to eq('Tester')
+      end
+    end
+  end
+
+  describe '#increment' do
+    it 'increases the count and updates the last_status_at timestamp' do
+      featured_tag = Fabricate :featured_tag
+      timestamp = 5.days.ago
+
+      expect { featured_tag.increment(timestamp) }
+        .to change(featured_tag, :statuses_count).from(0).to(1)
+        .and change(featured_tag, :last_status_at).from(nil).to(be_within(0.1).of(timestamp))
+    end
+  end
+
+  describe '#decrement' do
+    it 'decreases the count and updates the last_status_at timestamp' do
+      tag = Fabricate :tag, name: 'test'
+      status = Fabricate :status, visibility: :public, created_at: 10.days.ago
+      status.tags << tag
+
+      featured_tag = Fabricate :featured_tag, name: 'test', account: status.account
+
+      expect { featured_tag.decrement(status.id) }
+        .to change(featured_tag, :statuses_count).from(1).to(0)
+        .and change(featured_tag, :last_status_at).to(nil)
+    end
+  end
 end

From 592a7af27f7699d4751d2bea7785149d3c0e5d58 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 9 Sep 2024 21:57:52 +0200
Subject: [PATCH 55/91] =?UTF-8?q?Fix=20translatable=20source=20string=20us?=
 =?UTF-8?q?ing=20=E2=80=9Csilenced=E2=80=9D=20instead=20of=20=E2=80=9Climi?=
 =?UTF-8?q?ted=E2=80=9D=20(#31822)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 config/locales/en.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/config/locales/en.yml b/config/locales/en.yml
index e8c901048..980bd481b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -25,7 +25,7 @@ en:
   admin:
     account_actions:
       action: Perform action
-      already_silenced: This account has already been silenced.
+      already_silenced: This account has already been limited.
       already_suspended: This account has already been suspended.
       title: Perform moderation action on %{acct}
     account_moderation_notes:

From 9ea710e5438ba862f135e972185a932910511715 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 10 Sep 2024 09:59:18 +0200
Subject: [PATCH 56/91] Update dependency oj to v3.16.6 (#31831)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 48fd05b1a..de541edfb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -458,7 +458,7 @@ GEM
     nokogiri (1.16.7)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    oj (3.16.5)
+    oj (3.16.6)
       bigdecimal (>= 3.0)
       ostruct (>= 0.2)
     omniauth (2.1.2)

From 5b995143f1ba3278e799e46b76646a9878977677 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 10 Sep 2024 04:03:45 -0400
Subject: [PATCH 57/91] Use `with_options` for shared Account validation option
 value (#31827)

---
 app/models/account.rb | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/app/models/account.rb b/app/models/account.rb
index d773d3344..4a7c752e7 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -111,10 +111,12 @@ class Account < ApplicationRecord
   validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? }
   validates :note, note_length: { maximum: NOTE_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_note? }
   validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? }
-  validates :uri, absence: true, if: :local?, on: :create
-  validates :inbox_url, absence: true, if: :local?, on: :create
-  validates :shared_inbox_url, absence: true, if: :local?, on: :create
-  validates :followers_url, absence: true, if: :local?, on: :create
+  with_options on: :create do
+    validates :uri, absence: true, if: :local?
+    validates :inbox_url, absence: true, if: :local?
+    validates :shared_inbox_url, absence: true, if: :local?
+    validates :followers_url, absence: true, if: :local?
+  end
 
   normalizes :username, with: ->(username) { username.squish }
 

From 5260233b81c301cf7b0c79b179858748b693460b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 10 Sep 2024 11:22:49 +0200
Subject: [PATCH 58/91] New Crowdin Translations (automated) (#31835)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/ca.json    |   2 +
 app/javascript/mastodon/locales/da.json    |   2 +
 app/javascript/mastodon/locales/de.json    |   2 +
 app/javascript/mastodon/locales/es-AR.json |   2 +
 app/javascript/mastodon/locales/fi.json    |   4 +-
 app/javascript/mastodon/locales/gl.json    |  12 +-
 app/javascript/mastodon/locales/he.json    |   2 +
 app/javascript/mastodon/locales/hu.json    |   2 +
 app/javascript/mastodon/locales/lt.json    |   2 +
 app/javascript/mastodon/locales/nl.json    |   2 +
 app/javascript/mastodon/locales/pl.json    |   2 +
 app/javascript/mastodon/locales/pt-PT.json |   2 +-
 app/javascript/mastodon/locales/tr.json    |   2 +
 app/javascript/mastodon/locales/uk.json    |   1 +
 app/javascript/mastodon/locales/zh-CN.json |   4 +-
 app/javascript/mastodon/locales/zh-TW.json |   2 +
 config/locales/ca.yml                      |  29 ++-
 config/locales/cy.yml                      |   1 -
 config/locales/da.yml                      |   2 +-
 config/locales/de.yml                      |   1 -
 config/locales/es-AR.yml                   |   2 +-
 config/locales/et.yml                      |   1 -
 config/locales/fi.yml                      |   2 +-
 config/locales/fo.yml                      |   1 -
 config/locales/fr-CA.yml                   |   1 -
 config/locales/fr.yml                      |   1 -
 config/locales/ga.yml                      |   1 -
 config/locales/gd.yml                      |   1 -
 config/locales/gl.yml                      |   4 +-
 config/locales/he.yml                      |   3 +
 config/locales/hu.yml                      |   1 -
 config/locales/ia.yml                      |   1 -
 config/locales/is.yml                      |   1 -
 config/locales/it.yml                      |   2 +-
 config/locales/ko.yml                      |   1 -
 config/locales/lt.yml                      |   3 +
 config/locales/nn.yml                      |   1 +
 config/locales/pl.yml                      |   1 -
 config/locales/pt-PT.yml                   | 207 ++++++++++++---------
 config/locales/simple_form.he.yml          |   1 +
 config/locales/simple_form.lt.yml          |   1 +
 config/locales/simple_form.pl.yml          |   1 +
 config/locales/simple_form.pt-PT.yml       |   9 +-
 config/locales/simple_form.tr.yml          |   1 +
 config/locales/sq.yml                      |   1 -
 config/locales/sv.yml                      |   1 +
 config/locales/th.yml                      |   1 -
 config/locales/tr.yml                      |   4 +
 config/locales/uk.yml                      |   1 +
 config/locales/vi.yml                      |   1 -
 config/locales/zh-CN.yml                   |   2 +-
 config/locales/zh-TW.yml                   |   4 +-
 52 files changed, 210 insertions(+), 131 deletions(-)

diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index d99b5c737..5981c1df8 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Marca",
   "status.cancel_reblog_private": "Desfés l'impuls",
   "status.cannot_reblog": "No es pot impulsar aquest tut",
+  "status.continued_thread": "Continuació del fil",
   "status.copy": "Copia l'enllaç al tut",
   "status.delete": "Elimina",
   "status.detailed_status": "Vista detallada de la conversa",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Encara no ha impulsat ningú aquest tut. Quan algú ho faci, apareixerà aquí.",
   "status.redraft": "Esborra i reescriu",
   "status.remove_bookmark": "Elimina el marcador",
+  "status.replied_in_thread": "Respost al fil",
   "status.replied_to": "En resposta a {name}",
   "status.reply": "Respon",
   "status.replyAll": "Respon al fil",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 732e4a100..4155dc9fc 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Bogmærk",
   "status.cancel_reblog_private": "Fjern boost",
   "status.cannot_reblog": "Dette indlæg kan ikke fremhæves",
+  "status.continued_thread": "Fortsat tråd",
   "status.copy": "Kopiér link til indlæg",
   "status.delete": "Slet",
   "status.detailed_status": "Detaljeret samtalevisning",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Ingen har endnu fremhævet dette indlæg. Når nogen gør, vil det fremgå hér.",
   "status.redraft": "Slet og omformulér",
   "status.remove_bookmark": "Fjern bogmærke",
+  "status.replied_in_thread": "Svaret i tråd",
   "status.replied_to": "Besvarede {name}",
   "status.reply": "Besvar",
   "status.replyAll": "Besvar alle",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 3c180f9f5..e37a59546 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Beitrag als Lesezeichen setzen",
   "status.cancel_reblog_private": "Beitrag nicht mehr teilen",
   "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
+  "status.continued_thread": "Fortgeführter Thread",
   "status.copy": "Link zum Beitrag kopieren",
   "status.delete": "Beitrag löschen",
   "status.detailed_status": "Detaillierte Ansicht der Unterhaltung",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.",
   "status.redraft": "Löschen und neu erstellen",
   "status.remove_bookmark": "Lesezeichen entfernen",
+  "status.replied_in_thread": "Antwortete im Thread",
   "status.replied_to": "Antwortete {name}",
   "status.reply": "Antworten",
   "status.replyAll": "Allen antworten",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index c3915c9b7..20d93ed01 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Marcar",
   "status.cancel_reblog_private": "Quitar adhesión",
   "status.cannot_reblog": "No se puede adherir a este mensaje",
+  "status.continued_thread": "Continuación de hilo",
   "status.copy": "Copiar enlace al mensaje",
   "status.delete": "Eliminar",
   "status.detailed_status": "Vista de conversación detallada",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Todavía nadie adhirió a este mensaje. Cuando alguien lo haga, se mostrará acá.",
   "status.redraft": "Eliminar mensaje original y editarlo",
   "status.remove_bookmark": "Quitar marcador",
+  "status.replied_in_thread": "Respuesta en hilo",
   "status.replied_to": "Respondió a {name}",
   "status.reply": "Responder",
   "status.replyAll": "Responder al hilo",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 79ca95a58..8f9cc5fe4 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -1,6 +1,6 @@
 {
   "about.blocks": "Moderoidut palvelimet",
-  "about.contact": "Yhteystiedot:",
+  "about.contact": "Yhteydenotto:",
   "about.disclaimer": "Mastodon on vapaa avoimen lähdekoodin ohjelmisto ja Mastodon gGmbH:n tavaramerkki.",
   "about.domain_blocks.no_reason_available": "Syy ei ole tiedossa",
   "about.domain_blocks.preamble": "Mastodonin avulla voi yleensä tarkastella minkä tahansa fediversumiin kuuluvan palvelimen sisältöä ja olla yhteyksissä eri palvelinten käyttäjien kanssa. Nämä poikkeukset koskevat yksin tätä palvelinta.",
@@ -780,6 +780,7 @@
   "status.bookmark": "Lisää kirjanmerkki",
   "status.cancel_reblog_private": "Peru tehostus",
   "status.cannot_reblog": "Tätä julkaisua ei voi tehostaa",
+  "status.continued_thread": "Jatkoi ketjua",
   "status.copy": "Kopioi linkki julkaisuun",
   "status.delete": "Poista",
   "status.detailed_status": "Yksityiskohtainen keskustelunäkymä",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Kukaan ei ole vielä tehostanut tätä julkaisua. Kun joku tekee niin, tulee hän tähän näkyviin.",
   "status.redraft": "Poista ja palauta muokattavaksi",
   "status.remove_bookmark": "Poista kirjanmerkki",
+  "status.replied_in_thread": "Vastasi ketjuun",
   "status.replied_to": "Vastaus käyttäjälle {name}",
   "status.reply": "Vastaa",
   "status.replyAll": "Vastaa ketjuun",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 5a5ac1dd3..df477fe9e 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -223,7 +223,7 @@
   "domain_block_modal.title": "Bloquear dominio?",
   "domain_block_modal.you_will_lose_followers": "Vanse eliminar todas as túas seguidoras deste servidor.",
   "domain_block_modal.you_wont_see_posts": "Non verás publicacións ou notificacións das usuarias deste servidor.",
-  "domain_pill.activitypub_lets_connect": "Permíteche conectar e interactuar con persoas non só de Mastodon, se non tamén con outras apps sociais.",
+  "domain_pill.activitypub_lets_connect": "Permíteche conectar e interactuar con persoas non só de Mastodon, se non tamén con outras  sociais.",
   "domain_pill.activitypub_like_language": "ActivityPub é algo así como o idioma que Mastodon fala con outras redes sociais.",
   "domain_pill.server": "Servidor",
   "domain_pill.their_handle": "O seu alcume:",
@@ -231,8 +231,8 @@
   "domain_pill.their_username": "O seu identificador único no seu servidor. É posible atopar usuarias co mesmo nome de usuaria en diferentes servidores.",
   "domain_pill.username": "Nome de usuaria",
   "domain_pill.whats_in_a_handle": "As partes do alcume?",
-  "domain_pill.who_they_are": "O alcume dinos quen é esa persoa e onde está, para que poidas interactuar con ela en toda a web social de <button>plataformas ActivityPub</button>.",
-  "domain_pill.who_you_are": "Como o teu alcume informa de quen es e onde estás, as persoas poden interactuar contigo desde toda a web social de <button>plataformas ActivityPub</button>.",
+  "domain_pill.who_they_are": "O alcume dinos quen é esa persoa e onde está, para que poidas interactuar con ela en toda a web social das <button>plataformas ActivityPub</button>.",
+  "domain_pill.who_you_are": "Como o teu alcume informa de quen es e onde estás, as persoas poden interactuar contigo desde toda a web social das <button>plataformas ActivityPub</button>.",
   "domain_pill.your_handle": "O teu alcume:",
   "domain_pill.your_server": "O teu fogar dixital, onde están as túas publicacións. Non é do teu agrado? Podes cambiar de servidor cando queiras levando as túas seguidoras contigo.",
   "domain_pill.your_username": "O teu identificador único neste servidor. É posible que atopes usuarias co mesmo nome de usuaria en outros servidores.",
@@ -272,7 +272,7 @@
   "empty_column.list": "Aínda non hai nada nesta listaxe. Cando as usuarias incluídas na listaxe publiquen mensaxes, amosaranse aquí.",
   "empty_column.lists": "Aínda non tes listaxes. Cando crees unha, amosarase aquí.",
   "empty_column.mutes": "Aínda non silenciaches a ningúnha usuaria.",
-  "empty_column.notification_requests": "Todo ben! Nada por aquí. Cando recibas novas notificación aparecerán aquí seguindo o criterio dos teus axustes.",
+  "empty_column.notification_requests": "Todo ben! Nada por aquí. Cando recibas novas notificacións aparecerán aquí seguindo o criterio dos teus axustes.",
   "empty_column.notifications": "Aínda non tes notificacións. Aparecerán cando outras persoas interactúen contigo.",
   "empty_column.public": "Nada por aquí! Escribe algo de xeito público, ou segue de xeito manual usuarias doutros servidores para ir enchéndoo",
   "error.unexpected_crash.explanation": "Debido a un erro no noso código ou a unha compatilidade co teu navegador, esta páxina non pode ser amosada correctamente.",
@@ -641,7 +641,7 @@
   "onboarding.steps.publish_status.title": "Escribe a túa primeira publicación",
   "onboarding.steps.setup_profile.body": "Ao engadir información ao teu perfil é máis probable que teñas máis interaccións.",
   "onboarding.steps.setup_profile.title": "Personaliza o perfil",
-  "onboarding.steps.share_profile.body": "Dille ás amizades como poden atoparte en Mastodon!",
+  "onboarding.steps.share_profile.body": "Dille ás amizades como poden atoparte en Mastodon.",
   "onboarding.steps.share_profile.title": "Comparte o teu perfil en Mastodon",
   "onboarding.tips.2fa": "<strong>Sabes que?</strong> Podes protexer a túa conta configurando un segundo factor de autenticación nos axustes. Funciona con calquera app TOTP, non precisas un número de teléfono!",
   "onboarding.tips.accounts_from_other_servers": "<strong>Sabes que?</strong> Como Mastodon é descentralizado, algúns perfís que atopes estarán en servidores diferentes ao teu. Pero podes interactuar igualmente con eles! O seu servidor é o que ven despois da @ no seu identificador!",
@@ -780,6 +780,7 @@
   "status.bookmark": "Marcar",
   "status.cancel_reblog_private": "Desfacer compartido",
   "status.cannot_reblog": "Esta publicación non pode ser promovida",
+  "status.continued_thread": "Continua co fío",
   "status.copy": "Copiar ligazón á publicación",
   "status.delete": "Eliminar",
   "status.detailed_status": "Vista detallada da conversa",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Aínda ninguén promoveu esta publicación. Cando alguén o faga, amosarase aquí.",
   "status.redraft": "Eliminar e reescribir",
   "status.remove_bookmark": "Eliminar marcador",
+  "status.replied_in_thread": "Respondeu nun fío",
   "status.replied_to": "Respondeu a {name}",
   "status.reply": "Responder",
   "status.replyAll": "Responder ao tema",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 44c95d64c..47fc444e8 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -780,6 +780,7 @@
   "status.bookmark": "סימניה",
   "status.cancel_reblog_private": "הסרת הדהוד",
   "status.cannot_reblog": "לא ניתן להדהד חצרוץ זה",
+  "status.continued_thread": "שרשור מתמשך",
   "status.copy": "העתק/י קישור להודעה זו",
   "status.delete": "מחיקה",
   "status.detailed_status": "תצוגת שיחה מפורטת",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "עוד לא הידהדו את ההודעה הזו. כאשר זה יקרה, ההדהודים יופיעו כאן.",
   "status.redraft": "מחיקה ועריכה מחדש",
   "status.remove_bookmark": "הסרת סימניה",
+  "status.replied_in_thread": "תגובה לשרשור",
   "status.replied_to": "בתגובה לחשבון {name}",
   "status.reply": "תגובה",
   "status.replyAll": "תגובה לשרשור",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index d55a85ee3..f0f08ca50 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Könyvjelzőzés",
   "status.cancel_reblog_private": "Megtolás visszavonása",
   "status.cannot_reblog": "Ezt a bejegyzést nem lehet megtolni",
+  "status.continued_thread": "Folytatott szál",
   "status.copy": "Link másolása bejegyzésbe",
   "status.delete": "Törlés",
   "status.detailed_status": "Részletes beszélgetési nézet",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Senki sem tolta még meg ezt a bejegyzést. Ha valaki megteszi, itt fog megjelenni.",
   "status.redraft": "Törlés és újraírás",
   "status.remove_bookmark": "Könyvjelző eltávolítása",
+  "status.replied_in_thread": "Válaszolva a szálban",
   "status.replied_to": "Megválaszolva {name} számára",
   "status.reply": "Válasz",
   "status.replyAll": "Válasz a beszélgetésre",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index f58c405f2..55ef2afce 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -770,6 +770,7 @@
   "status.bookmark": "Pridėti į žymės",
   "status.cancel_reblog_private": "Nebepakelti",
   "status.cannot_reblog": "Šis įrašas negali būti pakeltas.",
+  "status.continued_thread": "Tęsiama gijoje",
   "status.copy": "Kopijuoti nuorodą į įrašą",
   "status.delete": "Ištrinti",
   "status.detailed_status": "Išsami pokalbio peržiūra",
@@ -802,6 +803,7 @@
   "status.reblogs.empty": "Šio įrašo dar niekas nepakėlė. Kai kas nors tai padarys, jie bus rodomi čia.",
   "status.redraft": "Ištrinti ir parengti iš naujo",
   "status.remove_bookmark": "Pašalinti žymę",
+  "status.replied_in_thread": "Atsakyta gijoje",
   "status.replied_to": "Atsakyta į {name}",
   "status.reply": "Atsakyti",
   "status.replyAll": "Atsakyti į giją",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 352656824..795643536 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Bladwijzer toevoegen",
   "status.cancel_reblog_private": "Niet langer boosten",
   "status.cannot_reblog": "Dit bericht kan niet geboost worden",
+  "status.continued_thread": "Vervolgt het gesprek",
   "status.copy": "Link naar bericht kopiëren",
   "status.delete": "Verwijderen",
   "status.detailed_status": "Uitgebreide gespreksweergave",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Niemand heeft dit bericht nog geboost. Wanneer iemand dit doet, valt dat hier te zien.",
   "status.redraft": "Verwijderen en herschrijven",
   "status.remove_bookmark": "Bladwijzer verwijderen",
+  "status.replied_in_thread": "Reageerde in gesprek",
   "status.replied_to": "Reageerde op {name}",
   "status.reply": "Reageren",
   "status.replyAll": "Op iedereen reageren",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 09ac63618..6bf6252d7 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Dodaj zakładkę",
   "status.cancel_reblog_private": "Cofnij podbicie",
   "status.cannot_reblog": "Ten wpis nie może zostać podbity",
+  "status.continued_thread": "Ciąg dalszy wątku",
   "status.copy": "Skopiuj odnośnik do wpisu",
   "status.delete": "Usuń",
   "status.detailed_status": "Szczegółowy widok konwersacji",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Nikt nie podbił jeszcze tego wpisu. Gdy ktoś to zrobi, pojawi się tutaj.",
   "status.redraft": "Usuń i przeredaguj",
   "status.remove_bookmark": "Usuń zakładkę",
+  "status.replied_in_thread": "Odpowiedź w wątku",
   "status.replied_to": "Odpowiedź do wpisu użytkownika {name}",
   "status.reply": "Odpowiedz",
   "status.replyAll": "Odpowiedz na wątek",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 2d0013d60..fd811d683 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -278,7 +278,7 @@
   "error.unexpected_crash.explanation": "Devido a um erro no nosso código ou a um problema de compatibilidade do navegador, esta página não pôde ser apresentada corretamente.",
   "error.unexpected_crash.explanation_addons": "Esta página não pôde ser exibida corretamente. Este erro provavelmente é causado por um complemento do navegador ou ferramentas de tradução automática.",
   "error.unexpected_crash.next_steps": "Tente atualizar a página. Se isso não ajudar, pode usar o Mastodon através de um navegador diferente ou uma aplicação nativa.",
-  "error.unexpected_crash.next_steps_addons": "Tente desabilitá-los e atualizar a página. Se isso não ajudar, você ainda poderá usar o Mastodon por meio de um navegador diferente ou de um aplicativo nativo.",
+  "error.unexpected_crash.next_steps_addons": "Tente desativá-los e atualizar a página. Se isso não ajudar, poderá ainda ser possível utilizar o Mastodon através de um navegador diferente ou de uma aplicação nativa.",
   "errors.unexpected_crash.copy_stacktrace": "Copiar a stacktrace para o clipboard",
   "errors.unexpected_crash.report_issue": "Reportar problema",
   "explore.search_results": "Resultados da pesquisa",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 9b94ede76..4873fa943 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Yer işareti ekle",
   "status.cancel_reblog_private": "Yeniden paylaşımı geri al",
   "status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz",
+  "status.continued_thread": "Devam eden akış",
   "status.copy": "Gönderi bağlantısını kopyala",
   "status.delete": "Sil",
   "status.detailed_status": "Ayrıntılı sohbet görünümü",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Henüz hiç kimse bu gönderiyi yeniden paylaşmadı. Herhangi bir kullanıcı yeniden paylaştığında burada görüntülenecek.",
   "status.redraft": "Sil,Düzenle ve Yeniden paylaş",
   "status.remove_bookmark": "Yer işaretini kaldır",
+  "status.replied_in_thread": "Akışta yanıtlandı",
   "status.replied_to": "{name} kullanıcısına yanıt verdi",
   "status.reply": "Yanıtla",
   "status.replyAll": "Konuyu yanıtla",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 714348db2..b63596037 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -780,6 +780,7 @@
   "status.bookmark": "Додати до закладок",
   "status.cancel_reblog_private": "Скасувати поширення",
   "status.cannot_reblog": "Цей допис не може бути поширений",
+  "status.continued_thread": "Continued thread",
   "status.copy": "Копіювати посилання на допис",
   "status.delete": "Видалити",
   "status.detailed_status": "Детальний вигляд бесіди",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index ea4ebcda4..94adc9af2 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -95,7 +95,7 @@
   "block_modal.they_cant_see_posts": "他们看不到你的嘟文,你也看不到他们的嘟文。",
   "block_modal.they_will_know": "他们将能看到他们被屏蔽。",
   "block_modal.title": "是否屏蔽该用户?",
-  "block_modal.you_wont_see_mentions": "你将无法看到提及他们的嘟文。",
+  "block_modal.you_wont_see_mentions": "你将不会看到提及他们的嘟文。",
   "boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
   "boost_modal.reblog": "是否转嘟?",
   "boost_modal.undo_reblog": "是否取消转嘟?",
@@ -780,6 +780,7 @@
   "status.bookmark": "添加到书签",
   "status.cancel_reblog_private": "取消转贴",
   "status.cannot_reblog": "这条嘟文不允许被转嘟",
+  "status.continued_thread": "继续线程",
   "status.copy": "复制嘟文链接",
   "status.delete": "删除",
   "status.detailed_status": "详细的对话视图",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "没有人转嘟过此条嘟文。如果有人转嘟了,就会显示在这里。",
   "status.redraft": "删除并重新编辑",
   "status.remove_bookmark": "移除书签",
+  "status.replied_in_thread": "已在线程中回复",
   "status.replied_to": "回复给 {name}",
   "status.reply": "回复",
   "status.replyAll": "回复所有人",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 257bec016..e67180357 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -780,6 +780,7 @@
   "status.bookmark": "書籤",
   "status.cancel_reblog_private": "取消轉嘟",
   "status.cannot_reblog": "這則嘟文無法被轉嘟",
+  "status.continued_thread": "接續討論串",
   "status.copy": "複製嘟文連結",
   "status.delete": "刪除",
   "status.detailed_status": "詳細的對話內容",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "還沒有人轉嘟過這則嘟文。當有人轉嘟時,它將於此顯示。",
   "status.redraft": "刪除並重新編輯",
   "status.remove_bookmark": "移除書籤",
+  "status.replied_in_thread": "於討論串中回覆",
   "status.replied_to": "回覆 {name}",
   "status.reply": "回覆",
   "status.replyAll": "回覆討論串",
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 8918228f4..121266916 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -25,7 +25,7 @@ ca:
   admin:
     account_actions:
       action: Realitza l'acció
-      already_silenced: Aquest compte ja s'ha silenciat.
+      already_silenced: Aquest compte ja s'ha limitat.
       already_suspended: Aquest compte ja s'ha suspès.
       title: Fer l'acció de moderació a %{acct}
     account_moderation_notes:
@@ -61,6 +61,7 @@ ca:
       demote: Degrada
       destroyed_msg: Les dades de %{username} son a la cua per a ser esborrades en breu
       disable: Inhabilita
+      disable_sign_in_token_auth: Desactivar l'autenticació de token per correu-e
       disable_two_factor_authentication: Desactiva 2FA
       disabled: Inhabilitat
       display_name: Nom visible
@@ -69,6 +70,7 @@ ca:
       email: Adreça electrònica
       email_status: Estat de l'adreça electrònica
       enable: Habilita
+      enable_sign_in_token_auth: Activar l'autenticació de token per correu-e
       enabled: Habilitat
       enabled_msg: El compte de %{username} s’ha descongelat amb èxit
       followers: Seguidors
@@ -201,8 +203,10 @@ ca:
         destroy_user_role: Destrueix Rol
         disable_2fa_user: Desactiva 2FA
         disable_custom_emoji: Desactiva l'emoji personalitzat
+        disable_sign_in_token_auth_user: Desactivar l'autenticació de token per correu-e per a l'usuari
         disable_user: Deshabilita l'usuari
         enable_custom_emoji: Activa l'emoji personalitzat
+        enable_sign_in_token_auth_user: Activar l'autenticació de token per correu-e per a l'usuari
         enable_user: Activa l'usuari
         memorialize_account: Memoritza el compte
         promote_user: Promou l'usuari
@@ -237,17 +241,21 @@ ca:
         confirm_user_html: "%{name} ha confirmat l'adreça del correu electrònic de l'usuari %{target}"
         create_account_warning_html: "%{name} ha enviat un avís a %{target}"
         create_announcement_html: "%{name} ha creat un nou anunci %{target}"
+        create_canonical_email_block_html: "%{name} ha blocat l'adreça de correu electrònic amb el hash %{target}"
         create_custom_emoji_html: "%{name} ha pujat un emoji nou %{target}"
         create_domain_allow_html: "%{name} ha permès la federació amb el domini %{target}"
         create_domain_block_html: "%{name} ha bloquejat el domini %{target}"
+        create_email_domain_block_html: "%{name} ha blocat el domini de correu electrònic %{target}"
         create_ip_block_html: "%{name} ha creat una regla per a l'IP %{target}"
         create_unavailable_domain_html: "%{name} ha aturat el lliurament al domini %{target}"
         create_user_role_html: "%{name} ha creat el rol %{target}"
         demote_user_html: "%{name} ha degradat l'usuari %{target}"
         destroy_announcement_html: "%{name} ha eliminat l'anunci %{target}"
+        destroy_canonical_email_block_html: "%{name} ha desblocat el correu electrònic amb el hash %{target}"
         destroy_custom_emoji_html: "%{name} ha esborrat l'emoji %{target}"
         destroy_domain_allow_html: "%{name} no permet la federació amb el domini %{target}"
         destroy_domain_block_html: "%{name} ha desbloquejat el domini %{target}"
+        destroy_email_domain_block_html: "%{name} ha desblocat el domini de correu electrònic %{target}"
         destroy_instance_html: "%{name} ha purgat el domini %{target}"
         destroy_ip_block_html: "%{name} ha esborrat la regla per a l'IP %{target}"
         destroy_status_html: "%{name} ha eliminat el tut de %{target}"
@@ -255,8 +263,10 @@ ca:
         destroy_user_role_html: "%{name} ha esborrat el rol %{target}"
         disable_2fa_user_html: "%{name} ha desactivat el requisit de dos factors per a l'usuari %{target}"
         disable_custom_emoji_html: "%{name} ha desactivat l'emoji %{target}"
+        disable_sign_in_token_auth_user_html: "%{name} ha desactivat l'autenticació de token per correu-e per a %{target}"
         disable_user_html: "%{name} ha desactivat l'accés del usuari %{target}"
         enable_custom_emoji_html: "%{name} ha activat l'emoji %{target}"
+        enable_sign_in_token_auth_user_html: "%{name} ha activat l'autenticació de token per correu-e per a %{target}"
         enable_user_html: "%{name} ha activat l'accés del usuari %{target}"
         memorialize_account_html: "%{name} ha convertit el compte %{target} en una pàgina de memorial"
         promote_user_html: "%{name} ha promogut l'usuari %{target}"
@@ -264,6 +274,7 @@ ca:
         reject_user_html: "%{name} ha rebutjat el registre de %{target}"
         remove_avatar_user_html: "%{name} ha eliminat l'avatar de %{target}"
         reopen_report_html: "%{name} ha reobert l'informe %{target}"
+        resend_user_html: "%{name} ha reenviat el correu-e de confirmació per %{target}"
         reset_password_user_html: "%{name} ha restablert la contrasenya de l'usuari %{target}"
         resolve_report_html: "%{name} ha resolt l'informe %{target}"
         sensitive_account_html: "%{name} ha marcat els mèdia de %{target} com a sensibles"
@@ -432,6 +443,7 @@ ca:
       new:
         create: Afegir un domini
         resolve: Resol domini
+        title: Blocar el nou domini de correu-e
       not_permitted: No permés
       resolved_through_html: Resolt mitjançant %{domain}
     export_domain_allows:
@@ -1402,6 +1414,7 @@ ca:
     authentication_methods:
       otp: aplicació d'autenticació de dos factors
       password: contrasenya
+      sign_in_token: codi de seguretat per correu electrònic
       webauthn: claus de seguretat
     description_html: Si veus activitat que no reconeixes, considera canviar la teva contrasenya i activar l'autenticació de dos factors.
     empty: Historial d'autenticació no disponible
@@ -1412,6 +1425,16 @@ ca:
     unsubscribe:
       action: Sí, canceŀla la subscripció
       complete: Subscripció cancel·lada
+      confirmation_html: Segur que vols donar-te de baixa de rebre %{type} de Mastodon a %{domain} a %{email}? Sempre pots subscriure't de nou des de la <a href="%{settings_path}">configuració de les notificacions per correu electrònic</a>.
+      emails:
+        notification_emails:
+          favourite: notificacions dels favorits per correu electrònic
+          follow: notificacions dels seguiments per correu electrònic
+          follow_request: correus electrònics de peticions de seguiment
+          mention: correus electrònics de notificacions de mencions
+          reblog: correus electrònics de notificacions d'impulsos
+      resubscribe_html: Si ets dones de baixa per error pots donar-te d'alta des de la <a href="%{settings_path}">configuració de les notificacions per correu electrònic</a>.
+      success_html: Ja no rebràs %{type} de Mastodon a %{domain} a %{email}.
       title: Cancel·la la subscripció
   media_attachments:
     validations:
@@ -1493,6 +1516,8 @@ ca:
     update:
       subject: "%{name} ha editat una publicació"
   notifications:
+    administration_emails: Notificacions per correu-e de l'administrador
+    email_events: Esdeveniments per a notificacions de correu electrònic
     email_events_hint: 'Selecciona els esdeveniments per als quals vols rebre notificacions:'
   number:
     human:
@@ -1651,6 +1676,7 @@ ca:
     import: Importació
     import_and_export: Importació i exportació
     migrate: Migració del compte
+    notifications: Notificacions per correu electrònic
     preferences: Preferències
     profile: Perfil
     relationships: Seguits i seguidors
@@ -1897,6 +1923,7 @@ ca:
     invalid_otp_token: El codi de dos factors no és correcte
     otp_lost_help_html: Si has perdut l'accés a tots dos pots contactar per %{email}
     rate_limited: Excessius intents d'autenticació, torneu-hi més tard.
+    seamless_external_login: Has iniciat sessió via un servei extern. Així, els ajustos de contrasenya i correu electrònic no estan disponibles.
     signed_in_as: 'Sessió iniciada com a:'
   verification:
     extra_instructions_html: <strong>Consell:</strong> l'enllaç al vostre lloc web pot ser invisible. La part important és <code>rel="me"</code> que evita que us suplantin la identitat a llocs web amb contingut generat pels usuaris. Fins i tot podeu generar una etiqueta <code>link</code> a la capçalera de la pàgina en comptes d'una <code>a</code>, però el codi HTML ha de ser accessible sense requerir executar JavaScript.
diff --git a/config/locales/cy.yml b/config/locales/cy.yml
index 4a01967e2..b3886ffeb 100644
--- a/config/locales/cy.yml
+++ b/config/locales/cy.yml
@@ -33,7 +33,6 @@ cy:
   admin:
     account_actions:
       action: Cyflawni gweithred
-      already_silenced: Mae'r cyfrif hwn eisoes wedi'i dewi.
       already_suspended: Mae'r cyfrif hwn eisoes wedi'i atal.
       title: Cyflawni gweithred cymedroli ar %{acct}
     account_moderation_notes:
diff --git a/config/locales/da.yml b/config/locales/da.yml
index e3f834345..89430e37e 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -25,7 +25,7 @@ da:
   admin:
     account_actions:
       action: Udfør handling
-      already_silenced: Denne konto er allerede gjort tavs.
+      already_silenced: Denne konto er allerede blevet begrænset.
       already_suspended: Denne konto er allerede suspenderet.
       title: Udfør moderatorhandling på %{acct}
     account_moderation_notes:
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 2952d22d7..975ec517f 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -25,7 +25,6 @@ de:
   admin:
     account_actions:
       action: Aktion ausführen
-      already_silenced: Dieses Konto wurde bereits stummgeschaltet.
       already_suspended: Dieses Konto wurde bereits gesperrt.
       title: "@%{acct} moderieren"
     account_moderation_notes:
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index 520211e2e..bd15c9862 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -25,7 +25,7 @@ es-AR:
   admin:
     account_actions:
       action: Ejecutar acción
-      already_silenced: Esta cuenta ya ha sido limitada.
+      already_silenced: Esta cuenta ya fue limitada.
       already_suspended: Esta cuenta ya ha sido suspendida.
       title: Ejecutar acción de moderación en %{acct}
     account_moderation_notes:
diff --git a/config/locales/et.yml b/config/locales/et.yml
index bbd1b4ab2..d2a4bc605 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -25,7 +25,6 @@ et:
   admin:
     account_actions:
       action: Täida tegevus
-      already_silenced: See konto on juba vaigistatud.
       already_suspended: See konto on juba peatatud.
       title: Rakenda moderaatori tegevus kasutajale %{acct}
     account_moderation_notes:
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 0c5d5ef98..c94f04a9b 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -981,7 +981,7 @@ fi:
         used_by_over_week:
           one: Käyttänyt yksi käyttäjä viimeisen viikon aikana
           other: Käyttänyt %{count} käyttäjää viimeisen viikon aikana
-      title: Suositukset ja suuntaukset
+      title: Suositukset ja trendit
       trending: Trendaus
     warning_presets:
       add_new: Lisää uusi
diff --git a/config/locales/fo.yml b/config/locales/fo.yml
index 040e312d7..f3d9aee4c 100644
--- a/config/locales/fo.yml
+++ b/config/locales/fo.yml
@@ -25,7 +25,6 @@ fo:
   admin:
     account_actions:
       action: Frem atgerð
-      already_silenced: Hendan kontan er longu gjørd kvirr.
       already_suspended: Hendan kontan er longu ógildað.
       title: Frem umsjónaratgerð á %{acct}
     account_moderation_notes:
diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml
index dd1f73c45..766ba71d8 100644
--- a/config/locales/fr-CA.yml
+++ b/config/locales/fr-CA.yml
@@ -25,7 +25,6 @@ fr-CA:
   admin:
     account_actions:
       action: Effectuer l'action
-      already_silenced: Ce compte est déjà limité.
       already_suspended: Ce compte est déjà suspendu.
       title: Effectuer une action de modération sur %{acct}
     account_moderation_notes:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 7e30b517a..420a4d314 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -25,7 +25,6 @@ fr:
   admin:
     account_actions:
       action: Effectuer l'action
-      already_silenced: Ce compte est déjà limité.
       already_suspended: Ce compte est déjà suspendu.
       title: Effectuer une action de modération sur %{acct}
     account_moderation_notes:
diff --git a/config/locales/ga.yml b/config/locales/ga.yml
index 4ea9cef73..d2783a5a6 100644
--- a/config/locales/ga.yml
+++ b/config/locales/ga.yml
@@ -31,7 +31,6 @@ ga:
   admin:
     account_actions:
       action: Déan gníomh
-      already_silenced: Tá an cuntas seo ina thost cheana féin.
       already_suspended: Tá an cuntas seo curtha ar fionraí cheana féin.
       title: Dean gníomh modhnóireachta ar %{acct}
     account_moderation_notes:
diff --git a/config/locales/gd.yml b/config/locales/gd.yml
index f824e3081..c08ba6021 100644
--- a/config/locales/gd.yml
+++ b/config/locales/gd.yml
@@ -29,7 +29,6 @@ gd:
   admin:
     account_actions:
       action: Gabh an gnìomh
-      already_silenced: Chaidh an cunntas seo a chuingeachadh mu thràth.
       already_suspended: Chaidh an cunntas seo a chur à rèim mu thràth.
       title: Gabh gnìomh maorsainneachd air %{acct}
     account_moderation_notes:
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index de4840dda..657908922 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -25,7 +25,7 @@ gl:
   admin:
     account_actions:
       action: Executar acción
-      already_silenced: Esta conta xa está silenciada.
+      already_silenced: A conta xa está limitada
       already_suspended: Esta conta xa está suspendida.
       title: Executar acción de moderación a %{acct}
     account_moderation_notes:
@@ -1854,7 +1854,7 @@ gl:
     failed_2fa:
       details: 'Detalles do intento de acceso:'
       explanation: Alguén intentou acceder á túa conta mais fíxoo cun segundo factor de autenticación non válido.
-      further_actions_html: Se non foches ti, recomendámosche %{action} inmediatamente xa que a conta podería estar en risco.
+      further_actions_html: Se non foches ti, recomendámosche %{action} inmediatamente porque a conta podería estar en risco.
       subject: Fallo co segundo factor de autenticación
       title: Fallou o segundo factor de autenticación
     suspicious_sign_in:
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 47ec5cafb..a838c6963 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -29,6 +29,7 @@ he:
   admin:
     account_actions:
       action: בצע/י פעולה
+      already_suspended: חשבון זה הושעה.
       title: ביצוע פעולות הנהלה על %{acct}
     account_moderation_notes:
       create: ליצור
@@ -50,6 +51,7 @@ he:
         title: שינוי כתובת דוא"ל עבור המשתמש.ת %{username}
       change_role:
         changed_msg: התפקיד שונה בהצלחה!
+        edit_roles: נהל תפקידי משתמש
         label: שינוי תפקיד
         no_role: ללא תפקיד
         title: שינוי תפקיד עבור %{username}
@@ -626,6 +628,7 @@ he:
         suspend_description_html: חשבון זה על כל תכניו יחסמו וברבות הימים ימחקו, כל פעילות מולו לא תתאפשר. הפעולה ניתנת לביטול תוך 30 ימים, והיא תסגור כל דיווח התלוי ועומד נגד החשבון.
       actions_description_html: בחר/י איזו פעולה לבצע על מנת לפתור את הדו"ח. אם תופעל פעולת ענישה כנגד החשבון המדווח, הודעת דוא"ל תשלח אליהם, אלא אם נבחרה קטגוריית ה<strong>ספאם</strong>.
       actions_description_remote_html: בחרו איזו פעולה לבצע כדי לפתור את הדיווח שהוגש. פעולה זו תשפיע רק על התקשורת מול השרת <strong>שלך</strong> עם החשבון המרוחק ותוכנו.
+      actions_no_posts: דווח זה לא כולל הודעות למחיקה
       add_to_report: הוספת פרטים לדיווח
       already_suspended_badges:
         local: כבר הודח בשרת זה
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 60fb96a12..71af13830 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -25,7 +25,6 @@ hu:
   admin:
     account_actions:
       action: Művelet végrehajtása
-      already_silenced: Ezt a fiókot már elnémították.
       already_suspended: Ezt a fiókot már felfüggesztették.
       title: 'Moderálási művelet végrehajtása ezen: %{acct}'
     account_moderation_notes:
diff --git a/config/locales/ia.yml b/config/locales/ia.yml
index 683edbe7c..8827e084d 100644
--- a/config/locales/ia.yml
+++ b/config/locales/ia.yml
@@ -25,7 +25,6 @@ ia:
   admin:
     account_actions:
       action: Exequer action
-      already_silenced: Iste conto jam ha essite silentiate.
       already_suspended: Iste conto jam ha essite suspendite.
       title: Exequer action de moderation sur %{acct}
     account_moderation_notes:
diff --git a/config/locales/is.yml b/config/locales/is.yml
index 0854d8812..748d931ff 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -25,7 +25,6 @@ is:
   admin:
     account_actions:
       action: Framkvæma aðgerð
-      already_silenced: Þessi aðgangur hefur þegar verið þaggaður.
       already_suspended: Þessi aðgangur hefur þegar verið settur í frysti.
       title: Framkvæma umsjónaraðgerð á %{acct}
     account_moderation_notes:
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 66a462e61..a1ed71a7a 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -25,7 +25,7 @@ it:
   admin:
     account_actions:
       action: Esegui azione
-      already_silenced: Questo account è già stato silenziato.
+      already_silenced: Questo account è già stato limitato.
       already_suspended: Questo account è già stato sospeso.
       title: Esegui l'azione di moderazione su %{acct}
     account_moderation_notes:
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 9bec26c45..962ecdd05 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -23,7 +23,6 @@ ko:
   admin:
     account_actions:
       action: 조치 취하기
-      already_silenced: 이 계정은 이미 침묵되었습니다.
       already_suspended: 이 계정은 이미 정지되었습니다.
       title: "%{acct} 계정에 중재 취하기"
     account_moderation_notes:
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 0fd71f52e..c48b8ef93 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -29,6 +29,7 @@ lt:
   admin:
     account_actions:
       action: Atlikti veiksmą
+      already_suspended: Ši paskyra jau sustabdyta.
       title: Atlikti prižiūrėjimo veiksmą %{acct}
     account_moderation_notes:
       create: Palikti pastabą
@@ -49,6 +50,7 @@ lt:
         title: Keisti el. paštą %{username}
       change_role:
         changed_msg: Vaidmuo sėkmingai pakeistas.
+        edit_roles: Tvarkyti naudotojų vaidmenis
         label: Keisti vaidmenį
         no_role: Jokios vaidmenį
         title: Keisti vaidmenį %{username}
@@ -485,6 +487,7 @@ lt:
       destroyed_msg: Skundo žinutė sekmingai ištrinta!
     reports:
       action_taken_by: Veiksmo ėmėsi
+      actions_no_posts: Ši ataskaita neturi jokių susijusių įrašų ištrinti
       already_suspended_badges:
         local: Jau sustabdytas šiame serveryje
         remote: Jau sustabdytas jų serveryje
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index f301b8ca9..f7c2f7426 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -25,6 +25,7 @@ nn:
   admin:
     account_actions:
       action: Utfør
+      already_silenced: Denne kontoen har allereie vorte avgrensa.
       title: Utfør moderatorhandling på %{acct}
     account_moderation_notes:
       create: Legg igjen merknad
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 2a1a0c8d7..7a8d208b0 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -29,7 +29,6 @@ pl:
   admin:
     account_actions:
       action: Wykonaj działanie
-      already_silenced: To konto zostało już wyciszone.
       already_suspended: To konto zostało już zawieszone.
       title: Wykonaj działanie moderacyjne na %{acct}
     account_moderation_notes:
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index 1bd724595..96cb92efd 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -4,8 +4,8 @@ pt-PT:
     about_mastodon_html: 'A rede social do futuro: sem publicidade, e sem vigilância empresarial; desenho ético, e descentralizado! Tome posse dos seus dados com o Mastodon!'
     contact_missing: Por definir
     contact_unavailable: n.d.
-    hosted_on: Mastodon em %{domain}
-    title: Acerca de
+    hosted_on: Mastodon alojado em %{domain}
+    title: Sobre
   accounts:
     follow: Seguir
     followers:
@@ -25,15 +25,17 @@ pt-PT:
   admin:
     account_actions:
       action: Executar acção
+      already_suspended: Esta conta já foi suspensa.
       title: Executar ação de moderação em %{acct}
     account_moderation_notes:
       create: Deixar uma nota
       created_msg: Nota de moderação criada com sucesso!
       destroyed_msg: Nota de moderação destruída!
     accounts:
+      add_email_domain_block: Bloquear domínio de e-mail
       approve: Aprovar
       approved_msg: Inscrição de %{username} aprovada com sucesso
-      are_you_sure: Tens a certeza?
+      are_you_sure: Tem a certeza?
       avatar: Imagem de perfil
       by_domain: Domínio
       change_email:
@@ -45,18 +47,20 @@ pt-PT:
         title: Alterar e-mail para %{username}
       change_role:
         changed_msg: Função alterada com sucesso!
+        edit_roles: Gerir funções de utilizador
         label: Alterar função
         no_role: Nenhuma função
         title: Alterar a função de %{username}
       confirm: Confirmar
       confirmed: Confirmado
       confirming: A confirmar
-      custom: Personalizar
+      custom: Personalizado
       delete: Eliminar dados
       deleted: Eliminada
-      demote: Despromoveu
+      demote: Despromovida
       destroyed_msg: Os dados de %{username} estão agora em fila de espera para serem eliminados de imediato
       disable: Congelar
+      disable_sign_in_token_auth: Desativar token de autenticação por e-mail
       disable_two_factor_authentication: Desativar autenticação por dois fatores (2FA)
       disabled: Congelada
       display_name: Nome a mostrar
@@ -65,6 +69,7 @@ pt-PT:
       email: E-mail
       email_status: Estado do e-mail
       enable: Descongelar
+      enable_sign_in_token_auth: Ativar token de autenticação por e-mail
       enabled: Ativado
       enabled_msg: Descongelou a conta %{username}
       followers: Seguidores
@@ -100,8 +105,8 @@ pt-PT:
       no_limits_imposed: Sem limites impostos
       no_role_assigned: Nenhuma função atribuída
       not_subscribed: Não inscrito
-      pending: Pendente de revisão
-      perform_full_suspension: Fazer suspensão completa
+      pending: Revisão pendente
+      perform_full_suspension: Suspender
       previous_strikes: Reprimendas anteriores
       previous_strikes_description_html:
         one: Esta conta tem <strong>1</strong> reprimenda.
@@ -109,7 +114,7 @@ pt-PT:
       promote: Promover
       protocol: Protocolo
       public: Público
-      push_subscription_expires: A Inscrição PuSH expira
+      push_subscription_expires: A inscrição PuSH expira
       redownload: Atualizar perfil
       redownloaded_msg: Perfil de %{username} atualizado a partir da origem com sucesso
       reject: Rejeitar
@@ -122,13 +127,14 @@ pt-PT:
       removed_header_msg: Imagem de cabeçalho de %{username} removida
       resend_confirmation:
         already_confirmed: Este utilizador já está confirmado
-        send: Reenviar link de confirmação
-        success: Link de confirmação enviado com sucesso!
-      reset: Reiniciar
+        send: Reenviar hiperligação de confirmação
+        success: Hiperligação de confirmação enviada com sucesso!
+      reset: Repor
       reset_password: Criar nova palavra-passe
       resubscribe: Reinscrever
       role: Função
       search: Pesquisar
+      search_same_email_domain: Outros utilizadores com o mesmo domínio de e-mail
       search_same_ip: Outros utilizadores com o mesmo IP
       security: Segurança
       security_measures:
@@ -136,7 +142,7 @@ pt-PT:
         password_and_2fa: Palavra-passe e 2FA
       sensitive: Marcar como problemático
       sensitized: Marcada como problemática
-      shared_inbox_url: URL da caixa de entrada compartilhada
+      shared_inbox_url: URL da caixa de entrada partilhada
       show:
         created_reports: Denúncias realizadas
         targeted_reports: Denunciada por outros
@@ -151,101 +157,116 @@ pt-PT:
       suspension_reversible_hint_html: A conta foi suspensa e os dados serão totalmente eliminados em %{date}. Até lá, a conta poderá ser recuperada sem quaisquer efeitos negativos. Se deseja eliminar todos os dados desta conta imediatamente, pode fazê-lo em baixo.
       title: Contas
       unblock_email: Desbloquear endereço de e-mail
-      unblocked_email_msg: Endereço de e-mail de %{username} desbloqueado
+      unblocked_email_msg: Endereço de e-mail de %{username} desbloqueado com sucesso
       unconfirmed_email: E-mail por confirmar
       undo_sensitized: Desmarcar como problemático
       undo_silenced: Desfazer silenciar
       undo_suspension: Desfazer supensão
-      unsilenced_msg: Removeu as limitações da conta %{username}
+      unsilenced_msg: Limitações da conta %{username} removidas com sucesso
       unsubscribe: Cancelar inscrição
       unsuspended_msg: Removeu a suspensão da conta %{username}
       username: Nome de utilizador
       view_domain: Ver resumo do domínio
       warn: Advertir
-      web: Teia
+      web: Web
       whitelisted: Permitido para a federação
     action_logs:
       action_types:
         approve_appeal: Aprovar recurso
         approve_user: Aprovar utilizador
-        assigned_to_self_report: Atribuir Denúncia
-        change_role_user: Alterar Função do Utilizador
-        confirm_user: Confirmar Utilizador
-        create_account_warning: Criar Aviso
+        assigned_to_self_report: Atribuir denúncia
+        change_email_user: Alterar e-mail do utilizador
+        change_role_user: Alterar função do utilizador
+        confirm_user: Confirmar utilizador
+        create_account_warning: Criar aviso
         create_announcement: Criar comunicado
-        create_custom_emoji: Criar Emoji Personalizado
-        create_domain_allow: Criar Permissão de Domínio
-        create_domain_block: Criar Bloqueio de Domínio
+        create_canonical_email_block: Criar bloqueio de e-mail
+        create_custom_emoji: Criar emoji personalizado
+        create_domain_allow: Criar permissão de domínio
+        create_domain_block: Criar bloqueio de domínio
+        create_email_domain_block: Criar bloqueio de domínio de e-mail
         create_ip_block: Criar regra de IP
-        create_unavailable_domain: Criar Domínio Indisponível
-        create_user_role: Criar Função
-        demote_user: Despromover Utilizador
-        destroy_announcement: Apagar comunicado
-        destroy_custom_emoji: Eliminar Emoji Personalizado
-        destroy_domain_allow: Eliminar Permissão de Domínio
-        destroy_domain_block: Eliminar Bloqueio de Domínio
-        destroy_instance: Purgar Domínio
+        create_unavailable_domain: Criar domínio indisponível
+        create_user_role: Criar função
+        demote_user: Despromover utilizador
+        destroy_announcement: Eliminar comunicado
+        destroy_canonical_email_block: Eliminar bloqueio de e-mail
+        destroy_custom_emoji: Eliminar emoji personalizado
+        destroy_domain_allow: Eliminar permissão de domínio
+        destroy_domain_block: Eliminar bloqueio de domínio
+        destroy_email_domain_block: Eliminar bloqueio de domínio de e-mail
+        destroy_instance: Purgar domínio
         destroy_ip_block: Eliminar regra de IP
-        destroy_status: Eliminar Publicação
-        destroy_unavailable_domain: Eliminar Domínio Indisponível
-        destroy_user_role: Eliminar Função
+        destroy_status: Eliminar publicação
+        destroy_unavailable_domain: Eliminar domínio indisponível
+        destroy_user_role: Eliminar função
         disable_2fa_user: Desativar 2FA
-        disable_custom_emoji: Desativar Emoji Personalizado
-        disable_user: Desativar Utilizador
-        enable_custom_emoji: Ativar Emoji Personalizado
-        enable_user: Ativar Utilizador
-        memorialize_account: Tornar conta num memorial
-        promote_user: Promover Utilizador
-        reject_appeal: Rejeitar Recurso
-        reject_user: Rejeitar Utilizador
-        remove_avatar_user: Remover Imagem de Perfil
-        reopen_report: Reabrir Denúncia
-        resend_user: Reenviar E-mail de Confirmação
-        reset_password_user: Repor Password
-        resolve_report: Resolver Denúncia
+        disable_custom_emoji: Desativar emoji personalizado
+        disable_sign_in_token_auth_user: Desativar token de autenticação por e-mail para o utilizador
+        disable_user: Desativar utilizador
+        enable_custom_emoji: Ativar emoji personalizado
+        enable_sign_in_token_auth_user: Ativar token de autenticação por e-mail para o utilizador
+        enable_user: Ativar utilizador
+        memorialize_account: Transformar conta num memorial
+        promote_user: Promover utilizador
+        reject_appeal: Rejeitar recurso
+        reject_user: Rejeitar utilizador
+        remove_avatar_user: Remover imagem de perfil
+        reopen_report: Reabrir denúncia
+        resend_user: Reenviar e-mail de confirmação
+        reset_password_user: Repor palavra-passe
+        resolve_report: Resolver denúncia
         sensitive_account: Marcar a media na sua conta como problemática
         silence_account: Limitar conta
         suspend_account: Suspender conta
-        unassigned_report: Desatribuir Denúncia
+        unassigned_report: Anular atribuição desta denúncia
         unblock_email_account: Desbloquear endereço de e-mail
         unsensitive_account: Desmarcar a conta como problemática
-        unsilence_account: Deixar de Silenciar Conta
-        unsuspend_account: Retirar Suspensão à Conta
+        unsilence_account: Deixar de silenciar conta
+        unsuspend_account: Retirar suspensão da conta
         update_announcement: Atualizar comunicado
-        update_custom_emoji: Atualizar Emoji Personalizado
-        update_domain_block: Atualizar Bloqueio de Domínio
+        update_custom_emoji: Atualizar emoji personalizado
+        update_domain_block: Atualizar bloqueio de domínio
         update_ip_block: Atualizar regra de IP
-        update_report: Atualizar Relatório
-        update_status: Atualizar Estado
-        update_user_role: Atualizar Função
+        update_report: Atualizar denúncia
+        update_status: Atualizar publicação
+        update_user_role: Atualizar função
       actions:
         approve_appeal_html: "%{name} aprovou recurso da decisão de moderação de %{target}"
         approve_user_html: "%{name} aprovou a inscrição de %{target}"
         assigned_to_self_report_html: "%{name} atribuiu a denúncia %{target} a si próprio"
+        change_email_user_html: "%{name} alterou o endereço de e-mail do utilizador %{target}"
         change_role_user_html: "%{name} alterou a função de %{target}"
+        confirm_user_html: "%{name} confirmou o endereço de e-mail do utilizador %{target}"
         create_account_warning_html: "%{name} enviou um aviso para %{target}"
         create_announcement_html: "%{name} criou o novo anúncio %{target}"
-        create_custom_emoji_html: "%{name} carregou o novo emoji %{target}"
+        create_canonical_email_block_html: "%{name} bloqueou o e-mail com a hash %{target}"
+        create_custom_emoji_html: "%{name} enviou o novo emoji %{target}"
         create_domain_allow_html: "%{name} permitiu a federação com o domínio %{target}"
         create_domain_block_html: "%{name} bloqueou o domínio %{target}"
-        create_ip_block_html: "%{name} criou regra para o IP %{target}"
-        create_unavailable_domain_html: "%{name} parou a entrega ao domínio %{target}"
+        create_email_domain_block_html: "%{name} bloqueou o domínio de e-mail %{target}"
+        create_ip_block_html: "%{name} criou uma regra para o IP %{target}"
+        create_unavailable_domain_html: "%{name} parou as entregas ao domínio %{target}"
         create_user_role_html: "%{name} criou a função %{target}"
         demote_user_html: "%{name} despromoveu o utilizador %{target}"
         destroy_announcement_html: "%{name} eliminou o anúncio %{target}"
+        destroy_canonical_email_block_html: "%{name} desbloqueou o e-mail com a hash %{target}"
         destroy_custom_emoji_html: "%{name} eliminou o emoji %{target}"
-        destroy_domain_allow_html: "%{name} desabilitou a federação com o domínio %{target}"
+        destroy_domain_allow_html: "%{name} bloqueou a federação com o domínio %{target}"
         destroy_domain_block_html: "%{name} desbloqueou o domínio %{target}"
+        destroy_email_domain_block_html: "%{name} desbloqueou o domínio de e-mail %{target}"
         destroy_instance_html: "%{name} purgou o domínio %{target}"
-        destroy_ip_block_html: "%{name} eliminou regra para o IP %{target}"
+        destroy_ip_block_html: "%{name} eliminou a regra para o IP %{target}"
         destroy_status_html: "%{name} removeu a publicação de %{target}"
-        destroy_unavailable_domain_html: "%{name} retomou a entrega ao domínio %{target}"
+        destroy_unavailable_domain_html: "%{name} retomou as entregas ao domínio %{target}"
         destroy_user_role_html: "%{name} eliminou a função %{target}"
         disable_2fa_user_html: "%{name} desativou o requerimento de autenticação em dois passos para o utilizador %{target}"
-        disable_custom_emoji_html: "%{name} desabilitou o emoji %{target}"
-        disable_user_html: "%{name} desativou o acesso para o utilizador %{target}"
+        disable_custom_emoji_html: "%{name} desativou o emoji %{target}"
+        disable_sign_in_token_auth_user_html: "%{name} desativou o token de autenticação por e-mail para %{target}"
+        disable_user_html: "%{name} desativou o início de sessão para o utilizador %{target}"
         enable_custom_emoji_html: "%{name} ativou o emoji %{target}"
-        enable_user_html: "%{name} ativou o acesso para o utilizador %{target}"
+        enable_sign_in_token_auth_user_html: "%{name} ativou o token de autenticação por e-mail para %{target}"
+        enable_user_html: "%{name} ativou o início de sessão para o utilizador %{target}"
         memorialize_account_html: "%{name} transformou a conta de %{target} em um memorial"
         promote_user_html: "%{name} promoveu o utilizador %{target}"
         reject_appeal_html: "%{name} rejeitou recurso da decisão de moderação de %{target}"
@@ -352,7 +373,7 @@ pt-PT:
       title: Painel de controlo
       top_languages: Principais idiomas ativos
       top_servers: Servidores mais ativos
-      website: Página na teia
+      website: Website
     disputes:
       appeals:
         empty: Nenhum recurso encontrado.
@@ -669,39 +690,39 @@ pt-PT:
       privileges:
         administrator: Administrador
         administrator_description: Utilizadores com esta permissão irão contornar todas as permissões
-        delete_user_data: Eliminar Dados de Utilizador
+        delete_user_data: Eliminar dados de utilizador
         delete_user_data_description: Permite que os utilizadores eliminem os dados doutros utilizadores sem tempo de espera
-        invite_users: Convidar Utilizadores
+        invite_users: Convidar utilizadores
         invite_users_description: Permite aos utilizadores convidar pessoas novas para o servidor
         manage_announcements: Gerir comunicados
         manage_announcements_description: Permite aos utilizadores gerirem os comunicados no servidor
         manage_appeals: Gerir apelos
         manage_appeals_description: Permite aos utilizadores rever recursos de moderação
-        manage_blocks: Gerir Bloqueios
-        manage_custom_emojis: Gerir Emojis Personalizados
+        manage_blocks: Gerir bloqueios
+        manage_custom_emojis: Gerir emojis personalizados
         manage_custom_emojis_description: Permite aos utilizadores gerirem os emojis personalizados do servidor
-        manage_federation: Gerir Federação
+        manage_federation: Gerir federação
         manage_federation_description: Permite aos utilizadores bloquear ou permitir federação com outros domínios e controlar a entregabilidade
-        manage_invites: Gerir Convites
+        manage_invites: Gerir convites
         manage_invites_description: Permite aos utilizadores pesquisarem e desativarem ligações de convite
-        manage_reports: Gerir Relatórios
-        manage_reports_description: Permite aos utilizadores rever relatórios e executar ações de moderação contra eles
-        manage_roles: Gerir Funções
+        manage_reports: Gerir denúncias
+        manage_reports_description: Permite aos utilizadores rever denúncias e executar ações de moderação contra eles
+        manage_roles: Gerir funções
         manage_roles_description: Permite aos utilizadores a gestão e atribuição de funções abaixo dos seus
-        manage_rules: Gerir Regras
+        manage_rules: Gerir regras
         manage_rules_description: Permite aos utilizadores alterar as regras do servidor
-        manage_settings: Gerir Configurações
-        manage_settings_description: Permite aos utilizadores alterar as configurações do sítio na teia
-        manage_taxonomies: Gerir Taxonomias
+        manage_settings: Gerir configurações
+        manage_settings_description: Permite aos utilizadores alterar as configurações do site
+        manage_taxonomies: Gerir taxonomias
         manage_taxonomies_description: Permite aos utilizadores rever o conteúdo em tendência e atualizar as configurações de hashtag
-        manage_user_access: Gerir Acesso de Utilizador
-        manage_users: Gerir Utilizadores
+        manage_user_access: Gerir acesso de utilizador
+        manage_users: Gerir utilizadores
         manage_users_description: Permite aos utilizadores ver os detalhes de outros utilizadores e executar ações de moderação contra eles
-        manage_webhooks: Gerir Webhooks
+        manage_webhooks: Gerir webhooks
         manage_webhooks_description: Permite aos utilizadores configurar webhooks para eventos administrativos
-        view_audit_log: Ver Registo de Auditoria
+        view_audit_log: Ver registo de auditoria
         view_audit_log_description: Permite aos utilizadores ver um histórico de ações administrativas no servidor
-        view_dashboard: Ver Painel de Controlo
+        view_dashboard: Ver painel de controlo
         view_dashboard_description: Permite aos utilizadores acederem ao painel de controlo e a várias estatísticas
         view_devops: DevOps
         view_devops_description: Permite aos utilizadores aceder aos painéis de controlo do Sidekiq e pgHero
@@ -723,14 +744,14 @@ pt-PT:
         preamble: Personalize a interface web do Mastodon.
         title: Aspeto
       branding:
-        preamble: A marca do seu servidor diferencia-a doutros servidores na rede. Essa informação pode ser exibida em vários contexos, como a interface na teia do Mastodon, aplicações nativas, visualizações de hiperligações noutros sites, em aplicações de mensagens, etc. Por esta razão, é melhor manter esta informação clara, curta e concisa.
+        preamble: A marca do seu servidor diferencia-a de outros servidores na rede. Essa informação pode ser mostrada em vários ambientes, como a interface web do Mastodon, aplicações nativas, visualizações de hiperligações em outros sites e dentro de aplicações de mensagens, etc. Por esta razão, é melhor manter esta informação clara, curta e concisa.
         title: Marca
       captcha_enabled:
         desc_html: Isto depende de scripts externos da hCaptcha, o que pode ser uma preocupação de segurança e privacidade. Além disso, <strong>isto pode tornar o processo de registo menos acessível para algumas pessoas (especialmente as com limitações físicas)</strong>. Por isso, considere medidas alternativas tais como registo mediante aprovação ou sob convite.
         title: Requerer que novos utilizadores resolvam um CAPTCHA para confirmar a sua conta
       content_retention:
         danger_zone: Zona de perigo
-        preamble: Controle como o conteúdo gerado pelos utilizadores é armazenado no Mastodon.
+        preamble: Controle a forma como o conteúdo gerado pelo utilizador é armazenado no Mastodon.
         title: Retenção de conteúdo
       default_noindex:
         desc_html: Afeta todos os utilizadores que não alteraram esta configuração
@@ -1198,7 +1219,7 @@ pt-PT:
       content: Desculpe, mas algo correu mal da nossa parte.
       title: Esta página não está correta
     '503': A página não pôde ser apresentada devido a uma falha temporária do servidor.
-    noscript_html: Para usar a aplicação da teia do Mastodon, por favor active o JavaScript. Em alternativa, experimenta uma das <a href="%{apps_path}">aplicações nativas</a> do Mastodon para a sua plataforma.
+    noscript_html: Para usar a aplicação web do Mastodon, ative o JavaScript. Alternativamente, experimente uma das <a href="%{apps_path}">aplicações nativas</a> para o Mastodon na sua plataforma.
   existing_username_validator:
     not_found: não foi possível encontrar um utilizador local com esse nome
     not_found_multiple: não foi possível encontrar %{usernames}
@@ -1236,7 +1257,7 @@ pt-PT:
       statuses_hint_html: Este filtro aplica-se a publicações individuais selecionadas independentemente de estas corresponderem às palavras-chave abaixo. <a href="%{path}">Reveja ou remova publicações do filtro</a>.
       title: Editar filtros
     errors:
-      deprecated_api_multiple_keywords: Estes parâmetros não podem ser alterados a partir desta aplicação porque se aplicam a mais que um filtro de palavra-chave. Use uma aplicação mais recente ou a interface na teia.
+      deprecated_api_multiple_keywords: Estes parâmetros não podem ser alterados a partir desta aplicação porque se aplicam a mais de um filtro de palavra-chave. Use uma aplicação mais recente ou a interface web.
       invalid_context: Inválido ou nenhum contexto fornecido
     index:
       contexts: Filtros em %{contexts}
@@ -1664,7 +1685,7 @@ pt-PT:
     edited_at_html: Editado em %{date}
     errors:
       in_reply_not_found: A publicação a que está a tentar responder parece não existir.
-    open_in_web: Abrir na Teia
+    open_in_web: Abrir na web
     over_character_limit: limite de caracter excedeu %{max}
     pin_errors:
       direct: Publicações visíveis apenas para utilizadores mencionados não podem ser afixadas
@@ -1826,7 +1847,7 @@ pt-PT:
     welcome:
       apps_android_action: Baixe no Google Play
       apps_ios_action: Baixar na App Store
-      apps_step: Baixe nossos aplicativos oficiais.
+      apps_step: Descarregue as nossas aplicações oficiais.
       apps_title: Apps Mastodon
       checklist_subtitle: 'Vamos começar nesta nova fronteira social:'
       checklist_title: Checklist de Boas-vindas
@@ -1835,11 +1856,11 @@ pt-PT:
       edit_profile_title: Personalize seu perfil
       explanation: Aqui estão algumas dicas para começar
       feature_action: Mais informações
-      feature_audience: Mastodon oferece uma possibilidade única de gerenciar seu público sem intermediários. O Mastodon implantado em sua própria infraestrutura permite que você siga e seja seguido de qualquer outro servidor Mastodon online e não esteja sob o controle de ninguém além do seu.
+      feature_audience: O Mastodon oferece-lhe uma possibilidade única de gerir a sua audiência sem intermediários. O Mastodon implantado na sua própria infraestrutura permite-lhe seguir e ser seguido a partir de qualquer outro servidor Mastodon online e não está sob o controlo de ninguém a não ser o seu.
       feature_audience_title: Construa seu público em confiança
-      feature_control: Você sabe melhor o que deseja ver no feed da sua casa. Sem algoritmos ou anúncios para desperdiçar seu tempo. Siga qualquer pessoa em qualquer servidor Mastodon a partir de uma única conta e receba suas postagens em ordem cronológica, deixando seu canto da internet um pouco mais parecido com você.
-      feature_control_title: Fique no controle da sua própria linha do tempo
-      feature_creativity: Mastodon suporta postagens de áudio, vídeo e imagens, descrições de acessibilidade, enquetes, avisos de conteúdo, avatares animados, emojis personalizados, controle de corte de miniaturas e muito mais, para ajudá-lo a se expressar online. Esteja você publicando sua arte, sua música ou seu podcast, o Mastodon está lá para você.
+      feature_control: Você sabe melhor o que quer ver no seu feed. Não há algoritmos ou anúncios que o façam perder tempo. Siga qualquer pessoa em qualquer servidor Mastodon a partir de uma única conta e receba as suas mensagens por ordem cronológica e torne o seu canto da Internet um pouco mais parecido consigo.
+      feature_control_title: Mantenha o controlo da sua própria cronologia
+      feature_creativity: O Mastodon suporta publicações de áudio, vídeo e imagens, descrições de acessibilidade, sondagens, avisos de conteúdo, avatares animados, emojis personalizados, controlo de corte de miniaturas e muito mais, para o ajudar a expressar-se online. Quer esteja a publicar a sua arte, a sua música ou o seu podcast, o Mastodon está lá para si.
       feature_creativity_title: Criatividade inigualável
       feature_moderation: Mastodon coloca a tomada de decisões de volta em suas mãos. Cada servidor cria as suas próprias regras e regulamentos, que são aplicados localmente e não de cima para baixo como as redes sociais corporativas, tornando-o mais flexível na resposta às necessidades de diferentes grupos de pessoas. Junte-se a um servidor com as regras com as quais você concorda ou hospede as suas próprias.
       feature_moderation_title: Moderando como deve ser
@@ -1856,7 +1877,7 @@ pt-PT:
       hashtags_title: Etiquetas em tendência
       hashtags_view_more: Ver mais etiquetas em tendência
       post_action: Compor
-      post_step: Diga olá para o mundo com texto, fotos, vídeos ou enquetes.
+      post_step: Diga olá para o mundo com texto, fotos, vídeos ou sondagens.
       post_title: Faça a sua primeira publicação
       share_action: Compartilhar
       share_step: Diga aos seus amigos como te encontrar no Mastodon.
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index f595a3199..26edab3b2 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -130,6 +130,7 @@ he:
         name: ניתן רק להחליף בין אותיות קטנות וגדולות, למשל כדי לשפר את הקריאות
       user:
         chosen_languages: אם פעיל, רק הודעות בשפות הנבחרות יוצגו לפידים הפומביים
+        role: התפקיד שולט על אילו הרשאות יש למשתמש.
       user_role:
         color: צבע לתפקיד בממשק המשתמש, כ RGB בפורמט הקסדצימלי
         highlighted: מאפשר נראות ציבורית של התפקיד
diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml
index ecbf50138..99cb269e3 100644
--- a/config/locales/simple_form.lt.yml
+++ b/config/locales/simple_form.lt.yml
@@ -101,6 +101,7 @@ lt:
         show_application: Neatsižvelgiant į tai, visada galėsi matyti, kuri programėlė paskelbė tavo įrašą.
       user:
         chosen_languages: Kai pažymėta, viešose laiko skalėse bus rodomi tik įrašai pasirinktomis kalbomis.
+        role: Vaidmuo valdo, kokius leidimus naudotojas turi.
     labels:
       account:
         discoverable: Rekomenduoti profilį ir įrašus į atradimo algoritmus
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index 741916989..b113e0eed 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -130,6 +130,7 @@ pl:
         name: Możesz zmieniać tylko wielkość liter, np. aby były bardziej widoczne
       user:
         chosen_languages: Jeżeli zaznaczone, tylko wpisy w wybranych językach będą wyświetlane na publicznych osiach czasu
+        role: Rola kontroluje uprawnienia użytkownika.
       user_role:
         color: Kolor używany dla roli w całym interfejsie użytkownika, wyrażony jako RGB w formacie szesnastkowym
         highlighted: To sprawia, że rola jest widoczna publicznie
diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml
index 971773b2d..3b606df03 100644
--- a/config/locales/simple_form.pt-PT.yml
+++ b/config/locales/simple_form.pt-PT.yml
@@ -77,7 +77,7 @@ pt-PT:
           warn: Ocultar o conteúdo filtrado por trás de um aviso mencionando o título do filtro
       form_admin_settings:
         activity_api_enabled: Contagem, em blocos semanais, de publicações locais, utilizadores ativos e novos registos
-        app_icon: WEBP, PNG, GIF ou JPG. Substitui o ícone padrão do aplicativo em dispositivos móveis por um ícone personalizado.
+        app_icon: WEBP, PNG, GIF ou JPG. Substitui o ícone padrão da aplicação em dispositivos móveis por um ícone personalizado.
         backups_retention_period: Os utilizadores têm a possibilidade de gerar arquivos das suas mensagens para descarregar mais tarde. Quando definido para um valor positivo, estes arquivos serão automaticamente eliminados do seu armazenamento após o número de dias especificado.
         bootstrap_timeline_accounts: Estas contas serão destacadas no topo das recomendações aos novos utilizadores.
         closed_registrations_message: Apresentado quando as inscrições estiverem encerradas
@@ -130,12 +130,13 @@ pt-PT:
         name: Só pode alterar a capitalização das letras, por exemplo, para torná-las mais legíveis
       user:
         chosen_languages: Quando selecionado, só serão mostradas nas cronologias públicas as publicações nos idiomas escolhidos
+        role: A função controla as permissões que o utilizador tem.
       user_role:
         color: Cor a ser utilizada para a função em toda a interface de utilizador, como RGB no formato hexadecimal
         highlighted: Isto torna a função visível publicamente
-        name: Nome público do cargo, se este estiver definido para ser apresentada com um emblema
+        name: Nome público da função, se esta estiver definida para ser apresentada com um emblema
         permissions_as_keys: Utilizadores com esta função terão acesso a...
-        position: Cargos mais altos decidem a resolução de conflitos em certas situações. Certas ações só podem ser executadas em cargos com uma menor prioridade
+        position: Funções mais altas decidem a resolução de conflitos em certas situações. Certas ações só podem ser executadas com certas funções com uma menor prioridade
       webhook:
         events: Selecione os eventos a enviar
         template: Componha o seu próprio conteúdo JSON utilizando a interpolação de variáveis. Deixar em branco para o JSON predefinido.
@@ -315,7 +316,7 @@ pt-PT:
         trendable: Permitir que esta etiqueta apareça nas tendências
         usable: Permitir que as publicações usem esta hashtag localmente
       user:
-        role: Cargo
+        role: Função
         time_zone: Fuso horário
       user_role:
         color: Cor do emblema
diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml
index 89fb1675f..2fcf23b15 100644
--- a/config/locales/simple_form.tr.yml
+++ b/config/locales/simple_form.tr.yml
@@ -130,6 +130,7 @@ tr:
         name: Harflerin, örneğin daha okunabilir yapmak için, sadece büyük/küçük harf durumlarını değiştirebilirsiniz
       user:
         chosen_languages: İşaretlendiğinde, yalnızca seçilen dillerdeki gönderiler genel zaman çizelgelerinde görüntülenir
+        role: Rol, kullanıcıların sahip olduğu izinleri denetler.
       user_role:
         color: Arayüz boyunca rol için kullanılacak olan renk, hex biçiminde RGB
         highlighted: Bu rolü herkese açık hale getirir
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index 0f43f4398..6907c1ee3 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -25,7 +25,6 @@ sq:
   admin:
     account_actions:
       action: Kryeje veprimin
-      already_silenced: Kjo llogari është heshtuar tashmë.
       already_suspended: Kjo llogari është pezulluar tashmë.
       title: Kryeni veprim moderimi te %{acct}
     account_moderation_notes:
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index bcf1e3b81..df403e602 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -25,6 +25,7 @@ sv:
   admin:
     account_actions:
       action: Utför åtgärd
+      already_silenced: Detta konto är redan begränsat.
       title: Utför aktivitet för moderering på %{acct}
     account_moderation_notes:
       create: Lämna kommentar
diff --git a/config/locales/th.yml b/config/locales/th.yml
index f11910211..f409a512d 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -23,7 +23,6 @@ th:
   admin:
     account_actions:
       action: ทำการกระทำ
-      already_silenced: มีการทำให้บัญชีนี้เงียบไปแล้ว
       already_suspended: มีการระงับบัญชีนี้ไปแล้ว
       title: ทำการกระทำการกลั่นกรองต่อ %{acct}
     account_moderation_notes:
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 4318f4eac..d2d499077 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -25,6 +25,8 @@ tr:
   admin:
     account_actions:
       action: Eylemi gerçekleştir
+      already_silenced: Bu hesap zaten askıya alınmış.
+      already_suspended: Bu hesap zaten askıya alınmış.
       title: "%{acct} üzerinde denetleme eylemi gerçekleştir"
     account_moderation_notes:
       create: Not bırak
@@ -46,6 +48,7 @@ tr:
         title: "%{username} için e-postayı değiştir"
       change_role:
         changed_msg: Rol başarıyla değiştirildi!
+        edit_roles: Kullanıcı rollerini yönetin
         label: Rolü değiştir
         no_role: Rol yok
         title: "%{username} için rolü değiştir"
@@ -602,6 +605,7 @@ tr:
         suspend_description_html: Bu hesap ve tüm içeriği erişilmez olacak ve nihayetinde silinecek ve bu hesapla etkileşim mümkün olmayacaktır. 30 gün içinde geri alınabilir. Bu hesaba yönelik tüm bildiriimleri kapatır.
       actions_description_html: Bu bildirimi çözmek için ne yapılması gerektiğine karar verin. Bildirilen hesap için ceza işlemi yaparsanız, <strong>İstenmeyen</strong> kategorisi seçilmemişse, onlara bir e-posta duyurusu gönderilecektir.
       actions_description_remote_html: Bu bildirimi çözmek için hangi eylemi yapmak istediğinize karar verin. Bu yalnızca <strong>sizin</strong> sunucunuzun bu uzak hesapla nasıl etkileşeğini ve içeriğiyle ne yapacağını etkiler.
+      actions_no_posts: Bu raporun ilişkili olduğu silinecek gönderi yok
       add_to_report: Bildirime daha fazlasını ekle
       already_suspended_badges:
         local: Bu sunucuda zaten askıya alınmış
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 261c87cd7..4e70b192d 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -628,6 +628,7 @@ uk:
         suspend_description_html: Обліковий запис і весь його вміст будуть недоступними й врешті-решт видалені, і взаємодіяти з ним буде неможливо. Відновлення можливе протягом 30 днів. Закриває всі скарги на цей обліковий запис.
       actions_description_html: Визначте, які дії слід вжити для розв'язання цієї скарги. Якщо ви оберете каральні дії проти зареєстрованого облікового запису, про них буде надіслано сповіщення електронним листом, крім випадків, коли вибрано категорію <strong>Спам</strong>.
       actions_description_remote_html: Визначте, які дії слід вжити для розв'язання цього звіту. Це вплине тільки на те, як <strong>ваш</strong> сервер з'єднується з цим віддаленим обліковим записом і обробляє його вміст.
+      actions_no_posts: Ця скарга не має жодних пов'язаних дописів для видалення
       add_to_report: Додати ще подробиць до скарги
       already_suspended_badges:
         local: Вже призупинено на цьому сервері
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index 975df3024..d211b9e74 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -23,7 +23,6 @@ vi:
   admin:
     account_actions:
       action: Thực hiện hành động
-      already_silenced: Tài khoản này đã bị hạn chế.
       already_suspended: Tài khoản này đã bị vô hiệu hóa.
       title: Áp đặt kiểm duyệt với %{acct}
     account_moderation_notes:
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 6b399d349..7407d81db 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -23,7 +23,7 @@ zh-CN:
   admin:
     account_actions:
       action: 执行操作
-      already_silenced: 此帐户已受限。
+      already_silenced: 此账户已受限。
       already_suspended: 此帐户已被封禁。
       title: 在 %{acct} 上执行管理操作
     account_moderation_notes:
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 8288e9bfa..d92d53f39 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -23,7 +23,7 @@ zh-TW:
   admin:
     account_actions:
       action: 執行動作
-      already_silenced: 此帳號已被靜音。
+      already_silenced: 此帳號已被限制。
       already_suspended: 此帳號已被停權。
       title: 對 %{acct} 執行站務動作
     account_moderation_notes:
@@ -1269,7 +1269,7 @@ zh-TW:
       home: 首頁時間軸
       notifications: 通知
       public: 公開時間軸
-      thread: 對話
+      thread: 討論串
     edit:
       add_keyword: 新增關鍵字
       keywords: 關鍵字

From 3929e3c6d21226ad42f743a283576004d9c1c7eb Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Tue, 10 Sep 2024 11:29:17 +0200
Subject: [PATCH 59/91] Change design of hide media button in web UI (#31807)

---
 .../mastodon/components/media_gallery.jsx     | 32 +++++-----
 app/javascript/mastodon/locales/en.json       |  2 +-
 .../styles/mastodon/components.scss           | 60 +++++++++++--------
 3 files changed, 51 insertions(+), 43 deletions(-)

diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx
index ed4805b05..9a8f85212 100644
--- a/app/javascript/mastodon/components/media_gallery.jsx
+++ b/app/javascript/mastodon/components/media_gallery.jsx
@@ -1,7 +1,7 @@
 import PropTypes from 'prop-types';
 import { PureComponent } from 'react';
 
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
 
@@ -10,17 +10,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 
 import { debounce } from 'lodash';
 
-import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
 import { Blurhash } from 'mastodon/components/blurhash';
 
 import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
 
-import { IconButton } from './icon_button';
-
-const messages = defineMessages({
-  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' },
-});
-
 class Item extends PureComponent {
 
   static propTypes = {
@@ -215,7 +208,6 @@ class MediaGallery extends PureComponent {
     size: PropTypes.object,
     height: PropTypes.number.isRequired,
     onOpenMedia: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
     defaultWidth: PropTypes.number,
     cacheWidth: PropTypes.func,
     visible: PropTypes.bool,
@@ -291,7 +283,7 @@ class MediaGallery extends PureComponent {
   }
 
   render () {
-    const { media, lang, intl, sensitive, defaultWidth, autoplay } = this.props;
+    const { media, lang, sensitive, defaultWidth, autoplay } = this.props;
     const { visible } = this.state;
     const width = this.state.width || defaultWidth;
 
@@ -323,9 +315,7 @@ class MediaGallery extends PureComponent {
           </span>
         </button>
       );
-    } else if (visible) {
-      spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' iconComponent={VisibilityOffIcon} overlay onClick={this.handleOpen} ariaHidden />;
-    } else {
+    } else if (!visible) {
       spoilerButton = (
         <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
           <span className='spoiler-button__overlay__label'>
@@ -338,15 +328,23 @@ class MediaGallery extends PureComponent {
 
     return (
       <div className='media-gallery' style={style} ref={this.handleRef}>
-        <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
-          {spoilerButton}
-        </div>
+        {(!visible || uncached) && (
+          <div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
+            {spoilerButton}
+          </div>
+        )}
 
         {children}
+
+        {(visible && !uncached) && (
+          <div className='media-gallery__actions'>
+            <button className='media-gallery__actions__pill' onClick={this.handleOpen}><FormattedMessage id='media_gallery.hide' defaultMessage='Hide' /></button>
+          </div>
+        )}
       </div>
     );
   }
 
 }
 
-export default injectIntl(MediaGallery);
+export default MediaGallery;
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 7c1d7f126..39ee7b858 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading…",
-  "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
+  "media_gallery.hide": "Hide",
   "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Hide from notifications",
   "mute_modal.hide_options": "Hide options",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 92d203463..570c006fa 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4718,22 +4718,14 @@ a.status-card {
   position: absolute;
   z-index: 100;
 
-  &--minified {
-    display: block;
-    inset-inline-start: 4px;
-    top: 4px;
-    width: auto;
-    height: auto;
+  &--hidden {
+    display: none;
   }
 
   &--click-thru {
     pointer-events: none;
   }
 
-  &--hidden {
-    display: none;
-  }
-
   &__overlay {
     display: flex;
     align-items: center;
@@ -4745,19 +4737,20 @@ a.status-card {
     margin: 0;
     border: 0;
     color: $white;
+    line-height: 20px;
+    font-size: 14px;
 
     &__label {
       background-color: rgba($black, 0.45);
       backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
-      border-radius: 6px;
-      padding: 10px 15px;
+      border-radius: 8px;
+      padding: 12px 16px;
       display: flex;
       align-items: center;
       justify-content: center;
-      gap: 8px;
+      gap: 4px;
       flex-direction: column;
-      font-weight: 500;
-      font-size: 14px;
+      font-weight: 600;
     }
 
     &__action {
@@ -6838,10 +6831,32 @@ a.status-card {
   z-index: 9999;
 }
 
-.media-gallery__item__badges {
+.media-gallery__actions {
   position: absolute;
   bottom: 6px;
-  inset-inline-start: 6px;
+  inset-inline-end: 6px;
+  display: flex;
+  gap: 2px;
+  z-index: 2;
+
+  &__pill {
+    display: block;
+    color: $white;
+    border: 0;
+    background: rgba($black, 0.65);
+    backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
+    padding: 3px 12px;
+    border-radius: 99px;
+    font-size: 14px;
+    font-weight: 700;
+    line-height: 20px;
+  }
+}
+
+.media-gallery__item__badges {
+  position: absolute;
+  bottom: 8px;
+  inset-inline-start: 8px;
   display: flex;
   gap: 2px;
 }
@@ -6854,18 +6869,13 @@ a.status-card {
   color: $white;
   background: rgba($black, 0.65);
   backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
-  padding: 2px 6px;
+  padding: 3px 8px;
   border-radius: 4px;
-  font-size: 11px;
+  font-size: 12px;
   font-weight: 700;
   z-index: 1;
   pointer-events: none;
-  line-height: 18px;
-
-  .icon {
-    width: 15px;
-    height: 15px;
-  }
+  line-height: 20px;
 }
 
 .attachment-list {

From e0c27a504788bdc6cd518072e557313e4ec5ee7a Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Tue, 10 Sep 2024 14:00:40 +0200
Subject: [PATCH 60/91] Add ability to manage which websites can credit you in
 link previews (#31819)

---
 .../settings/verifications_controller.rb      |  20 ++-
 app/helpers/context_helper.rb                 |   1 +
 app/javascript/styles/mastodon/forms.scss     |  35 +++++
 app/models/account.rb                         |   2 +
 .../concerns/account/attribution_domains.rb   |  25 ++++
 .../activitypub/actor_serializer.rb           |   3 +-
 .../activitypub/process_account_service.rb    |   1 +
 app/services/fetch_link_card_service.rb       |   7 +-
 app/validators/domain_validator.rb            |  21 ++-
 app/validators/lines_validator.rb             |   9 ++
 .../settings/verifications/show.html.haml     |  34 ++++-
 config/locales/activerecord.en.yml            |   6 +
 config/locales/an.yml                         |   2 -
 config/locales/ar.yml                         |   2 -
 config/locales/be.yml                         |   2 -
 config/locales/bg.yml                         |   2 -
 config/locales/ca.yml                         |   2 -
 config/locales/ckb.yml                        |   2 -
 config/locales/co.yml                         |   2 -
 config/locales/cs.yml                         |   2 -
 config/locales/cy.yml                         |   2 -
 config/locales/da.yml                         |   2 -
 config/locales/de.yml                         |   2 -
 config/locales/el.yml                         |   2 -
 config/locales/en-GB.yml                      |   2 -
 config/locales/en.yml                         |   9 +-
 config/locales/eo.yml                         |   2 -
 config/locales/es-AR.yml                      |   2 -
 config/locales/es-MX.yml                      |   2 -
 config/locales/es.yml                         |   2 -
 config/locales/et.yml                         |   2 -
 config/locales/eu.yml                         |   2 -
 config/locales/fa.yml                         |   2 -
 config/locales/fi.yml                         |   2 -
 config/locales/fo.yml                         |   2 -
 config/locales/fr-CA.yml                      |   2 -
 config/locales/fr.yml                         |   2 -
 config/locales/fy.yml                         |   2 -
 config/locales/ga.yml                         |   2 -
 config/locales/gd.yml                         |   2 -
 config/locales/gl.yml                         |   2 -
 config/locales/he.yml                         |   2 -
 config/locales/hu.yml                         |   2 -
 config/locales/hy.yml                         |   2 -
 config/locales/ia.yml                         |   2 -
 config/locales/id.yml                         |   2 -
 config/locales/ie.yml                         |   2 -
 config/locales/io.yml                         |   2 -
 config/locales/is.yml                         |   2 -
 config/locales/it.yml                         |   2 -
 config/locales/ja.yml                         |   2 -
 config/locales/kk.yml                         |   2 -
 config/locales/ko.yml                         |   2 -
 config/locales/ku.yml                         |   2 -
 config/locales/lad.yml                        |   2 -
 config/locales/lv.yml                         |   2 -
 config/locales/ms.yml                         |   2 -
 config/locales/my.yml                         |   2 -
 config/locales/nl.yml                         |   2 -
 config/locales/nn.yml                         |   2 -
 config/locales/no.yml                         |   2 -
 config/locales/oc.yml                         |   2 -
 config/locales/pl.yml                         |   2 -
 config/locales/pt-BR.yml                      |   2 -
 config/locales/pt-PT.yml                      |   2 -
 config/locales/ru.yml                         |   2 -
 config/locales/sc.yml                         |   2 -
 config/locales/sco.yml                        |   2 -
 config/locales/si.yml                         |   2 -
 config/locales/simple_form.en.yml             |   2 +
 config/locales/sk.yml                         |   2 -
 config/locales/sl.yml                         |   2 -
 config/locales/sq.yml                         |   2 -
 config/locales/sr-Latn.yml                    |   2 -
 config/locales/sr.yml                         |   2 -
 config/locales/sv.yml                         |   2 -
 config/locales/th.yml                         |   2 -
 config/locales/tr.yml                         |   2 -
 config/locales/uk.yml                         |   2 -
 config/locales/vi.yml                         |   2 -
 config/locales/zh-CN.yml                      |   2 -
 config/locales/zh-HK.yml                      |   2 -
 config/locales/zh-TW.yml                      |   2 -
 config/routes/settings.rb                     |   2 +-
 ...637_add_attribution_domains_to_accounts.rb |   7 +
 db/schema.rb                                  |   3 +-
 .../settings/migrations_controller_spec.rb    |   1 +
 spec/models/account_spec.rb                   |  16 +++
 .../process_account_service_spec.rb           |  20 +++
 spec/services/bulk_import_service_spec.rb     |   8 +-
 spec/validators/domain_validator_spec.rb      | 125 ++++++++++++++++++
 spec/validators/lines_validator_spec.rb       |  46 +++++++
 92 files changed, 381 insertions(+), 160 deletions(-)
 create mode 100644 app/models/concerns/account/attribution_domains.rb
 create mode 100644 app/validators/lines_validator.rb
 create mode 100644 db/migrate/20240909014637_add_attribution_domains_to_accounts.rb
 create mode 100644 spec/validators/domain_validator_spec.rb
 create mode 100644 spec/validators/lines_validator_spec.rb

diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb
index fc4f23bb1..4e0663253 100644
--- a/app/controllers/settings/verifications_controller.rb
+++ b/app/controllers/settings/verifications_controller.rb
@@ -2,14 +2,30 @@
 
 class Settings::VerificationsController < Settings::BaseController
   before_action :set_account
+  before_action :set_verified_links
 
-  def show
-    @verified_links = @account.fields.select(&:verified?)
+  def show; end
+
+  def update
+    if UpdateAccountService.new.call(@account, account_params)
+      ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+      redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
+    else
+      render :show
+    end
   end
 
   private
 
+  def account_params
+    params.require(:account).permit(:attribution_domains_as_text)
+  end
+
   def set_account
     @account = current_account
   end
+
+  def set_verified_links
+    @verified_links = @account.fields.select(&:verified?)
+  end
 end
diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb
index cbefe0fe5..a0c1781d2 100644
--- a/app/helpers/context_helper.rb
+++ b/app/helpers/context_helper.rb
@@ -41,6 +41,7 @@ module ContextHelper
       'cipherText' => 'toot:cipherText',
     },
     suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
+    attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
   }.freeze
 
   def full_context
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 926df4e96..56f7b893f 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -12,6 +12,41 @@ code {
   margin: 50px auto;
 }
 
+.form-section {
+  border-radius: 8px;
+  background: var(--surface-background-color);
+  padding: 24px;
+  margin-bottom: 24px;
+}
+
+.fade-out-top {
+  position: relative;
+  overflow: hidden;
+  height: 160px;
+
+  &::after {
+    content: '';
+    display: block;
+    background: linear-gradient(
+      to bottom,
+      var(--surface-background-color),
+      transparent
+    );
+    position: absolute;
+    top: 0;
+    inset-inline-start: 0;
+    width: 100%;
+    height: 100px;
+    pointer-events: none;
+  }
+
+  & > div {
+    position: absolute;
+    inset-inline-start: 0;
+    bottom: 0;
+  }
+}
+
 .indicator-icon {
   display: flex;
   align-items: center;
diff --git a/app/models/account.rb b/app/models/account.rb
index 4a7c752e7..0a2cff2fe 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -51,6 +51,7 @@
 #  reviewed_at                   :datetime
 #  requested_review_at           :datetime
 #  indexable                     :boolean          default(FALSE), not null
+#  attribution_domains           :string           default([]), is an Array
 #
 
 class Account < ApplicationRecord
@@ -88,6 +89,7 @@ class Account < ApplicationRecord
   include Account::Merging
   include Account::Search
   include Account::StatusesSearch
+  include Account::AttributionDomains
   include DomainMaterializable
   include DomainNormalizable
   include Paginable
diff --git a/app/models/concerns/account/attribution_domains.rb b/app/models/concerns/account/attribution_domains.rb
new file mode 100644
index 000000000..37a498a15
--- /dev/null
+++ b/app/models/concerns/account/attribution_domains.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Account::AttributionDomains
+  extend ActiveSupport::Concern
+
+  included do
+    validates :attribution_domains_as_text, domain: { multiline: true }, lines: { maximum: 100 }, if: -> { local? && will_save_change_to_attribution_domains? }
+  end
+
+  def attribution_domains_as_text
+    self[:attribution_domains].join("\n")
+  end
+
+  def attribution_domains_as_text=(str)
+    self[:attribution_domains] = str.split.filter_map do |line|
+      line.strip.delete_prefix('*.')
+    end
+  end
+
+  def can_be_attributed_from?(domain)
+    segments = domain.split('.')
+    variants = segments.map.with_index { |_, i| segments[i..].join('.') }.to_set
+    self[:attribution_domains].to_set.intersect?(variants)
+  end
+end
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 4ab48ff20..a6281e23b 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -8,7 +8,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
 
   context_extensions :manually_approves_followers, :featured, :also_known_as,
                      :moved_to, :property_value, :discoverable, :olm, :suspended,
-                     :memorial, :indexable
+                     :memorial, :indexable, :attribution_domains
 
   attributes :id, :type, :following, :followers,
              :inbox, :outbox, :featured, :featured_tags,
@@ -25,6 +25,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
   attribute :moved_to, if: :moved?
   attribute :also_known_as, if: :also_known_as?
   attribute :suspended, if: :suspended?
+  attribute :attribution_domains, if: -> { object.attribution_domains.any? }
 
   class EndpointsSerializer < ActivityPub::Serializer
     include RoutingHelper
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index b667e97f4..1e2d614d7 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -117,6 +117,7 @@ class ActivityPub::ProcessAccountService < BaseService
     @account.discoverable            = @json['discoverable'] || false
     @account.indexable               = @json['indexable'] || false
     @account.memorial                = @json['memorial'] || false
+    @account.attribution_domains     = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) }
   end
 
   def set_fetchable_key!
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 36d5c490a..7662fc1f2 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -153,12 +153,13 @@ class FetchLinkCardService < BaseService
     return if html.nil?
 
     link_details_extractor = LinkDetailsExtractor.new(@url, @html, @html_charset)
-    provider = PreviewCardProvider.matching_domain(Addressable::URI.parse(link_details_extractor.canonical_url).normalized_host)
-    linked_account = ResolveAccountService.new.call(link_details_extractor.author_account, suppress_errors: true) if link_details_extractor.author_account.present? && provider&.trendable?
+    domain = Addressable::URI.parse(link_details_extractor.canonical_url).normalized_host
+    provider = PreviewCardProvider.matching_domain(domain)
+    linked_account = ResolveAccountService.new.call(link_details_extractor.author_account, suppress_errors: true) if link_details_extractor.author_account.present?
 
     @card = PreviewCard.find_or_initialize_by(url: link_details_extractor.canonical_url) if link_details_extractor.canonical_url != @card.url
     @card.assign_attributes(link_details_extractor.to_preview_card_attributes)
-    @card.author_account = linked_account
+    @card.author_account = linked_account if linked_account&.can_be_attributed_from?(domain) || provider&.trendable?
     @card.save_with_optional_image! unless @card.title.blank? && @card.html.blank?
   end
 end
diff --git a/app/validators/domain_validator.rb b/app/validators/domain_validator.rb
index 3a951f9a7..718fd190f 100644
--- a/app/validators/domain_validator.rb
+++ b/app/validators/domain_validator.rb
@@ -1,22 +1,29 @@
 # frozen_string_literal: true
 
 class DomainValidator < ActiveModel::EachValidator
+  MAX_DOMAIN_LENGTH = 256
+  MIN_LABEL_LENGTH = 1
+  MAX_LABEL_LENGTH = 63
+  ALLOWED_CHARACTERS_RE = /^[a-z0-9\-]+$/i
+
   def validate_each(record, attribute, value)
     return if value.blank?
 
-    domain = if options[:acct]
-               value.split('@').last
-             else
-               value
-             end
+    (options[:multiline] ? value.split : [value]).each do |domain|
+      _, domain = domain.split('@') if options[:acct]
 
-    record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(domain)
+      next if domain.blank?
+
+      record.errors.add(attribute, options[:multiline] ? :invalid_domain_on_line : :invalid, value: domain) unless compliant?(domain)
+    end
   end
 
   private
 
   def compliant?(value)
-    Addressable::URI.new.tap { |uri| uri.host = value }
+    uri = Addressable::URI.new
+    uri.host = value
+    uri.normalized_host.size < MAX_DOMAIN_LENGTH && uri.normalized_host.split('.').all? { |label| label.size.between?(MIN_LABEL_LENGTH, MAX_LABEL_LENGTH) && label =~ ALLOWED_CHARACTERS_RE }
   rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
     false
   end
diff --git a/app/validators/lines_validator.rb b/app/validators/lines_validator.rb
new file mode 100644
index 000000000..27a108bb2
--- /dev/null
+++ b/app/validators/lines_validator.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class LinesValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    return if value.blank?
+
+    record.errors.add(attribute, :too_many_lines, limit: options[:maximum]) if options[:maximum].present? && value.split.size > options[:maximum]
+  end
+end
diff --git a/app/views/settings/verifications/show.html.haml b/app/views/settings/verifications/show.html.haml
index 4fb291801..5318b0767 100644
--- a/app/views/settings/verifications/show.html.haml
+++ b/app/views/settings/verifications/show.html.haml
@@ -5,7 +5,9 @@
   %h2= t('settings.profile')
   = render partial: 'settings/shared/profile_navigation'
 
-.simple_form
+.simple_form.form-section
+  %h3= t('verification.website_verification')
+
   %p.lead= t('verification.hint_html')
 
   %h4= t('verification.here_is_how')
@@ -28,3 +30,33 @@
           %span.verified-badge
             = material_symbol 'check', class: 'verified-badge__mark'
             %span= field.value
+
+= simple_form_for @account, url: settings_verification_path, html: { method: :put, class: 'form-section' } do |f|
+  = render 'shared/error_messages', object: @account
+
+  %h3= t('author_attribution.title')
+
+  %p.lead= t('author_attribution.hint_html')
+
+  .fields-row
+    .fields-row__column.fields-row__column-6
+      .fields-group
+        = f.input :attribution_domains_as_text, as: :text, wrapper: :with_block_label, input_html: { placeholder: "example1.com\nexample2.com\nexample3.com", rows: 4 }
+    .fields-row__column.fields-row__column-6
+      .fields-group.fade-out-top
+        %div
+          .status-card.expanded.bottomless
+            .status-card__image
+              = image_tag frontend_asset_url('images/preview.png'), alt: '', class: 'status-card__image-image'
+            .status-card__content
+              %span.status-card__host
+                %span= t('author_attribution.s_blog', name: @account.username)
+                ·
+                %time.time-ago{ datetime: 1.year.ago.to_date.iso8601 }
+              %strong.status-card__title= t('author_attribution.example_title')
+          .more-from-author
+            = logo_as_symbol(:icon)
+            = t('author_attribution.more_from_html', name: link_to(root_url, class: 'story__details__shared__author-link') { image_tag(@account.avatar.url, class: 'account__avatar', width: 16, height: 16, alt: '') + content_tag(:bdi, display_name(@account)) })
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml
index a53c7c6e9..e13585603 100644
--- a/config/locales/activerecord.en.yml
+++ b/config/locales/activerecord.en.yml
@@ -15,6 +15,12 @@ en:
       user/invite_request:
         text: Reason
     errors:
+      attributes:
+        domain:
+          invalid: is not a valid domain name
+      messages:
+        invalid_domain_on_line: "%{value} is not a valid domain name"
+        too_many_lines: is over the limit of %{limit} lines
       models:
         account:
           attributes:
diff --git a/config/locales/an.yml b/config/locales/an.yml
index 9afc9e881..41eeee461 100644
--- a/config/locales/an.yml
+++ b/config/locales/an.yml
@@ -1017,8 +1017,6 @@ an:
       your_appeal_approved: S'aprebó la tuya apelación
       your_appeal_pending: Has ninviau una apelación
       your_appeal_rejected: La tuya apelación ha estau refusada
-  domain_validator:
-    invalid_domain: no ye un nombre de dominio valido
   errors:
     '400': La solicitut que has ninviau no ye valida u yera malformada.
     '403': No tiens permiso pa acceder ta esta pachina.
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index ee05684b6..06cea7ecb 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -1239,8 +1239,6 @@ ar:
       your_appeal_approved: تمت الموافقة على طعنك
       your_appeal_pending: لقد قمت بتقديم طعن
       your_appeal_rejected: تم رفض طعنك
-  domain_validator:
-    invalid_domain: ليس بإسم نطاق صالح
   edit_profile:
     basic_information: معلومات أساسية
     hint_html: "<strong>قم بتخصيص ما سيراه الناس في ملفك الشخصي العام وبجوار منشوراتك.</strong> من المرجح أن يتابعك أشخاص آخرون ويتفاعلون معك إن كان لديك صفحة شخصية مملوء وصورة."
diff --git a/config/locales/be.yml b/config/locales/be.yml
index fbeb55add..31a31e9e6 100644
--- a/config/locales/be.yml
+++ b/config/locales/be.yml
@@ -1256,8 +1256,6 @@ be:
       your_appeal_approved: Ваша абскарджанне было ўхвалена
       your_appeal_pending: Вы адправілі апеляцыю
       your_appeal_rejected: Ваша абскарджанне было адхілена
-  domain_validator:
-    invalid_domain: не з'яўляецца сапраўдным даменным імем
   edit_profile:
     basic_information: Асноўная інфармацыя
     hint_html: "<strong>Наладзьце тое, што людзі будуць бачыць у вашым профілі і побач з вашымі паведамленнямі.</strong> Іншыя людзі з большай верагоднасцю будуць сачыць і ўзаемадзейнічаць з вамі, калі ў вас ёсць запоўнены профіль і фота профілю."
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 56ab75917..42a626c69 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -1179,8 +1179,6 @@ bg:
       your_appeal_approved: Вашето обжалване е одобрено
       your_appeal_pending: Подадохте обжалване
       your_appeal_rejected: Вашето обжалване е отхвърлено
-  domain_validator:
-    invalid_domain: не е валидно име на домейн
   edit_profile:
     basic_information: Основна информация
     hint_html: "<strong>Персонализирайте какво хората виждат в обществения ви профил и до публикациите ви.</strong> Другите хора са по-склонни да ви последват и да взаимодействат с вас, когато имате попълнен профил и снимка на профила."
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 121266916..e1025563e 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -1216,8 +1216,6 @@ ca:
       your_appeal_approved: La teva apel·lació s'ha aprovat
       your_appeal_pending: Has enviat una apel·lació
       your_appeal_rejected: La teva apel·lació ha estat rebutjada
-  domain_validator:
-    invalid_domain: no es un nom de domini vàlid
   edit_profile:
     basic_information: Informació bàsica
     hint_html: "<strong>Personalitza el que la gent veu en el teu perfil públic i a prop dels teus tuts..</strong> És més probable que altres persones et segueixin i interaccionin amb tu quan tens emplenat el teu perfil i amb la teva imatge."
diff --git a/config/locales/ckb.yml b/config/locales/ckb.yml
index 15c5690cd..3ecef4bb4 100644
--- a/config/locales/ckb.yml
+++ b/config/locales/ckb.yml
@@ -646,8 +646,6 @@ ckb:
     strikes:
       title_actions:
         none: ئاگاداری
-  domain_validator:
-    invalid_domain: ناوی دۆمەین بڕوادار نییە
   errors:
     '400': داواکاریەکەی کە پێشکەشت کردووە نادروستە یان نەیپێکا.
     '403': تۆ مۆڵەتت نیە بۆ بینینی ئەم لاپەڕەیە.
diff --git a/config/locales/co.yml b/config/locales/co.yml
index 5ee69ff8a..7c0695a77 100644
--- a/config/locales/co.yml
+++ b/config/locales/co.yml
@@ -603,8 +603,6 @@ co:
       more_details_html: Per più di ditagli, videte a <a href="%{terms_path}">pulitica di vita privata</a>.
       username_available: U vostru cugnome riduvinterà dispunibule
       username_unavailable: U vostru cugnome ùn sarà sempre micca dispunibule
-  domain_validator:
-    invalid_domain: ùn hè micca un nome di duminiu currettu
   errors:
     '400': A richiesta mandata ùn era micca valida o curretta.
     '403': Ùn site micca auturizatu·a à vede sta pagina.
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 98e2c3052..7d4d2296c 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -1213,8 +1213,6 @@ cs:
       your_appeal_approved: Vaše odvolání bylo schváleno
       your_appeal_pending: Podali jste odvolání
       your_appeal_rejected: Vaše odvolání bylo zamítnuto
-  domain_validator:
-    invalid_domain: není platné doménové jméno
   edit_profile:
     basic_information: Základní informace
     hint_html: "<strong>Nastavte si, co lidé uvidí na vašem veřejném profilu a vedle vašich příspěvků.</strong> Ostatní lidé vás budou spíše sledovat a komunikovat s vámi, když budete mít vyplněný profil a profilový obrázek."
diff --git a/config/locales/cy.yml b/config/locales/cy.yml
index b3886ffeb..d317efad3 100644
--- a/config/locales/cy.yml
+++ b/config/locales/cy.yml
@@ -1306,8 +1306,6 @@ cy:
       your_appeal_approved: Mae eich apêl wedi'i chymeradwyo
       your_appeal_pending: Rydych wedi cyflwyno apêl
       your_appeal_rejected: Mae eich apêl wedi'i gwrthod
-  domain_validator:
-    invalid_domain: ddim yn enw parth dilys
   edit_profile:
     basic_information: Gwybodaeth Sylfaenol
     hint_html: "<strong>Addaswch yr hyn y mae pobl yn ei weld ar eich proffil cyhoeddus ac wrth ymyl eich postiadau.</strong> Mae pobl eraill yn fwy tebygol o'ch dilyn yn ôl a rhyngweithio â chi pan fydd gennych broffil wedi'i lenwi a llun proffil."
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 89430e37e..19ee9b9d8 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -1235,8 +1235,6 @@ da:
       your_appeal_approved: Din appel er godkendt
       your_appeal_pending: Du har indgivet en appel
       your_appeal_rejected: Din appel er afvist
-  domain_validator:
-    invalid_domain: er ikke et gyldigt domænenavn
   edit_profile:
     basic_information: Oplysninger
     hint_html: "<strong>Tilpas hvad folk ser på din offentlige profil og ved siden af dine indlæg.</strong> Andre personer vil mere sandsynligt følge dig tilbage og interagere med dig, når du har en udfyldt profil og et profilbillede."
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 975ec517f..b75aada76 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -1234,8 +1234,6 @@ de:
       your_appeal_approved: Dein Einspruch wurde angenommen
       your_appeal_pending: Du hast Einspruch erhoben
       your_appeal_rejected: Dein Einspruch wurde abgelehnt
-  domain_validator:
-    invalid_domain: ist keine gültige Domain
   edit_profile:
     basic_information: Allgemeine Informationen
     hint_html: "<strong>Bestimme, was andere auf deinem öffentlichen Profil und neben deinen Beiträgen sehen können.</strong> Wenn du ein Profilbild festlegst und dein Profil vervollständigst, werden andere eher mit dir interagieren und dir folgen."
diff --git a/config/locales/el.yml b/config/locales/el.yml
index 6b1d8bc0e..3cb9075c3 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -1192,8 +1192,6 @@ el:
       your_appeal_approved: Η έφεση σου έχει εγκριθεί
       your_appeal_pending: Υπέβαλλες έφεση
       your_appeal_rejected: Η έφεση σου απορρίφθηκε
-  domain_validator:
-    invalid_domain: δεν είναι έγκυρο όνομα τομέα
   edit_profile:
     basic_information: Βασικές πληροφορίες
     hint_html: "<strong>Τροποποίησε τί βλέπουν άτομα στο δημόσιο προφίλ σου και δίπλα στις αναρτήσεις σου.</strong> Είναι πιο πιθανό κάποιος να σε ακολουθήσει πίσω και να αλληλεπιδράσουν μαζί σου αν έχεις ολοκληρωμένο προφίλ και εικόνα προφίλ."
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml
index bcde9956c..a3036c2f9 100644
--- a/config/locales/en-GB.yml
+++ b/config/locales/en-GB.yml
@@ -1202,8 +1202,6 @@ en-GB:
       your_appeal_approved: Your appeal has been approved
       your_appeal_pending: You have submitted an appeal
       your_appeal_rejected: Your appeal has been rejected
-  domain_validator:
-    invalid_domain: is not a valid domain name
   edit_profile:
     basic_information: Basic information
     hint_html: "<strong>Customise what people see on your public profile and next to your posts.</strong> Other people are more likely to follow you back and interact with you when you have a filled out profile and a profile picture."
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 980bd481b..05300acea 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1161,6 +1161,12 @@ en:
       view_strikes: View past strikes against your account
     too_fast: Form submitted too fast, try again.
     use_security_key: Use security key
+  author_attribution:
+    example_title: Sample text
+    hint_html: Control how you're credited when links are shared on Mastodon.
+    more_from_html: More from %{name}
+    s_blog: "%{name}'s Blog"
+    title: Author attribution
   challenge:
     confirm: Continue
     hint_html: "<strong>Tip:</strong> We won't ask you for your password again for the next hour."
@@ -1235,8 +1241,6 @@ en:
       your_appeal_approved: Your appeal has been approved
       your_appeal_pending: You have submitted an appeal
       your_appeal_rejected: Your appeal has been rejected
-  domain_validator:
-    invalid_domain: is not a valid domain name
   edit_profile:
     basic_information: Basic information
     hint_html: "<strong>Customize what people see on your public profile and next to your posts.</strong> Other people are more likely to follow you back and interact with you when you have a filled out profile and a profile picture."
@@ -1952,6 +1956,7 @@ en:
     instructions_html: Copy and paste the code below into the HTML of your website. Then add the address of your website into one of the extra fields on your profile from the "Edit profile" tab and save changes.
     verification: Verification
     verified_links: Your verified links
+    website_verification: Website verification
   webauthn_credentials:
     add: Add new security key
     create:
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index 85aa3a1f3..c1873c2f2 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -1105,8 +1105,6 @@ eo:
       your_appeal_approved: Via apelacio aprobitas
       your_appeal_pending: Vi sendis apelacion
       your_appeal_rejected: Via apelacio malakceptitas
-  domain_validator:
-    invalid_domain: ne estas valida domajna nomo
   edit_profile:
     basic_information: Baza informo
     other: Alia
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index bd15c9862..f4d88d732 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -1235,8 +1235,6 @@ es-AR:
       your_appeal_approved: Se aprobó tu apelación
       your_appeal_pending: Enviaste una apelación
       your_appeal_rejected: Se rechazó tu apelación
-  domain_validator:
-    invalid_domain: no es un nombre de dominio válido
   edit_profile:
     basic_information: Información básica
     hint_html: "<strong>Personalizá lo que la gente ve en tu perfil público y junto a tus publicaciones.</strong> Es más probable que otras personas te sigan e interactúen con vos cuando tengas un perfil completo y una foto de perfil."
diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml
index 52e440ffe..84663aa89 100644
--- a/config/locales/es-MX.yml
+++ b/config/locales/es-MX.yml
@@ -1231,8 +1231,6 @@ es-MX:
       your_appeal_approved: Se aprobó tu apelación
       your_appeal_pending: Has enviado una apelación
       your_appeal_rejected: Tu apelación ha sido rechazada
-  domain_validator:
-    invalid_domain: no es un nombre de dominio válido
   edit_profile:
     basic_information: Información básica
     hint_html: "<strong>Personaliza lo que la gente ve en tu perfil público junto a tus publicaciones.</strong> Es más probable que otras personas te sigan e interactúen contigo cuando completes tu perfil y agregues una foto."
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 21b900192..e245dde5d 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -1231,8 +1231,6 @@ es:
       your_appeal_approved: Se aprobó tu apelación
       your_appeal_pending: Has enviado una apelación
       your_appeal_rejected: Tu apelación ha sido rechazada
-  domain_validator:
-    invalid_domain: no es un nombre de dominio válido
   edit_profile:
     basic_information: Información básica
     hint_html: "<strong>Personaliza lo que la gente ve en tu perfil público junto a tus publicaciones.</strong> Es más probable que otras personas te sigan e interactúen contigo cuando completas tu perfil y foto."
diff --git a/config/locales/et.yml b/config/locales/et.yml
index d2a4bc605..88d48fefc 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -1212,8 +1212,6 @@ et:
       your_appeal_approved: Su vaidlustus on heakskiidetud
       your_appeal_pending: Vaidlustus on esitatud
       your_appeal_rejected: Vaidlustus on tagasi lükatud
-  domain_validator:
-    invalid_domain: ei ole sobiv domeeni nimi
   edit_profile:
     basic_information: Põhiinfo
     hint_html: "<strong>Kohanda, mida inimesed näevad su avalikul profiilil ja postituste kõrval.</strong> Inimesed alustavad tõenäolisemalt sinu jälgimist ja interakteeruvad sinuga, kui sul on täidetud profiil ja profiilipilt."
diff --git a/config/locales/eu.yml b/config/locales/eu.yml
index 5e7d4a7f0..e5ae0ab79 100644
--- a/config/locales/eu.yml
+++ b/config/locales/eu.yml
@@ -1155,8 +1155,6 @@ eu:
       your_appeal_approved: Zure apelazioa onartu da
       your_appeal_pending: Apelazio bat bidali duzu
       your_appeal_rejected: Zure apelazioa baztertu da
-  domain_validator:
-    invalid_domain: ez da domeinu izen baliogarria
   edit_profile:
     basic_information: Oinarrizko informazioa
     hint_html: "<strong>Pertsonalizatu jendeak zer ikusi dezakeen zure profil publikoan eta zure bidalketen baitan.</strong> Segur aski, jende gehiagok jarraituko dizu eta interakzio gehiago izango dituzu profila osatuta baduzu, profil irudia eta guzti."
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 42782da20..ce8a61e3f 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -986,8 +986,6 @@ fa:
       your_appeal_approved: درخواست تجدیدنظرتان پذیرفته شد
       your_appeal_pending: شما یک درخواست تجدیدنظر فرستادید
       your_appeal_rejected: درخواست تجدیدنظرتان رد شد
-  domain_validator:
-    invalid_domain: نام دامین معتبر نیست
   edit_profile:
     basic_information: اطلاعات پایه
     hint_html: "<strong>شخصی‌سازی آن چه مردم روی نمایهٔ عمومیتان و کنار فرسته‌هایتان می‌بینند.</strong> هنگامی که نمایه‌ای کامل و یک تصویر نمایه داشته باشید،‌ احتمال پی‌گیری متقابل و تعامل با شما بیش‌تر است."
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index c94f04a9b..90c1e7776 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -1235,8 +1235,6 @@ fi:
       your_appeal_approved: Valituksesi on hyväksytty
       your_appeal_pending: Olet lähettänyt valituksen
       your_appeal_rejected: Valituksesi on hylätty
-  domain_validator:
-    invalid_domain: ei ole kelvollinen verkkotunnus
   edit_profile:
     basic_information: Perustiedot
     hint_html: "<strong>Mukauta, mitä ihmiset näkevät julkisessa profiilissasi ja julkaisujesi vieressä.</strong> Sinua seurataan takaisin ja kanssasi ollaan vuorovaikutuksessa todennäköisemmin, kun sinulla on täytetty profiili ja profiilikuva."
diff --git a/config/locales/fo.yml b/config/locales/fo.yml
index f3d9aee4c..5bc192e9d 100644
--- a/config/locales/fo.yml
+++ b/config/locales/fo.yml
@@ -1234,8 +1234,6 @@ fo:
       your_appeal_approved: Kæra tín er góðkend
       your_appeal_pending: Tú hevur kært
       your_appeal_rejected: Kæra tín er vrakað
-  domain_validator:
-    invalid_domain: er ikki eitt loyvt økisnavn
   edit_profile:
     basic_information: Grundleggjandi upplýsingar
     hint_html: "<strong>Tillaga tað, sum fólk síggja á tínum almenna vanga og við síðna av tínum postum.</strong> Sannlíkindini fyri, at onnur fylgja tær og virka saman við tær eru størri, tá tú hevur fylt út vangan og eina vangamynd."
diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml
index 766ba71d8..27e09d1f9 100644
--- a/config/locales/fr-CA.yml
+++ b/config/locales/fr-CA.yml
@@ -1222,8 +1222,6 @@ fr-CA:
       your_appeal_approved: Votre appel a été approuvé
       your_appeal_pending: Vous avez soumis un appel
       your_appeal_rejected: Votre appel a été rejeté
-  domain_validator:
-    invalid_domain: n’est pas un nom de domaine valide
   edit_profile:
     basic_information: Informations de base
     hint_html: "<strong>Personnalisez ce que les gens voient sur votre profil public et à côté de vos messages.</strong> Les autres personnes seront plus susceptibles de vous suivre et d’interagir avec vous lorsque vous avez un profil complet et une photo."
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 420a4d314..055b50900 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1222,8 +1222,6 @@ fr:
       your_appeal_approved: Votre appel a été approuvé
       your_appeal_pending: Vous avez soumis un appel
       your_appeal_rejected: Votre appel a été rejeté
-  domain_validator:
-    invalid_domain: n’est pas un nom de domaine valide
   edit_profile:
     basic_information: Informations de base
     hint_html: "<strong>Personnalisez ce que les gens voient sur votre profil public et à côté de vos messages.</strong> Les autres personnes seront plus susceptibles de vous suivre et d’interagir avec vous lorsque vous avez un profil complet et une photo."
diff --git a/config/locales/fy.yml b/config/locales/fy.yml
index de7495dd5..27fcaf3af 100644
--- a/config/locales/fy.yml
+++ b/config/locales/fy.yml
@@ -1231,8 +1231,6 @@ fy:
       your_appeal_approved: Jo beswier is goedkard
       your_appeal_pending: Jo hawwe in beswier yntsjinne
       your_appeal_rejected: Jo beswier is ôfwêzen
-  domain_validator:
-    invalid_domain: is in ûnjildige domeinnamme
   edit_profile:
     basic_information: Algemiene ynformaasje
     hint_html: "<strong>Pas oan wat minsken op jo iepenbiere profyl en njonken jo berjochten sjogge.</strong> Oare minsken sille jo earder folgje en mei jo kommunisearje wannear’t jo profyl ynfolle is en jo in profylfoto hawwe."
diff --git a/config/locales/ga.yml b/config/locales/ga.yml
index d2783a5a6..870f79cba 100644
--- a/config/locales/ga.yml
+++ b/config/locales/ga.yml
@@ -1288,8 +1288,6 @@ ga:
       your_appeal_approved: Tá d’achomharc ceadaithe
       your_appeal_pending: Chuir tú achomharc isteach
       your_appeal_rejected: Diúltaíodh do d'achomharc
-  domain_validator:
-    invalid_domain: nach ainm fearainn bailí é
   edit_profile:
     basic_information: Eolas bunúsach
     hint_html: "<strong>Saincheap a bhfeiceann daoine ar do phróifíl phoiblí agus in aice le do phostálacha.</strong> Is dóichí go leanfaidh daoine eile ar ais tú agus go n-idirghníomhóidh siad leat nuair a bhíonn próifíl líonta agus pictiúr próifíle agat."
diff --git a/config/locales/gd.yml b/config/locales/gd.yml
index c08ba6021..dda918d15 100644
--- a/config/locales/gd.yml
+++ b/config/locales/gd.yml
@@ -1270,8 +1270,6 @@ gd:
       your_appeal_approved: Chaidh aontachadh ris an ath-thagradh agad
       your_appeal_pending: Chuir thu ath-thagradh a-null
       your_appeal_rejected: Chaidh an t-ath-thagradh agad a dhiùltadh
-  domain_validator:
-    invalid_domain: "– chan eil seo ’na ainm àrainne dligheach"
   edit_profile:
     basic_information: Fiosrachadh bunasach
     hint_html: "<strong>Gnàthaich na chithear air a’ phròifil phoblach agad is ri taobh nam postaichean agad.</strong> Bidh càch nas buailtiche do leantainn agus conaltradh leat nuair a bhios tu air a’ phròifil agad a lìonadh agus dealbh rithe."
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index 657908922..c8fa1d833 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -1235,8 +1235,6 @@ gl:
       your_appeal_approved: A apelación foi aprobada
       your_appeal_pending: Enviaches unha apelación
       your_appeal_rejected: A apelación foi rexeitada
-  domain_validator:
-    invalid_domain: non é un nome de dominio válido
   edit_profile:
     basic_information: Información básica
     hint_html: "<strong>Personaliza o que van ver no teu perfil público e ao lado das túas publicacións.</strong> As outras persoas estarán máis animadas a seguirte e interactuar contigo se engades algún dato sobre ti así como unha imaxe de perfil."
diff --git a/config/locales/he.yml b/config/locales/he.yml
index a838c6963..025b2de9e 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -1270,8 +1270,6 @@ he:
       your_appeal_approved: ערעורך התקבל
       your_appeal_pending: הגשת ערעור
       your_appeal_rejected: ערעורך נדחה
-  domain_validator:
-    invalid_domain: הוא לא שם דומיין קביל
   edit_profile:
     basic_information: מידע בסיסי
     hint_html: "<strong>התאמה אישית של מה שיראו אחרים בפרופיל הציבורי שלך וליד הודעותיך.</strong> אחרים עשויים יותר להחזיר עוקב וליצור אתך שיחה אם הפרופיל והתמונה יהיו מלאים."
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 71af13830..141f5b7d0 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -1234,8 +1234,6 @@ hu:
       your_appeal_approved: A fellebbezésedet jóváhagyták
       your_appeal_pending: Beküldtél egy fellebbezést
       your_appeal_rejected: A fellebbezésedet visszautasították
-  domain_validator:
-    invalid_domain: nem egy valódi domain név
   edit_profile:
     basic_information: Általános információk
     hint_html: "<strong>Tedd egyedivé, mi látnak mások a profilodon és a bejegyzéseid mellett.</strong> Mások nagyobb eséllyel követnek vissza és lépnek veled kapcsolatba, ha van kitöltött profilod és profilképed."
diff --git a/config/locales/hy.yml b/config/locales/hy.yml
index dfb280ac4..c7128a2a4 100644
--- a/config/locales/hy.yml
+++ b/config/locales/hy.yml
@@ -523,8 +523,6 @@ hy:
     success_msg: Հաշիւդ բարեյաջող ջնջուեց
     warning:
       username_available: Քո օգտանունը կրկին հասանելի կը դառնայ
-  domain_validator:
-    invalid_domain: անվաւէր տիրոյթի անուն
   errors:
     '404': Էջը, որը փնտրում ես գոյութիւն չունի։
     '429': Չափազանց շատ հարցումներ
diff --git a/config/locales/ia.yml b/config/locales/ia.yml
index 8827e084d..073e5032b 100644
--- a/config/locales/ia.yml
+++ b/config/locales/ia.yml
@@ -1220,8 +1220,6 @@ ia:
       your_appeal_approved: Tu appello ha essite approbate
       your_appeal_pending: Tu ha submittite un appello
       your_appeal_rejected: Tu appello ha essite rejectate
-  domain_validator:
-    invalid_domain: non es un nomine de dominio valide
   edit_profile:
     basic_information: Information basic
     hint_html: "<strong>Personalisa lo que le personas vide sur tu profilo public e presso tu messages.</strong> Il es plus probabile que altere personas te seque e interage con te quando tu ha un profilo complete e un photo."
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 73b421839..575daddca 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -1000,8 +1000,6 @@ id:
       your_appeal_approved: Banding Anda disetujui
       your_appeal_pending: Anda telah mengirim banding
       your_appeal_rejected: Banding Anda ditolak
-  domain_validator:
-    invalid_domain: bukan nama domain yang valid
   errors:
     '400': Permintaan yang dikirim tidak valid atau cacat.
     '403': Anda tidak mempunyai izin untuk melihat halaman ini.
diff --git a/config/locales/ie.yml b/config/locales/ie.yml
index 4ee869d92..1529ea04b 100644
--- a/config/locales/ie.yml
+++ b/config/locales/ie.yml
@@ -1153,8 +1153,6 @@ ie:
       your_appeal_approved: Tui apelle ha esset aprobat
       your_appeal_pending: Tu ha fat un apelle
       your_appeal_rejected: Tui apelle ha esset rejectet
-  domain_validator:
-    invalid_domain: ne es un valid dominia-nómine
   edit_profile:
     basic_information: Basic information
     hint_html: "<strong>Customisa ti quel gente vide sur tui public profil e apu tui postas.</strong> Altri persones es plu probabil sequer te e interacter con te si tu have un detalliat profil e un profil-image."
diff --git a/config/locales/io.yml b/config/locales/io.yml
index a1d268b9f..d1b9aef3e 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -1128,8 +1128,6 @@ io:
       your_appeal_approved: Vua konto aprobesis
       your_appeal_pending: Vu sendis apelo
       your_appeal_rejected: Vua apelo refuzesis
-  domain_validator:
-    invalid_domain: ne esas valida domennomo
   edit_profile:
     basic_information: Fundamentala informo
     other: Altra
diff --git a/config/locales/is.yml b/config/locales/is.yml
index 748d931ff..1c84c45c8 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -1238,8 +1238,6 @@ is:
       your_appeal_approved: Áfrýjun þín hefur verið samþykkt
       your_appeal_pending: Þú hefur sent inn áfrýjun
       your_appeal_rejected: Áfrýjun þinni hefur verið hafnað
-  domain_validator:
-    invalid_domain: er ekki leyfilegt nafn á léni
   edit_profile:
     basic_information: Grunnupplýsingar
     hint_html: "<strong>Sérsníddu hvað fólk sér á opinbera notandasniðinu þínu og næst færslunum þínum.</strong> Annað fólk er líklegra til að fylgjast með þér og eiga í samskiptum við þig ef þú fyllir út notandasniðið og setur auðkennismynd."
diff --git a/config/locales/it.yml b/config/locales/it.yml
index a1ed71a7a..a429c6339 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -1237,8 +1237,6 @@ it:
       your_appeal_approved: Il tuo appello è stato approvato
       your_appeal_pending: Hai presentato un appello
       your_appeal_rejected: Il tuo appello è stato respinto
-  domain_validator:
-    invalid_domain: non è un nome di dominio valido
   edit_profile:
     basic_information: Informazioni di base
     hint_html: "<strong>Personalizza ciò che le persone vedono sul tuo profilo pubblico e accanto ai tuoi post.</strong> È più probabile che altre persone ti seguano e interagiscano con te quando hai un profilo compilato e un'immagine del profilo."
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index af9173cfc..3f50b0d7a 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -1213,8 +1213,6 @@ ja:
       your_appeal_approved: 申し立てが承認されました
       your_appeal_pending: 申し立てを送信しました
       your_appeal_rejected: 申し立ては拒否されました
-  domain_validator:
-    invalid_domain: は無効なドメイン名です
   edit_profile:
     basic_information: 基本情報
     hint_html: "<strong>アカウントのトップページや投稿の隣に表示される公開情報です。</strong>プロフィールとアイコンを設定することで、ほかのユーザーは親しみやすく、またフォローしやすくなります。"
diff --git a/config/locales/kk.yml b/config/locales/kk.yml
index 065cd801f..f89bdee62 100644
--- a/config/locales/kk.yml
+++ b/config/locales/kk.yml
@@ -385,8 +385,6 @@ kk:
       more_details_html: Қосымша мәліметтер алу үшін <a href="%{terms_path}"> құпиялылық саясатын </a> қараңыз.
       username_available: Аккаунтыңыз қайтадан қолжетімді болады
       username_unavailable: Логиніңіз қолжетімді болмайды
-  domain_validator:
-    invalid_domain: жарамды домен атауы емес
   errors:
     '400': Сіз жіберген сұрау жарамсыз немесе дұрыс емес.
     '403': Бұны көру үшін сізде рұқсат жоқ.
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 962ecdd05..ab377df30 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -1218,8 +1218,6 @@ ko:
       your_appeal_approved: 소명이 받아들여졌습니다
       your_appeal_pending: 소명을 제출했습니다
       your_appeal_rejected: 소명이 기각되었습니다
-  domain_validator:
-    invalid_domain: 올바른 도메인 네임이 아닙니다
   edit_profile:
     basic_information: 기본 정보
     hint_html: "<strong>사람들이 공개 프로필을 보고서 게시물을 볼 때를 위한 프로필을 꾸밉니다.</strong> 프로필과 프로필 사진을 채우면 다른 사람들이 나를 팔로우하고 나와 교류할 기회가 더 많아집니다."
diff --git a/config/locales/ku.yml b/config/locales/ku.yml
index cbb6b7640..20fe6cf6d 100644
--- a/config/locales/ku.yml
+++ b/config/locales/ku.yml
@@ -1014,8 +1014,6 @@ ku:
       your_appeal_approved: Îtîraza te hate pejirandin
       your_appeal_pending: Te îtîrazek şand
       your_appeal_rejected: Îtîraza te nehate pejirandin
-  domain_validator:
-    invalid_domain: ne naveke navper a derbasdar e
   errors:
     '400': Daxwaza ku te şand nederbasdar an çewt bû.
     '403': Ji bo dîtina vê rûpelê mafê te nîn e.
diff --git a/config/locales/lad.yml b/config/locales/lad.yml
index 5d60e6e9a..164967159 100644
--- a/config/locales/lad.yml
+++ b/config/locales/lad.yml
@@ -1186,8 +1186,6 @@ lad:
       your_appeal_approved: Tu apelasyon fue achetada
       your_appeal_pending: Tienes enviado una apelasyon
       your_appeal_rejected: Tu apelasyon fue refuzada
-  domain_validator:
-    invalid_domain: no es un nombre de domeno valido
   edit_profile:
     basic_information: Enformasyon bazika
     hint_html: "<strong>Personaliza lo ke la djente ve en tu profil publiko i kon tus publikasyones.</strong> Es mas probavle ke otras personas te sigan i enteraktuen kontigo kuando kompletas tu profil i foto."
diff --git a/config/locales/lv.yml b/config/locales/lv.yml
index 43b6995e2..5dd6ff9e1 100644
--- a/config/locales/lv.yml
+++ b/config/locales/lv.yml
@@ -1162,8 +1162,6 @@ lv:
       your_appeal_approved: Jūsu apelācija ir apstiprināta
       your_appeal_pending: Jūs esat iesniedzis apelāciju
       your_appeal_rejected: Jūsu apelācija ir noraidīta
-  domain_validator:
-    invalid_domain: nav derīgs domēna nosaukums
   edit_profile:
     basic_information: Pamata informācija
     hint_html: "<strong>Pielāgo, ko cilvēki redz Tavā publiskajā profilā un blakus Taviem ierakstiem.</strong> Ir lielāka iespējamība, ka citi clivēki sekos Tev un mijiedarbosies ar Tevi, ja Tev ir aizpildīts profils un profila attēls."
diff --git a/config/locales/ms.yml b/config/locales/ms.yml
index 28a2993d3..c91a62423 100644
--- a/config/locales/ms.yml
+++ b/config/locales/ms.yml
@@ -1115,8 +1115,6 @@ ms:
       your_appeal_approved: Rayuan anda telah diluluskan
       your_appeal_pending: Anda telah menghantar rayuan
       your_appeal_rejected: Rayuan anda telah ditolak
-  domain_validator:
-    invalid_domain: bukan nama domain yang sah
   edit_profile:
     basic_information: Maklumat Asas
     hint_html: "<strong>Sesuaikan perkara yang orang lihat pada profil awam anda dan di sebelah siaran anda.</strong> Orang lain lebih berkemungkinan mengikuti anda kembali dan berinteraksi dengan anda apabila anda mempunyai profil dan gambar profil yang telah diisi."
diff --git a/config/locales/my.yml b/config/locales/my.yml
index 76372ba17..771fbba57 100644
--- a/config/locales/my.yml
+++ b/config/locales/my.yml
@@ -1108,8 +1108,6 @@ my:
       your_appeal_approved: သင့်တင်သွင်းခြင်းကို အတည်ပြုပြီးပါပြီ
       your_appeal_pending: အယူခံဝင်ရန် တင်သွင်းထားသည်
       your_appeal_rejected: အယူခံဝင်မှုကို ပယ်ချလိုက်သည်
-  domain_validator:
-    invalid_domain: တရားဝင်ဒိုမိန်းအမည်မဟုတ်ပါ
   edit_profile:
     basic_information: အခြေခံသတင်းအချက်အလက်
     hint_html: "<strong>သင်၏ အများမြင်ပရိုဖိုင်နှင့် သင့်ပို့စ်များဘေးရှိ တွေ့မြင်ရသည့်အရာကို စိတ်ကြိုက်ပြင်ဆင်ပါ။ </strong> သင့်တွင် ပရိုဖိုင်နှင့် ပရိုဖိုင်ပုံတစ်ခု ဖြည့်သွင်းထားပါက အခြားသူများအနေဖြင့် သင်နှင့် အပြန်အလှန် တုံ့ပြန်နိုင်ခြေပိုများပါသည်။"
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 725d3915c..c74bf488b 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -1235,8 +1235,6 @@ nl:
       your_appeal_approved: Jouw bezwaar is goedgekeurd
       your_appeal_pending: Je hebt een bezwaar ingediend
       your_appeal_rejected: Jouw bezwaar is afgewezen
-  domain_validator:
-    invalid_domain: is een ongeldige domeinnaam
   edit_profile:
     basic_information: Algemene informatie
     hint_html: "<strong>Wat mensen op jouw openbare profiel en naast je berichten zien aanpassen.</strong> Andere mensen gaan je waarschijnlijk eerder volgen en hebben vaker interactie met je, wanneer je profiel is ingevuld en je een profielfoto hebt."
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index f7c2f7426..5c46a0d43 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -1232,8 +1232,6 @@ nn:
       your_appeal_approved: Din klage har blitt godkjent
       your_appeal_pending: Du har levert en klage
       your_appeal_rejected: Din klage har blitt avvist
-  domain_validator:
-    invalid_domain: er ikkje eit gangbart domenenamn
   edit_profile:
     basic_information: Grunnleggande informasjon
     hint_html: "<strong>Tilpass kva folk ser på den offentlege profilen din og ved sida av innlegga dine.</strong> Andre vil i større grad fylgja og samhandla med deg når du har eit profilbilete og har fyllt ut profilen din."
diff --git a/config/locales/no.yml b/config/locales/no.yml
index e2ede9328..b3eebd8ec 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -1147,8 +1147,6 @@
       your_appeal_approved: Anken din har blitt godkjent
       your_appeal_pending: Du har levert en anke
       your_appeal_rejected: Anken din har blitt avvist
-  domain_validator:
-    invalid_domain: er ikke et gyldig domenenavn
   edit_profile:
     basic_information: Grunnleggende informasjon
     hint_html: "<strong>Tilpass hva folk ser på din offentlige profil og ved siden av dine innlegg.</strong> Det er mer sannsynlig at andre mennesker følger deg tilbake og samhandler med deg når du har fylt ut en profil og et profilbilde."
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 8373513c9..e88f8a3f6 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -538,8 +538,6 @@ oc:
     strikes:
       title_actions:
         none: Avertiment
-  domain_validator:
-    invalid_domain: es pas un nom de domeni valid
   errors:
     '403': Avètz pas l’autorizacion de veire aquesta pagina.
     '404': La pagina que cercatz existís pas aquí.
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 7a8d208b0..c90d448a7 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -1270,8 +1270,6 @@ pl:
       your_appeal_approved: Twoje odwołanie zostało zatwierdzone
       your_appeal_pending: Zgłosiłeś odwołanie
       your_appeal_rejected: Twoje odwołanie zostało odrzucone
-  domain_validator:
-    invalid_domain: nie jest prawidłową nazwą domeny
   edit_profile:
     basic_information: Podstawowe informacje
     hint_html: "<strong>Dostosuj to, co ludzie widzą na Twoim profilu publicznym i obok Twoich wpisów.</strong> Inne osoby są bardziej skłonne obserwować Cię i wchodzić z Tobą w interakcje, gdy masz wypełniony profil i zdjęcie profilowe."
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 18496b9e1..3c14589e6 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -1231,8 +1231,6 @@ pt-BR:
       your_appeal_approved: Sua revisão foi aprovada
       your_appeal_pending: Você enviou uma revisão
       your_appeal_rejected: Sua revisão foi rejeitada
-  domain_validator:
-    invalid_domain: não é um nome de domínio válido
   edit_profile:
     basic_information: Informações básicas
     hint_html: "<strong>Personalize o que as pessoas veem no seu perfil público e ao lado de suas publicações.</strong> É mais provável que outras pessoas o sigam de volta e interajam com você quando você tiver um perfil preenchido e uma foto de perfil."
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index 96cb92efd..0f0d6f36e 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -1199,8 +1199,6 @@ pt-PT:
       your_appeal_approved: O seu recurso foi deferido
       your_appeal_pending: Submeteu um recurso
       your_appeal_rejected: O seu recurso foi indeferido
-  domain_validator:
-    invalid_domain: não é um nome de domínio válido
   edit_profile:
     basic_information: Informação básica
     hint_html: "<strong>Personalize o que as pessoas veem no seu perfil público e junto das suas publicações.</strong> É mais provável que as outras pessoas o sigam de volta ou interajam consigo se tiver um perfil preenchido e uma imagem de perfil."
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index b9c582843..9d6b2946a 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -1193,8 +1193,6 @@ ru:
       your_appeal_approved: Ваша апелляция одобрена
       your_appeal_pending: Вы подали апелляцию
       your_appeal_rejected: Ваша апелляция отклонена
-  domain_validator:
-    invalid_domain: не является корректным доменным именем
   edit_profile:
     basic_information: Основная информация
     hint_html: "<strong>Настройте то, что люди видят в вашем публичном профиле и рядом с вашими сообщениями.</strong> Другие люди с большей вероятностью подпишутся на Вас и будут взаимодействовать с вами, если у Вас заполнен профиль и добавлено изображение."
diff --git a/config/locales/sc.yml b/config/locales/sc.yml
index ee66cef83..fee79a132 100644
--- a/config/locales/sc.yml
+++ b/config/locales/sc.yml
@@ -734,8 +734,6 @@ sc:
       title_actions:
         delete_statuses: Cantzelladura de publicatziones
         none: Atentzione
-  domain_validator:
-    invalid_domain: no est unu nòmine de domìniu vàlidu
   edit_profile:
     basic_information: Informatzione bàsica
     other: Àteru
diff --git a/config/locales/sco.yml b/config/locales/sco.yml
index 4b7b9e56d..967706f03 100644
--- a/config/locales/sco.yml
+++ b/config/locales/sco.yml
@@ -1004,8 +1004,6 @@ sco:
       your_appeal_approved: Yer appeal haes been approved
       your_appeal_pending: Ye hae submittit a appeal
       your_appeal_rejected: Yer appeal haes been rejectit
-  domain_validator:
-    invalid_domain: isnae a valid domain nemm
   errors:
     '400': The request thit ye submittit wisnae valid or it wis illformt.
     '403': Ye dinnae hae permission fir tae luik at this page.
diff --git a/config/locales/si.yml b/config/locales/si.yml
index e68da4321..c7968e247 100644
--- a/config/locales/si.yml
+++ b/config/locales/si.yml
@@ -892,8 +892,6 @@ si:
       your_appeal_approved: ඔබගේ අභියාචනය අනුමත කර ඇත
       your_appeal_pending: ඔබ අභියාචනයක් ඉදිරිපත් කර ඇත
       your_appeal_rejected: ඔබගේ අභියාචනය ප්‍රතික්ෂේප කර ඇත
-  domain_validator:
-    invalid_domain: වලංගු ඩොමේන් නාමයක් නොවේ
   edit_profile:
     basic_information: මූලික තොරතුරු
     other: වෙනත්
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index c1fae7e83..8f6137c8c 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -3,6 +3,7 @@ en:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Protects from false attributions.
         discoverable: Your public posts and profile may be featured or recommended in various areas of Mastodon and your profile may be suggested to other users.
         display_name: Your full name or your fun name.
         fields: Your homepage, pronouns, age, anything you want.
@@ -143,6 +144,7 @@ en:
         url: Where events will be sent to
     labels:
       account:
+        attribution_domains_as_text: Only allow specific websites
         discoverable: Feature profile and posts in discovery algorithms
         fields:
           name: Label
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index ad9f8fb5e..c49da0fc3 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -905,8 +905,6 @@ sk:
         silence: Obmedzenie účtu
       your_appeal_approved: Tvoja námietka bola schválená
       your_appeal_pending: Odoslal si námietku
-  domain_validator:
-    invalid_domain: nieje správny tvar domény
   edit_profile:
     basic_information: Základné informácie
     other: Ostatné
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index 3384049a1..d0440abb0 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -1263,8 +1263,6 @@ sl:
       your_appeal_approved: Vaša pritožba je bila odobrena
       your_appeal_pending: Oddali ste pritožbo
       your_appeal_rejected: Vaša pritožba je bila zavržena
-  domain_validator:
-    invalid_domain: ni veljavno ime domene
   edit_profile:
     basic_information: Osnovni podatki
     hint_html: "<strong>Prilagodite, kaj ljudje vidijo na vašem javnem profilu in poleg vaših objav.</strong> Drugi vam bodo raje sledili nazaj in z vami klepetali, če boste imeli izpolnjen profil in nastavljeno profilno sliko."
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index 6907c1ee3..5e8a3c912 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -1226,8 +1226,6 @@ sq:
       your_appeal_approved: Apelimi juaj u miratua
       your_appeal_pending: Keni parashtruar një apelim
       your_appeal_rejected: Apelimi juaj është hedhur poshtë
-  domain_validator:
-    invalid_domain: s’është emër i vlefshëm përkatësie
   edit_profile:
     basic_information: Hollësi elementare
     hint_html: "<strong>Përshtatni ç’shohin njerëzit në profilin tuaj publik dhe në krah të postimeve tuaja.</strong> Personat e tjerë ka më shumë gjasa t’ju ndjekin dhe ndërveprojnë me ju, kur keni të plotësuar profilin dhe një foto profili."
diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml
index dfc8a635c..428b9cb08 100644
--- a/config/locales/sr-Latn.yml
+++ b/config/locales/sr-Latn.yml
@@ -1174,8 +1174,6 @@ sr-Latn:
       your_appeal_approved: Vaša žalba je uvažena
       your_appeal_pending: Priložili ste žalbu
       your_appeal_rejected: Vaša žalba je odbijena
-  domain_validator:
-    invalid_domain: nelegitimno ime domena
   edit_profile:
     basic_information: Osnovne informacije
     hint_html: "<strong>Prilagodite šta ljudi vide na vašem javnom profilu i pored vaših objava.</strong> Veća je verovatnoća da će vas drugi pratiti i komunicirati sa vama kada imate popunjen profil i sliku profila."
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 2bbc4ef13..08fbf39fb 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -1204,8 +1204,6 @@ sr:
       your_appeal_approved: Ваша жалба је уважена
       your_appeal_pending: Приложили сте жалбу
       your_appeal_rejected: Ваша жалба је одбијена
-  domain_validator:
-    invalid_domain: нелегитимно име домена
   edit_profile:
     basic_information: Основне информације
     hint_html: "<strong>Прилагодите шта људи виде на вашем јавном профилу и поред ваших објава.</strong> Већа је вероватноћа да ће вас други пратити и комуницирати са вама када имате попуњен профил и слику профила."
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index df403e602..0cc47ca92 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -1182,8 +1182,6 @@ sv:
       your_appeal_approved: Din överklagan har godkänts
       your_appeal_pending: Du har lämnat in en överklagan
       your_appeal_rejected: Din överklagan har avvisats
-  domain_validator:
-    invalid_domain: är inte ett giltigt domännamn
   edit_profile:
     basic_information: Allmän information
     hint_html: "<strong>Anpassa vad folk ser på din offentliga profil och bredvid dina inlägg.</strong> Andra personer är mer benägna att följa dig och interagera med dig när du har en ifylld profil och en profilbild."
diff --git a/config/locales/th.yml b/config/locales/th.yml
index f409a512d..d1de9fd81 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -1216,8 +1216,6 @@ th:
       your_appeal_approved: อนุมัติการอุทธรณ์ของคุณแล้ว
       your_appeal_pending: คุณได้ส่งการอุทธรณ์
       your_appeal_rejected: ปฏิเสธการอุทธรณ์ของคุณแล้ว
-  domain_validator:
-    invalid_domain: ไม่ใช่ชื่อโดเมนที่ถูกต้อง
   edit_profile:
     basic_information: ข้อมูลพื้นฐาน
     hint_html: "<strong>ปรับแต่งสิ่งที่ผู้คนเห็นในโปรไฟล์สาธารณะของคุณและถัดจากโพสต์ของคุณ</strong> ผู้คนอื่น ๆ มีแนวโน้มที่จะติดตามคุณกลับและโต้ตอบกับคุณมากขึ้นเมื่อคุณมีโปรไฟล์ที่กรอกแล้วและรูปภาพโปรไฟล์"
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index d2d499077..d8f5eff6e 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -1235,8 +1235,6 @@ tr:
       your_appeal_approved: İtirazınız onaylandı
       your_appeal_pending: Bir itiraz gönderdiniz
       your_appeal_rejected: İtirazınız reddedildi
-  domain_validator:
-    invalid_domain: geçerli bir alan adı değil
   edit_profile:
     basic_information: Temel bilgiler
     hint_html: "<strong>İnsanlara herkese açık profilinizde ve gönderilerinizin yanında ne göstermek istediğinizi düzenleyin.</strong> Dolu bir profile ve bir profil resmine sahip olduğunuzda diğer insanlar daha yüksek ihtimalle sizi takip etmek ve sizinle etkileşime geçmek isteyeceklerdir."
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 4e70b192d..600239107 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -1270,8 +1270,6 @@ uk:
       your_appeal_approved: Вашу апеляцію було схвалено
       your_appeal_pending: Ви не подавали апеляцій
       your_appeal_rejected: Вашу апеляцію було відхилено
-  domain_validator:
-    invalid_domain: не є допустимим ім'ям домену
   edit_profile:
     basic_information: Основна інформація
     hint_html: "<strong>Налаштуйте те, що люди бачитимуть у вашому загальнодоступному профілі та поруч із вашими дописами.</strong> Інші люди з більшою ймовірністю підпишуться на вас та взаємодіятимуть з вами, якщо у вас є заповнений профіль та зображення профілю."
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index d211b9e74..dfb36c02d 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -1216,8 +1216,6 @@ vi:
       your_appeal_approved: Khiếu nại của bạn được chấp nhận
       your_appeal_pending: Bạn đã gửi một khiếu nại
       your_appeal_rejected: Khiếu nại của bạn bị từ chối
-  domain_validator:
-    invalid_domain: không phải là một tên miền hợp lệ
   edit_profile:
     basic_information: Thông tin cơ bản
     hint_html: "<strong>Tùy chỉnh những gì mọi người nhìn thấy trên hồ sơ công khai và bên cạnh tút của bạn.</strong> Mọi người sẽ muốn theo dõi và tương tác với bạn hơn nếu bạn có ảnh đại diện và một hồ sơ hoàn chỉnh."
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 7407d81db..b97ab65f0 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -1217,8 +1217,6 @@ zh-CN:
       your_appeal_approved: 你的申诉已被批准
       your_appeal_pending: 你已提交申诉
       your_appeal_rejected: 你的申诉已被驳回
-  domain_validator:
-    invalid_domain: 不是一个有效的域名
   edit_profile:
     basic_information: 基本信息
     hint_html: "<strong>自定义公开资料和嘟文旁边显示的内容。</strong>当您填写完整的个人资料并设置了头像时,其他人更有可能关注您并与您互动。"
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 26743d2cb..90227b911 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -1135,8 +1135,6 @@ zh-HK:
       your_appeal_approved: 你的申訴已獲批准
       your_appeal_pending: 你已提交申訴
       your_appeal_rejected: 你的申訴已被駁回
-  domain_validator:
-    invalid_domain: 不是一個可用域名
   edit_profile:
     basic_information: 基本資料
     hint_html: "<strong>自訂你的公開個人檔案和帖文內容。</strong>當你有完整的個人檔案和頭像時,其他人更願意追蹤你和與你互動。"
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index d92d53f39..052773e32 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -1219,8 +1219,6 @@ zh-TW:
       your_appeal_approved: 您的申訴已被批准
       your_appeal_pending: 您已遞交申訴
       your_appeal_rejected: 您的申訴已被駁回
-  domain_validator:
-    invalid_domain: 並非一個有效網域
   edit_profile:
     basic_information: 基本資訊
     hint_html: "<strong>自訂人們能於您個人檔案及嘟文旁所見之內容。</strong>當您完成填寫個人檔案及設定大頭貼後,其他人們比較願意跟隨您並與您互動。"
diff --git a/config/routes/settings.rb b/config/routes/settings.rb
index 297b80942..cefa24316 100644
--- a/config/routes/settings.rb
+++ b/config/routes/settings.rb
@@ -60,7 +60,7 @@ namespace :settings do
 
   resource :delete, only: [:show, :destroy]
   resource :migration, only: [:show, :create]
-  resource :verification, only: :show
+  resource :verification, only: [:show, :update]
   resource :privacy, only: [:show, :update], controller: 'privacy'
 
   namespace :migration do
diff --git a/db/migrate/20240909014637_add_attribution_domains_to_accounts.rb b/db/migrate/20240909014637_add_attribution_domains_to_accounts.rb
new file mode 100644
index 000000000..e90f6f1ed
--- /dev/null
+++ b/db/migrate/20240909014637_add_attribution_domains_to_accounts.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddAttributionDomainsToAccounts < ActiveRecord::Migration[7.1]
+  def change
+    add_column :accounts, :attribution_domains, :string, array: true, default: []
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f01e11792..540a97133 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.1].define(version: 2024_08_08_125420) do
+ActiveRecord::Schema[7.1].define(version: 2024_09_09_014637) do
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
 
@@ -200,6 +200,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_08_125420) do
     t.datetime "reviewed_at", precision: nil
     t.datetime "requested_review_at", precision: nil
     t.boolean "indexable", default: false, null: false
+    t.string "attribution_domains", default: [], array: true
     t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
     t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
     t.index ["domain", "id"], name: "index_accounts_on_domain_and_id"
diff --git a/spec/controllers/settings/migrations_controller_spec.rb b/spec/controllers/settings/migrations_controller_spec.rb
index 93c5de089..67d5ab54f 100644
--- a/spec/controllers/settings/migrations_controller_spec.rb
+++ b/spec/controllers/settings/migrations_controller_spec.rb
@@ -95,6 +95,7 @@ RSpec.describe Settings::MigrationsController do
 
         before do
           moved_to = Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)])
+          p moved_to.acct
           user.account.migrations.create!(acct: moved_to.acct)
         end
 
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 1e8e4b1e4..2ec7aafc5 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -747,6 +747,22 @@ RSpec.describe Account do
     end
   end
 
+  describe '#can_be_attributed_from?' do
+    subject { Fabricate(:account, attribution_domains: %w(example.com)) }
+
+    it 'returns true for a matching domain' do
+      expect(subject.can_be_attributed_from?('example.com')).to be true
+    end
+
+    it 'returns true for a subdomain of a domain' do
+      expect(subject.can_be_attributed_from?('foo.example.com')).to be true
+    end
+
+    it 'returns false for a non-matching domain' do
+      expect(subject.can_be_attributed_from?('hoge.com')).to be false
+    end
+  end
+
   describe 'Normalizations' do
     describe 'username' do
       it { is_expected.to normalize(:username).from(" \u3000bob \t \u00a0 \n ").to('bob') }
diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb
index 86314e6b4..e4a36cf18 100644
--- a/spec/services/activitypub/process_account_service_spec.rb
+++ b/spec/services/activitypub/process_account_service_spec.rb
@@ -63,6 +63,26 @@ RSpec.describe ActivityPub::ProcessAccountService do
     end
   end
 
+  context 'with attribution domains' do
+    let(:payload) do
+      {
+        id: 'https://foo.test',
+        type: 'Actor',
+        inbox: 'https://foo.test/inbox',
+        attributionDomains: [
+          'example.com',
+        ],
+      }.with_indifferent_access
+    end
+
+    it 'parses attribution domains' do
+      account = subject.call('alice', 'example.com', payload)
+
+      expect(account.attribution_domains)
+        .to match_array(%w(example.com))
+    end
+  end
+
   context 'when account is not suspended' do
     subject { described_class.new.call(account.username, account.domain, payload) }
 
diff --git a/spec/services/bulk_import_service_spec.rb b/spec/services/bulk_import_service_spec.rb
index e8bec96c8..0197f81a4 100644
--- a/spec/services/bulk_import_service_spec.rb
+++ b/spec/services/bulk_import_service_spec.rb
@@ -274,7 +274,7 @@ RSpec.describe BulkImportService do
       let(:rows) do
         [
           { 'domain' => 'blocked.com' },
-          { 'domain' => 'to_block.com' },
+          { 'domain' => 'to-block.com' },
         ]
       end
 
@@ -286,7 +286,7 @@ RSpec.describe BulkImportService do
 
       it 'blocks all the new domains' do
         subject.call(import)
-        expect(account.domain_blocks.pluck(:domain)).to contain_exactly('alreadyblocked.com', 'blocked.com', 'to_block.com')
+        expect(account.domain_blocks.pluck(:domain)).to contain_exactly('alreadyblocked.com', 'blocked.com', 'to-block.com')
       end
 
       it 'marks the import as finished' do
@@ -302,7 +302,7 @@ RSpec.describe BulkImportService do
       let(:rows) do
         [
           { 'domain' => 'blocked.com' },
-          { 'domain' => 'to_block.com' },
+          { 'domain' => 'to-block.com' },
         ]
       end
 
@@ -314,7 +314,7 @@ RSpec.describe BulkImportService do
 
       it 'blocks all the new domains' do
         subject.call(import)
-        expect(account.domain_blocks.pluck(:domain)).to contain_exactly('blocked.com', 'to_block.com')
+        expect(account.domain_blocks.pluck(:domain)).to contain_exactly('blocked.com', 'to-block.com')
       end
 
       it 'marks the import as finished' do
diff --git a/spec/validators/domain_validator_spec.rb b/spec/validators/domain_validator_spec.rb
new file mode 100644
index 000000000..0b4cb0d3f
--- /dev/null
+++ b/spec/validators/domain_validator_spec.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe DomainValidator do
+  let(:record) { record_class.new }
+
+  context 'with no options' do
+    let(:record_class) do
+      Class.new do
+        include ActiveModel::Validations
+
+        attr_accessor :domain
+
+        validates :domain, domain: true
+      end
+    end
+
+    describe '#validate_each' do
+      context 'with a nil value' do
+        it 'does not add errors' do
+          record.domain = nil
+
+          expect(record).to be_valid
+          expect(record.errors).to be_empty
+        end
+      end
+
+      context 'with a valid domain' do
+        it 'does not add errors' do
+          record.domain = 'example.com'
+
+          expect(record).to be_valid
+          expect(record.errors).to be_empty
+        end
+      end
+
+      context 'with a domain that is too long' do
+        it 'adds an error' do
+          record.domain = "#{'a' * 300}.com"
+
+          expect(record).to_not be_valid
+          expect(record.errors.where(:domain)).to_not be_empty
+        end
+      end
+
+      context 'with a domain with an empty segment' do
+        it 'adds an error' do
+          record.domain = '.example.com'
+
+          expect(record).to_not be_valid
+          expect(record.errors.where(:domain)).to_not be_empty
+        end
+      end
+
+      context 'with a domain with an invalid character' do
+        it 'adds an error' do
+          record.domain = '*.example.com'
+
+          expect(record).to_not be_valid
+          expect(record.errors.where(:domain)).to_not be_empty
+        end
+      end
+
+      context 'with a domain that would fail parsing' do
+        it 'adds an error' do
+          record.domain = '/'
+
+          expect(record).to_not be_valid
+          expect(record.errors.where(:domain)).to_not be_empty
+        end
+      end
+    end
+  end
+
+  context 'with acct option' do
+    let(:record_class) do
+      Class.new do
+        include ActiveModel::Validations
+
+        attr_accessor :acct
+
+        validates :acct, domain: { acct: true }
+      end
+    end
+
+    describe '#validate_each' do
+      context 'with a nil value' do
+        it 'does not add errors' do
+          record.acct = nil
+
+          expect(record).to be_valid
+          expect(record.errors).to be_empty
+        end
+      end
+
+      context 'with no domain' do
+        it 'does not add errors' do
+          record.acct = 'hoge_123'
+
+          expect(record).to be_valid
+          expect(record.errors).to be_empty
+        end
+      end
+
+      context 'with a valid domain' do
+        it 'does not add errors' do
+          record.acct = 'hoge_123@example.com'
+
+          expect(record).to be_valid
+          expect(record.errors).to be_empty
+        end
+      end
+
+      context 'with an invalid domain' do
+        it 'adds an error' do
+          record.acct = 'hoge_123@.example.com'
+
+          expect(record).to_not be_valid
+          expect(record.errors.where(:acct)).to_not be_empty
+        end
+      end
+    end
+  end
+end
diff --git a/spec/validators/lines_validator_spec.rb b/spec/validators/lines_validator_spec.rb
new file mode 100644
index 000000000..a80dbbaf3
--- /dev/null
+++ b/spec/validators/lines_validator_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe LinesValidator do
+  let(:record_class) do
+    Class.new do
+      include ActiveModel::Validations
+
+      attr_accessor :text
+
+      validates :text, lines: { maximum: 5 }
+    end
+  end
+
+  let(:record) { record_class.new }
+
+  describe '#validate_each' do
+    context 'with a nil value' do
+      it 'does not add errors' do
+        record.text = nil
+
+        expect(record).to be_valid
+        expect(record.errors).to be_empty
+      end
+    end
+
+    context 'with lines below the limit' do
+      it 'does not add errors' do
+        record.text = "hoge\n" * 5
+
+        expect(record).to be_valid
+        expect(record.errors).to be_empty
+      end
+    end
+
+    context 'with more lines than limit' do
+      it 'adds an error' do
+        record.text = "hoge\n" * 6
+
+        expect(record).to_not be_valid
+        expect(record.errors.where(:text)).to_not be_empty
+      end
+    end
+  end
+end

From da07adfe6c2137b07f3def1716b370329a9ec9cb Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 10 Sep 2024 09:21:40 -0400
Subject: [PATCH 61/91] Add `CustomEmoji.enabled` scope (#31830)

---
 app/lib/entity_cache.rb             | 2 +-
 app/models/announcement_reaction.rb | 2 +-
 app/models/custom_emoji.rb          | 3 ++-
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/app/lib/entity_cache.rb b/app/lib/entity_cache.rb
index 80b0046ee..e647dcab7 100644
--- a/app/lib/entity_cache.rb
+++ b/app/lib/entity_cache.rb
@@ -27,7 +27,7 @@ class EntityCache
     end
 
     unless uncached_ids.empty?
-      uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).index_by(&:shortcode)
+      uncached = CustomEmoji.enabled.where(shortcode: shortcodes, domain: domain).index_by(&:shortcode)
       uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
     end
 
diff --git a/app/models/announcement_reaction.rb b/app/models/announcement_reaction.rb
index 9881892c4..f953402b7 100644
--- a/app/models/announcement_reaction.rb
+++ b/app/models/announcement_reaction.rb
@@ -27,7 +27,7 @@ class AnnouncementReaction < ApplicationRecord
   private
 
   def set_custom_emoji
-    self.custom_emoji = CustomEmoji.local.find_by(disabled: false, shortcode: name) if name.present?
+    self.custom_emoji = CustomEmoji.local.enabled.find_by(shortcode: name) if name.present?
   end
 
   def queue_publish
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 31ba91ad0..6e788c0c1 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -48,9 +48,10 @@ class CustomEmoji < ApplicationRecord
 
   scope :local, -> { where(domain: nil) }
   scope :remote, -> { where.not(domain: nil) }
+  scope :enabled, -> { where(disabled: false) }
   scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
   scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) }
-  scope :listed, -> { local.where(disabled: false).where(visible_in_picker: true) }
+  scope :listed, -> { local.enabled.where(visible_in_picker: true) }
 
   remotable_attachment :image, LIMIT
 

From c4b09d684e864d58fc5eaa171fafcc942e4d6937 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 10 Sep 2024 09:23:55 -0400
Subject: [PATCH 62/91] Extract method for account-referencing in CLI prune
 task (#31824)

---
 lib/mastodon/cli/accounts.rb | 37 +++++++++++++++++++++++++-----------
 1 file changed, 26 insertions(+), 11 deletions(-)

diff --git a/lib/mastodon/cli/accounts.rb b/lib/mastodon/cli/accounts.rb
index 8a138323d..08a28e5f5 100644
--- a/lib/mastodon/cli/accounts.rb
+++ b/lib/mastodon/cli/accounts.rb
@@ -502,17 +502,7 @@ module Mastodon::CLI
       - not muted/blocked by us
     LONG_DESC
     def prune
-      query = Account.remote.non_automated
-      query = query.where('NOT EXISTS (SELECT 1 FROM mentions WHERE account_id = accounts.id)')
-      query = query.where('NOT EXISTS (SELECT 1 FROM favourites WHERE account_id = accounts.id)')
-      query = query.where('NOT EXISTS (SELECT 1 FROM statuses WHERE account_id = accounts.id)')
-      query = query.where('NOT EXISTS (SELECT 1 FROM follows WHERE account_id = accounts.id OR target_account_id = accounts.id)')
-      query = query.where('NOT EXISTS (SELECT 1 FROM blocks WHERE account_id = accounts.id OR target_account_id = accounts.id)')
-      query = query.where('NOT EXISTS (SELECT 1 FROM mutes WHERE target_account_id = accounts.id)')
-      query = query.where('NOT EXISTS (SELECT 1 FROM reports WHERE target_account_id = accounts.id)')
-      query = query.where('NOT EXISTS (SELECT 1 FROM follow_requests WHERE account_id = accounts.id OR target_account_id = accounts.id)')
-
-      _, deleted = parallelize_with_progress(query) do |account|
+      _, deleted = parallelize_with_progress(prunable_accounts) do |account|
         next if account.bot? || account.group?
         next if account.suspended?
         next if account.silenced?
@@ -577,6 +567,31 @@ module Mastodon::CLI
 
     private
 
+    def prunable_accounts
+      Account
+        .remote
+        .non_automated
+        .where.not(referencing_account(Mention, :account_id))
+        .where.not(referencing_account(Favourite, :account_id))
+        .where.not(referencing_account(Status, :account_id))
+        .where.not(referencing_account(Follow, :account_id))
+        .where.not(referencing_account(Follow, :target_account_id))
+        .where.not(referencing_account(Block, :account_id))
+        .where.not(referencing_account(Block, :target_account_id))
+        .where.not(referencing_account(Mute, :target_account_id))
+        .where.not(referencing_account(Report, :target_account_id))
+        .where.not(referencing_account(FollowRequest, :account_id))
+        .where.not(referencing_account(FollowRequest, :target_account_id))
+    end
+
+    def referencing_account(model, attribute)
+      model
+        .where(model.arel_table[attribute].eq Account.arel_table[:id])
+        .select(1)
+        .arel
+        .exists
+    end
+
     def report_errors(errors)
       message = errors.map do |error|
         <<~STRING

From 4ffaced8bcbcb9227722c8e09756c7ca1909aa86 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Tue, 10 Sep 2024 16:00:23 +0200
Subject: [PATCH 63/91] Second attempt at disabling Codecov annotations
 (#31841)

---
 .github/codecov.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/codecov.yml b/.github/codecov.yml
index d9b43b259..21af6d0d4 100644
--- a/.github/codecov.yml
+++ b/.github/codecov.yml
@@ -1,4 +1,3 @@
-annotations: false
 comment: false # Do not leave PR comments
 coverage:
   status:
@@ -10,3 +9,5 @@ coverage:
       default:
         # GitHub status check is not blocking
         informational: true
+github_checks:
+  annotations: false

From 0c3c06f7cc50c9207a44f56e184ce6a41f953171 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 10 Sep 2024 13:32:58 -0400
Subject: [PATCH 64/91] Remove vendor prefix from `mobile-web-app-capable` meta
 tag (#31845)

---
 app/views/layouts/application.html.haml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index a6b34c8a3..99e89d45c 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -21,7 +21,7 @@
       %link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/
     %link{ rel: 'manifest', href: manifest_path(format: :json) }/
     = theme_color_tags current_theme
-    %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
+    %meta{ name: 'mobile-web-app-capable', content: 'yes' }/
 
     %title= html_title
 

From e09f9f885e3d2859f495d5e5e6107bba9bde3b5a Mon Sep 17 00:00:00 2001
From: Michael Stanclift <mx@vmstan.com>
Date: Tue, 10 Sep 2024 12:33:55 -0500
Subject: [PATCH 65/91] Fix alt text modal styling (#31844)

---
 app/javascript/styles/mastodon-light/diff.scss | 17 +++++++----------
 app/javascript/styles/mastodon/components.scss | 16 +++++++++-------
 2 files changed, 16 insertions(+), 17 deletions(-)

diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index 1df556b42..c0cabf837 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -159,8 +159,8 @@
 .error-modal,
 .onboarding-modal,
 .compare-history-modal,
-.report-modal__comment .setting-text__wrapper,
-.report-modal__comment .setting-text,
+.report-modal__comment,
+.report-modal__comment,
 .announcements,
 .picture-in-picture__header,
 .picture-in-picture__footer,
@@ -169,6 +169,11 @@
   border: 1px solid var(--background-border-color);
 }
 
+.setting-text__wrapper,
+.setting-text {
+  border: 1px solid var(--background-border-color);
+}
+
 .reactions-bar__item:hover,
 .reactions-bar__item:focus,
 .reactions-bar__item:active {
@@ -198,14 +203,6 @@
   color: $white;
 }
 
-.report-modal__comment {
-  border-right-color: lighten($ui-base-color, 8%);
-}
-
-.report-modal__container {
-  border-top-color: lighten($ui-base-color, 8%);
-}
-
 .column-settings__hashtags .column-select__option {
   color: $white;
 }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 570c006fa..382491eb7 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3911,7 +3911,7 @@ $ui-header-logo-wordmark-width: 99px;
 
   &__wrapper {
     background: $white;
-    border: 1px solid $ui-secondary-color;
+    border: 1px solid var(--background-border-color);
     margin-bottom: 10px;
     border-radius: 4px;
 
@@ -6298,9 +6298,10 @@ a.status-card {
 .report-modal,
 .actions-modal,
 .compare-history-modal {
-  background: lighten($ui-secondary-color, 8%);
-  color: $inverted-text-color;
-  border-radius: 8px;
+  background: var(--background-color);
+  color: $primary-text-color;
+  border-radius: 4px;
+  border: 1px solid var(--background-border-color);
   overflow: hidden;
   max-width: 90vw;
   width: 480px;
@@ -6344,6 +6345,7 @@ a.status-card {
 .report-modal {
   width: 90vw;
   max-width: 700px;
+  border: 1px solid var(--background-border-color);
 }
 
 .report-dialog-modal {
@@ -6567,7 +6569,7 @@ a.status-card {
 
 .report-modal__container {
   display: flex;
-  border-top: 1px solid $ui-secondary-color;
+  border-top: 1px solid var(--background-border-color);
 
   @media screen and (width <= 480px) {
     flex-wrap: wrap;
@@ -6625,7 +6627,7 @@ a.status-card {
 
 .report-modal__comment {
   padding: 20px;
-  border-inline-end: 1px solid $ui-secondary-color;
+  border-inline-end: 1px solid var(--background-border-color);
   max-width: 320px;
 
   p {
@@ -6636,7 +6638,7 @@ a.status-card {
 
   .setting-text-label {
     display: block;
-    color: $inverted-text-color;
+    color: $secondary-text-color;
     font-size: 14px;
     font-weight: 500;
     margin-bottom: 10px;

From e6f5b36a12396daf0a8166d0d2ea8865cd589ee2 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 10 Sep 2024 19:45:16 +0200
Subject: [PATCH 66/91] Update dependency express to v4.20.0 (#31836)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 90 ++++++++++++++++++++++++++++++++++++-------------------
 1 file changed, 59 insertions(+), 31 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 332f6d012..90e764f49 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5470,9 +5470,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"body-parser@npm:1.20.2":
-  version: 1.20.2
-  resolution: "body-parser@npm:1.20.2"
+"body-parser@npm:1.20.3":
+  version: 1.20.3
+  resolution: "body-parser@npm:1.20.3"
   dependencies:
     bytes: "npm:3.1.2"
     content-type: "npm:~1.0.5"
@@ -5482,11 +5482,11 @@ __metadata:
     http-errors: "npm:2.0.0"
     iconv-lite: "npm:0.4.24"
     on-finished: "npm:2.4.1"
-    qs: "npm:6.11.0"
+    qs: "npm:6.13.0"
     raw-body: "npm:2.5.2"
     type-is: "npm:~1.6.18"
     unpipe: "npm:1.0.0"
-  checksum: 10c0/06f1438fff388a2e2354c96aa3ea8147b79bfcb1262dfcc2aae68ec13723d01d5781680657b74e9f83c808266d5baf52804032fbde2b7382b89bd8cdb273ace9
+  checksum: 10c0/0a9a93b7518f222885498dcecaad528cf010dd109b071bf471c93def4bfe30958b83e03496eb9c1ad4896db543d999bb62be1a3087294162a88cfa1b42c16310
   languageName: node
   linkType: hard
 
@@ -7499,6 +7499,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"encodeurl@npm:~2.0.0":
+  version: 2.0.0
+  resolution: "encodeurl@npm:2.0.0"
+  checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb
+  languageName: node
+  linkType: hard
+
 "encoding@npm:^0.1.13":
   version: 0.1.13
   resolution: "encoding@npm:0.1.13"
@@ -8336,41 +8343,41 @@ __metadata:
   linkType: hard
 
 "express@npm:^4.17.1, express@npm:^4.18.2":
-  version: 4.19.2
-  resolution: "express@npm:4.19.2"
+  version: 4.20.0
+  resolution: "express@npm:4.20.0"
   dependencies:
     accepts: "npm:~1.3.8"
     array-flatten: "npm:1.1.1"
-    body-parser: "npm:1.20.2"
+    body-parser: "npm:1.20.3"
     content-disposition: "npm:0.5.4"
     content-type: "npm:~1.0.4"
     cookie: "npm:0.6.0"
     cookie-signature: "npm:1.0.6"
     debug: "npm:2.6.9"
     depd: "npm:2.0.0"
-    encodeurl: "npm:~1.0.2"
+    encodeurl: "npm:~2.0.0"
     escape-html: "npm:~1.0.3"
     etag: "npm:~1.8.1"
     finalhandler: "npm:1.2.0"
     fresh: "npm:0.5.2"
     http-errors: "npm:2.0.0"
-    merge-descriptors: "npm:1.0.1"
+    merge-descriptors: "npm:1.0.3"
     methods: "npm:~1.1.2"
     on-finished: "npm:2.4.1"
     parseurl: "npm:~1.3.3"
-    path-to-regexp: "npm:0.1.7"
+    path-to-regexp: "npm:0.1.10"
     proxy-addr: "npm:~2.0.7"
     qs: "npm:6.11.0"
     range-parser: "npm:~1.2.1"
     safe-buffer: "npm:5.2.1"
-    send: "npm:0.18.0"
-    serve-static: "npm:1.15.0"
+    send: "npm:0.19.0"
+    serve-static: "npm:1.16.0"
     setprototypeof: "npm:1.2.0"
     statuses: "npm:2.0.1"
     type-is: "npm:~1.6.18"
     utils-merge: "npm:1.0.1"
     vary: "npm:~1.1.2"
-  checksum: 10c0/e82e2662ea9971c1407aea9fc3c16d6b963e55e3830cd0ef5e00b533feda8b770af4e3be630488ef8a752d7c75c4fcefb15892868eeaafe7353cb9e3e269fdcb
+  checksum: 10c0/626e440e9feffa3f82ebce5e7dc0ad7a74fa96079994f30048cce450f4855a258abbcabf021f691aeb72154867f0d28440a8498c62888805faf667a829fb65aa
   languageName: node
   linkType: hard
 
@@ -11808,10 +11815,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"merge-descriptors@npm:1.0.1":
-  version: 1.0.1
-  resolution: "merge-descriptors@npm:1.0.1"
-  checksum: 10c0/b67d07bd44cfc45cebdec349bb6e1f7b077ee2fd5beb15d1f7af073849208cb6f144fe403e29a36571baf3f4e86469ac39acf13c318381e958e186b2766f54ec
+"merge-descriptors@npm:1.0.3":
+  version: 1.0.3
+  resolution: "merge-descriptors@npm:1.0.3"
+  checksum: 10c0/866b7094afd9293b5ea5dcd82d71f80e51514bed33b4c4e9f516795dc366612a4cbb4dc94356e943a8a6914889a914530badff27f397191b9b75cda20b6bae93
   languageName: node
   linkType: hard
 
@@ -12930,10 +12937,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"path-to-regexp@npm:0.1.7":
-  version: 0.1.7
-  resolution: "path-to-regexp@npm:0.1.7"
-  checksum: 10c0/50a1ddb1af41a9e68bd67ca8e331a705899d16fb720a1ea3a41e310480948387daf603abb14d7b0826c58f10146d49050a1291ba6a82b78a382d1c02c0b8f905
+"path-to-regexp@npm:0.1.10":
+  version: 0.1.10
+  resolution: "path-to-regexp@npm:0.1.10"
+  checksum: 10c0/34196775b9113ca6df88e94c8d83ba82c0e1a2063dd33bfe2803a980da8d49b91db8104f49d5191b44ea780d46b8670ce2b7f4a5e349b0c48c6779b653f1afe4
   languageName: node
   linkType: hard
 
@@ -14335,12 +14342,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"qs@npm:^6.11.0":
-  version: 6.11.2
-  resolution: "qs@npm:6.11.2"
+"qs@npm:6.13.0, qs@npm:^6.11.0":
+  version: 6.13.0
+  resolution: "qs@npm:6.13.0"
   dependencies:
-    side-channel: "npm:^1.0.4"
-  checksum: 10c0/4f95d4ff18ed480befcafa3390022817ffd3087fc65f146cceb40fc5edb9fa96cb31f648cae2fa96ca23818f0798bd63ad4ca369a0e22702fcd41379b3ab6571
+    side-channel: "npm:^1.0.6"
+  checksum: 10c0/62372cdeec24dc83a9fb240b7533c0fdcf0c5f7e0b83343edd7310f0ab4c8205a5e7c56406531f2e47e1b4878a3821d652be4192c841de5b032ca83619d8f860
   languageName: node
   linkType: hard
 
@@ -15614,6 +15621,27 @@ __metadata:
   languageName: node
   linkType: hard
 
+"send@npm:0.19.0":
+  version: 0.19.0
+  resolution: "send@npm:0.19.0"
+  dependencies:
+    debug: "npm:2.6.9"
+    depd: "npm:2.0.0"
+    destroy: "npm:1.2.0"
+    encodeurl: "npm:~1.0.2"
+    escape-html: "npm:~1.0.3"
+    etag: "npm:~1.8.1"
+    fresh: "npm:0.5.2"
+    http-errors: "npm:2.0.0"
+    mime: "npm:1.6.0"
+    ms: "npm:2.1.3"
+    on-finished: "npm:2.4.1"
+    range-parser: "npm:~1.2.1"
+    statuses: "npm:2.0.1"
+  checksum: 10c0/ea3f8a67a8f0be3d6bf9080f0baed6d2c51d11d4f7b4470de96a5029c598a7011c497511ccc28968b70ef05508675cebff27da9151dd2ceadd60be4e6cf845e3
+  languageName: node
+  linkType: hard
+
 "serialize-javascript@npm:^5.0.1":
   version: 5.0.1
   resolution: "serialize-javascript@npm:5.0.1"
@@ -15647,15 +15675,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"serve-static@npm:1.15.0":
-  version: 1.15.0
-  resolution: "serve-static@npm:1.15.0"
+"serve-static@npm:1.16.0":
+  version: 1.16.0
+  resolution: "serve-static@npm:1.16.0"
   dependencies:
     encodeurl: "npm:~1.0.2"
     escape-html: "npm:~1.0.3"
     parseurl: "npm:~1.3.3"
     send: "npm:0.18.0"
-  checksum: 10c0/fa9f0e21a540a28f301258dfe1e57bb4f81cd460d28f0e973860477dd4acef946a1f41748b5bd41c73b621bea2029569c935faa38578fd34cd42a9b4947088ba
+  checksum: 10c0/d7a5beca08cc55f92998d8b87c111dd842d642404231c90c11f504f9650935da4599c13256747b0a988442a59851343271fe8e1946e03e92cd79c447b5f3ae01
   languageName: node
   linkType: hard
 

From 9e12fa254e8956f1f8765a76b2c6ea54e47cfdae Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 10 Sep 2024 17:45:32 +0000
Subject: [PATCH 67/91] Update dependency propshaft to v1 (#31832)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 Gemfile.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index de541edfb..1f4e042f2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -601,7 +601,7 @@ GEM
       actionmailer (>= 3)
       net-smtp
       premailer (~> 1.7, >= 1.7.9)
-    propshaft (0.9.1)
+    propshaft (1.0.0)
       actionpack (>= 7.0.0)
       activesupport (>= 7.0.0)
       rack
@@ -691,7 +691,7 @@ GEM
     redlock (1.3.2)
       redis (>= 3.0.0, < 6.0)
     regexp_parser (2.9.2)
-    reline (0.5.9)
+    reline (0.5.10)
       io-console (~> 0.5)
     request_store (1.6.0)
       rack (>= 1.4)

From a3215c0f88bb5f436bed665ad26175923544d9d4 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Wed, 11 Sep 2024 09:29:18 +0200
Subject: [PATCH 68/91] Change inner borders in media galleries in web UI
 (#31852)

---
 .../mastodon/components/media_gallery.jsx     |  2 +-
 .../styles/mastodon/components.scss           | 58 +++++++++++++++++++
 2 files changed, 59 insertions(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx
index 9a8f85212..ba54b7f90 100644
--- a/app/javascript/mastodon/components/media_gallery.jsx
+++ b/app/javascript/mastodon/components/media_gallery.jsx
@@ -327,7 +327,7 @@ class MediaGallery extends PureComponent {
     }
 
     return (
-      <div className='media-gallery' style={style} ref={this.handleRef}>
+      <div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}>
         {(!visible || uncached) && (
           <div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
             {spoilerButton}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 382491eb7..5a8fa3e5c 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6953,6 +6953,64 @@ a.status-card {
   grid-template-columns: 50% 50%;
   grid-template-rows: 50% 50%;
   gap: 2px;
+
+  &--layout-2 {
+    .media-gallery__item:nth-child(1) {
+      border-end-end-radius: 0;
+      border-start-end-radius: 0;
+    }
+
+    .media-gallery__item:nth-child(2) {
+      border-start-start-radius: 0;
+      border-end-start-radius: 0;
+    }
+  }
+
+  &--layout-3 {
+    .media-gallery__item:nth-child(1) {
+      border-end-end-radius: 0;
+      border-start-end-radius: 0;
+    }
+
+    .media-gallery__item:nth-child(2) {
+      border-start-start-radius: 0;
+      border-end-start-radius: 0;
+      border-end-end-radius: 0;
+    }
+
+    .media-gallery__item:nth-child(3) {
+      border-start-start-radius: 0;
+      border-end-start-radius: 0;
+      border-start-end-radius: 0;
+    }
+  }
+
+  &--layout-4 {
+    .media-gallery__item:nth-child(1) {
+      border-end-end-radius: 0;
+      border-start-end-radius: 0;
+      border-end-start-radius: 0;
+    }
+
+    .media-gallery__item:nth-child(2) {
+      border-start-start-radius: 0;
+      border-end-start-radius: 0;
+      border-end-end-radius: 0;
+    }
+
+    .media-gallery__item:nth-child(3) {
+      border-start-start-radius: 0;
+      border-start-end-radius: 0;
+      border-end-start-radius: 0;
+      border-end-end-radius: 0;
+    }
+
+    .media-gallery__item:nth-child(4) {
+      border-start-start-radius: 0;
+      border-end-start-radius: 0;
+      border-start-end-radius: 0;
+    }
+  }
 }
 
 .media-gallery__item {

From cee71b9892d14d934f4f14e91c4d7d7843fc13d9 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 11 Sep 2024 03:47:16 -0400
Subject: [PATCH 69/91] Remove `fa_` prefix from status visibility icon method
 (#31846)

---
 app/helpers/statuses_helper.rb                | 20 +++++++++----------
 app/views/admin/reports/_status.html.haml     |  2 +-
 .../filters/statuses/_status_filter.html.haml |  2 +-
 spec/helpers/statuses_helper_spec.rb          | 18 ++++++++---------
 4 files changed, 20 insertions(+), 22 deletions(-)

diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb
index d956e4fcd..bba6d64a4 100644
--- a/app/helpers/statuses_helper.rb
+++ b/app/helpers/statuses_helper.rb
@@ -4,6 +4,13 @@ module StatusesHelper
   EMBEDDED_CONTROLLER = 'statuses'
   EMBEDDED_ACTION = 'embed'
 
+  VISIBLITY_ICONS = {
+    public: 'globe',
+    unlisted: 'lock_open',
+    private: 'lock',
+    direct: 'alternate_email',
+  }.freeze
+
   def nothing_here(extra_classes = '')
     content_tag(:div, class: "nothing-here #{extra_classes}") do
       t('accounts.nothing_here')
@@ -57,17 +64,8 @@ module StatusesHelper
     embedded_view? ? '_blank' : nil
   end
 
-  def fa_visibility_icon(status)
-    case status.visibility
-    when 'public'
-      material_symbol 'globe'
-    when 'unlisted'
-      material_symbol 'lock_open'
-    when 'private'
-      material_symbol 'lock'
-    when 'direct'
-      material_symbol 'alternate_email'
-    end
+  def visibility_icon(status)
+    VISIBLITY_ICONS[status.visibility.to_sym]
   end
 
   def embedded_view?
diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml
index 11be38ef8..e0870503d 100644
--- a/app/views/admin/reports/_status.html.haml
+++ b/app/views/admin/reports/_status.html.haml
@@ -33,7 +33,7 @@
         = material_symbol('repeat_active')
         = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account))
       - else
-        = fa_visibility_icon(status)
+        = material_symbol visibility_icon(status)
         = t("statuses.visibilities.#{status.visibility}")
       - if status.proper.sensitive?
         ·
diff --git a/app/views/filters/statuses/_status_filter.html.haml b/app/views/filters/statuses/_status_filter.html.haml
index 31aa9ec23..d0d04638d 100644
--- a/app/views/filters/statuses/_status_filter.html.haml
+++ b/app/views/filters/statuses/_status_filter.html.haml
@@ -29,7 +29,7 @@
         ·
         = t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted'))
       ·
-      = fa_visibility_icon(status)
+      = material_symbol visibility_icon(status)
       = t("statuses.visibilities.#{status.visibility}")
       - if status.sensitive?
         ·
diff --git a/spec/helpers/statuses_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb
index 8809d0afa..edd3e8f2f 100644
--- a/spec/helpers/statuses_helper_spec.rb
+++ b/spec/helpers/statuses_helper_spec.rb
@@ -36,14 +36,14 @@ RSpec.describe StatusesHelper do
     end
   end
 
-  describe 'fa_visibility_icon' do
+  describe 'visibility_icon' do
     context 'with a status that is public' do
       let(:status) { Status.new(visibility: 'public') }
 
       it 'returns the correct fa icon' do
-        result = helper.fa_visibility_icon(status)
+        result = helper.visibility_icon(status)
 
-        expect(result).to match('material-globe')
+        expect(result).to match('globe')
       end
     end
 
@@ -51,9 +51,9 @@ RSpec.describe StatusesHelper do
       let(:status) { Status.new(visibility: 'unlisted') }
 
       it 'returns the correct fa icon' do
-        result = helper.fa_visibility_icon(status)
+        result = helper.visibility_icon(status)
 
-        expect(result).to match('material-lock_open')
+        expect(result).to match('lock_open')
       end
     end
 
@@ -61,9 +61,9 @@ RSpec.describe StatusesHelper do
       let(:status) { Status.new(visibility: 'private') }
 
       it 'returns the correct fa icon' do
-        result = helper.fa_visibility_icon(status)
+        result = helper.visibility_icon(status)
 
-        expect(result).to match('material-lock')
+        expect(result).to match('lock')
       end
     end
 
@@ -71,9 +71,9 @@ RSpec.describe StatusesHelper do
       let(:status) { Status.new(visibility: 'direct') }
 
       it 'returns the correct fa icon' do
-        result = helper.fa_visibility_icon(status)
+        result = helper.visibility_icon(status)
 
-        expect(result).to match('material-alternate_email')
+        expect(result).to match('alternate_email')
       end
     end
   end

From 9769ffdcc2d93fbb33b5daec52ea02854dbb1574 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 11 Sep 2024 09:47:37 +0200
Subject: [PATCH 70/91] Update dependency aws-sdk-s3 to v1.161.0 (#31853)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 Gemfile.lock | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 1f4e042f2..1564c267b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -100,16 +100,16 @@ GEM
     attr_required (1.0.2)
     awrence (1.2.1)
     aws-eventstream (1.3.0)
-    aws-partitions (1.970.0)
-    aws-sdk-core (3.203.0)
+    aws-partitions (1.973.0)
+    aws-sdk-core (3.204.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.9)
       jmespath (~> 1, >= 1.6.1)
-    aws-sdk-kms (1.89.0)
+    aws-sdk-kms (1.90.0)
       aws-sdk-core (~> 3, >= 3.203.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.160.0)
+    aws-sdk-s3 (1.161.0)
       aws-sdk-core (~> 3, >= 3.203.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)

From cdcd834f3c08f12eb1b7cbf66aec3c716b232663 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 11 Sep 2024 04:01:32 -0400
Subject: [PATCH 71/91] Add coverage for `AnnualReport::*` source child classes
 (#31849)

---
 .../commonly_interacted_with_accounts_spec.rb | 41 +++++++++++++++
 .../most_reblogged_accounts_spec.rb           | 41 +++++++++++++++
 spec/lib/annual_report/most_used_apps_spec.rb | 40 +++++++++++++++
 spec/lib/annual_report/percentiles_spec.rb    | 44 ++++++++++++++++
 spec/lib/annual_report/time_series_spec.rb    | 46 +++++++++++++++++
 spec/lib/annual_report/top_hashtags_spec.rb   | 43 ++++++++++++++++
 spec/lib/annual_report/top_statuses_spec.rb   | 50 +++++++++++++++++++
 .../annual_report/type_distribution_spec.rb   | 48 ++++++++++++++++++
 8 files changed, 353 insertions(+)
 create mode 100644 spec/lib/annual_report/commonly_interacted_with_accounts_spec.rb
 create mode 100644 spec/lib/annual_report/most_reblogged_accounts_spec.rb
 create mode 100644 spec/lib/annual_report/most_used_apps_spec.rb
 create mode 100644 spec/lib/annual_report/percentiles_spec.rb
 create mode 100644 spec/lib/annual_report/time_series_spec.rb
 create mode 100644 spec/lib/annual_report/top_hashtags_spec.rb
 create mode 100644 spec/lib/annual_report/top_statuses_spec.rb
 create mode 100644 spec/lib/annual_report/type_distribution_spec.rb

diff --git a/spec/lib/annual_report/commonly_interacted_with_accounts_spec.rb b/spec/lib/annual_report/commonly_interacted_with_accounts_spec.rb
new file mode 100644
index 000000000..e99d3cb4a
--- /dev/null
+++ b/spec/lib/annual_report/commonly_interacted_with_accounts_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AnnualReport::CommonlyInteractedWithAccounts do
+  describe '#generate' do
+    subject { described_class.new(account, Time.zone.now.year) }
+
+    context 'with an inactive account' do
+      let(:account) { Fabricate :account }
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            commonly_interacted_with_accounts: be_an(Array).and(be_empty)
+          )
+      end
+    end
+
+    context 'with an active account' do
+      let(:account) { Fabricate :account }
+
+      let(:other_account) { Fabricate :account }
+
+      before do
+        _other = Fabricate :status
+        Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: other_account).id
+        Fabricate :status, account: account, reply: true, in_reply_to_id: Fabricate(:status, account: other_account).id
+      end
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            commonly_interacted_with_accounts: contain_exactly(
+              include(account_id: other_account.id, count: 2)
+            )
+          )
+      end
+    end
+  end
+end
diff --git a/spec/lib/annual_report/most_reblogged_accounts_spec.rb b/spec/lib/annual_report/most_reblogged_accounts_spec.rb
new file mode 100644
index 000000000..0280ba199
--- /dev/null
+++ b/spec/lib/annual_report/most_reblogged_accounts_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AnnualReport::MostRebloggedAccounts do
+  describe '#generate' do
+    subject { described_class.new(account, Time.zone.now.year) }
+
+    context 'with an inactive account' do
+      let(:account) { Fabricate :account }
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            most_reblogged_accounts: be_an(Array).and(be_empty)
+          )
+      end
+    end
+
+    context 'with an active account' do
+      let(:account) { Fabricate :account }
+
+      let(:other_account) { Fabricate :account }
+
+      before do
+        _other = Fabricate :status
+        Fabricate :status, account: account, reblog: Fabricate(:status, account: other_account)
+        Fabricate :status, account: account, reblog: Fabricate(:status, account: other_account)
+      end
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            most_reblogged_accounts: contain_exactly(
+              include(account_id: other_account.id, count: 2)
+            )
+          )
+      end
+    end
+  end
+end
diff --git a/spec/lib/annual_report/most_used_apps_spec.rb b/spec/lib/annual_report/most_used_apps_spec.rb
new file mode 100644
index 000000000..d2fcecc4d
--- /dev/null
+++ b/spec/lib/annual_report/most_used_apps_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AnnualReport::MostUsedApps do
+  describe '#generate' do
+    subject { described_class.new(account, Time.zone.now.year) }
+
+    context 'with an inactive account' do
+      let(:account) { Fabricate :account }
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            most_used_apps: be_an(Array).and(be_empty)
+          )
+      end
+    end
+
+    context 'with an active account' do
+      let(:account) { Fabricate :account }
+
+      let(:application) { Fabricate :application }
+
+      before do
+        _other = Fabricate :status
+        Fabricate.times 2, :status, account: account, application: application
+      end
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            most_used_apps: contain_exactly(
+              include(name: application.name, count: 2)
+            )
+          )
+      end
+    end
+  end
+end
diff --git a/spec/lib/annual_report/percentiles_spec.rb b/spec/lib/annual_report/percentiles_spec.rb
new file mode 100644
index 000000000..1d1df3166
--- /dev/null
+++ b/spec/lib/annual_report/percentiles_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AnnualReport::Percentiles do
+  describe '#generate' do
+    subject { described_class.new(account, Time.zone.now.year) }
+
+    context 'with an inactive account' do
+      let(:account) { Fabricate :account }
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            percentiles: include(
+              followers: 0,
+              statuses: 0
+            )
+          )
+      end
+    end
+
+    context 'with an active account' do
+      let(:account) { Fabricate :account }
+
+      before do
+        Fabricate.times 2, :status # Others as `account`
+        Fabricate.times 2, :follow # Others as `target_account`
+        Fabricate.times 2, :status, account: account
+        Fabricate.times 2, :follow, target_account: account
+      end
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            percentiles: include(
+              followers: 50,
+              statuses: 50
+            )
+          )
+      end
+    end
+  end
+end
diff --git a/spec/lib/annual_report/time_series_spec.rb b/spec/lib/annual_report/time_series_spec.rb
new file mode 100644
index 000000000..219d6c083
--- /dev/null
+++ b/spec/lib/annual_report/time_series_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AnnualReport::TimeSeries do
+  describe '#generate' do
+    subject { described_class.new(account, Time.zone.now.year) }
+
+    context 'with an inactive account' do
+      let(:account) { Fabricate :account }
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            time_series: match(
+              include(followers: 0, following: 0, month: 1, statuses: 0)
+            )
+          )
+      end
+    end
+
+    context 'with an active account' do
+      let(:account) { Fabricate :account }
+
+      let(:month_one_date) { DateTime.new(Time.zone.now.year, 1, 1, 12, 12, 12) }
+
+      let(:tag) { Fabricate :tag }
+
+      before do
+        _other = Fabricate :status
+        Fabricate :status, account: account, created_at: month_one_date
+        Fabricate :follow, account: account, created_at: month_one_date
+        Fabricate :follow, target_account: account, created_at: month_one_date
+      end
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            time_series: match(
+              include(followers: 1, following: 1, month: 1, statuses: 1)
+            )
+          )
+      end
+    end
+  end
+end
diff --git a/spec/lib/annual_report/top_hashtags_spec.rb b/spec/lib/annual_report/top_hashtags_spec.rb
new file mode 100644
index 000000000..58a915218
--- /dev/null
+++ b/spec/lib/annual_report/top_hashtags_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AnnualReport::TopHashtags do
+  describe '#generate' do
+    subject { described_class.new(account, Time.zone.now.year) }
+
+    context 'with an inactive account' do
+      let(:account) { Fabricate :account }
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            top_hashtags: be_an(Array).and(be_empty)
+          )
+      end
+    end
+
+    context 'with an active account' do
+      let(:account) { Fabricate :account }
+
+      let(:tag) { Fabricate :tag }
+
+      before do
+        _other = Fabricate :status
+        first = Fabricate :status, account: account
+        first.tags << tag
+        last = Fabricate :status, account: account
+        last.tags << tag
+      end
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            top_hashtags: contain_exactly(
+              include(name: tag.name, count: 2)
+            )
+          )
+      end
+    end
+  end
+end
diff --git a/spec/lib/annual_report/top_statuses_spec.rb b/spec/lib/annual_report/top_statuses_spec.rb
new file mode 100644
index 000000000..b956b0397
--- /dev/null
+++ b/spec/lib/annual_report/top_statuses_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AnnualReport::TopStatuses do
+  describe '#generate' do
+    subject { described_class.new(account, Time.zone.now.year) }
+
+    context 'with an inactive account' do
+      let(:account) { Fabricate :account }
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            top_statuses: include(
+              by_reblogs: be_nil,
+              by_favourites: be_nil,
+              by_replies: be_nil
+            )
+          )
+      end
+    end
+
+    context 'with an active account' do
+      let(:account) { Fabricate :account }
+
+      let(:reblogged_status) { Fabricate :status, account: account }
+      let(:favourited_status) { Fabricate :status, account: account }
+      let(:replied_status) { Fabricate :status, account: account }
+
+      before do
+        _other = Fabricate :status
+        reblogged_status.status_stat.update(reblogs_count: 123)
+        favourited_status.status_stat.update(favourites_count: 123)
+        replied_status.status_stat.update(replies_count: 123)
+      end
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            top_statuses: include(
+              by_reblogs: reblogged_status.id,
+              by_favourites: favourited_status.id,
+              by_replies: replied_status.id
+            )
+          )
+      end
+    end
+  end
+end
diff --git a/spec/lib/annual_report/type_distribution_spec.rb b/spec/lib/annual_report/type_distribution_spec.rb
new file mode 100644
index 000000000..89a31fb20
--- /dev/null
+++ b/spec/lib/annual_report/type_distribution_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AnnualReport::TypeDistribution do
+  describe '#generate' do
+    subject { described_class.new(account, Time.zone.now.year) }
+
+    context 'with an inactive account' do
+      let(:account) { Fabricate :account }
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            type_distribution: include(
+              total: 0,
+              reblogs: 0,
+              replies: 0,
+              standalone: 0
+            )
+          )
+      end
+    end
+
+    context 'with an active account' do
+      let(:account) { Fabricate :account }
+
+      before do
+        _other = Fabricate :status
+        Fabricate :status, reblog: Fabricate(:status), account: account
+        Fabricate :status, in_reply_to_id: Fabricate(:status).id, account: account, reply: true
+        Fabricate :status, account: account
+      end
+
+      it 'builds a report for an account' do
+        expect(subject.generate)
+          .to include(
+            type_distribution: include(
+              total: 3,
+              reblogs: 1,
+              replies: 1,
+              standalone: 1
+            )
+          )
+      end
+    end
+  end
+end

From 2babfafaffd56ff69b6213f3a550c7d0b3d3283c Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 11 Sep 2024 10:18:10 +0200
Subject: [PATCH 72/91] New Crowdin Translations (automated) (#31855)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/an.json      |  1 -
 app/javascript/mastodon/locales/ar.json      |  1 -
 app/javascript/mastodon/locales/ast.json     |  1 -
 app/javascript/mastodon/locales/be.json      |  1 -
 app/javascript/mastodon/locales/bg.json      |  1 -
 app/javascript/mastodon/locales/bn.json      |  1 -
 app/javascript/mastodon/locales/br.json      |  1 -
 app/javascript/mastodon/locales/ca.json      |  2 +-
 app/javascript/mastodon/locales/ckb.json     |  1 -
 app/javascript/mastodon/locales/co.json      |  1 -
 app/javascript/mastodon/locales/cs.json      |  1 -
 app/javascript/mastodon/locales/cy.json      |  1 -
 app/javascript/mastodon/locales/da.json      |  2 +-
 app/javascript/mastodon/locales/de.json      |  2 +-
 app/javascript/mastodon/locales/el.json      |  1 -
 app/javascript/mastodon/locales/en-GB.json   |  4 +-
 app/javascript/mastodon/locales/eo.json      |  1 -
 app/javascript/mastodon/locales/es-AR.json   |  1 -
 app/javascript/mastodon/locales/es-MX.json   |  1 -
 app/javascript/mastodon/locales/es.json      |  1 -
 app/javascript/mastodon/locales/et.json      |  1 -
 app/javascript/mastodon/locales/eu.json      |  1 -
 app/javascript/mastodon/locales/fa.json      |  1 -
 app/javascript/mastodon/locales/fi.json      |  2 +-
 app/javascript/mastodon/locales/fo.json      |  4 +-
 app/javascript/mastodon/locales/fr-CA.json   |  1 -
 app/javascript/mastodon/locales/fr.json      |  1 -
 app/javascript/mastodon/locales/fy.json      |  1 -
 app/javascript/mastodon/locales/ga.json      |  1 -
 app/javascript/mastodon/locales/gd.json      |  1 -
 app/javascript/mastodon/locales/gl.json      |  6 +--
 app/javascript/mastodon/locales/he.json      |  2 +-
 app/javascript/mastodon/locales/hi.json      |  1 +
 app/javascript/mastodon/locales/hr.json      |  1 -
 app/javascript/mastodon/locales/hu.json      |  2 +-
 app/javascript/mastodon/locales/hy.json      |  1 -
 app/javascript/mastodon/locales/ia.json      |  3 +-
 app/javascript/mastodon/locales/id.json      |  1 -
 app/javascript/mastodon/locales/ie.json      |  1 -
 app/javascript/mastodon/locales/io.json      |  1 -
 app/javascript/mastodon/locales/is.json      |  4 +-
 app/javascript/mastodon/locales/it.json      |  2 +-
 app/javascript/mastodon/locales/ja.json      |  1 -
 app/javascript/mastodon/locales/ka.json      |  1 -
 app/javascript/mastodon/locales/kab.json     |  1 -
 app/javascript/mastodon/locales/kk.json      |  1 -
 app/javascript/mastodon/locales/ko.json      | 22 +++++------
 app/javascript/mastodon/locales/ku.json      |  1 -
 app/javascript/mastodon/locales/kw.json      |  1 -
 app/javascript/mastodon/locales/la.json      |  1 -
 app/javascript/mastodon/locales/lad.json     |  1 -
 app/javascript/mastodon/locales/lt.json      |  2 +-
 app/javascript/mastodon/locales/lv.json      |  1 -
 app/javascript/mastodon/locales/ms.json      |  1 -
 app/javascript/mastodon/locales/my.json      |  1 -
 app/javascript/mastodon/locales/nl.json      |  2 +-
 app/javascript/mastodon/locales/nn.json      |  4 +-
 app/javascript/mastodon/locales/no.json      |  1 -
 app/javascript/mastodon/locales/oc.json      |  1 -
 app/javascript/mastodon/locales/pl.json      |  2 +-
 app/javascript/mastodon/locales/pt-BR.json   |  4 +-
 app/javascript/mastodon/locales/pt-PT.json   |  1 -
 app/javascript/mastodon/locales/ro.json      |  1 -
 app/javascript/mastodon/locales/ru.json      |  1 -
 app/javascript/mastodon/locales/sa.json      |  1 -
 app/javascript/mastodon/locales/sc.json      |  1 -
 app/javascript/mastodon/locales/sco.json     |  1 -
 app/javascript/mastodon/locales/sk.json      |  1 -
 app/javascript/mastodon/locales/sl.json      |  1 -
 app/javascript/mastodon/locales/sq.json      |  4 +-
 app/javascript/mastodon/locales/sr-Latn.json |  1 -
 app/javascript/mastodon/locales/sr.json      |  1 -
 app/javascript/mastodon/locales/sv.json      |  2 +-
 app/javascript/mastodon/locales/ta.json      |  1 -
 app/javascript/mastodon/locales/te.json      |  1 -
 app/javascript/mastodon/locales/th.json      |  1 -
 app/javascript/mastodon/locales/tok.json     |  1 -
 app/javascript/mastodon/locales/tr.json      |  2 +-
 app/javascript/mastodon/locales/uk.json      |  5 ++-
 app/javascript/mastodon/locales/uz.json      |  1 -
 app/javascript/mastodon/locales/vi.json      |  1 -
 app/javascript/mastodon/locales/zgh.json     |  1 -
 app/javascript/mastodon/locales/zh-CN.json   |  2 +-
 app/javascript/mastodon/locales/zh-HK.json   |  1 -
 app/javascript/mastodon/locales/zh-TW.json   |  2 +-
 config/locales/activerecord.ca.yml           |  6 +++
 config/locales/activerecord.da.yml           |  6 +++
 config/locales/activerecord.de.yml           |  6 +++
 config/locales/activerecord.en-GB.yml        |  6 +++
 config/locales/activerecord.fi.yml           |  6 +++
 config/locales/activerecord.fo.yml           |  6 +++
 config/locales/activerecord.gl.yml           |  6 +++
 config/locales/activerecord.he.yml           |  6 +++
 config/locales/activerecord.hu.yml           |  6 +++
 config/locales/activerecord.ia.yml           |  5 +++
 config/locales/activerecord.is.yml           |  6 +++
 config/locales/activerecord.it.yml           |  6 +++
 config/locales/activerecord.lt.yml           |  6 +++
 config/locales/activerecord.nl.yml           |  6 +++
 config/locales/activerecord.nn.yml           |  6 +++
 config/locales/activerecord.pl.yml           |  6 +++
 config/locales/activerecord.pt-BR.yml        |  6 +++
 config/locales/activerecord.sv.yml           |  2 +
 config/locales/activerecord.tr.yml           |  6 +++
 config/locales/activerecord.uk.yml           |  6 +++
 config/locales/activerecord.zh-CN.yml        |  6 +++
 config/locales/activerecord.zh-TW.yml        |  6 +++
 config/locales/ca.yml                        | 20 ++++++++++
 config/locales/da.yml                        |  7 ++++
 config/locales/de.yml                        |  8 ++++
 config/locales/en-GB.yml                     | 41 ++++++++++++++++++++
 config/locales/fi.yml                        |  5 +++
 config/locales/fo.yml                        |  8 ++++
 config/locales/gl.yml                        |  7 ++++
 config/locales/he.yml                        |  8 ++++
 config/locales/hu.yml                        | 10 ++++-
 config/locales/ia.yml                        |  6 +++
 config/locales/is.yml                        |  8 ++++
 config/locales/it.yml                        |  7 ++++
 config/locales/lt.yml                        |  8 ++++
 config/locales/nl.yml                        |  7 ++++
 config/locales/nn.yml                        | 10 +++++
 config/locales/pl.yml                        |  8 ++++
 config/locales/pt-BR.yml                     | 11 ++++++
 config/locales/simple_form.ca.yml            |  2 +
 config/locales/simple_form.da.yml            |  2 +
 config/locales/simple_form.de.yml            |  2 +
 config/locales/simple_form.en-GB.yml         |  3 ++
 config/locales/simple_form.fi.yml            |  1 +
 config/locales/simple_form.fo.yml            |  2 +
 config/locales/simple_form.gl.yml            |  2 +
 config/locales/simple_form.he.yml            |  2 +
 config/locales/simple_form.hu.yml            |  2 +
 config/locales/simple_form.ia.yml            |  1 +
 config/locales/simple_form.is.yml            |  2 +
 config/locales/simple_form.it.yml            |  2 +
 config/locales/simple_form.lt.yml            |  2 +
 config/locales/simple_form.nl.yml            |  2 +
 config/locales/simple_form.nn.yml            |  3 ++
 config/locales/simple_form.pl.yml            |  2 +
 config/locales/simple_form.pt-BR.yml         |  3 ++
 config/locales/simple_form.tr.yml            |  2 +
 config/locales/simple_form.uk.yml            |  2 +
 config/locales/simple_form.zh-CN.yml         |  2 +
 config/locales/simple_form.zh-TW.yml         |  2 +
 config/locales/sq.yml                        |  1 +
 config/locales/sv.yml                        |  4 ++
 config/locales/tr.yml                        |  9 ++++-
 config/locales/uk.yml                        |  8 ++++
 config/locales/zh-CN.yml                     |  7 ++++
 config/locales/zh-TW.yml                     |  7 ++++
 151 files changed, 435 insertions(+), 99 deletions(-)

diff --git a/app/javascript/mastodon/locales/an.json b/app/javascript/mastodon/locales/an.json
index 7974cea64..893882fe7 100644
--- a/app/javascript/mastodon/locales/an.json
+++ b/app/javascript/mastodon/locales/an.json
@@ -308,7 +308,6 @@
   "lists.search": "Buscar entre la chent a la quala sigues",
   "lists.subheading": "Las tuyas listas",
   "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
-  "media_gallery.toggle_visible": "{number, plural, one {Amaga la imachen} other {Amaga las imáchens}}",
   "moved_to_account_banner.text": "La tuya cuenta {disabledAccount} ye actualment deshabilitada perque t'has mudau a {movedToAccount}.",
   "navigation_bar.about": "Sobre",
   "navigation_bar.blocks": "Usuarios blocaus",
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 722f2bc98..0dbeb0a19 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -443,7 +443,6 @@
   "lists.subheading": "قوائمك",
   "load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
   "loading_indicator.label": "جاري التحميل…",
-  "media_gallery.toggle_visible": "{number, plural, zero {} one {اخف الصورة} two {اخف الصورتين} few {اخف الصور} many {اخف الصور} other {اخف الصور}}",
   "moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.",
   "mute_modal.hide_from_notifications": "إخفاء من قائمة الإشعارات",
   "mute_modal.hide_options": "إخفاء الخيارات",
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index 74eb7021d..507703023 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -268,7 +268,6 @@
   "lists.search": "Buscar ente los perfiles que sigues",
   "lists.subheading": "Les tos llistes",
   "load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
-  "media_gallery.toggle_visible": "{number, plural, one {Anubrir la imaxe} other {Anubrir les imáxenes}}",
   "navigation_bar.about": "Tocante a",
   "navigation_bar.blocks": "Perfiles bloquiaos",
   "navigation_bar.bookmarks": "Marcadores",
diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json
index 57431b0a5..412eae148 100644
--- a/app/javascript/mastodon/locales/be.json
+++ b/app/javascript/mastodon/locales/be.json
@@ -437,7 +437,6 @@
   "lists.subheading": "Вашыя спісы",
   "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}",
   "loading_indicator.label": "Загрузка…",
-  "media_gallery.toggle_visible": "{number, plural, one {Схаваць відарыс} other {Схаваць відарысы}}",
   "moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны таму што вы перанесены на {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Схаваць з апавяшчэнняў",
   "mute_modal.hide_options": "Схаваць опцыі",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 828092d43..b89b539dc 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -444,7 +444,6 @@
   "lists.subheading": "Вашите списъци",
   "load_pending": "{count, plural, one {# нов елемент} other {# нови елемента}}",
   "loading_indicator.label": "Зареждане…",
-  "media_gallery.toggle_visible": "Скриване на {number, plural, one {изображение} other {изображения}}",
   "moved_to_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен, защото се преместихте в {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Скриване от известията",
   "mute_modal.hide_options": "Скриване на възможностите",
diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json
index 584bf303b..df97abdff 100644
--- a/app/javascript/mastodon/locales/bn.json
+++ b/app/javascript/mastodon/locales/bn.json
@@ -288,7 +288,6 @@
   "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
   "lists.subheading": "আপনার তালিকা",
   "load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
-  "media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
   "navigation_bar.about": "পরিচিতি",
   "navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
   "navigation_bar.bookmarks": "বুকমার্ক",
diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json
index c8bb4975d..226ff756f 100644
--- a/app/javascript/mastodon/locales/br.json
+++ b/app/javascript/mastodon/locales/br.json
@@ -360,7 +360,6 @@
   "lists.subheading": "Ho listennoù",
   "load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
   "loading_indicator.label": "O kargañ…",
-  "media_gallery.toggle_visible": "{number, plural, one {Kuzhat ar skeudenn} other {Kuzhat ar skeudenn}}",
   "navigation_bar.about": "Diwar-benn",
   "navigation_bar.blocks": "Implijer·ezed·ien berzet",
   "navigation_bar.bookmarks": "Sinedoù",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 5981c1df8..91f28bd15 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Les teves llistes",
   "load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
   "loading_indicator.label": "Es carrega…",
-  "media_gallery.toggle_visible": "{number, plural, one {Amaga la imatge} other {Amaga les imatges}}",
+  "media_gallery.hide": "Amaga",
   "moved_to_account_banner.text": "El teu compte {disabledAccount} està desactivat perquè l'has mogut a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Amaga de les notificacions",
   "mute_modal.hide_options": "Amaga les opcions",
diff --git a/app/javascript/mastodon/locales/ckb.json b/app/javascript/mastodon/locales/ckb.json
index 9def7533a..61b81c9f3 100644
--- a/app/javascript/mastodon/locales/ckb.json
+++ b/app/javascript/mastodon/locales/ckb.json
@@ -355,7 +355,6 @@
   "lists.search": "بگەڕێ لەناو ئەو کەسانەی کە شوێنیان کەوتویت",
   "lists.subheading": "لیستەکانت",
   "load_pending": "{count, plural, one {# بەڕگەی نوێ} other {# بەڕگەی نوێ}}",
-  "media_gallery.toggle_visible": "شاردنەوەی {number, plural, one {image} other {images}}",
   "moved_to_account_banner.text": "ئەکاونتەکەت {disabledAccount} لە ئێستادا لەکارخراوە چونکە تۆ چوویتە {movedToAccount}.",
   "navigation_bar.about": "دەربارە",
   "navigation_bar.blocks": "بەکارهێنەرە بلۆککراوەکان",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index 3a72ecd3f..180616b78 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -214,7 +214,6 @@
   "lists.search": "Circà indè i vostr'abbunamenti",
   "lists.subheading": "E vo liste",
   "load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
-  "media_gallery.toggle_visible": "Piattà {number, plural, one {ritrattu} other {ritratti}}",
   "navigation_bar.blocks": "Utilizatori bluccati",
   "navigation_bar.bookmarks": "Segnalibri",
   "navigation_bar.community_timeline": "Linea pubblica lucale",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 116a9c014..8dd8f31fa 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -435,7 +435,6 @@
   "lists.subheading": "Vaše seznamy",
   "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
   "loading_indicator.label": "Načítání…",
-  "media_gallery.toggle_visible": "{number, plural, one {Skrýt obrázek} few {Skrýt obrázky} many {Skrýt obrázky} other {Skrýt obrázky}}",
   "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Skrýt z notifikací",
   "mute_modal.hide_options": "Skrýt možnosti",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index 158f31aeb..d34b7a970 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Eich rhestrau",
   "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
   "loading_indicator.label": "Yn llwytho…",
-  "media_gallery.toggle_visible": "{number, plural, one {Cuddio delwedd} other {Cuddio delwedd}}",
   "moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau",
   "mute_modal.hide_options": "Cuddio'r dewis",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 4155dc9fc..dea313ffe 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Dine lister",
   "load_pending": "{count, plural, one {# nyt emne} other {# nye emner}}",
   "loading_indicator.label": "Indlæser…",
-  "media_gallery.toggle_visible": "{number, plural, one {Skjul billede} other {Skjul billeder}}",
+  "media_gallery.hide": "Skjul",
   "moved_to_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret, da du flyttede til {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Skjul fra notifikationer",
   "mute_modal.hide_options": "Skjul valgmuligheder",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index e37a59546..46bb08d7c 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Deine Listen",
   "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
   "loading_indicator.label": "Wird geladen …",
-  "media_gallery.toggle_visible": "{number, plural, one {Medium ausblenden} other {Medien ausblenden}}",
+  "media_gallery.hide": "Ausblenden",
   "moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.",
   "mute_modal.hide_from_notifications": "Benachrichtigungen ausblenden",
   "mute_modal.hide_options": "Einstellungen ausblenden",
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index a3adaaf9d..64a603923 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -452,7 +452,6 @@
   "lists.subheading": "Οι λίστες σου",
   "load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}",
   "loading_indicator.label": "Φόρτωση…",
-  "media_gallery.toggle_visible": "{number, plural, one {Απόκρυψη εικόνας} other {Απόκρυψη εικόνων}}",
   "moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Απόκρυψη από ειδοποιήσεις",
   "mute_modal.hide_options": "Απόκρυψη επιλογών",
diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json
index e9007c865..c727b8e49 100644
--- a/app/javascript/mastodon/locales/en-GB.json
+++ b/app/javascript/mastodon/locales/en-GB.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "Loading…",
-  "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
+  "media_gallery.hide": "Hide",
   "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Hide from notifications",
   "mute_modal.hide_options": "Hide options",
@@ -780,6 +780,7 @@
   "status.bookmark": "Bookmark",
   "status.cancel_reblog_private": "Unboost",
   "status.cannot_reblog": "This post cannot be boosted",
+  "status.continued_thread": "Continued thread",
   "status.copy": "Copy link to status",
   "status.delete": "Delete",
   "status.detailed_status": "Detailed conversation view",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
   "status.redraft": "Delete & re-draft",
   "status.remove_bookmark": "Remove bookmark",
+  "status.replied_in_thread": "Replied in thread",
   "status.replied_to": "Replied to {name}",
   "status.reply": "Reply",
   "status.replyAll": "Reply to thread",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 40eee9805..d8ec27748 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -390,7 +390,6 @@
   "lists.subheading": "Viaj listoj",
   "load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
   "loading_indicator.label": "Ŝargado…",
-  "media_gallery.toggle_visible": "{number, plural, one {Kaŝi la bildon} other {Kaŝi la bildojn}}",
   "moved_to_account_banner.text": "Via konto {disabledAccount} estas malvalidigita ĉar vi movis ĝin al {movedToAccount}.",
   "navigation_bar.about": "Pri",
   "navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index 20d93ed01..a0f91c476 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Tus listas",
   "load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}",
   "loading_indicator.label": "Cargando…",
-  "media_gallery.toggle_visible": "Ocultar {number, plural, one {imagen} other {imágenes}}",
   "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ocultar en las notificaciones",
   "mute_modal.hide_options": "Ocultar opciones",
diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json
index 348300f72..63f3d5381 100644
--- a/app/javascript/mastodon/locales/es-MX.json
+++ b/app/javascript/mastodon/locales/es-MX.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Tus listas",
   "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
   "loading_indicator.label": "Cargando…",
-  "media_gallery.toggle_visible": "Cambiar visibilidad",
   "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ocultar de las notificaciones",
   "mute_modal.hide_options": "Ocultar opciones",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 85b160d7a..b01eb6822 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Tus listas",
   "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
   "loading_indicator.label": "Cargando…",
-  "media_gallery.toggle_visible": "Cambiar visibilidad",
   "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ocultar de las notificaciones",
   "mute_modal.hide_options": "Ocultar opciones",
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
index d0fc80e60..ca37a152f 100644
--- a/app/javascript/mastodon/locales/et.json
+++ b/app/javascript/mastodon/locales/et.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Sinu nimekirjad",
   "load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
   "loading_indicator.label": "Laadimine…",
-  "media_gallery.toggle_visible": "{number, plural, one {Varja pilt} other {Varja pildid}}",
   "moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Peida teavituste hulgast",
   "mute_modal.hide_options": "Peida valikud",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index f169e2905..15dd63486 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Zure zerrendak",
   "load_pending": "{count, plural, one {elementu berri #} other {# elementu berri}}",
   "loading_indicator.label": "Kargatzen…",
-  "media_gallery.toggle_visible": "Txandakatu ikusgaitasuna",
   "moved_to_account_banner.text": "Zure {disabledAccount} kontua desgaituta dago une honetan, {movedToAccount} kontura aldatu zinelako.",
   "mute_modal.hide_from_notifications": "Ezkutatu jakinarazpenetatik",
   "mute_modal.hide_options": "Ezkutatu aukerak",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 50c376b3b..d2b520e1d 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -447,7 +447,6 @@
   "lists.subheading": "سیاهه‌هایتان",
   "load_pending": "{count, plural, one {# مورد جدید} other {# مورد جدید}}",
   "loading_indicator.label": "در حال بارگذاری…",
-  "media_gallery.toggle_visible": "{number, plural, one {نهفتن تصویر} other {نهفتن تصاویر}}",
   "moved_to_account_banner.text": "حسابتان {disabledAccount} اکنون از کار افتاده؛ چرا که به {movedToAccount} منتقل شدید.",
   "mute_modal.hide_from_notifications": "نهفتن از آگاهی‌ها",
   "mute_modal.hide_options": "گزینه‌های نهفتن",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 8f9cc5fe4..d8c5b7204 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Omat listasi",
   "load_pending": "{count, plural, one {# uusi kohde} other {# uutta kohdetta}}",
   "loading_indicator.label": "Ladataan…",
-  "media_gallery.toggle_visible": "{number, plural, one {Piilota kuva} other {Piilota kuvat}}",
+  "media_gallery.hide": "Piilota",
   "moved_to_account_banner.text": "Tilisi {disabledAccount} on tällä hetkellä poissa käytöstä, koska teit siirron tiliin {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Piilota ilmoituksista",
   "mute_modal.hide_options": "Piilota vaihtoehdot",
diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json
index d4e5d9ad5..6e7eb3b7c 100644
--- a/app/javascript/mastodon/locales/fo.json
+++ b/app/javascript/mastodon/locales/fo.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Tínir listar",
   "load_pending": "{count, plural, one {# nýtt evni} other {# nýggj evni}}",
   "loading_indicator.label": "Innlesur…",
-  "media_gallery.toggle_visible": "{number, plural, one {Fjal mynd} other {Fjal myndir}}",
+  "media_gallery.hide": "Fjal",
   "moved_to_account_banner.text": "Konta tín {disabledAccount} er í løtuni óvirkin, tí tú flutti til {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Fjal boð",
   "mute_modal.hide_options": "Fjal valmøguleikar",
@@ -780,6 +780,7 @@
   "status.bookmark": "Goym",
   "status.cancel_reblog_private": "Strika stimbran",
   "status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin",
+  "status.continued_thread": "Framhaldandi tráður",
   "status.copy": "Kopiera leinki til postin",
   "status.delete": "Strika",
   "status.detailed_status": "Útgreinað samrøðusýni",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Eingin hevur stimbrað hendan postin enn. Tá onkur stimbrar postin, verður hann sjónligur her.",
   "status.redraft": "Strika & ger nýggja kladdu",
   "status.remove_bookmark": "Gloym",
+  "status.replied_in_thread": "Svaraði í tráðnum",
   "status.replied_to": "Svaraði {name}",
   "status.reply": "Svara",
   "status.replyAll": "Svara tráðnum",
diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json
index a1d4061eb..9f51aaaa7 100644
--- a/app/javascript/mastodon/locales/fr-CA.json
+++ b/app/javascript/mastodon/locales/fr-CA.json
@@ -456,7 +456,6 @@
   "lists.subheading": "Vos listes",
   "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}",
   "loading_indicator.label": "Chargement…",
-  "media_gallery.toggle_visible": "{number, plural, one {Cacher l’image} other {Cacher les images}}",
   "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous avez déménagé sur {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Cacher des notifications",
   "mute_modal.hide_options": "Masquer les options",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 9f55634b2..5bcc1c946 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -456,7 +456,6 @@
   "lists.subheading": "Vos listes",
   "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}",
   "loading_indicator.label": "Chargement…",
-  "media_gallery.toggle_visible": "{number, plural, one {Cacher l’image} other {Cacher les images}}",
   "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous l'avez déplacé à {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Cacher des notifications",
   "mute_modal.hide_options": "Masquer les options",
diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json
index bb409b63c..badd6a1c2 100644
--- a/app/javascript/mastodon/locales/fy.json
+++ b/app/javascript/mastodon/locales/fy.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Jo listen",
   "load_pending": "{count, plural, one {# nij item} other {# nije items}}",
   "loading_indicator.label": "Lade…",
-  "media_gallery.toggle_visible": "{number, plural, one {ôfbylding ferstopje} other {ôfbyldingen ferstopje}}",
   "moved_to_account_banner.text": "Omdat jo nei {movedToAccount} ferhuze binne is jo account {disabledAccount} op dit stuit útskeakele.",
   "mute_modal.hide_from_notifications": "Meldingen ferstopje",
   "mute_modal.hide_options": "Opsjes ferstopje",
diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json
index b51d4adf6..95bb29385 100644
--- a/app/javascript/mastodon/locales/ga.json
+++ b/app/javascript/mastodon/locales/ga.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Do liostaí",
   "load_pending": "{count, plural, one {# mír nua} two {# mír nua} few {# mír nua} many {# mír nua} other {# mír nua}}",
   "loading_indicator.label": "Á lódáil…",
-  "media_gallery.toggle_visible": "{number, plural, one {Folaigh íomhá} two {Folaigh íomhánna} few {Folaigh íomhánna} many {Folaigh íomhánna} other {Folaigh íomhánna}}",
   "moved_to_account_banner.text": "Tá do chuntas {disabledAccount} díchumasaithe faoi láthair toisc gur bhog tú go {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Folaigh ó fhógraí",
   "mute_modal.hide_options": "Folaigh roghanna",
diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json
index 1090df088..e2f67bc29 100644
--- a/app/javascript/mastodon/locales/gd.json
+++ b/app/javascript/mastodon/locales/gd.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Na liostaichean agad",
   "load_pending": "{count, plural, one {# nì ùr} two {# nì ùr} few {# nithean ùra} other {# nì ùr}}",
   "loading_indicator.label": "’Ga luchdadh…",
-  "media_gallery.toggle_visible": "{number, plural, 1 {Falaich an dealbh} one {Falaich na dealbhan} two {Falaich na dealbhan} few {Falaich na dealbhan} other {Falaich na dealbhan}}",
   "moved_to_account_banner.text": "Tha an cunntas {disabledAccount} agad à comas on a rinn thu imrich gu {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Falaich o na brathan",
   "mute_modal.hide_options": "Roghainnean falaich",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index df477fe9e..b0b530e88 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -315,7 +315,7 @@
   "follow_suggestions.curated_suggestion": "Suxestións do Servidor",
   "follow_suggestions.dismiss": "Non mostrar máis",
   "follow_suggestions.featured_longer": "Elección persoal do equipo de {domain}",
-  "follow_suggestions.friends_of_friends_longer": "Popular entre as persoas que sigues",
+  "follow_suggestions.friends_of_friends_longer": "Popular entre as persoas que segues",
   "follow_suggestions.hints.featured": "Este perfil foi escollido pola administración de {domain}.",
   "follow_suggestions.hints.friends_of_friends": "Este perfil é popular entre as persoas que segues.",
   "follow_suggestions.hints.most_followed": "Este perfil é un dos máis seguidos en {domain}.",
@@ -457,9 +457,9 @@
   "lists.subheading": "As túas listaxes",
   "load_pending": "{count, plural, one {# novo elemento} other {# novos elementos}}",
   "loading_indicator.label": "Estase a cargar…",
-  "media_gallery.toggle_visible": "Agochar {number, plural, one {imaxe} other {imaxes}}",
+  "media_gallery.hide": "Agochar",
   "moved_to_account_banner.text": "A túa conta {disabledAccount} está actualmente desactivada porque movéchela a {movedToAccount}.",
-  "mute_modal.hide_from_notifications": "Ocultar nas notificacións",
+  "mute_modal.hide_from_notifications": "Agochar nas notificacións",
   "mute_modal.hide_options": "Opcións ao ocultar",
   "mute_modal.indefinite": "Ata que as reactive",
   "mute_modal.show_options": "Mostrar opcións",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 47fc444e8..80d9f054c 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -457,7 +457,7 @@
   "lists.subheading": "הרשימות שלך",
   "load_pending": "{count, plural, one {# פריט חדש} other {# פריטים חדשים}}",
   "loading_indicator.label": "בטעינה…",
-  "media_gallery.toggle_visible": "{number, plural, one {להסתיר תמונה} two {להסתיר תמונותיים} many {להסתיר תמונות} other {להסתיר תמונות}}",
+  "media_gallery.hide": "להסתיר",
   "moved_to_account_banner.text": "חשבונך {disabledAccount} אינו פעיל כרגע עקב מעבר ל{movedToAccount}.",
   "mute_modal.hide_from_notifications": "להסתיר מהתראות",
   "mute_modal.hide_options": "הסתרת אפשרויות",
diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json
index 0b4025173..58b04a20c 100644
--- a/app/javascript/mastodon/locales/hi.json
+++ b/app/javascript/mastodon/locales/hi.json
@@ -367,6 +367,7 @@
   "lists.replies_policy.none": "कोई नहीं",
   "lists.replies_policy.title": "इसके जवाब दिखाएं:",
   "lists.subheading": "आपकी सूचियाँ",
+  "media_gallery.hide": "छिपाएं",
   "navigation_bar.about": "विवरण",
   "navigation_bar.blocks": "ब्लॉक्ड यूज़र्स",
   "navigation_bar.bookmarks": "पुस्तकचिह्न:",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 0b42c4933..9f5782767 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -310,7 +310,6 @@
   "lists.replies_policy.none": "Nitko",
   "lists.search": "Traži među praćenim ljudima",
   "lists.subheading": "Vaše liste",
-  "media_gallery.toggle_visible": "Sakrij {number, plural, one {sliku} other {slike}}",
   "navigation_bar.about": "O aplikaciji",
   "navigation_bar.advanced_interface": "Otvori u naprednom web sučelju",
   "navigation_bar.blocks": "Blokirani korisnici",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index f0f08ca50..1e4e02cb9 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Saját listák",
   "load_pending": "{count, plural, one {# új elem} other {# új elem}}",
   "loading_indicator.label": "Betöltés…",
-  "media_gallery.toggle_visible": "{number, plural, one {Kép elrejtése} other {Képek elrejtése}}",
+  "media_gallery.hide": "Elrejtés",
   "moved_to_account_banner.text": "A(z) {disabledAccount} fiókod jelenleg le van tiltva, mert átköltöztél ide: {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Elrejtés az értesítések közül",
   "mute_modal.hide_options": "Beállítások elrejtése",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 1d82e884f..d1475338f 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -289,7 +289,6 @@
   "lists.search": "Փնտրել քո հետեւած մարդկանց մէջ",
   "lists.subheading": "Քո ցանկերը",
   "load_pending": "{count, plural, one {# նոր նիւթ} other {# նոր նիւթ}}",
-  "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել",
   "navigation_bar.about": "Մասին",
   "navigation_bar.blocks": "Արգելափակուած օգտատէրեր",
   "navigation_bar.bookmarks": "Էջանիշեր",
diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json
index 8a448920c..9d7e3ff2c 100644
--- a/app/javascript/mastodon/locales/ia.json
+++ b/app/javascript/mastodon/locales/ia.json
@@ -447,7 +447,7 @@
   "lists.subheading": "Tu listas",
   "load_pending": "{count, plural, one {# nove entrata} other {# nove entratas}}",
   "loading_indicator.label": "Cargante…",
-  "media_gallery.toggle_visible": "{number, plural, one {Celar imagine} other {Celar imagines}}",
+  "media_gallery.hide": "Celar",
   "moved_to_account_banner.text": "Tu conto {disabledAccount} es actualmente disactivate perque tu ha cambiate de conto a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Celar in notificationes",
   "mute_modal.hide_options": "Celar optiones",
@@ -784,6 +784,7 @@
   "status.reblogs.empty": "Necuno ha ancora impulsate iste message. Quando alcuno lo face, le impulsos apparera hic.",
   "status.redraft": "Deler e reconciper",
   "status.remove_bookmark": "Remover marcapagina",
+  "status.replied_in_thread": "Respondite in le discussion",
   "status.replied_to": "Respondite a {name}",
   "status.reply": "Responder",
   "status.replyAll": "Responder al discussion",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 29f0bfda9..3e2cc0314 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -403,7 +403,6 @@
   "lists.subheading": "Daftar Anda",
   "load_pending": "{count, plural, other {# item baru}}",
   "loading_indicator.label": "Memuat…",
-  "media_gallery.toggle_visible": "Tampil/Sembunyikan",
   "moved_to_account_banner.text": "Akun {disabledAccount} Anda kini dinonaktifkan karena Anda pindah ke {movedToAccount}.",
   "mute_modal.hide_options": "Sembunyikan opsi",
   "mute_modal.title": "Bisukan pengguna?",
diff --git a/app/javascript/mastodon/locales/ie.json b/app/javascript/mastodon/locales/ie.json
index 4002767cf..7a176dfbb 100644
--- a/app/javascript/mastodon/locales/ie.json
+++ b/app/javascript/mastodon/locales/ie.json
@@ -419,7 +419,6 @@
   "lists.subheading": "Tui listes",
   "load_pending": "{count, plural, one {# nov element} other {# nov elementes}}",
   "loading_indicator.label": "Cargant…",
-  "media_gallery.toggle_visible": "{number, plural, one {Celar image} other {Celar images}}",
   "moved_to_account_banner.text": "Tui conto {disabledAccount} es actualmen desactivisat pro que tu movet te a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Celar de notificationes",
   "mute_modal.hide_options": "Celar optiones",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 132987518..d0ccb923b 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -362,7 +362,6 @@
   "lists.subheading": "Vua listi",
   "load_pending": "{count, plural, one {# nova kozo} other {# nova kozi}}",
   "loading_indicator.label": "Kargante…",
-  "media_gallery.toggle_visible": "Chanjar videbleso",
   "moved_to_account_banner.text": "Vua konto {disabledAccount} es nune desaktiva pro ke vu movis a {movedToAccount}.",
   "navigation_bar.about": "Pri co",
   "navigation_bar.advanced_interface": "Apertez per retintervizajo",
diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json
index 54fbee48e..83932f1b4 100644
--- a/app/javascript/mastodon/locales/is.json
+++ b/app/javascript/mastodon/locales/is.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Listarnir þínir",
   "load_pending": "{count, plural, one {# nýtt atriði} other {# ný atriði}}",
   "loading_indicator.label": "Hleð inn…",
-  "media_gallery.toggle_visible": "Víxla sýnileika",
+  "media_gallery.hide": "Fela",
   "moved_to_account_banner.text": "Aðgangurinn þinn {disabledAccount} er óvirkur í augnablikinu vegna þess að þú fluttir þig yfir á {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Fela úr tilkynningum",
   "mute_modal.hide_options": "Fela valkosti",
@@ -780,6 +780,7 @@
   "status.bookmark": "Bókamerki",
   "status.cancel_reblog_private": "Taka úr endurbirtingu",
   "status.cannot_reblog": "Þessa færslu er ekki hægt að endurbirta",
+  "status.continued_thread": "Hélt samtali áfram",
   "status.copy": "Afrita tengil í færslu",
   "status.delete": "Eyða",
   "status.detailed_status": "Nákvæm spjallþráðasýn",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Enginn hefur ennþá endurbirt þessa færslu. Þegar einhver gerir það, mun það birtast hér.",
   "status.redraft": "Eyða og endurvinna drög",
   "status.remove_bookmark": "Fjarlægja bókamerki",
+  "status.replied_in_thread": "Svaraði í samtali",
   "status.replied_to": "Svaraði til {name}",
   "status.reply": "Svara",
   "status.replyAll": "Svara þræði",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 46d1fdd7a..1c8cff549 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Le tue liste",
   "load_pending": "{count, plural, one {# nuovo oggetto} other {# nuovi oggetti}}",
   "loading_indicator.label": "Caricamento…",
-  "media_gallery.toggle_visible": "{number, plural, one {Nascondi immagine} other {Nascondi immagini}}",
+  "media_gallery.hide": "Nascondi",
   "moved_to_account_banner.text": "Il tuo profilo {disabledAccount} è correntemente disabilitato perché ti sei spostato a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Nascondi dalle notifiche",
   "mute_modal.hide_options": "Nascondi le opzioni",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 489ff9c8e..60dd8b12e 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -457,7 +457,6 @@
   "lists.subheading": "あなたのリスト",
   "load_pending": "{count}件の新着",
   "loading_indicator.label": "読み込み中…",
-  "media_gallery.toggle_visible": "{number, plural, one {画像を閉じる} other {画像を閉じる}}",
   "moved_to_account_banner.text": "あなたのアカウント『{disabledAccount}』は『{movedToAccount}』に移動したため現在無効になっています。",
   "mute_modal.hide_from_notifications": "通知をオフにする",
   "mute_modal.hide_options": "オプションを閉じる",
diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json
index 5713fe60e..0bd86a247 100644
--- a/app/javascript/mastodon/locales/ka.json
+++ b/app/javascript/mastodon/locales/ka.json
@@ -153,7 +153,6 @@
   "lists.new.title_placeholder": "ახალი სიის სათაური",
   "lists.search": "ძებნა ადამიანებს შორის რომელთაც მიჰყვებით",
   "lists.subheading": "თქვენი სიები",
-  "media_gallery.toggle_visible": "ხილვადობის ჩართვა",
   "navigation_bar.blocks": "დაბლოკილი მომხმარებლები",
   "navigation_bar.community_timeline": "ლოკალური თაიმლაინი",
   "navigation_bar.compose": "Compose new toot",
diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json
index a1e95fd8d..ae783b6c2 100644
--- a/app/javascript/mastodon/locales/kab.json
+++ b/app/javascript/mastodon/locales/kab.json
@@ -351,7 +351,6 @@
   "lists.subheading": "Tibdarin-ik·im",
   "load_pending": "{count, plural, one {# n uferdis amaynut} other {# n yiferdisen imaynuten}}",
   "loading_indicator.label": "Yessalay-d …",
-  "media_gallery.toggle_visible": "{number, plural, one {Ffer tugna} other {Ffer tugniwin}}",
   "mute_modal.hide_from_notifications": "Ffer-it deg ulɣuten",
   "mute_modal.hide_options": "Ffer tinefrunin",
   "mute_modal.indefinite": "Alamma ssnesreɣ asgugem fell-as",
diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json
index 85b2fdc00..eace5d95b 100644
--- a/app/javascript/mastodon/locales/kk.json
+++ b/app/javascript/mastodon/locales/kk.json
@@ -222,7 +222,6 @@
   "lists.search": "Сіз іздеген адамдар арасында іздеу",
   "lists.subheading": "Тізімдеріңіз",
   "load_pending": "{count, plural, one {# жаңа нәрсе} other {# жаңа нәрсе}}",
-  "media_gallery.toggle_visible": "Көрінуді қосу",
   "navigation_bar.blocks": "Бұғатталғандар",
   "navigation_bar.bookmarks": "Бетбелгілер",
   "navigation_bar.community_timeline": "Жергілікті желі",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index b3d3476e4..4c78f4332 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -34,9 +34,9 @@
   "account.follow_back": "맞팔로우 하기",
   "account.followers": "팔로워",
   "account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.",
-  "account.followers_counter": "{count, plural, other {{counter} 팔로워}}",
+  "account.followers_counter": "{count, plural, other {팔로워 {counter}명}}",
   "account.following": "팔로잉",
-  "account.following_counter": "{count, plural, other {{counter} 팔로잉}}",
+  "account.following_counter": "{count, plural, other {팔로잉 {counter}명}}",
   "account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.",
   "account.go_to_profile": "프로필로 이동",
   "account.hide_reblogs": "@{name}의 부스트를 숨기기",
@@ -62,7 +62,7 @@
   "account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다",
   "account.share": "@{name}의 프로필 공유",
   "account.show_reblogs": "@{name}의 부스트 보기",
-  "account.statuses_counter": "{count, plural, other {{counter} 게시물}}",
+  "account.statuses_counter": "{count, plural, other {게시물 {counter}개}}",
   "account.unblock": "차단 해제",
   "account.unblock_domain": "도메인 {domain} 차단 해제",
   "account.unblock_short": "차단 해제",
@@ -156,7 +156,7 @@
   "compose_form.placeholder": "지금 무슨 생각을 하고 있나요?",
   "compose_form.poll.duration": "투표 기간",
   "compose_form.poll.multiple": "다중 선택",
-  "compose_form.poll.option_placeholder": "{option}번째 항목",
+  "compose_form.poll.option_placeholder": "{number}번째 옵션",
   "compose_form.poll.single": "단일 선택",
   "compose_form.poll.switch_to_multiple": "다중 선택이 가능한 투표로 변경",
   "compose_form.poll.switch_to_single": "단일 선택 투표로 변경",
@@ -347,12 +347,12 @@
   "hashtag.column_settings.tag_mode.any": "어느것이든",
   "hashtag.column_settings.tag_mode.none": "이것들을 제외하고",
   "hashtag.column_settings.tag_toggle": "추가 해시태그를 이 컬럼에 추가합니다",
-  "hashtag.counter_by_accounts": "{count, plural, other {{counter} 명의 참여자}}",
-  "hashtag.counter_by_uses": "{count, plural, other {{counter} 개의 게시물}}",
-  "hashtag.counter_by_uses_today": "오늘 {count, plural, other {{counter} 개의 게시물}}",
-  "hashtag.follow": "팔로우",
-  "hashtag.unfollow": "팔로우 해제",
-  "hashtags.and_other": "…그리고 {count, plural,other {# 개 더}}",
+  "hashtag.counter_by_accounts": "{count, plural, other {참여자 {counter}명}}",
+  "hashtag.counter_by_uses": "{count, plural, other {게시물 {counter}개}}",
+  "hashtag.counter_by_uses_today": "금일 {count, plural, other {게시물 {counter}개}}",
+  "hashtag.follow": "해시태그 팔로우",
+  "hashtag.unfollow": "해시태그 팔로우 해제",
+  "hashtags.and_other": "…및 {count, plural,other {#개}}",
   "hints.profiles.followers_may_be_missing": "이 프로필의 팔로워 목록은 일부 누락되었을 수 있습니다.",
   "hints.profiles.follows_may_be_missing": "이 프로필의 팔로우 목록은 일부 누락되었을 수 있습니다.",
   "hints.profiles.posts_may_be_missing": "이 프로필의 게시물은 일부 누락되었을 수 있습니다.",
@@ -457,7 +457,7 @@
   "lists.subheading": "리스트",
   "load_pending": "{count, plural, other {#}} 개의 새 항목",
   "loading_indicator.label": "불러오는 중...",
-  "media_gallery.toggle_visible": "이미지 숨기기",
+  "media_gallery.hide": "숨기기",
   "moved_to_account_banner.text": "당신의 계정 {disabledAccount}는 {movedToAccount}로 이동하였기 때문에 현재 비활성화 상태입니다.",
   "mute_modal.hide_from_notifications": "알림에서 숨기기",
   "mute_modal.hide_options": "옵션 숨기기",
diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json
index 73cfa69f4..d69f4b0d0 100644
--- a/app/javascript/mastodon/locales/ku.json
+++ b/app/javascript/mastodon/locales/ku.json
@@ -313,7 +313,6 @@
   "lists.search": "Di navbera kesên ku te dişopînin bigere",
   "lists.subheading": "Lîsteyên te",
   "load_pending": "{count, plural, one {# hêmaneke nû} other {#hêmaneke nû}}",
-  "media_gallery.toggle_visible": "{number, plural, one {Wêneyê veşêre} other {Wêneyan veşêre}}",
   "moved_to_account_banner.text": "Ajimêrê te {disabledAccount} niha neçalak e ji ber ku te bar kir bo {movedToAccount}.",
   "navigation_bar.about": "Derbar",
   "navigation_bar.blocks": "Bikarhênerên astengkirî",
diff --git a/app/javascript/mastodon/locales/kw.json b/app/javascript/mastodon/locales/kw.json
index 0d60d09e3..046910daf 100644
--- a/app/javascript/mastodon/locales/kw.json
+++ b/app/javascript/mastodon/locales/kw.json
@@ -213,7 +213,6 @@
   "lists.search": "Hwilas yn-mysk tus a holyewgh",
   "lists.subheading": "Agas rolyow",
   "load_pending": "{count, plural, one {# daklennowydh} other {# a daklennow nowydh}}",
-  "media_gallery.toggle_visible": "Hide {number, plural, one {aven} other {aven}}",
   "navigation_bar.blocks": "Devnydhyoryon lettys",
   "navigation_bar.bookmarks": "Folennosow",
   "navigation_bar.community_timeline": "Amserlin leel",
diff --git a/app/javascript/mastodon/locales/la.json b/app/javascript/mastodon/locales/la.json
index dc0796144..d894cc01c 100644
--- a/app/javascript/mastodon/locales/la.json
+++ b/app/javascript/mastodon/locales/la.json
@@ -141,7 +141,6 @@
   "lists.new.create": "Addere tabella",
   "lists.subheading": "Tuae tabulae",
   "load_pending": "{count, plural, one {# novum item} other {# nova itema}}",
-  "media_gallery.toggle_visible": "{number, plural, one {Cēla imaginem} other {Cēla imagines}}",
   "moved_to_account_banner.text": "Tua ratione {disabledAccount} interdum reposita est, quod ad {movedToAccount} migrāvisti.",
   "mute_modal.you_wont_see_mentions": "Non videbis nuntios quī eōs commemorant.",
   "navigation_bar.about": "De",
diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json
index 3d0bfcb4b..e63c22ec6 100644
--- a/app/javascript/mastodon/locales/lad.json
+++ b/app/javascript/mastodon/locales/lad.json
@@ -418,7 +418,6 @@
   "lists.subheading": "Tus listas",
   "load_pending": "{count, plural, one {# muevo elemento} other {# muevos elementos}}",
   "loading_indicator.label": "Eskargando…",
-  "media_gallery.toggle_visible": "{number, plural, one {Eskonde imaje} other {Eskonde imajes}}",
   "moved_to_account_banner.text": "Tu kuento {disabledAccount} esta aktualmente inkapasitado porke transferates a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Eskonde de avizos",
   "mute_modal.hide_options": "Eskonde opsyones",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 55ef2afce..cc905e3a2 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Tavo sąrašai",
   "load_pending": "{count, plural, one {# naujas elementas} few {# nauji elementai} many {# naujo elemento} other {# naujų elementų}}",
   "loading_indicator.label": "Kraunama…",
-  "media_gallery.toggle_visible": "{number, plural, one {Slėpti vaizdą} few {Slėpti vaizdus} many {Slėpti vaizdo} other {Slėpti vaizdų}}",
+  "media_gallery.hide": "Slėpti",
   "moved_to_account_banner.text": "Tavo paskyra {disabledAccount} šiuo metu išjungta, nes persikėlei į {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Slėpti nuo pranešimų",
   "mute_modal.hide_options": "Slėpti parinktis",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 6cd15afbe..37dc1d06d 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -388,7 +388,6 @@
   "lists.subheading": "Tavi saraksti",
   "load_pending": "{count, plural, one {# jauna lieta} other {# jaunas lietas}}",
   "loading_indicator.label": "Ielādē…",
-  "media_gallery.toggle_visible": "{number, plural, one {Slēpt attēlu} other {Slēpt attēlus}}",
   "moved_to_account_banner.text": "Tavs konts {disabledAccount} pašlaik ir atspējots, jo Tu pārcēlies uz kontu {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Paslēpt paziņojumos",
   "mute_modal.hide_options": "Paslēpt iespējas",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index 4a32b1d82..c6bc630bf 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -383,7 +383,6 @@
   "lists.subheading": "Senarai anda",
   "load_pending": "{count, plural, one {# item baharu} other {# item baharu}}",
   "loading_indicator.label": "Memuatkan…",
-  "media_gallery.toggle_visible": "{number, plural, other {Sembunyikan imej}}",
   "moved_to_account_banner.text": "Akaun anda {disabledAccount} kini dinyahdayakan kerana anda berpindah ke {movedToAccount}.",
   "navigation_bar.about": "Perihal",
   "navigation_bar.advanced_interface": "Buka dalam antara muka web lanjutan",
diff --git a/app/javascript/mastodon/locales/my.json b/app/javascript/mastodon/locales/my.json
index b042ebbcc..2127d69ba 100644
--- a/app/javascript/mastodon/locales/my.json
+++ b/app/javascript/mastodon/locales/my.json
@@ -362,7 +362,6 @@
   "lists.subheading": "သင့်၏စာရင်းများ",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
   "loading_indicator.label": "လုပ်ဆောင်နေသည်…",
-  "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
   "moved_to_account_banner.text": "{movedToAccount} အကောင့်သို့ပြောင်းလဲထားသဖြင့် {disabledAccount} အကောင့်မှာပိတ်ထားသည်",
   "navigation_bar.about": "အကြောင်း",
   "navigation_bar.advanced_interface": "အဆင့်မြင့်ဝဘ်ပုံစံ ဖွင့်ပါ",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 795643536..e5d8e8d2e 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Jouw lijsten",
   "load_pending": "{count, plural, one {# nieuw item} other {# nieuwe items}}",
   "loading_indicator.label": "Laden…",
-  "media_gallery.toggle_visible": "{number, plural, one {afbeelding verbergen} other {afbeeldingen verbergen}}",
+  "media_gallery.hide": "Verbergen",
   "moved_to_account_banner.text": "Omdat je naar {movedToAccount} bent verhuisd is jouw account {disabledAccount} momenteel uitgeschakeld.",
   "mute_modal.hide_from_notifications": "Onder meldingen verbergen",
   "mute_modal.hide_options": "Opties verbergen",
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index 393946ff1..d21ebffe2 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Listene dine",
   "load_pending": "{count, plural, one {# nytt element} other {# nye element}}",
   "loading_indicator.label": "Lastar…",
-  "media_gallery.toggle_visible": "{number, plural, one {Skjul bilete} other {Skjul bilete}}",
+  "media_gallery.hide": "Gøym",
   "moved_to_account_banner.text": "Kontoen din, {disabledAccount} er for tida deaktivert fordi du har flytta til {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ikkje vis varslingar",
   "mute_modal.hide_options": "Gøym val",
@@ -780,6 +780,7 @@
   "status.bookmark": "Set bokmerke",
   "status.cancel_reblog_private": "Opphev framheving",
   "status.cannot_reblog": "Du kan ikkje framheva dette innlegget",
+  "status.continued_thread": "Framhald til tråden",
   "status.copy": "Kopier lenke til status",
   "status.delete": "Slett",
   "status.detailed_status": "Detaljert samtalevisning",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Ingen har framheva dette tutet enno. Om nokon gjer, så dukkar det opp her.",
   "status.redraft": "Slett & skriv på nytt",
   "status.remove_bookmark": "Fjern bokmerke",
+  "status.replied_in_thread": "Svara i tråden",
   "status.replied_to": "Svarte {name}",
   "status.reply": "Svar",
   "status.replyAll": "Svar til tråd",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index bb41754e7..b805b9852 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Dine lister",
   "load_pending": "{count, plural,one {# ny gjenstand} other {# nye gjenstander}}",
   "loading_indicator.label": "Laster…",
-  "media_gallery.toggle_visible": "Veksle synlighet",
   "moved_to_account_banner.text": "Din konto {disabledAccount} er for øyeblikket deaktivert fordi du flyttet til {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ikke varsle",
   "mute_modal.hide_options": "Skjul alternativer",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index a4e552ba4..1d9008dbd 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -319,7 +319,6 @@
   "lists.subheading": "Vòstras listas",
   "load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}",
   "loading_indicator.label": "Cargament…",
-  "media_gallery.toggle_visible": "Modificar la visibilitat",
   "navigation_bar.about": "A prepaus",
   "navigation_bar.advanced_interface": "Dobrir l’interfàcia web avançada",
   "navigation_bar.blocks": "Personas blocadas",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 6bf6252d7..2ef437ef0 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Twoje listy",
   "load_pending": "{count, plural, one {# nowa pozycja} other {nowe pozycje}}",
   "loading_indicator.label": "Ładowanie…",
-  "media_gallery.toggle_visible": "Przełącz widoczność",
+  "media_gallery.hide": "Ukryj",
   "moved_to_account_banner.text": "Twoje konto {disabledAccount} jest obecnie wyłączone, ponieważ zostało przeniesione na {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ukryj z powiadomień",
   "mute_modal.hide_options": "Ukryj opcje",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index aeff65485..9978bf764 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Suas listas",
   "load_pending": "{count, plural, one {# novo item} other {# novos items}}",
   "loading_indicator.label": "Carregando…",
-  "media_gallery.toggle_visible": "{number, plural, one {Ocultar mídia} other {Ocultar mídias}}",
+  "media_gallery.hide": "Ocultar",
   "moved_to_account_banner.text": "Sua conta {disabledAccount} está desativada porque você a moveu para {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ocultar das notificações",
   "mute_modal.hide_options": "Ocultar opções",
@@ -780,6 +780,7 @@
   "status.bookmark": "Salvar",
   "status.cancel_reblog_private": "Desfazer boost",
   "status.cannot_reblog": "Este toot não pode receber boost",
+  "status.continued_thread": "Continuação da conversa",
   "status.copy": "Copiar link",
   "status.delete": "Excluir",
   "status.detailed_status": "Visão detalhada da conversa",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Nada aqui. Quando alguém der boost, o usuário aparecerá aqui.",
   "status.redraft": "Excluir e rascunhar",
   "status.remove_bookmark": "Remover do Salvos",
+  "status.replied_in_thread": "Respondido na discussão",
   "status.replied_to": "Em resposta a {name}",
   "status.reply": "Responder",
   "status.replyAll": "Responder a conversa",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index fd811d683..24f599311 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -444,7 +444,6 @@
   "lists.subheading": "As tuas listas",
   "load_pending": "{count, plural, one {# novo item} other {# novos itens}}",
   "loading_indicator.label": "A carregar…",
-  "media_gallery.toggle_visible": "Alternar visibilidade",
   "moved_to_account_banner.text": "A sua conta {disabledAccount} está, no momento, desativada, porque você migrou para {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ocultar das notificações",
   "mute_modal.hide_options": "Ocultar opções",
diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json
index 5b1901fbe..18380928b 100644
--- a/app/javascript/mastodon/locales/ro.json
+++ b/app/javascript/mastodon/locales/ro.json
@@ -355,7 +355,6 @@
   "lists.search": "Caută printre persoanele la care ești abonat",
   "lists.subheading": "Listele tale",
   "load_pending": "{count, plural, one {# element nou} other {# elemente noi}}",
-  "media_gallery.toggle_visible": "{number, plural, one {Ascunde imaginea} other {Ascunde imaginile}}",
   "moved_to_account_banner.text": "Contul tău {disabledAccount} este în acest moment dezactivat deoarece te-ai mutat la {movedToAccount}.",
   "navigation_bar.about": "Despre",
   "navigation_bar.advanced_interface": "Deschide în interfața web avansată",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 2c55da90b..ab8974346 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -432,7 +432,6 @@
   "lists.subheading": "Ваши списки",
   "load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}",
   "loading_indicator.label": "Загрузка…",
-  "media_gallery.toggle_visible": "Показать/скрыть {number, plural, =1 {изображение} other {изображения}}",
   "moved_to_account_banner.text": "Ваша учетная запись {disabledAccount} в настоящее время заморожена, потому что вы переехали на {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Скрыть из уведомлений",
   "mute_modal.hide_options": "Скрыть параметры",
diff --git a/app/javascript/mastodon/locales/sa.json b/app/javascript/mastodon/locales/sa.json
index 6ca4eafe1..ac715e718 100644
--- a/app/javascript/mastodon/locales/sa.json
+++ b/app/javascript/mastodon/locales/sa.json
@@ -319,7 +319,6 @@
   "lists.search": "त्वया अनुसारितजनेषु अन्विष्य",
   "lists.subheading": "तव सूचयः",
   "load_pending": "{count, plural, one {# नूतनवस्तु} other {# नूतनवस्तूनि}}",
-  "media_gallery.toggle_visible": "{number, plural, one {चित्रं प्रच्छादय} other {चित्राणि प्रच्छादय}}",
   "moved_to_account_banner.text": "तव एकौण्ट् {disabledAccount} अधुना निष्कृतो यतोहि {movedToAccount} अस्मिन्त्वमसार्षीः।",
   "navigation_bar.about": "विषये",
   "navigation_bar.blocks": "निषिद्धभोक्तारः",
diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json
index 461383191..0e055716e 100644
--- a/app/javascript/mastodon/locales/sc.json
+++ b/app/javascript/mastodon/locales/sc.json
@@ -370,7 +370,6 @@
   "lists.subheading": "Is listas tuas",
   "load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}",
   "loading_indicator.label": "Carrighende…",
-  "media_gallery.toggle_visible": "Cua {number, plural, one {immàgine} other {immàgines}}",
   "navigation_bar.about": "Informatziones",
   "navigation_bar.blocks": "Persones blocadas",
   "navigation_bar.bookmarks": "Sinnalibros",
diff --git a/app/javascript/mastodon/locales/sco.json b/app/javascript/mastodon/locales/sco.json
index e8ae521ae..b1b17b57d 100644
--- a/app/javascript/mastodon/locales/sco.json
+++ b/app/javascript/mastodon/locales/sco.json
@@ -304,7 +304,6 @@
   "lists.search": "Seirch amang the fowk ye ken",
   "lists.subheading": "Yer lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
-  "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
   "moved_to_account_banner.text": "Yer accoont {disabledAccount} is disabilt the noo acause ye flittit tae {movedToAccount}.",
   "navigation_bar.about": "Aboot",
   "navigation_bar.blocks": "Dingied uisers",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 3d7c5416c..f89e8cf5a 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -420,7 +420,6 @@
   "lists.subheading": "Vaše zoznamy",
   "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položiek} other {# nových položiek}}",
   "loading_indicator.label": "Načítavanie…",
-  "media_gallery.toggle_visible": "{number, plural, one {Skryť obrázok} other {Skryť obrázky}}",
   "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálne deaktivovaný, pretože ste sa presunuli na {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ukryť z upozornení",
   "mute_modal.hide_options": "Skryť možnosti",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index 6e8ac52df..4e83b1508 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -437,7 +437,6 @@
   "lists.subheading": "Vaši seznami",
   "load_pending": "{count, plural, one {# nov element} two {# nova elementa} few {# novi elementi} other {# novih elementov}}",
   "loading_indicator.label": "Nalaganje …",
-  "media_gallery.toggle_visible": "{number, plural,one {Skrij sliko} two {Skrij sliki} other {Skrij slike}}",
   "moved_to_account_banner.text": "Vaš račun {disabledAccount} je trenutno onemogočen, ker ste se prestavili na {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Skrijte se pred obvestili",
   "mute_modal.hide_options": "Skrij možnosti",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 51b7a551c..1904c186f 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Listat tuaja",
   "load_pending": "{count, plural,one {# objekt i ri }other {# objekte të rinj }}",
   "loading_indicator.label": "Po ngarkohet…",
-  "media_gallery.toggle_visible": "Fshihni {number, plural, one {figurë} other {figura}}",
+  "media_gallery.hide": "Fshihe",
   "moved_to_account_banner.text": "Llogaria juaj {disabledAccount} aktualisht është e çaktivizuar, ngaqë kaluat te {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Fshihe prej njoftimeve",
   "mute_modal.hide_options": "Fshihi mundësitë",
@@ -780,6 +780,7 @@
   "status.bookmark": "Faqeruaje",
   "status.cancel_reblog_private": "Shpërforcojeni",
   "status.cannot_reblog": "Ky postim s’mund të përforcohet",
+  "status.continued_thread": "Vazhdoi rrjedhën",
   "status.copy": "Kopjoje lidhjen për te mesazhi",
   "status.delete": "Fshije",
   "status.detailed_status": "Pamje e hollësishme bisede",
@@ -813,6 +814,7 @@
   "status.reblogs.empty": "Këtë mesazh s’e ka përforcuar njeri deri tani. Kur ta bëjë dikush, kjo do të duket këtu.",
   "status.redraft": "Fshijeni & rihartojeni",
   "status.remove_bookmark": "Hiqe faqerojtësin",
+  "status.replied_in_thread": "U përgjigj te rrjedha",
   "status.replied_to": "Iu përgjigj {name}",
   "status.reply": "Përgjigjuni",
   "status.replyAll": "Përgjigjuni rrjedhës",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index d550f6517..02be70f5b 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -424,7 +424,6 @@
   "lists.subheading": "Vaše liste",
   "load_pending": "{count, plural, one {# nova stavka} few {# nove stavke} other {# novih stavki}}",
   "loading_indicator.label": "Učitavanje…",
-  "media_gallery.toggle_visible": "{number, plural, one {Sakrij sliku} few {Sakrij slike} other {Sakrij slike}}",
   "moved_to_account_banner.text": "Vaš nalog {disabledAccount} je trenutno onemogućen jer ste prešli na {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Sakrij iz obaveštenja",
   "mute_modal.hide_options": "Sakrij opcije",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index f608d46a2..dfd10579e 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -424,7 +424,6 @@
   "lists.subheading": "Ваше листе",
   "load_pending": "{count, plural, one {# нова ставка} few {# нове ставке} other {# нових ставки}}",
   "loading_indicator.label": "Учитавање…",
-  "media_gallery.toggle_visible": "{number, plural, one {Сакриј слику} few {Сакриј слике} other {Сакриј слике}}",
   "moved_to_account_banner.text": "Ваш налог {disabledAccount} је тренутно онемогућен јер сте прешли на {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Сакриј из обавештења",
   "mute_modal.hide_options": "Сакриј опције",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 76b46f342..61cad916c 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -454,7 +454,7 @@
   "lists.subheading": "Dina listor",
   "load_pending": "{count, plural, one {# nytt objekt} other {# nya objekt}}",
   "loading_indicator.label": "Laddar…",
-  "media_gallery.toggle_visible": "Växla synlighet",
+  "media_gallery.hide": "Dölj",
   "moved_to_account_banner.text": "Ditt konto {disabledAccount} är för närvarande inaktiverat eftersom du flyttat till {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Dölj från aviseringslistan",
   "mute_modal.hide_options": "Dölj alternativ",
diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json
index 4bded4567..4f209b7e3 100644
--- a/app/javascript/mastodon/locales/ta.json
+++ b/app/javascript/mastodon/locales/ta.json
@@ -252,7 +252,6 @@
   "lists.search": "நீங்கள் பின்தொடரும் நபர்கள் மத்தியில் தேடுதல்",
   "lists.subheading": "உங்கள் பட்டியல்கள்",
   "load_pending": "{count, plural,one {# புதியது}other {# புதியவை}}",
-  "media_gallery.toggle_visible": "நிலைமாற்று தெரியும்",
   "navigation_bar.blocks": "தடுக்கப்பட்ட பயனர்கள்",
   "navigation_bar.bookmarks": "அடையாளக்குறிகள்",
   "navigation_bar.community_timeline": "உள்ளூர் காலக்கெடு",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 52cb612d8..a6dc74b29 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -167,7 +167,6 @@
   "lists.new.title_placeholder": "కొత్త జాబితా శీర్షిక",
   "lists.search": "మీరు అనుసరించే వ్యక్తులలో శోధించండి",
   "lists.subheading": "మీ జాబితాలు",
-  "media_gallery.toggle_visible": "దృశ్యమానతను టోగుల్ చేయండి",
   "navigation_bar.blocks": "బ్లాక్ చేయబడిన వినియోగదారులు",
   "navigation_bar.community_timeline": "స్థానిక కాలక్రమం",
   "navigation_bar.compose": "కొత్త టూట్ను రాయండి",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 55f0fe02b..81424a949 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -457,7 +457,6 @@
   "lists.subheading": "รายการของคุณ",
   "load_pending": "{count, plural, other {# รายการใหม่}}",
   "loading_indicator.label": "กำลังโหลด…",
-  "media_gallery.toggle_visible": "{number, plural, other {ซ่อนภาพ}}",
   "moved_to_account_banner.text": "มีการปิดใช้งานบัญชีของคุณ {disabledAccount} ในปัจจุบันเนื่องจากคุณได้ย้ายไปยัง {movedToAccount}",
   "mute_modal.hide_from_notifications": "ซ่อนจากการแจ้งเตือน",
   "mute_modal.hide_options": "ซ่อนตัวเลือก",
diff --git a/app/javascript/mastodon/locales/tok.json b/app/javascript/mastodon/locales/tok.json
index 19e33233c..0ca30c57d 100644
--- a/app/javascript/mastodon/locales/tok.json
+++ b/app/javascript/mastodon/locales/tok.json
@@ -270,7 +270,6 @@
   "lists.subheading": "kulupu lipu sina",
   "load_pending": "{count, plural, other {ijo sin #}}",
   "loading_indicator.label": "ni li kama…",
-  "media_gallery.toggle_visible": "{number, plural, other {o len e sitelen}}",
   "mute_modal.title": "sina wile ala wile kute e jan ni?",
   "navigation_bar.about": "sona",
   "navigation_bar.blocks": "jan weka",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 4873fa943..6577737bf 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Listeleriniz",
   "load_pending": "{count, plural, one {# yeni öğe} other {# yeni öğe}}",
   "loading_indicator.label": "Yükleniyor…",
-  "media_gallery.toggle_visible": "{number, plural, one {Resmi} other {Resimleri}} gizle",
+  "media_gallery.hide": "Gizle",
   "moved_to_account_banner.text": "{disabledAccount} hesabınız, {movedToAccount} hesabına taşıdığınız için şu an devre dışı.",
   "mute_modal.hide_from_notifications": "Bildirimlerde gizle",
   "mute_modal.hide_options": "Seçenekleri gizle",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index b63596037..638a84b64 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -457,7 +457,7 @@
   "lists.subheading": "Ваші списки",
   "load_pending": "{count, plural, one {# новий елемент} other {# нових елементів}}",
   "loading_indicator.label": "Завантаження…",
-  "media_gallery.toggle_visible": "{number, plural, one {Приховати зображення} other {Приховати зображення}}",
+  "media_gallery.hide": "Сховати",
   "moved_to_account_banner.text": "Ваш обліковий запис {disabledAccount} наразі вимкнений, оскільки вас перенесено до {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Сховати зі сповіщень",
   "mute_modal.hide_options": "Сховати опції",
@@ -780,7 +780,7 @@
   "status.bookmark": "Додати до закладок",
   "status.cancel_reblog_private": "Скасувати поширення",
   "status.cannot_reblog": "Цей допис не може бути поширений",
-  "status.continued_thread": "Continued thread",
+  "status.continued_thread": "Продовження у потоці",
   "status.copy": "Копіювати посилання на допис",
   "status.delete": "Видалити",
   "status.detailed_status": "Детальний вигляд бесіди",
@@ -814,6 +814,7 @@
   "status.reblogs.empty": "Ніхто ще не поширив цей допис. Коли хтось це зроблять, вони будуть зображені тут.",
   "status.redraft": "Видалити та виправити",
   "status.remove_bookmark": "Видалити закладку",
+  "status.replied_in_thread": "Відповідь у потоці",
   "status.replied_to": "Відповідь для {name}",
   "status.reply": "Відповісти",
   "status.replyAll": "Відповісти на ланцюжок",
diff --git a/app/javascript/mastodon/locales/uz.json b/app/javascript/mastodon/locales/uz.json
index 048f8e775..6dae368ff 100644
--- a/app/javascript/mastodon/locales/uz.json
+++ b/app/javascript/mastodon/locales/uz.json
@@ -299,7 +299,6 @@
   "lists.search": "Siz kuzatadigan odamlar orasidan qidiring",
   "lists.subheading": "Sizning ro'yxatlaringiz",
   "load_pending": "{count, plural, one {# yangi element} other {# yangi elementlar}}",
-  "media_gallery.toggle_visible": "{number, plural, one {Rasmni yashirish} other {Rasmlarni yashirish}}",
   "moved_to_account_banner.text": "{movedToAccount} hisobiga koʻchganingiz uchun {disabledAccount} hisobingiz hozirda oʻchirib qoʻyilgan.",
   "navigation_bar.about": "Haqida",
   "navigation_bar.blocks": "Bloklangan foydalanuvchilar",
diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json
index 80a766683..09f288469 100644
--- a/app/javascript/mastodon/locales/vi.json
+++ b/app/javascript/mastodon/locales/vi.json
@@ -457,7 +457,6 @@
   "lists.subheading": "Danh sách của bạn",
   "load_pending": "{count, plural, one {# tút mới} other {# tút mới}}",
   "loading_indicator.label": "Đang tải…",
-  "media_gallery.toggle_visible": "{number, plural, other {Ẩn hình ảnh}}",
   "moved_to_account_banner.text": "Tài khoản {disabledAccount} của bạn hiện không khả dụng vì bạn đã chuyển sang {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ẩn thông báo",
   "mute_modal.hide_options": "Tùy chọn ẩn",
diff --git a/app/javascript/mastodon/locales/zgh.json b/app/javascript/mastodon/locales/zgh.json
index d9367520e..2fe63fe83 100644
--- a/app/javascript/mastodon/locales/zgh.json
+++ b/app/javascript/mastodon/locales/zgh.json
@@ -118,7 +118,6 @@
   "lists.replies_policy.title": "ⵙⴽⵏ ⵜⵉⵔⴰⵔⵉⵏ ⵉ:",
   "lists.subheading": "ⵜⵉⵍⴳⴰⵎⵉⵏ ⵏⵏⴽ",
   "load_pending": "{count, plural, one {# ⵓⴼⵔⴷⵉⵙ ⴰⵎⴰⵢⵏⵓ} other {# ⵉⴼⵔⴷⴰⵙ ⵉⵎⴰⵢⵏⵓⵜⵏ}}",
-  "media_gallery.toggle_visible": "ⴼⴼⵔ {number, plural, one {ⵜⴰⵡⵍⴰⴼⵜ} other {ⵜⵉⵡⵍⴰⴼⵉⵏ}}",
   "navigation_bar.compose": "Compose new toot",
   "navigation_bar.domain_blocks": "Hidden domains",
   "navigation_bar.follow_requests": "ⵜⵓⵜⵔⴰⵡⵉⵏ ⵏ ⵓⴹⴼⴰⵕ",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 94adc9af2..9a6076f23 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -457,7 +457,7 @@
   "lists.subheading": "你的列表",
   "load_pending": "{count} 项",
   "loading_indicator.label": "加载中…",
-  "media_gallery.toggle_visible": "{number, plural, other {隐藏图像}}",
+  "media_gallery.hide": "隐藏",
   "moved_to_account_banner.text": "您的账号 {disabledAccount} 已禁用,因为您已迁移到 {movedToAccount}。",
   "mute_modal.hide_from_notifications": "从通知中隐藏",
   "mute_modal.hide_options": "隐藏选项",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 8543090b9..2a55e6fc2 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -421,7 +421,6 @@
   "lists.subheading": "列表",
   "load_pending": "{count, plural, other {# 個新項目}}",
   "loading_indicator.label": "載入中…",
-  "media_gallery.toggle_visible": "隱藏圖片",
   "moved_to_account_banner.text": "您的帳號 {disabledAccount} 目前已停用,因為您已搬家至 {movedToAccount}。",
   "mute_modal.hide_from_notifications": "隱藏通知",
   "mute_modal.hide_options": "隱藏選項",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index e67180357..cdc6c977b 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -457,7 +457,7 @@
   "lists.subheading": "您的列表",
   "load_pending": "{count, plural, one {# 個新項目} other {# 個新項目}}",
   "loading_indicator.label": "正在載入...",
-  "media_gallery.toggle_visible": "切換可見性",
+  "media_gallery.hide": "隱藏",
   "moved_to_account_banner.text": "您的帳號 {disabledAccount} 目前已停用,因為您已搬家至 {movedToAccount}。",
   "mute_modal.hide_from_notifications": "於推播通知中隱藏",
   "mute_modal.hide_options": "隱藏選項",
diff --git a/config/locales/activerecord.ca.yml b/config/locales/activerecord.ca.yml
index 021cc38b4..9fa0f704b 100644
--- a/config/locales/activerecord.ca.yml
+++ b/config/locales/activerecord.ca.yml
@@ -15,6 +15,12 @@ ca:
       user/invite_request:
         text: Motiu
     errors:
+      attributes:
+        domain:
+          invalid: no és un nom de domini vàlid
+      messages:
+        invalid_domain_on_line: "%{value} no és un nom de domini vàlid"
+        too_many_lines: sobrepassa el límit de %{limit} línies
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.da.yml b/config/locales/activerecord.da.yml
index fd94a6cf9..35151f477 100644
--- a/config/locales/activerecord.da.yml
+++ b/config/locales/activerecord.da.yml
@@ -15,6 +15,12 @@ da:
       user/invite_request:
         text: Årsag
     errors:
+      attributes:
+        domain:
+          invalid: er ikke et gyldigt domænenavn
+      messages:
+        invalid_domain_on_line: "%{value} er ikke et gyldigt domænenavn"
+        too_many_lines: overstiger grænsen på %{limit} linjer
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.de.yml b/config/locales/activerecord.de.yml
index ca590bec7..b4bcd660d 100644
--- a/config/locales/activerecord.de.yml
+++ b/config/locales/activerecord.de.yml
@@ -15,6 +15,12 @@ de:
       user/invite_request:
         text: Begründung
     errors:
+      attributes:
+        domain:
+          invalid: ist kein gültiger Domain-Name
+      messages:
+        invalid_domain_on_line: "%{value} ist kein gültiger Domain-Name"
+        too_many_lines: übersteigt das Limit von %{limit} Zeilen
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.en-GB.yml b/config/locales/activerecord.en-GB.yml
index 2b1cb05a6..72edf5e02 100644
--- a/config/locales/activerecord.en-GB.yml
+++ b/config/locales/activerecord.en-GB.yml
@@ -15,6 +15,12 @@ en-GB:
       user/invite_request:
         text: Reason
     errors:
+      attributes:
+        domain:
+          invalid: is not a valid domain name
+      messages:
+        invalid_domain_on_line: "%{value} is not a valid domain name"
+        too_many_lines: is over the limit of %{limit} lines
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.fi.yml b/config/locales/activerecord.fi.yml
index 9da69b7db..b4d91a5f1 100644
--- a/config/locales/activerecord.fi.yml
+++ b/config/locales/activerecord.fi.yml
@@ -15,6 +15,12 @@ fi:
       user/invite_request:
         text: Syy
     errors:
+      attributes:
+        domain:
+          invalid: ei ole kelvollinen verkkotunnus
+      messages:
+        invalid_domain_on_line: "%{value} ei ole kelvollinen verkkotunnus"
+        too_many_lines: ylittää %{limit} rivin rajan
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.fo.yml b/config/locales/activerecord.fo.yml
index cf447a9db..61b924e5b 100644
--- a/config/locales/activerecord.fo.yml
+++ b/config/locales/activerecord.fo.yml
@@ -15,6 +15,12 @@ fo:
       user/invite_request:
         text: Orsøk
     errors:
+      attributes:
+        domain:
+          invalid: er ikki eitt virkið økisnavn
+      messages:
+        invalid_domain_on_line: "%{value} er ikki eitt virkið økisnavn"
+        too_many_lines: er longri enn markið á %{limit} reglur
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.gl.yml b/config/locales/activerecord.gl.yml
index 477db570e..961c96edb 100644
--- a/config/locales/activerecord.gl.yml
+++ b/config/locales/activerecord.gl.yml
@@ -15,6 +15,12 @@ gl:
       user/invite_request:
         text: Razón
     errors:
+      attributes:
+        domain:
+          invalid: non é un nome de dominio válido
+      messages:
+        invalid_domain_on_line: "%{value} non é un nome de dominio válido"
+        too_many_lines: superou o límite de %{limit} liñas
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.he.yml b/config/locales/activerecord.he.yml
index 211d98486..1729084a4 100644
--- a/config/locales/activerecord.he.yml
+++ b/config/locales/activerecord.he.yml
@@ -15,6 +15,12 @@ he:
       user/invite_request:
         text: סיבה
     errors:
+      attributes:
+        domain:
+          invalid: אינו שם מתחם קביל
+      messages:
+        invalid_domain_on_line: "%{value} אינו שם מתחם קביל"
+        too_many_lines: מעבר למגבלה של %{limit} שורות
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.hu.yml b/config/locales/activerecord.hu.yml
index f34ade044..6e376dd67 100644
--- a/config/locales/activerecord.hu.yml
+++ b/config/locales/activerecord.hu.yml
@@ -15,6 +15,12 @@ hu:
       user/invite_request:
         text: Indoklás
     errors:
+      attributes:
+        domain:
+          invalid: nem egy érvényes domain név
+      messages:
+        invalid_domain_on_line: "%{value} nem egy érvényes domain név"
+        too_many_lines: túllépi a(z) %{limit} soros korlátot
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.ia.yml b/config/locales/activerecord.ia.yml
index bf1fbc67e..bccfb9660 100644
--- a/config/locales/activerecord.ia.yml
+++ b/config/locales/activerecord.ia.yml
@@ -15,6 +15,11 @@ ia:
       user/invite_request:
         text: Motivo
     errors:
+      attributes:
+        domain:
+          invalid: non es un nomine de dominio valide
+      messages:
+        invalid_domain_on_line: "%{value} non es un nomine de dominio valide"
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.is.yml b/config/locales/activerecord.is.yml
index 4423cb6e4..e274cc0a9 100644
--- a/config/locales/activerecord.is.yml
+++ b/config/locales/activerecord.is.yml
@@ -15,6 +15,12 @@ is:
       user/invite_request:
         text: Ástæða
     errors:
+      attributes:
+        domain:
+          invalid: er ekki leyfilegt nafn á léni
+      messages:
+        invalid_domain_on_line: "%{value} er ekki leyfilegt nafn á léni"
+        too_many_lines: er yfir takmörkum á %{limit} línum
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.it.yml b/config/locales/activerecord.it.yml
index f23513e34..3d5be6c25 100644
--- a/config/locales/activerecord.it.yml
+++ b/config/locales/activerecord.it.yml
@@ -15,6 +15,12 @@ it:
       user/invite_request:
         text: Motivo
     errors:
+      attributes:
+        domain:
+          invalid: non è un nome di dominio valido
+      messages:
+        invalid_domain_on_line: "%{value} non è un nome di dominio valido"
+        too_many_lines: è oltre il limite di %{limit} righe
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.lt.yml b/config/locales/activerecord.lt.yml
index cb6e21d8e..2e4b54c62 100644
--- a/config/locales/activerecord.lt.yml
+++ b/config/locales/activerecord.lt.yml
@@ -15,6 +15,12 @@ lt:
       user/invite_request:
         text: Priežastis
     errors:
+      attributes:
+        domain:
+          invalid: nėra tinkamas domeno vardas.
+      messages:
+        invalid_domain_on_line: "%{value} nėra tinkamas domeno vardas."
+        too_many_lines: yra daugiau nei %{limit} eilučių ribojimą.
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.nl.yml b/config/locales/activerecord.nl.yml
index ce2c28a81..ee3c8bf26 100644
--- a/config/locales/activerecord.nl.yml
+++ b/config/locales/activerecord.nl.yml
@@ -15,6 +15,12 @@ nl:
       user/invite_request:
         text: Reden
     errors:
+      attributes:
+        domain:
+          invalid: is een ongeldige domeinnaam
+      messages:
+        invalid_domain_on_line: "%{value} is een ongeldige domeinnaam"
+        too_many_lines: overschrijdt de limiet van %{limit} regels
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.nn.yml b/config/locales/activerecord.nn.yml
index a303af624..a34cc7cf1 100644
--- a/config/locales/activerecord.nn.yml
+++ b/config/locales/activerecord.nn.yml
@@ -15,6 +15,12 @@ nn:
       user/invite_request:
         text: Grunn
     errors:
+      attributes:
+        domain:
+          invalid: er ikkje eit gyldig domenenamn
+      messages:
+        invalid_domain_on_line: "%{value} er ikkje gyldig i eit domenenamn"
+        too_many_lines: er over grensa på %{limit} liner
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.pl.yml b/config/locales/activerecord.pl.yml
index 5ae1d3778..d0e6dda58 100644
--- a/config/locales/activerecord.pl.yml
+++ b/config/locales/activerecord.pl.yml
@@ -15,6 +15,12 @@ pl:
       user/invite_request:
         text: Powód
     errors:
+      attributes:
+        domain:
+          invalid: nie jest prawidłową nazwą domeny
+      messages:
+        invalid_domain_on_line: "%{value} nie jest prawidłową nazwą domeny"
+        too_many_lines: przekracza limit %{limit} linii
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.pt-BR.yml b/config/locales/activerecord.pt-BR.yml
index 3199eb8e2..52f2b6ee8 100644
--- a/config/locales/activerecord.pt-BR.yml
+++ b/config/locales/activerecord.pt-BR.yml
@@ -15,6 +15,12 @@ pt-BR:
       user/invite_request:
         text: Razão
     errors:
+      attributes:
+        domain:
+          invalid: não é um nome de domínio válido
+      messages:
+        invalid_domain_on_line: "%{value} não é um nome de domínio válido"
+        too_many_lines: está acima do limite de %{limit} linhas
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.sv.yml b/config/locales/activerecord.sv.yml
index a3a45705e..1679dae46 100644
--- a/config/locales/activerecord.sv.yml
+++ b/config/locales/activerecord.sv.yml
@@ -15,6 +15,8 @@ sv:
       user/invite_request:
         text: Anledning
     errors:
+      messages:
+        invalid_domain_on_line: "%{value} Är inte ett giltigt domännamn"
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.tr.yml b/config/locales/activerecord.tr.yml
index d2b79d256..505289470 100644
--- a/config/locales/activerecord.tr.yml
+++ b/config/locales/activerecord.tr.yml
@@ -15,6 +15,12 @@ tr:
       user/invite_request:
         text: Gerekçe
     errors:
+      attributes:
+        domain:
+          invalid: geçerli bir alan adı değil
+      messages:
+        invalid_domain_on_line: "%{value} geçerli bir alan adı değil"
+        too_many_lines: "%{limit} satır sınırının üzerinde"
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.uk.yml b/config/locales/activerecord.uk.yml
index f16750ace..c9a4c8e1e 100644
--- a/config/locales/activerecord.uk.yml
+++ b/config/locales/activerecord.uk.yml
@@ -15,6 +15,12 @@ uk:
       user/invite_request:
         text: Причина
     errors:
+      attributes:
+        domain:
+          invalid: не є дійсним іменем домену
+      messages:
+        invalid_domain_on_line: "%{value} не є дійсним іменем домену"
+        too_many_lines: перевищує ліміт %{limit} рядків
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.zh-CN.yml b/config/locales/activerecord.zh-CN.yml
index 1b661266c..a4edf294a 100644
--- a/config/locales/activerecord.zh-CN.yml
+++ b/config/locales/activerecord.zh-CN.yml
@@ -15,6 +15,12 @@ zh-CN:
       user/invite_request:
         text: 理由
     errors:
+      attributes:
+        domain:
+          invalid: 不是有效的域名
+      messages:
+        invalid_domain_on_line: "%{value} 不是有效的域名"
+        too_many_lines: 超出 %{limit} 行的长度限制
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.zh-TW.yml b/config/locales/activerecord.zh-TW.yml
index 24609332c..742255066 100644
--- a/config/locales/activerecord.zh-TW.yml
+++ b/config/locales/activerecord.zh-TW.yml
@@ -15,6 +15,12 @@ zh-TW:
       user/invite_request:
         text: 原因
     errors:
+      attributes:
+        domain:
+          invalid: 並非一個有效網域
+      messages:
+        invalid_domain_on_line: "%{value} 並非一個有效網域"
+        too_many_lines: 已超過行數限制 (%{limit} 行)
       models:
         account:
           attributes:
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index e1025563e..63654ae70 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -435,6 +435,7 @@ ca:
       attempts_over_week:
         one: "%{count} intent en la darrera setmana"
         other: "%{count} intents de registre en la darrera setmana"
+      created_msg: S'ha blocat el domini de correu-e
       delete: Elimina
       dns:
         types:
@@ -444,8 +445,10 @@ ca:
         create: Afegir un domini
         resolve: Resol domini
         title: Blocar el nou domini de correu-e
+      no_email_domain_block_selected: No s'han canviat els bloqueigs de domini perquè no se n'ha seleccionat cap
       not_permitted: No permés
       resolved_through_html: Resolt mitjançant %{domain}
+      title: Dominis de correu-e blocats
     export_domain_allows:
       new:
         title: Importa dominis permesos
@@ -599,6 +602,7 @@ ca:
         resolve_description_html: No serà presa cap acció contra el compte denunciat, no se'n registrarà res i l'informe es tancarà.
         silence_description_html: El compte només serà visible a qui ja el seguia o l'ha cercat manualment, limitant-ne fortament l'abast. Sempre es pot revertir. Es tancaran tots els informes contra aquest compte.
         suspend_description_html: Aquest compte i tots els seus continguts seran inaccessibles i finalment eliminats, i interaccionar amb ell no serà possible. Reversible en 30 dies. Tanca tots els informes contra aquest compte.
+      actions_description_html: Decidiu quina acció a prendre per a resoldre aquest informe. Si preneu una acció punitiva contra el compte denunciat, se li enviarà una notificació per correu-e, excepte si se selecciona la categoria <strong>Spam</strong>.
       actions_description_remote_html: Decideix quina acció prendre per a resoldre aquest informe. Això només afectarà com <strong>el teu</strong> servidor es comunica amb aquest compte remot i en gestiona el contingut.
       actions_no_posts: Aquest informe no té associada cap publicació a esborrar
       add_to_report: Afegir més al informe
@@ -664,6 +668,7 @@ ca:
         delete_data_html: Esborra el perfil de <strong>@%{acct}</strong> i els seus continguts dins de 30 dies des d'ara a no ser que es desactivi la suspensió abans
         preview_preamble_html: "<strong>@%{acct}</strong> rebrà un avís amb el contingut següent:"
         record_strike_html: Registra una acció contra <strong>@%{acct}</strong> per ajudar a escalar-ho en futures violacions des d'aquest compte
+        send_email_html: Envia un avís per correu-e a <strong>@%{acct}</strong>
         warning_placeholder: Opcional raó adicional d'aquesta acció de moderació.
       target_origin: Origen del compte denunciat
       title: Informes
@@ -703,6 +708,7 @@ ca:
         manage_appeals: Gestiona apel·lacions
         manage_appeals_description: Permet als usuaris revisar les apel·lacions contra les accions de moderació
         manage_blocks: Gestiona blocs
+        manage_blocks_description: Permet als usuaris blocar adreces IP i proveïdors de correu-e
         manage_custom_emojis: Gestiona emojis personalitzats
         manage_custom_emojis_description: Permet als usuaris gestionar emojis personalitzats al servidor
         manage_federation: Gestiona federació
@@ -720,6 +726,7 @@ ca:
         manage_taxonomies: Gestionar taxonomies
         manage_taxonomies_description: Permet als usuaris revisar el contingut actual i actualitzar la configuració de l'etiqueta
         manage_user_access: Gestionar l'accés dels usuaris
+        manage_user_access_description: Permet als usuaris desactivar l'autenticació de dos factors d'altres usuaris, canviar la seva adreça de correu-e i restablir la seva contrasenya
         manage_users: Gestionar usuaris
         manage_users_description: Permet als usuaris veure els detalls d'altres usuaris i realitzar accions de moderació contra ells
         manage_webhooks: Gestionar Webhooks
@@ -1057,7 +1064,9 @@ ca:
       guide_link_text: Tothom hi pot contribuir.
     sensitive_content: Contingut sensible
   application_mailer:
+    notification_preferences: Canviar les preferències de correu-e
     salutation: "%{name},"
+    settings: 'Canviar les preferències de correu-e: %{link}'
     unsubscribe: Cancel·la la subscripció
     view: 'Visualització:'
     view_profile: Mostra el perfil
@@ -1077,6 +1086,7 @@ ca:
       hint_html: Una cosa més! Necessitem confirmar que ets una persona humana (és així com mantenim a ratlla l'spam). Resolt el CAPTCHA inferior i clica a "Segueix".
       title: Revisió de seguretat
     confirmations:
+      awaiting_review: S'ha confirmat la vostra adreça-e. El personal de %{domain} revisa ara el registre. Rebreu un correu si s'aprova el compte.
       awaiting_review_title: S'està revisant la teva inscripció
       clicking_this_link: en clicar aquest enllaç
       login_link: inici de sessió
@@ -1084,6 +1094,7 @@ ca:
       redirect_to_app_html: Se us hauria d'haver redirigit a l'app <strong>%{app_name}</strong>. Si això no ha passat, intenteu %{clicking_this_link} o torneu manualment a l'app.
       registration_complete: La teva inscripció a %{domain} ja és completa.
       welcome_title: Hola, %{name}!
+      wrong_email_hint: Si aquesta adreça de correu-e no és correcta, podeu canviar-la en els ajustos del compte.
     delete_account: Elimina el compte
     delete_account_html: Si vols suprimir el compte pots <a href="%{path}">fer-ho aquí</a>. Se't demanarà confirmació.
     description:
@@ -1126,6 +1137,7 @@ ca:
     security: Seguretat
     set_new_password: Estableix una contrasenya nova
     setup:
+      email_below_hint_html: Verifiqueu la carpeta de correu brossa o demaneu-ne un altre. Podeu corregir l'adreça de correu-e si no és correcta.
       email_settings_hint_html: Toca l'enllaç que t'hem enviat per a verificar %{email}. Esperarem aquí mateix.
       link_not_received: No has rebut l'enllaç?
       new_confirmation_instructions_sent: Rebràs un nou correu amb l'enllaç de confirmació en pocs minuts!
@@ -1139,12 +1151,19 @@ ca:
       title: Configurem-te a %{domain}.
     status:
       account_status: Estat del compte
+      confirming: Esperant que es completi la confirmació del correu-e.
       functional: El teu compte està completament operatiu.
       redirecting_to: El teu compte és inactiu perquè actualment està redirigint a %{acct}.
       self_destruct: Com que %{domain} tanca, només tindreu accés limitat al vostre compte.
       view_strikes: Veure accions del passat contra el teu compte
     too_fast: Formulari enviat massa ràpid, torna a provar-ho.
     use_security_key: Usa clau de seguretat
+  author_attribution:
+    example_title: Text d'exemple
+    hint_html: Controleu com se us acredita quan els enllaços es comparteixen a Mastodon.
+    more_from_html: Més de %{name}
+    s_blog: Blog de %{name}
+    title: Atribució d'autor
   challenge:
     confirm: Continua
     hint_html: "<strong>Pista:</strong> No et preguntarem un altre cop la teva contrasenya en la pròxima hora."
@@ -1930,6 +1949,7 @@ ca:
     instructions_html: Copieu i enganxeu el següent codi HTML al vostre lloc web. Després, afegiu l'adreça del vostre lloc web dins d'un dels camps extres del vostre perfil i deseu els canvis.
     verification: Verificació
     verified_links: Els teus enllaços verificats
+    website_verification: Verificació de web
   webauthn_credentials:
     add: Afegir nova clau de seguretat
     create:
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 19ee9b9d8..731c1f0b4 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -1161,6 +1161,12 @@ da:
       view_strikes: Se tidligere anmeldelser af din konto
     too_fast: Formularen indsendt for hurtigt, forsøg igen.
     use_security_key: Brug sikkerhedsnøgle
+  author_attribution:
+    example_title: Eksempeltekst
+    hint_html: Styrer, hvordan man krediteres, når links deles på Mastodon.
+    more_from_html: Flere fra %{name}
+    s_blog: "%{name}s blog"
+    title: Forfattertilskrivning
   challenge:
     confirm: Fortsæt
     hint_html: "<strong>Tip:</strong> Du bliver ikke anmodet om din adgangskode igen den næste time."
@@ -1949,6 +1955,7 @@ da:
     instructions_html: Kopier og indsæt koden nedenfor i HTML på din hjemmeside. Tilføj derefter adressen på din hjemmeside i et af de ekstra felter på din profil på fanen "Redigér profil" og gem ændringer.
     verification: Bekræftelse
     verified_links: Dine bekræftede links
+    website_verification: Webstedsbekræftelse
   webauthn_credentials:
     add: Tilføj ny sikkerhedsnøgle
     create:
diff --git a/config/locales/de.yml b/config/locales/de.yml
index b75aada76..cc049c590 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -25,6 +25,7 @@ de:
   admin:
     account_actions:
       action: Aktion ausführen
+      already_silenced: Dieses Konto wurde bereits eingeschränkt.
       already_suspended: Dieses Konto wurde bereits gesperrt.
       title: "@%{acct} moderieren"
     account_moderation_notes:
@@ -1160,6 +1161,12 @@ de:
       view_strikes: Vorherige Verstöße deines Kontos ansehen
     too_fast: Formular zu schnell übermittelt. Bitte versuche es erneut.
     use_security_key: Sicherheitsschlüssel verwenden
+  author_attribution:
+    example_title: Beispieltext
+    hint_html: Bestimme, wie du Anerkennungen durch geteilte Links auf Mastodon handhaben möchtest.
+    more_from_html: Mehr von %{name}
+    s_blog: Blog von %{name}
+    title: Anerkennung als Autor*in
   challenge:
     confirm: Fortfahren
     hint_html: "<strong>Hinweis:</strong> Wir werden dich für die nächste Stunde nicht erneut nach deinem Passwort fragen."
@@ -1948,6 +1955,7 @@ de:
     instructions_html: Kopiere den unten stehenden Code und füge ihn in das HTML deiner Website ein. Trage anschließend die Adresse deiner Website in ein Zusatzfeld auf deinem Profil ein und speichere die Änderungen. Die Zusatzfelder befinden sich im Reiter „Profil bearbeiten“.
     verification: Verifizierung
     verified_links: Deine verifizierten Links
+    website_verification: Website-Verifizierung
   webauthn_credentials:
     add: Sicherheitsschlüssel hinzufügen
     create:
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml
index a3036c2f9..dc07dcff0 100644
--- a/config/locales/en-GB.yml
+++ b/config/locales/en-GB.yml
@@ -25,6 +25,8 @@ en-GB:
   admin:
     account_actions:
       action: Perform action
+      already_silenced: This account has already been limited.
+      already_suspended: This account has already been suspended.
       title: Perform moderation action on %{acct}
     account_moderation_notes:
       create: Leave note
@@ -46,6 +48,7 @@ en-GB:
         title: Change email for %{username}
       change_role:
         changed_msg: Role successfully changed!
+        edit_roles: Manage user roles
         label: Change role
         no_role: No role
         title: Change role for %{username}
@@ -602,6 +605,7 @@ en-GB:
         suspend_description_html: The account and all its contents will be inaccessible and eventually deleted, and interacting with it will be impossible. Reversible within 30 days. Closes all reports against this account.
       actions_description_html: Decide which action to take to resolve this report. If you take a punitive action against the reported account, an email notification will be sent to them, except when the <strong>Spam</strong> category is selected.
       actions_description_remote_html: Decide which action to take to resolve this report. This will only affect how <strong>your</strong> server communicates with this remote account and handle its content.
+      actions_no_posts: This report doesn't have any associated posts to delete
       add_to_report: Add more to report
       already_suspended_badges:
         local: Already suspended on this server
@@ -723,6 +727,7 @@ en-GB:
         manage_taxonomies: Manage Taxonomies
         manage_taxonomies_description: Allows users to review trending content and update hashtag settings
         manage_user_access: Manage User Access
+        manage_user_access_description: Allows users to disable other users' two-factor authentication, change their email address, and reset their password
         manage_users: Manage Users
         manage_users_description: Allows users to view other users' details and perform moderation actions against them
         manage_webhooks: Manage Webhooks
@@ -797,6 +802,7 @@ en-GB:
       destroyed_msg: Site upload successfully deleted!
     software_updates:
       critical_update: Critical — please update quickly
+      description: It is recommended to keep your Mastodon installation up to date to benefit from the latest fixes and features. Moreover, it is sometimes critical to update Mastodon in a timely manner to avoid security issues. For these reasons, Mastodon checks for updates every 30 minutes, and will notify you according to your email notification preferences.
       documentation_link: Learn more
       release_notes: Release notes
       title: Available updates
@@ -883,16 +889,37 @@ en-GB:
         action: Check here for more information
         message_html: "<strong>Your object storage is misconfigured. The privacy of your users is at risk.</strong>"
     tags:
+      moderation:
+        not_trendable: Not trendable
+        not_usable: Not usable
+        pending_review: Pending review
+        review_requested: Review requested
+        reviewed: Reviewed
+        title: Status
+        trendable: Trendable
+        unreviewed: Unreviewed
+        usable: Usable
+      name: Name
+      newest: Newest
+      oldest: Oldest
+      open: View Publicly
+      reset: Reset
       review: Review status
+      search: Search
+      title: Hashtags
       updated_msg: Hashtag settings updated successfully
     title: Administration
     trends:
       allow: Allow
       approved: Approved
+      confirm_allow: Are you sure you want to allow selected tags?
+      confirm_disallow: Are you sure you want to disallow selected tags?
       disallow: Disallow
       links:
         allow: Allow link
         allow_provider: Allow publisher
+        confirm_allow: Are you sure you want to allow selected links?
+        confirm_allow_provider: Are you sure you want to allow selected providers?
         confirm_disallow: Are you sure you want to disallow selected links?
         confirm_disallow_provider: Are you sure you want to disallow selected providers?
         description_html: These are links that are currently being shared a lot by accounts that your server sees posts from. It can help your users find out what's going on in the world. No links are displayed publicly until you approve the publisher. You can also allow or reject individual links.
@@ -1039,7 +1066,9 @@ en-GB:
       guide_link_text: Everyone can contribute.
     sensitive_content: Sensitive content
   application_mailer:
+    notification_preferences: Change email preferences
     salutation: "%{name},"
+    settings: 'Change email preferences: %{link}'
     unsubscribe: Unsubscribe
     view: 'View:'
     view_profile: View profile
@@ -1059,6 +1088,7 @@ en-GB:
       hint_html: Just one more thing! We need to confirm you're a human (this is so we can keep the spam out!). Solve the CAPTCHA below and click "Continue".
       title: Security check
     confirmations:
+      awaiting_review: Your email address is confirmed! The %{domain} staff is now reviewing your registration. You will receive an email if they approve your account!
       awaiting_review_title: Your registration is being reviewed
       clicking_this_link: clicking this link
       login_link: log in
@@ -1066,6 +1096,7 @@ en-GB:
       redirect_to_app_html: You should have been redirected to the <strong>%{app_name}</strong> app. If that did not happen, try %{clicking_this_link} or manually return to the app.
       registration_complete: Your registration on %{domain} is now complete!
       welcome_title: Welcome, %{name}!
+      wrong_email_hint: If that email address is not correct, you can change it in account settings.
     delete_account: Delete account
     delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation.
     description:
@@ -1086,6 +1117,7 @@ en-GB:
     or_log_in_with: Or log in with
     privacy_policy_agreement_html: I have read and agree to the <a href="%{privacy_policy_path}" target="_blank">privacy policy</a>
     progress:
+      confirm: Confirm email
       details: Your details
       review: Our review
       rules: Accept rules
@@ -1107,6 +1139,7 @@ en-GB:
     security: Security
     set_new_password: Set new password
     setup:
+      email_below_hint_html: Check your spam folder, or request another one. You can correct your email address if it's wrong.
       email_settings_hint_html: Click the link we sent you to verify %{email}. We'll wait right here.
       link_not_received: Didn't get a link?
       new_confirmation_instructions_sent: You will receive a new email with the confirmation link in a few minutes!
@@ -1128,6 +1161,12 @@ en-GB:
       view_strikes: View past strikes against your account
     too_fast: Form submitted too fast, try again.
     use_security_key: Use security key
+  author_attribution:
+    example_title: Sample text
+    hint_html: Control how you're credited when links are shared on Mastodon.
+    more_from_html: More from %{name}
+    s_blog: "%{name}'s Blog"
+    title: Author attribution
   challenge:
     confirm: Continue
     hint_html: "<strong>Tip:</strong> We won't ask you for your password again for the next hour."
@@ -1423,6 +1462,7 @@ en-GB:
   media_attachments:
     validations:
       images_and_video: Cannot attach a video to a post that already contains images
+      not_found: Media %{ids} not found or already attached to another post
       not_ready: Cannot attach files that have not finished processing. Try again in a moment!
       too_many: Cannot attach more than 4 files
   migrations:
@@ -1915,6 +1955,7 @@ en-GB:
     instructions_html: Copy and paste the code below into the HTML of your website. Then add the address of your website into one of the extra fields on your profile from the "Edit profile" tab and save changes.
     verification: Verification
     verified_links: Your verified links
+    website_verification: Website verification
   webauthn_credentials:
     add: Add new security key
     create:
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 90c1e7776..5c39346a9 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -1161,6 +1161,10 @@ fi:
       view_strikes: Näytä aiemmat tiliäsi koskevat varoitukset
     too_fast: Lomake lähetettiin liian nopeasti, yritä uudelleen.
     use_security_key: Käytä suojausavainta
+  author_attribution:
+    example_title: Esimerkkiteksti
+    more_from_html: Lisää tekijältä %{name}
+    s_blog: Käyttäjän %{name} blogi
   challenge:
     confirm: Jatka
     hint_html: "<strong>Vihje:</strong> Emme pyydä sinulta salasanaa uudelleen seuraavan tunnin aikana."
@@ -1949,6 +1953,7 @@ fi:
     instructions_html: Kopioi ja liitä seuraava koodi verkkosivustosi HTML-lähdekoodiin. Lisää sitten verkkosivustosi osoite johonkin profiilisi lisäkentistä ”Muokkaa profiilia” -välilehdellä ja tallenna muutokset.
     verification: Vahvistus
     verified_links: Vahvistetut linkkisi
+    website_verification: Verkkosivuston vahvistus
   webauthn_credentials:
     add: Lisää uusi suojausavain
     create:
diff --git a/config/locales/fo.yml b/config/locales/fo.yml
index 5bc192e9d..6d7b38e18 100644
--- a/config/locales/fo.yml
+++ b/config/locales/fo.yml
@@ -25,6 +25,7 @@ fo:
   admin:
     account_actions:
       action: Frem atgerð
+      already_silenced: Hendan kontan er longu avmarkað.
       already_suspended: Hendan kontan er longu ógildað.
       title: Frem umsjónaratgerð á %{acct}
     account_moderation_notes:
@@ -1160,6 +1161,12 @@ fo:
       view_strikes: Vís eldri atsóknir móti tíni kontu
     too_fast: Oyðublaðið innsent ov skjótt, royn aftur.
     use_security_key: Brúka trygdarlykil
+  author_attribution:
+    example_title: Tekstadømi
+    hint_html: Kanna, hvussu tú verður viðurkend/ur, tá ið onnur deila slóðir á Mastodon.
+    more_from_html: Meiri frá %{name}
+    s_blog: Bloggurin hjá %{name}
+    title: Ískoyti høvundans
   challenge:
     confirm: Hald á
     hint_html: "<strong>Góð ráð:</strong> vit spyrja teg ikki aftur um loyniorðið næsta tíman."
@@ -1945,6 +1952,7 @@ fo:
     instructions_html: Avrita og innset koduna niðanfyri inn í HTML'ið á heimasíðuni hjá tær. Legg síðani adressuna á heimasíðuni hjá tær inn á eitt av eyka teigunum á vanganum hjá tær umvegis "Rætta vanga" teigin og goym broytingar.
     verification: Váttan
     verified_links: Tíni váttaðu leinki
+    website_verification: Heimasíðuváttan
   webauthn_credentials:
     add: Legg nýggjan trygdarlykil afturat
     create:
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index c8fa1d833..d275b844f 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -1161,6 +1161,12 @@ gl:
       view_strikes: Ver avisos anteriores respecto da túa conta
     too_fast: Formulario enviado demasiado rápido, inténtao outra vez.
     use_security_key: Usa chave de seguridade
+  author_attribution:
+    example_title: Texto de mostra
+    hint_html: Controla o xeito en que te acreditan cando se comparten ligazóns en Mastodon.
+    more_from_html: Máis de %{name}
+    s_blog: Blog de %{name}
+    title: Atribución da autoría
   challenge:
     confirm: Continuar
     hint_html: "<strong>Nota:</strong> Non che pediremos o contrasinal na seguinte hora."
@@ -1949,6 +1955,7 @@ gl:
     instructions_html: Copia e pega o código inferior no HTML do teu sitio web. Despois engade o enderezo da túa web nun dos campos de datos extra do teu perfil dispoñibles na lapela "Editar perfil" e garda os cambios.
     verification: Validación
     verified_links: As túas ligazóns verificadas
+    website_verification: Verificación do sitio web
   webauthn_credentials:
     add: Engadir nova chave de seguridade
     create:
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 025b2de9e..341e2bf02 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -29,6 +29,7 @@ he:
   admin:
     account_actions:
       action: בצע/י פעולה
+      already_silenced: חשבון זה הוגבל זה מכבר.
       already_suspended: חשבון זה הושעה.
       title: ביצוע פעולות הנהלה על %{acct}
     account_moderation_notes:
@@ -1196,6 +1197,12 @@ he:
       view_strikes: צפיה בעברות קודמות שנרשמו נגד חשבונך
     too_fast: הטופס הוגש מהר מדי, נסה/י שוב.
     use_security_key: שימוש במפתח אבטחה
+  author_attribution:
+    example_title: טקסט לדוגמה
+    hint_html: בחירה איך תקבלו קרדיטציה כאשר קישורים משותפים דרך מסטודון.
+    more_from_html: עוד מאת %{name}
+    s_blog: הבלוג של %{name}
+    title: ייחוס למפרסם
   challenge:
     confirm: המשך
     hint_html: "<strong>טיפ:</strong> לא נבקש את סיסמתך שוב בשעה הקרובה."
@@ -2014,6 +2021,7 @@ he:
     instructions_html: יש להדביק את הקוד שלמטה אל האתר שלך. ואז להוסיף את כתובת האתר לאחד השדות הנוספים בפרופיל מתוך טאב "עריכת פרופיל" ולשמור את השינויים.
     verification: אימות
     verified_links: קישוריך המאומתים
+    website_verification: אימות אתר רשת
   webauthn_credentials:
     add: הוספת מפתח אבטחה חדש
     create:
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 141f5b7d0..e29472f8d 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -25,6 +25,7 @@ hu:
   admin:
     account_actions:
       action: Művelet végrehajtása
+      already_silenced: Ezt a fiókot már korlátozták.
       already_suspended: Ezt a fiókot már felfüggesztették.
       title: 'Moderálási művelet végrehajtása ezen: %{acct}'
     account_moderation_notes:
@@ -1160,6 +1161,12 @@ hu:
       view_strikes: Fiókod ellen felrótt korábbi vétségek megtekintése
     too_fast: Túl gyorsan küldted el az űrlapot, próbáld később.
     use_security_key: Biztonsági kulcs használata
+  author_attribution:
+    example_title: Mintaszöveg
+    hint_html: Szabályozd, hogyan hivatkoznak rád, amikor linket osztanak meg Mastodonon.
+    more_from_html: 'Több tőle: %{name}'
+    s_blog: "%{name} blogja"
+    title: Szerző forrásmegjelölése
   challenge:
     confirm: Folytatás
     hint_html: "<strong>Hasznos:</strong> Nem fogjuk megint a jelszavadat kérdezni a következő órában."
@@ -1946,8 +1953,9 @@ hu:
     here_is_how: Itt van, hogyan kell
     hint_html: "<strong>A személyazonosságod ellenőrizhetősége a Mastodonon mindenki számára elérhető.</strong> Ez nyílt webes szabványok alapján, most és mindörökké szabadon és ingyenesen történik. Ehhez csak egy saját weboldalra van szükséged, mely alapján mások felismernek téged. Ha a profilodról erre a weboldalra hivatkozol, mi ellenőrizzük, hogy erről az oldalról visszahivatkozol-e a profilodra, és siker esetén erről vizuális jelzést is adunk a profilodon."
     instructions_html: Az alábbi kódot másold be a weboldalad HTML kódjába. Ezután add hozzá a weboldalad címét a profilod egyik extra mezőjéhez a "Profil szerkesztése" fülön és mentsd a változásokat.
-    verification: Hitelesítés
+    verification: Ellenőrzés
     verified_links: Ellenőrzött hivatkozásaid
+    website_verification: Weboldal ellenőrzése
   webauthn_credentials:
     add: Biztonsági kulcs hozzáadása
     create:
diff --git a/config/locales/ia.yml b/config/locales/ia.yml
index 073e5032b..87789562f 100644
--- a/config/locales/ia.yml
+++ b/config/locales/ia.yml
@@ -25,6 +25,7 @@ ia:
   admin:
     account_actions:
       action: Exequer action
+      already_silenced: Iste conto jam ha essite limitate.
       already_suspended: Iste conto jam ha essite suspendite.
       title: Exequer action de moderation sur %{acct}
     account_moderation_notes:
@@ -1146,6 +1147,10 @@ ia:
       view_strikes: Examinar le sanctiones passate contra tu conto
     too_fast: Formulario inviate troppo rapidemente. Tenta lo de novo.
     use_security_key: Usar clave de securitate
+  author_attribution:
+    example_title: Texto de exemplo
+    more_from_html: Plus de %{name}
+    s_blog: Blog de %{name}
   challenge:
     confirm: Continuar
     hint_html: "<strong>Consilio:</strong> Nos non te demandara tu contrasigno de novo in le proxime hora."
@@ -1933,6 +1938,7 @@ ia:
     instructions_html: Copia e colla le codice hic infra in le HTML de tu sito web. Alora adde le adresse de tu sito web in un del campos supplementari sur tu profilo desde le scheda “Modificar profilo” e salva le cambiamentos.
     verification: Verification
     verified_links: Tu ligamines verificate
+    website_verification: Verification de sito web
   webauthn_credentials:
     add: Adder un nove clave de securitate
     create:
diff --git a/config/locales/is.yml b/config/locales/is.yml
index 1c84c45c8..5fa147e8d 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -25,6 +25,7 @@ is:
   admin:
     account_actions:
       action: Framkvæma aðgerð
+      already_silenced: Þessi aðgangur hefur þegar verið takmarkaður.
       already_suspended: Þessi aðgangur hefur þegar verið settur í frysti.
       title: Framkvæma umsjónaraðgerð á %{acct}
     account_moderation_notes:
@@ -1164,6 +1165,12 @@ is:
       view_strikes: Skoða fyrri bönn notandaaðgangsins þíns
     too_fast: Innfyllingarform sent inn of hratt, prófaðu aftur.
     use_security_key: Nota öryggislykil
+  author_attribution:
+    example_title: Sýnitexti
+    hint_html: Stýrðu hvernig framlög þín birtast þegar tenglum er deilt á Mastodon.
+    more_from_html: Meira frá %{name}
+    s_blog: Bloggsvæði hjá %{name}
+    title: Framlag höfundar
   challenge:
     confirm: Halda áfram
     hint_html: "<strong>Ábending:</strong> Við munum ekki spyrja þig um lykilorðið aftur næstu klukkustundina."
@@ -1952,6 +1959,7 @@ is:
     instructions_html: Afritaðu og límdu kóðann hér fyrir neðan inn í HTML-kóða vefsvæðisins þíns. Bættu síðan slóð vefsvæðisins þíns inn í einn af auka-reitunum í flipanum "Breyta notandasniði" og vistaðu síðan breytingarnar.
     verification: Sannprófun
     verified_links: Staðfestu tenglarnir þínir
+    website_verification: Staðfesting vefsvæðis
   webauthn_credentials:
     add: Bæta við nýjum öryggislykli
     create:
diff --git a/config/locales/it.yml b/config/locales/it.yml
index a429c6339..7de24fe25 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -1163,6 +1163,12 @@ it:
       view_strikes: Visualizza le sanzioni precedenti prese nei confronti del tuo account
     too_fast: Modulo inviato troppo velocemente, riprova.
     use_security_key: Usa la chiave di sicurezza
+  author_attribution:
+    example_title: Testo di esempio
+    hint_html: Controlla come sei viene accreditato quando i link sono condivisi su Mastodon.
+    more_from_html: Altro da %{name}
+    s_blog: Blog di %{name}
+    title: Attribuzione autore
   challenge:
     confirm: Continua
     hint_html: "<strong>Suggerimento:</strong> Non ti chiederemo di nuovo la tua password per la prossima ora."
@@ -1951,6 +1957,7 @@ it:
     instructions_html: Copia e incolla il codice qui sotto nell'HTML del tuo sito web. Quindi, aggiungi l'indirizzo del tuo sito web in uno dei campi aggiuntivi del tuo profilo dalla scheda "Modifica profilo" e salva le modifiche.
     verification: Verifica
     verified_links: I tuoi collegamenti verificati
+    website_verification: Verifica del sito web
   webauthn_credentials:
     add: Aggiungi una nuova chiave di sicurezza
     create:
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index c48b8ef93..07e8ba75a 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -29,6 +29,7 @@ lt:
   admin:
     account_actions:
       action: Atlikti veiksmą
+      already_silenced: Ši paskyra jau buvo apribota.
       already_suspended: Ši paskyra jau sustabdyta.
       title: Atlikti prižiūrėjimo veiksmą %{acct}
     account_moderation_notes:
@@ -800,6 +801,12 @@ lt:
       redirecting_to: Tavo paskyra yra neaktyvi, nes šiuo metu ji nukreipiama į %{acct}.
       self_destruct: Kadangi %{domain} uždaromas, turėsi tik ribotą prieigą prie savo paskyros.
       view_strikes: Peržiūrėti ankstesnius savo paskyros pažeidimus
+  author_attribution:
+    example_title: Teksto pavyzdys
+    hint_html: Valdyk, kaip esi nurodomas (-a), kai nuorodos bendrinamos platformoje „Mastodon“.
+    more_from_html: Daugiau iš %{name}
+    s_blog: "%{name} tinklaraštis"
+    title: Autoriaus (-ės) atribucija
   challenge:
     hint_html: "<strong>Patarimas:</strong> artimiausią valandą daugiau neprašysime tavo slaptažodžio."
   datetime:
@@ -1230,6 +1237,7 @@ lt:
     instructions_html: Nukopijuok ir įklijuok toliau pateiktą kodą į savo svetainės HTML. Tada į vieną iš papildomų profilio laukų skirtuke Redaguoti profilį įrašyk savo svetainės adresą ir išsaugok pakeitimus.
     verification: Patvirtinimas
     verified_links: Tavo patikrintos nuorodos
+    website_verification: Svetainės patvirtinimas
   webauthn_credentials:
     add: Pridėti naują saugumo raktą
     create:
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index c74bf488b..63fcf1c85 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -1161,6 +1161,12 @@ nl:
       view_strikes: Bekijk de eerder door moderatoren vastgestelde overtredingen die je hebt gemaakt
     too_fast: Formulier is te snel ingediend. Probeer het nogmaals.
     use_security_key: Beveiligingssleutel gebruiken
+  author_attribution:
+    example_title: Voorbeeldtekst
+    hint_html: Bepaal hoe we je vermelden, wanneer jouw links op Mastodon worden gedeeld.
+    more_from_html: Meer van %{name}
+    s_blog: De weblog van %{name}
+    title: Auteur-attributie
   challenge:
     confirm: Doorgaan
     hint_html: "<strong>Tip:</strong> We vragen jou het komende uur niet meer naar jouw wachtwoord."
@@ -1949,6 +1955,7 @@ nl:
     instructions_html: Kopieer en plak de onderstaande code in de HTML van je website. Voeg vervolgens het adres van je website toe aan een van de extra velden op je profiel op het tabblad "Profiel bewerken" en sla de wijzigingen op.
     verification: Verificatie
     verified_links: Jouw geverifieerde links
+    website_verification: Website-verificatie
   webauthn_credentials:
     add: Nieuwe beveiligingssleutel toevoegen
     create:
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index 5c46a0d43..47dcc1ac8 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -26,6 +26,7 @@ nn:
     account_actions:
       action: Utfør
       already_silenced: Denne kontoen har allereie vorte avgrensa.
+      already_suspended: Denne kontoen er allereie sperra.
       title: Utfør moderatorhandling på %{acct}
     account_moderation_notes:
       create: Legg igjen merknad
@@ -47,6 +48,7 @@ nn:
         title: Byt e-post for %{username}
       change_role:
         changed_msg: Rolle endra!
+        edit_roles: Administrer brukarroller
         label: Endre rolle
         no_role: Inga rolle
         title: Endre rolle for %{username}
@@ -603,6 +605,7 @@ nn:
         suspend_description_html: Brukarkontoen og alt innhaldet vil bli utilgjengeleg og til slutt sletta, og det vil vera uråd å samhandla med brukaren. Du kan angra dette innan 30 dagar. Dette avsluttar alle rapportar om kontoen.
       actions_description_html: Avgjer kva som skal gjerast med denne rapporteringa. Dersom du utfører straffetiltak mot den rapporterte kontoen, vil dei motta ein e-post – så sant du ikkje har valt kategorien <strong>Spam</strong>.
       actions_description_remote_html: Avgjer kva du vil gjera for å løysa denne rapporten. Dette påverkar berre korleis tenaren <strong>din</strong> kommuniserer med kontoen på ein annan tenar, og korleis tenaren din handterer innhald derifrå.
+      actions_no_posts: Denne rapporten har ingen tilknytte innlegg å sletta
       add_to_report: Legg til i rapporten
       already_suspended_badges:
         local: Allereie utestengd på denne tenaren
@@ -1158,6 +1161,12 @@ nn:
       view_strikes: Vis tidligere advarsler mot kontoen din
     too_fast: Skjemaet ble sendt inn for raskt, prøv på nytt.
     use_security_key: Bruk sikkerhetsnøkkel
+  author_attribution:
+    example_title: Eksempeltekst
+    hint_html: Kontroller korleis du blir kreditert når nokon deler lenker på Mastodon.
+    more_from_html: Meir frå %{name}
+    s_blog: Bloggen til %{name}
+    title: Forfattarkreditering
   challenge:
     confirm: Hald fram
     hint_html: "<strong>Tips:</strong> Vi skal ikkje spørja deg om passordet ditt igjen i laupet av den neste timen."
@@ -1946,6 +1955,7 @@ nn:
     instructions_html: Kopier og lim inn i koden nedanfor i HTML-koden for nettsida di. Legg deretter adressa til nettsida di til i ei av ekstrafelta på profilen din frå fana "Rediger profil" og lagre endringane.
     verification: Stadfesting
     verified_links: Dine verifiserte lenker
+    website_verification: Stadfesting av nettside
   webauthn_credentials:
     add: Legg til ny sikkerhetsnøkkel
     create:
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index c90d448a7..b710df2e7 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -29,6 +29,7 @@ pl:
   admin:
     account_actions:
       action: Wykonaj działanie
+      already_silenced: To konto zostało już ograniczone.
       already_suspended: To konto zostało już zawieszone.
       title: Wykonaj działanie moderacyjne na %{acct}
     account_moderation_notes:
@@ -1196,6 +1197,12 @@ pl:
       view_strikes: Zobacz dawne ostrzeżenia nałożone na twoje konto
     too_fast: Zbyt szybko przesłano formularz, spróbuj ponownie.
     use_security_key: Użyj klucza bezpieczeństwa
+  author_attribution:
+    example_title: Przykładowy tekst
+    hint_html: Kontroluj przypisy do twoich wpisów widoczne na Mastodonie.
+    more_from_html: Więcej od %{name}
+    s_blog: Blog %{name}
+    title: Przypis do autora
   challenge:
     confirm: Kontynuuj
     hint_html: "<strong>Informacja:</strong> Nie będziemy prosić Cię o ponowne podanie hasła przez następną godzinę."
@@ -2014,6 +2021,7 @@ pl:
     instructions_html: Skopiuj poniższy kod HTML i wklej go na swoją stronę. Potem dodaj link do twojej strony do jednego z wolnych pól na profilu z zakładki "Edytuj profil".
     verification: Weryfikacja
     verified_links: Twoje zweryfikowane linki
+    website_verification: Weryfikacja strony internetowej
   webauthn_credentials:
     add: Dodaj nowy klucz bezpieczeństwa
     create:
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 3c14589e6..864a8b4d9 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -25,6 +25,8 @@ pt-BR:
   admin:
     account_actions:
       action: Tomar uma atitude
+      already_silenced: Esta conta já foi limitada.
+      already_suspended: Esta conta já foi suspensa.
       title: Moderar %{acct}
     account_moderation_notes:
       create: Deixar nota
@@ -46,6 +48,7 @@ pt-BR:
         title: Alterar e-mail para %{username}
       change_role:
         changed_msg: Função alterada com sucesso!
+        edit_roles: Gerenciar funções do usuário
         label: Alterar função
         no_role: Nenhuma função
         title: Alterar função para %{username}
@@ -602,6 +605,7 @@ pt-BR:
         suspend_description_html: A conta e todo o seu conteúdo ficará inacessível e, eventualmente, excluído e interagir com ela será impossível. Reversível dentro de 30 dias. Encerra todas as denúncias contra esta conta.
       actions_description_html: Decida qual ação tomar para responder a essa denúncia. Se você tomar uma ação punitiva contra a conta denunciada, uma notificação por e-mail será enviada ao usuário, exceto quando a categoria <strong>Spam</strong> for selecionada.
       actions_description_remote_html: Decida quais medidas tomará para resolver esta denúncia. Isso só afetará como <strong>seu servidor</strong> se comunica com esta conta remota e manipula seu conteúdo.
+      actions_no_posts: Essa denúncia não tem nenhuma publicação associada para excluir
       add_to_report: Adicionar mais à denúncia
       already_suspended_badges:
         local: Já suspenso neste servidor
@@ -1157,6 +1161,12 @@ pt-BR:
       view_strikes: Veja os avisos anteriores em relação à sua conta
     too_fast: O formulário foi enviado muito rapidamente, tente novamente.
     use_security_key: Usar chave de segurança
+  author_attribution:
+    example_title: Texto de amostra
+    hint_html: Controle como você é creditado quando links são compartilhados no Mastodon.
+    more_from_html: Mais de %{name}
+    s_blog: Blog do %{name}
+    title: Atribuição de autoria
   challenge:
     confirm: Continuar
     hint_html: "<strong>Dica:</strong> Não pediremos novamente sua senha pela próxima hora."
@@ -1945,6 +1955,7 @@ pt-BR:
     instructions_html: Copie o código abaixo e cole no HTML do seu site. Em seguida, adicione o endereço do seu site em um dos campos extras em seu perfil, na aba "Editar perfil", e salve as alterações.
     verification: Verificação
     verified_links: Seus links verificados
+    website_verification: Verificação do site
   webauthn_credentials:
     add: Adicionar nova chave de segurança
     create:
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index c628bebaa..7b651470b 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -3,6 +3,7 @@ ca:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Protegeix de falses atribucions.
         discoverable: El teu perfil i els teus tuts públics poden aparèixer o ser recomanats en diverses àreas de Mastodon i el teu perfil pot ser suggerit a altres usuaris.
         display_name: El teu nom complet o el teu nom divertit.
         fields: La teva pàgina d'inici, pronoms, edat, el que vulguis.
@@ -143,6 +144,7 @@ ca:
         url: On els esdeveniments seran enviats
     labels:
       account:
+        attribution_domains_as_text: Permet només webs específics
         discoverable: Permet el perfil i el tuts en els algorismes de descobriment
         fields:
           name: Etiqueta
diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml
index 8bacdc46c..e7b8fe337 100644
--- a/config/locales/simple_form.da.yml
+++ b/config/locales/simple_form.da.yml
@@ -3,6 +3,7 @@ da:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Beskytter mod falske tilskrivninger.
         discoverable: Dine offentlige indlæg og profil kan blive fremhævet eller anbefalet i forskellige områder af Mastodon, og profilen kan blive foreslået til andre brugere.
         display_name: Dit fulde navn eller dit sjove navn.
         fields: Din hjemmeside, dine pronominer, din alder, eller hvad du har lyst til.
@@ -143,6 +144,7 @@ da:
         url: Hvor begivenheder sendes til
     labels:
       account:
+        attribution_domains_as_text: Tillad kun bestemte websteder
         discoverable: Fremhæv profil og indlæg i opdagelsesalgoritmer
         fields:
           name: Etiket
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index ddb6621aa..f7e55f1a7 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -3,6 +3,7 @@ de:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Dadurch können falsche Zuschreibungen unterbunden werden.
         discoverable: Deine öffentlichen Beiträge und dein Profil können in verschiedenen Bereichen auf Mastodon angezeigt oder empfohlen werden und dein Profil kann anderen vorgeschlagen werden.
         display_name: Dein richtiger Name oder dein Fantasiename.
         fields: Deine Website, Pronomen, dein Alter – alles, was du möchtest.
@@ -143,6 +144,7 @@ de:
         url: Wohin Ereignisse gesendet werden
     labels:
       account:
+        attribution_domains_as_text: Nur ausgewählte Websites zulassen
         discoverable: Profil und Beiträge in Suchalgorithmen berücksichtigen
         fields:
           name: Beschriftung
diff --git a/config/locales/simple_form.en-GB.yml b/config/locales/simple_form.en-GB.yml
index 18776d67d..b802fd532 100644
--- a/config/locales/simple_form.en-GB.yml
+++ b/config/locales/simple_form.en-GB.yml
@@ -3,6 +3,7 @@ en-GB:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Protects from false attributions.
         discoverable: Your public posts and profile may be featured or recommended in various areas of Mastodon and your profile may be suggested to other users.
         display_name: Your full name or your fun name.
         fields: Your homepage, pronouns, age, anything you want.
@@ -130,6 +131,7 @@ en-GB:
         name: You can only change the casing of the letters, for example, to make it more readable
       user:
         chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
+        role: The role controls which permissions the user has.
       user_role:
         color: Color to be used for the role throughout the UI, as RGB in hex format
         highlighted: This makes the role publicly visible
@@ -142,6 +144,7 @@ en-GB:
         url: Where events will be sent to
     labels:
       account:
+        attribution_domains_as_text: Only allow specific websites
         discoverable: Feature profile and posts in discovery algorithms
         fields:
           name: Label
diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml
index d3f062863..a2b29566f 100644
--- a/config/locales/simple_form.fi.yml
+++ b/config/locales/simple_form.fi.yml
@@ -143,6 +143,7 @@ fi:
         url: Mihin tapahtumat lähetetään
     labels:
       account:
+        attribution_domains_as_text: Salli vain tietyt verkkosivustot
         discoverable: Pidä profiiliasi ja julkaisujasi esillä löytämisalgoritmeissa
         fields:
           name: Nimike
diff --git a/config/locales/simple_form.fo.yml b/config/locales/simple_form.fo.yml
index 35e42f6c7..afcd3b39a 100644
--- a/config/locales/simple_form.fo.yml
+++ b/config/locales/simple_form.fo.yml
@@ -3,6 +3,7 @@ fo:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Verjir fyri følskum ískoytum.
         discoverable: Tínir almennu postar og tín vangi kunnu vera drigin fram og viðmæld ymsa staðni í Mastodon og vangin hjá tær kann vera viðmæltur øðrum brúkarum.
         display_name: Títt fulla navn og títt stuttliga navn.
         fields: Heimasíðan hjá tær, fornøvn, aldur ella hvat tú vil.
@@ -143,6 +144,7 @@ fo:
         url: Hvar hendingar verða sendar til
     labels:
       account:
+        attribution_domains_as_text: Loyv einans ávísum heimasíðum
         discoverable: Framheva vanga og postar í uppdagingar-algoritmum
         fields:
           name: Spjaldur
diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml
index abb30fa48..cddeae5ce 100644
--- a/config/locales/simple_form.gl.yml
+++ b/config/locales/simple_form.gl.yml
@@ -3,6 +3,7 @@ gl:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Protéxete de falsas atribucións.
         discoverable: As túas publicacións públicas e perfil poden mostrarse ou recomendarse en varias zonas de Mastodon e o teu perfil ser suxerido a outras usuarias.
         display_name: O teu nome completo ou un nome divertido.
         fields: Páxina web, pronome, idade, o que ti queiras.
@@ -143,6 +144,7 @@ gl:
         url: A onde se enviarán os eventos
     labels:
       account:
+        attribution_domains_as_text: Permitir só os sitios web indicados
         discoverable: Perfil destacado e publicacións nos algoritmos de descubrimento
         fields:
           name: Etiqueta
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index 26edab3b2..1feebb0d6 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -3,6 +3,7 @@ he:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: הגנה מייחוסים שקריים.
         discoverable: הפוסטים והפרופיל שלך עשויים להיות מוצגים או מומלצים באזורים שונים באתר וייתכן שהפרופיל שלך יוצע למשתמשים אחרים.
         display_name: שמך המלא או שם הכיף שלך.
         fields: עמוד הבית שלך, לשון הפנייה, גיל, וכל מידע אחר לפי העדפתך האישית.
@@ -143,6 +144,7 @@ he:
         url: היעד שאליו יישלחו אירועים
     labels:
       account:
+        attribution_domains_as_text: רק אתרים מסויימים יאושרו
         discoverable: הצג משתמש ופוסטים בעמוד התגליות
         fields:
           name: תווית
diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml
index 545fd4a8e..383bdd076 100644
--- a/config/locales/simple_form.hu.yml
+++ b/config/locales/simple_form.hu.yml
@@ -3,6 +3,7 @@ hu:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Megvéd a hamis forrásmegjelölésektől.
         discoverable: A nyilvános bejegyzéseid és a profilod kiemelhető vagy ajánlható a Mastodon különböző területein, a profilod más felhasználóknak is javasolható.
         display_name: Teljes neved vagy vicces neved.
         fields: Weboldalad, megszólításaid, korod, bármi, amit szeretnél.
@@ -143,6 +144,7 @@ hu:
         url: Ahová az eseményket küldjük
     labels:
       account:
+        attribution_domains_as_text: Csak meghatározott weboldalak engedélyezése
         discoverable: Profil és bejegyzések szerepeltetése a felfedezési algoritmusokban
         fields:
           name: Címke
diff --git a/config/locales/simple_form.ia.yml b/config/locales/simple_form.ia.yml
index 85fa74f1e..dc5aad57a 100644
--- a/config/locales/simple_form.ia.yml
+++ b/config/locales/simple_form.ia.yml
@@ -142,6 +142,7 @@ ia:
         url: Ubi le eventos essera inviate
     labels:
       account:
+        attribution_domains_as_text: Solmente permitter sitos web specific
         discoverable: Evidentiar le profilo e messages in le algorithmos de discoperta
         fields:
           name: Etiquetta
diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml
index d615e391a..6f3a4fe8a 100644
--- a/config/locales/simple_form.is.yml
+++ b/config/locales/simple_form.is.yml
@@ -3,6 +3,7 @@ is:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Ver fyrir fölskum tilvísunum í höfunda.
         discoverable: Opinberar færslur og notandasnið þitt geta birst eða verið mælt með á hinum ýmsu svæðum í Mastodon auk þess sem hægt er að mæla með þér við aðra notendur.
         display_name: Fullt nafn þitt eða eitthvað til gamans.
         fields: Heimasíðan þín, fornöfn, aldur eða eitthvað sem þú vilt koma á framfæri.
@@ -143,6 +144,7 @@ is:
         url: Hvert atburðir verða sendir
     labels:
       account:
+        attribution_domains_as_text: Einungis leyfa tiltekin vefsvæði
         discoverable: Hafa notandasnið og færslur með í reikniritum leitar
         fields:
           name: Skýring
diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml
index 1068b2f92..7ed4c0d00 100644
--- a/config/locales/simple_form.it.yml
+++ b/config/locales/simple_form.it.yml
@@ -3,6 +3,7 @@ it:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Protegge da false attribuzioni.
         discoverable: I tuoi post pubblici e il tuo profilo potrebbero essere presenti o consigliati in varie aree di Mastodon e il tuo profilo potrebbe essere suggerito ad altri utenti.
         display_name: Il tuo nome completo o il tuo soprannome.
         fields: La tua homepage, i pronomi, l'età, tutto quello che vuoi.
@@ -143,6 +144,7 @@ it:
         url: Dove gli eventi saranno inviati
     labels:
       account:
+        attribution_domains_as_text: Consenti solo siti web specifici
         discoverable: Include il profilo e i post negli algoritmi di scoperta
         fields:
           name: Etichetta
diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml
index 99cb269e3..41f55f183 100644
--- a/config/locales/simple_form.lt.yml
+++ b/config/locales/simple_form.lt.yml
@@ -3,6 +3,7 @@ lt:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Apsaugo nuo klaidingų atributų.
         discoverable: Tavo vieši įrašai ir profilis gali būti rodomi arba rekomenduojami įvairiose Mastodon vietose, o profilis gali būti siūlomas kitiems naudotojams.
         display_name: Tavo pilnas vardas arba smagus vardas.
         fields: Tavo pagrindinis puslapis, įvardžiai, amžius, bet kas, ko tik nori.
@@ -104,6 +105,7 @@ lt:
         role: Vaidmuo valdo, kokius leidimus naudotojas turi.
     labels:
       account:
+        attribution_domains_as_text: Leisti tik konkrečias svetaines
         discoverable: Rekomenduoti profilį ir įrašus į atradimo algoritmus
         indexable: Įtraukti viešus įrašus į paieškos rezultatus
         show_collections: Rodyti sekimus ir sekėjus profilyje
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
index 066f6c2ac..7f8aaa01d 100644
--- a/config/locales/simple_form.nl.yml
+++ b/config/locales/simple_form.nl.yml
@@ -3,6 +3,7 @@ nl:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Beschermt tegen onjuiste attributies.
         discoverable: Jouw openbare berichten kunnen worden uitgelicht op verschillende plekken binnen Mastodon en jouw account kan worden aanbevolen aan andere gebruikers.
         display_name: Jouw volledige naam of een leuke bijnaam.
         fields: Jouw website, persoonlijke voornaamwoorden, leeftijd, alles wat je maar kwijt wilt.
@@ -143,6 +144,7 @@ nl:
         url: Waar gebeurtenissen naartoe worden verzonden
     labels:
       account:
+        attribution_domains_as_text: Alleen bepaalde websites toestaan
         discoverable: Jouw account en berichten laten uitlichten door Mastodon
         fields:
           name: Label
diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml
index 500a41c8c..ddd5ed899 100644
--- a/config/locales/simple_form.nn.yml
+++ b/config/locales/simple_form.nn.yml
@@ -3,6 +3,7 @@ nn:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Vernar mot falske krediteringar.
         discoverable: Dei offentlege innlegga dine og profilen din kan dukka opp i tilrådingar på ulike stader på Mastodon, og profilen din kan bli føreslegen for andre folk.
         display_name: Ditt fulle namn eller ditt tøysenamn.
         fields: Heimesida di, pronomen, alder, eller kva du måtte ynskje.
@@ -130,6 +131,7 @@ nn:
         name: Du kan berre endra bruken av store/små bokstavar, t. d. for å gjera det meir leseleg
       user:
         chosen_languages: Når merka vil berre tuta på dei valde språka synast på offentlege tidsliner
+        role: Rolla kontrollerer kva løyve brukaren har.
       user_role:
         color: Fargen som skal nyttast for denne rolla i heile brukargrensesnittet, som RGB i hex-format
         highlighted: Dette gjer rolla synleg offentleg
@@ -142,6 +144,7 @@ nn:
         url: Kvar hendingar skal sendast
     labels:
       account:
+        attribution_domains_as_text: Tillat berre visse nettstader
         discoverable: Ta med profilen og innlegga i oppdagingsalgoritmar
         fields:
           name: Merkelapp
diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml
index b113e0eed..bb404e56c 100644
--- a/config/locales/simple_form.pl.yml
+++ b/config/locales/simple_form.pl.yml
@@ -3,6 +3,7 @@ pl:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Chroni przed fałszywym przypisaniem wpisów.
         discoverable: Twój profil i publiczne wpisy mogą być promowane lub polecane na Mastodonie i twój profil może być sugerowany innym użytkownikom.
         display_name: Twoje imię lub pseudonim.
         fields: Co ci się tylko podoba – twoja strona domowa, zaimki, wiek…
@@ -143,6 +144,7 @@ pl:
         url: Dokąd będą wysłane zdarzenia
     labels:
       account:
+        attribution_domains_as_text: Zezwól tylko na konkretne strony
         discoverable: Udostępniaj profil i wpisy funkcjom odkrywania
         fields:
           name: Nazwa
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index 6b0bad0f0..96bc219e8 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -3,6 +3,7 @@ pt-BR:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Protege de atribuições falsas.
         discoverable: Suas publicações e perfil públicos podem ser destaques ou recomendados em várias áreas de Mastodon, e seu perfil pode ser sugerido a outros usuários.
         display_name: Seu nome completo ou apelido.
         fields: Sua página inicial, pronomes, idade ou qualquer coisa que quiser.
@@ -130,6 +131,7 @@ pt-BR:
         name: Você pode mudar a capitalização das letras, por exemplo, para torná-la mais legível
       user:
         chosen_languages: Apenas as publicações dos idiomas selecionados serão exibidas nas linhas públicas
+        role: A função controla quais permissões o usuário tem.
       user_role:
         color: Cor a ser usada para o cargo em toda a interface do usuário, como RGB no formato hexadecimal
         highlighted: Isso torna o cargo publicamente visível
@@ -142,6 +144,7 @@ pt-BR:
         url: Aonde os eventos serão enviados
     labels:
       account:
+        attribution_domains_as_text: Permitir apenas sites específicos
         discoverable: Destacar perfil e publicações nos algoritmos de descoberta
         fields:
           name: Rótulo
diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml
index 2fcf23b15..d90b97bf9 100644
--- a/config/locales/simple_form.tr.yml
+++ b/config/locales/simple_form.tr.yml
@@ -3,6 +3,7 @@ tr:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Sahte atıflardan korur.
         discoverable: Herkese açık gönderileriniz ve profiliniz Mastodon'un çeşitli kısımlarında öne çıkarılabilir veya önerilebilir ve profiliniz başka kullanıcılara önerilebilir.
         display_name: Tam adınız veya kullanıcı adınız.
         fields: Ana sayfanız, zamirleriniz, yaşınız, istediğiniz herhangi bir şey.
@@ -143,6 +144,7 @@ tr:
         url: Olayların gönderileceği yer
     labels:
       account:
+        attribution_domains_as_text: Yalnızca belirli websitelerine izin ver
         discoverable: Profil ve gönderileri keşif algoritmalarında kullan
         fields:
           name: Etiket
diff --git a/config/locales/simple_form.uk.yml b/config/locales/simple_form.uk.yml
index e2a1562b5..b584a6cad 100644
--- a/config/locales/simple_form.uk.yml
+++ b/config/locales/simple_form.uk.yml
@@ -3,6 +3,7 @@ uk:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Захищає від фальшивих атрибутів.
         discoverable: Ваші дописи та профіль можуть бути рекомендовані в різних частинах Mastodon і ваш профіль може бути запропонований іншим користувачам.
         display_name: Ваше повне ім'я або ваш псевдонім.
         fields: Ваша домашня сторінка, займенники, вік, все, що вам заманеться.
@@ -143,6 +144,7 @@ uk:
         url: Куди надсилатимуться події
     labels:
       account:
+        attribution_domains_as_text: Дозволити лише на певних вебсайтах
         discoverable: Функції профілю та дописів у алгоритмах виявлення
         fields:
           name: Мітка
diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml
index cb4341b55..419cb99ab 100644
--- a/config/locales/simple_form.zh-CN.yml
+++ b/config/locales/simple_form.zh-CN.yml
@@ -3,6 +3,7 @@ zh-CN:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: 保护作品免受虚假署名。
         discoverable: 您的公开嘟文和个人资料可能会在 Mastodon 的多个位置展示,您的个人资料可能会被推荐给其他用户。
         display_name: 你的全名或昵称。
         fields: 你的主页、人称代词、年龄,以及任何你想要添加的内容。
@@ -143,6 +144,7 @@ zh-CN:
         url: 事件将被发往的目的地
     labels:
       account:
+        attribution_domains_as_text: 仅允许特定网站
         discoverable: 在发现算法中展示你的个人资料和嘟文
         fields:
           name: 标签
diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml
index 8b4c44002..a5bc68363 100644
--- a/config/locales/simple_form.zh-TW.yml
+++ b/config/locales/simple_form.zh-TW.yml
@@ -3,6 +3,7 @@ zh-TW:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: 偽造署名保護。
         discoverable: 公開嘟文及個人檔案可能於各 Mastodon 功能中被推薦,並且您的個人檔案可能被推薦至其他使用者。
         display_name: 完整名稱或暱稱。
         fields: 烘培雞、自我認同代稱、年齡,及任何您想分享的。
@@ -143,6 +144,7 @@ zh-TW:
         url: 事件會被傳送至何處
     labels:
       account:
+        attribution_domains_as_text: 僅允許特定網站
         discoverable: 於探索演算法中推薦個人檔案及嘟文
         fields:
           name: 標籤
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index 5e8a3c912..dcc6ff7fb 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -25,6 +25,7 @@ sq:
   admin:
     account_actions:
       action: Kryeje veprimin
+      already_silenced: Kjo llogari është kufizuar tashmë.
       already_suspended: Kjo llogari është pezulluar tashmë.
       title: Kryeni veprim moderimi te %{acct}
     account_moderation_notes:
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 0cc47ca92..7b45f28c9 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -183,14 +183,17 @@ sv:
         create_custom_emoji: Skapa egen emoji
         create_domain_allow: Skapa tillåten domän
         create_domain_block: Skapa blockerad domän
+        create_email_domain_block: Skapa E-post domän block
         create_ip_block: Skapa IP-regel
         create_unavailable_domain: Skapa otillgänglig domän
         create_user_role: Skapa roll
         demote_user: Degradera användare
         destroy_announcement: Radera kungörelse
+        destroy_canonical_email_block: Ta bort e-post block
         destroy_custom_emoji: Radera egen emoji
         destroy_domain_allow: Ta bort tillåten domän
         destroy_domain_block: Ta bort blockerad domän
+        destroy_email_domain_block: Ta bort E-post domän block
         destroy_instance: Rensa domänen
         destroy_ip_block: Radera IP-regel
         destroy_status: Radera inlägg
@@ -233,6 +236,7 @@ sv:
         assigned_to_self_report_html: "%{name} tilldelade rapporten %{target} till sig själva"
         change_email_user_html: "%{name} bytte e-postadress för användaren %{target}"
         change_role_user_html: "%{name} ändrade roll för %{target}"
+        confirm_user_html: "%{name} bekräftad e-post adress av användare %{target}"
         create_account_warning_html: "%{name} skickade en varning till %{target}"
         create_announcement_html: "%{name} skapade kungörelsen %{target}"
         create_custom_emoji_html: "%{name} laddade upp ny emoji %{target}"
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index d8f5eff6e..785be3caf 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -25,7 +25,7 @@ tr:
   admin:
     account_actions:
       action: Eylemi gerçekleştir
-      already_silenced: Bu hesap zaten askıya alınmış.
+      already_silenced: Bu hesap zaten sınırlanmış.
       already_suspended: Bu hesap zaten askıya alınmış.
       title: "%{acct} üzerinde denetleme eylemi gerçekleştir"
     account_moderation_notes:
@@ -1161,6 +1161,12 @@ tr:
       view_strikes: Hesabınıza yönelik eski eylemleri görüntüleyin
     too_fast: Form çok hızlı gönderildi, tekrar deneyin.
     use_security_key: Güvenlik anahtarını kullan
+  author_attribution:
+    example_title: Örnek metin
+    hint_html: Mastodon'da bağlantılar paylaşıldığında nasıl tanınmak istediğinizi denetleyin.
+    more_from_html: "%{name} kişisinden daha fazlası"
+    s_blog: "%{name} kişisinin Günlüğü"
+    title: Yazar atıfı
   challenge:
     confirm: Devam et
     hint_html: "<strong>İpucu:</strong> Önümüzdeki saat boyunca sana parolanı sormayacağız."
@@ -1949,6 +1955,7 @@ tr:
     instructions_html: Aşağıdaki kodu kopyalayın ve websitenizin HTML'sine yapıştırın. Daha sonra "Profil Düzenle" sekmesini kullanarak profilinizdeki ek sahalardan birine websitenizin adresini ekleyin ve değişiklikleri kaydedin.
     verification: Doğrulama
     verified_links: Doğrulanmış bağlantılarınız
+    website_verification: Website doğrulama
   webauthn_credentials:
     add: Yeni güvenlik anahtarı ekle
     create:
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 600239107..f5cd40bad 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -29,6 +29,7 @@ uk:
   admin:
     account_actions:
       action: Виконати дію
+      already_silenced: Цей обліковий запис вже обмежено.
       already_suspended: Цей обліковий запис вже було призупинено.
       title: Здійснити модераційну дію над %{acct}
     account_moderation_notes:
@@ -1196,6 +1197,12 @@ uk:
       view_strikes: Переглянути попередні попередження вашому обліковому запису
     too_fast: Форму подано занадто швидко, спробуйте ще раз.
     use_security_key: Використовувати ключ безпеки
+  author_attribution:
+    example_title: Зразок тексту
+    hint_html: Контроль авторства поширених посилань на Mastodon.
+    more_from_html: Більше від %{name}
+    s_blog: Блог %{name}
+    title: Атрибути авторства
   challenge:
     confirm: Далі
     hint_html: "<strong>Підказка:</strong> ми не будемо запитувати ваш пароль впродовж наступної години."
@@ -2014,6 +2021,7 @@ uk:
     instructions_html: Скопіюйте та вставте наведений нижче код у HTML-код вашого сайту. Потім додайте адресу свого сайту в одне з додаткових полів вашого профілю на вкладці «Редагувати профіль» і збережіть зміни.
     verification: Підтвердження
     verified_links: Ваші підтверджені посилання
+    website_verification: Підтвердження вебсайтів
   webauthn_credentials:
     add: Додати новий ключ безпеки
     create:
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index b97ab65f0..747dcf373 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -1143,6 +1143,12 @@ zh-CN:
       view_strikes: 查看针对你账号的记录
     too_fast: 表单提交过快,请重试。
     use_security_key: 使用安全密钥
+  author_attribution:
+    example_title: 示例文本
+    hint_html: 控制在 Mastodon 上分享的链接如何显示你的署名。
+    more_from_html: 来自 %{name} 的更多内容
+    s_blog: "%{name} 的博客"
+    title: 作者归属
   challenge:
     confirm: 继续
     hint_html: "<strong>注意:</strong>接下来一小时内我们不会再次要求你输入密码。"
@@ -1916,6 +1922,7 @@ zh-CN:
     instructions_html: 将下面的代码复制并粘贴到你网站的HTML中。然后在“编辑个人资料”选项卡中的附加字段之一添加你网站的地址,并保存更改。
     verification: 验证
     verified_links: 你已验证的链接
+    website_verification: 网站验证
   webauthn_credentials:
     add: 添加新的安全密钥
     create:
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 052773e32..8eab176d7 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -1145,6 +1145,12 @@ zh-TW:
       view_strikes: 檢視針對您帳號過去的警示
     too_fast: 送出表單的速度太快跟不上,請稍後再試。
     use_security_key: 使用安全金鑰
+  author_attribution:
+    example_title: 範例文字
+    hint_html: 控制如何於 Mastodon 上分享連結時註明您的貢獻。
+    more_from_html: 來自 %{name} 之更多內容
+    s_blog: "%{name} 的部落格"
+    title: 作者署名
   challenge:
     confirm: 繼續
     hint_html: "<strong>温馨小提醒:</strong> 我們於接下來一小時內不會再要求您輸入密碼。"
@@ -1918,6 +1924,7 @@ zh-TW:
     instructions_html: 複製及貼上以下程式碼至您個人網站之 HTML 中。接著透過「編輯個人檔案」將您網站網址加入您個人網站之額外欄位中,並儲存變更。
     verification: 驗證連結
     verified_links: 已驗證連結
+    website_verification: 網站驗證
   webauthn_credentials:
     add: 新增安全金鑰
     create:

From a27f7f4e561c9d2fe21d984059603d2f500c005b Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 11 Sep 2024 15:59:46 +0200
Subject: [PATCH 73/91] Update typescript-eslint monorepo to v8 (major)
 (#31231)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
---
 .eslintrc.js                                  |   2 +-
 app/javascript/mastodon/actions/markers.ts    |   2 +-
 app/javascript/mastodon/actions/streaming.js  |   2 +-
 app/javascript/mastodon/common.js             |   2 +-
 .../mastodon/components/error_boundary.jsx    |   4 +-
 .../features/interaction_modal/index.jsx      |   2 +-
 app/javascript/mastodon/features/ui/index.jsx |   4 +-
 .../mastodon/locales/intl_provider.tsx        |   2 +-
 app/javascript/mastodon/settings.js           |   7 +-
 .../mastodon/store/middlewares/errors.ts      |   2 +-
 .../mastodon/store/middlewares/sounds.ts      |   2 +-
 package.json                                  |   4 +-
 yarn.lock                                     | 141 ++++++++++--------
 13 files changed, 99 insertions(+), 77 deletions(-)

diff --git a/.eslintrc.js b/.eslintrc.js
index d11826282..b6e4253e6 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -316,7 +316,7 @@ module.exports = defineConfig({
       ],
 
       parserOptions: {
-        project: true,
+        projectService: true,
         tsconfigRootDir: __dirname,
       },
 
diff --git a/app/javascript/mastodon/actions/markers.ts b/app/javascript/mastodon/actions/markers.ts
index 521859f6c..6254e3f08 100644
--- a/app/javascript/mastodon/actions/markers.ts
+++ b/app/javascript/mastodon/actions/markers.ts
@@ -65,7 +65,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
       client.setRequestHeader('Content-Type', 'application/json');
       client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
       client.send(JSON.stringify(params));
-    } catch (e) {
+    } catch {
       // Do not make the BeforeUnload handler error out
     }
   },
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index bfdd894b8..03013110c 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -151,7 +151,7 @@ async function refreshHomeTimelineAndNotification(dispatch, getState) {
     // TODO: polling for merged notifications
     try {
       await dispatch(pollRecentGroupNotifications());
-    } catch (error) {
+    } catch {
       // TODO
     }
   } else {
diff --git a/app/javascript/mastodon/common.js b/app/javascript/mastodon/common.js
index 28857de53..c61e02250 100644
--- a/app/javascript/mastodon/common.js
+++ b/app/javascript/mastodon/common.js
@@ -5,7 +5,7 @@ export function start() {
 
   try {
     Rails.start();
-  } catch (e) {
+  } catch {
     // If called twice
   }
 }
diff --git a/app/javascript/mastodon/components/error_boundary.jsx b/app/javascript/mastodon/components/error_boundary.jsx
index 7fea08e85..392a3ad61 100644
--- a/app/javascript/mastodon/components/error_boundary.jsx
+++ b/app/javascript/mastodon/components/error_boundary.jsx
@@ -60,8 +60,8 @@ export default class ErrorBoundary extends PureComponent {
     try {
       textarea.select();
       document.execCommand('copy');
-    } catch (e) {
-
+    } catch {
+      // do nothing
     } finally {
       document.body.removeChild(textarea);
     }
diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx
index 07f1e6fe5..723e27ae1 100644
--- a/app/javascript/mastodon/features/interaction_modal/index.jsx
+++ b/app/javascript/mastodon/features/interaction_modal/index.jsx
@@ -131,7 +131,7 @@ class LoginForm extends React.PureComponent {
     try {
       new URL(url);
       return true;
-    } catch(_) {
+    } catch {
       return false;
     }
   };
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 657c9e9e5..a6e364047 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -319,8 +319,8 @@ class UI extends PureComponent {
 
     try {
       e.dataTransfer.dropEffect = 'copy';
-    } catch (err) {
-
+    } catch {
+      // do nothing
     }
 
     return false;
diff --git a/app/javascript/mastodon/locales/intl_provider.tsx b/app/javascript/mastodon/locales/intl_provider.tsx
index 68d4fcbd9..94372f95b 100644
--- a/app/javascript/mastodon/locales/intl_provider.tsx
+++ b/app/javascript/mastodon/locales/intl_provider.tsx
@@ -17,7 +17,7 @@ function onProviderError(error: unknown) {
     error &&
     typeof error === 'object' &&
     error instanceof Error &&
-    error.message.match('MISSING_DATA')
+    /MISSING_DATA/.exec(error.message)
   ) {
     console.warn(error.message);
   }
diff --git a/app/javascript/mastodon/settings.js b/app/javascript/mastodon/settings.js
index f31aee0af..f4883dc40 100644
--- a/app/javascript/mastodon/settings.js
+++ b/app/javascript/mastodon/settings.js
@@ -14,7 +14,7 @@ export default class Settings {
       const encodedData = JSON.stringify(data);
       localStorage.setItem(key, encodedData);
       return data;
-    } catch (e) {
+    } catch {
       return null;
     }
   }
@@ -24,7 +24,7 @@ export default class Settings {
     try {
       const rawData = localStorage.getItem(key);
       return JSON.parse(rawData);
-    } catch (e) {
+    } catch {
       return null;
     }
   }
@@ -35,7 +35,8 @@ export default class Settings {
       const key = this.generateKey(id);
       try {
         localStorage.removeItem(key);
-      } catch (e) {
+      } catch {
+        // ignore if the key is not found
       }
     }
     return data;
diff --git a/app/javascript/mastodon/store/middlewares/errors.ts b/app/javascript/mastodon/store/middlewares/errors.ts
index e77cec34e..3ad3844d5 100644
--- a/app/javascript/mastodon/store/middlewares/errors.ts
+++ b/app/javascript/mastodon/store/middlewares/errors.ts
@@ -30,7 +30,7 @@ function isActionWithmaybeAlertParams(
   return isAction(action);
 }
 
-// eslint-disable-next-line @typescript-eslint/ban-types -- we need to use `{}` here to ensure the dispatch types can be merged
+// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- we need to use `{}` here to ensure the dispatch types can be merged
 export const errorsMiddleware: Middleware<{}, RootState> =
   ({ dispatch }) =>
   (next) =>
diff --git a/app/javascript/mastodon/store/middlewares/sounds.ts b/app/javascript/mastodon/store/middlewares/sounds.ts
index 91407b1ec..bb3a4aa47 100644
--- a/app/javascript/mastodon/store/middlewares/sounds.ts
+++ b/app/javascript/mastodon/store/middlewares/sounds.ts
@@ -51,7 +51,7 @@ const play = (audio: HTMLAudioElement) => {
 };
 
 export const soundsMiddleware = (): Middleware<
-  // eslint-disable-next-line @typescript-eslint/ban-types -- we need to use `{}` here to ensure the dispatch types can be merged
+  // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- we need to use `{}` here to ensure the dispatch types can be merged
   {},
   RootState
 > => {
diff --git a/package.json b/package.json
index ddf7ee75a..ea0c0246c 100644
--- a/package.json
+++ b/package.json
@@ -168,8 +168,8 @@
     "@types/requestidlecallback": "^0.3.5",
     "@types/webpack": "^4.41.33",
     "@types/webpack-env": "^1.18.4",
-    "@typescript-eslint/eslint-plugin": "^7.0.0",
-    "@typescript-eslint/parser": "^7.0.0",
+    "@typescript-eslint/eslint-plugin": "^8.0.0",
+    "@typescript-eslint/parser": "^8.0.0",
     "babel-jest": "^29.5.0",
     "eslint": "^8.41.0",
     "eslint-define-config": "^2.0.0",
diff --git a/yarn.lock b/yarn.lock
index 90e764f49..f498a5560 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -82,7 +82,19 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.4, @babel/generator@npm:^7.7.2":
+"@babel/generator@npm:^7.25.0, @babel/generator@npm:^7.7.2":
+  version: 7.25.0
+  resolution: "@babel/generator@npm:7.25.0"
+  dependencies:
+    "@babel/types": "npm:^7.25.0"
+    "@jridgewell/gen-mapping": "npm:^0.3.5"
+    "@jridgewell/trace-mapping": "npm:^0.3.25"
+    jsesc: "npm:^2.5.1"
+  checksum: 10c0/d0e2dfcdc8bdbb5dded34b705ceebf2e0bc1b06795a1530e64fb6a3ccf313c189db7f60c1616effae48114e1a25adc75855bc4496f3779a396b3377bae718ce7
+  languageName: node
+  linkType: hard
+
+"@babel/generator@npm:^7.25.4":
   version: 7.25.4
   resolution: "@babel/generator@npm:7.25.4"
   dependencies:
@@ -1521,7 +1533,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
+"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
+  version: 7.25.2
+  resolution: "@babel/types@npm:7.25.2"
+  dependencies:
+    "@babel/helper-string-parser": "npm:^7.24.8"
+    "@babel/helper-validator-identifier": "npm:^7.24.7"
+    to-fast-properties: "npm:^2.0.0"
+  checksum: 10c0/e489435856be239f8cc1120c90a197e4c2865385121908e5edb7223cfdff3768cba18f489adfe0c26955d9e7bbb1fb10625bc2517505908ceb0af848989bd864
+  languageName: node
+  linkType: hard
+
+"@babel/types@npm:^7.25.4":
   version: 7.25.4
   resolution: "@babel/types@npm:7.25.4"
   dependencies:
@@ -2791,8 +2814,8 @@ __metadata:
     "@types/requestidlecallback": "npm:^0.3.5"
     "@types/webpack": "npm:^4.41.33"
     "@types/webpack-env": "npm:^1.18.4"
-    "@typescript-eslint/eslint-plugin": "npm:^7.0.0"
-    "@typescript-eslint/parser": "npm:^7.0.0"
+    "@typescript-eslint/eslint-plugin": "npm:^8.0.0"
+    "@typescript-eslint/parser": "npm:^8.0.0"
     arrow-key-navigation: "npm:^1.2.0"
     async-mutex: "npm:^0.5.0"
     axios: "npm:^1.4.0"
@@ -4114,44 +4137,44 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/eslint-plugin@npm:^7.0.0":
-  version: 7.18.0
-  resolution: "@typescript-eslint/eslint-plugin@npm:7.18.0"
+"@typescript-eslint/eslint-plugin@npm:^8.0.0":
+  version: 8.1.0
+  resolution: "@typescript-eslint/eslint-plugin@npm:8.1.0"
   dependencies:
     "@eslint-community/regexpp": "npm:^4.10.0"
-    "@typescript-eslint/scope-manager": "npm:7.18.0"
-    "@typescript-eslint/type-utils": "npm:7.18.0"
-    "@typescript-eslint/utils": "npm:7.18.0"
-    "@typescript-eslint/visitor-keys": "npm:7.18.0"
+    "@typescript-eslint/scope-manager": "npm:8.1.0"
+    "@typescript-eslint/type-utils": "npm:8.1.0"
+    "@typescript-eslint/utils": "npm:8.1.0"
+    "@typescript-eslint/visitor-keys": "npm:8.1.0"
     graphemer: "npm:^1.4.0"
     ignore: "npm:^5.3.1"
     natural-compare: "npm:^1.4.0"
     ts-api-utils: "npm:^1.3.0"
   peerDependencies:
-    "@typescript-eslint/parser": ^7.0.0
-    eslint: ^8.56.0
+    "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0
+    eslint: ^8.57.0 || ^9.0.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/2b37948fa1b0dab77138909dabef242a4d49ab93e4019d4ef930626f0a7d96b03e696cd027fa0087881c20e73be7be77c942606b4a76fa599e6b37f6985304c3
+  checksum: 10c0/7bbeae588f859b59c34d6a76cac06ef0fa605921b40c5d3b65b94829984280ea84c4dd3f5cb9ce2eb326f5563e9abb4c90ebff05c47f83f4def296c2ea1fa86c
   languageName: node
   linkType: hard
 
-"@typescript-eslint/parser@npm:^7.0.0":
-  version: 7.18.0
-  resolution: "@typescript-eslint/parser@npm:7.18.0"
+"@typescript-eslint/parser@npm:^8.0.0":
+  version: 8.1.0
+  resolution: "@typescript-eslint/parser@npm:8.1.0"
   dependencies:
-    "@typescript-eslint/scope-manager": "npm:7.18.0"
-    "@typescript-eslint/types": "npm:7.18.0"
-    "@typescript-eslint/typescript-estree": "npm:7.18.0"
-    "@typescript-eslint/visitor-keys": "npm:7.18.0"
+    "@typescript-eslint/scope-manager": "npm:8.1.0"
+    "@typescript-eslint/types": "npm:8.1.0"
+    "@typescript-eslint/typescript-estree": "npm:8.1.0"
+    "@typescript-eslint/visitor-keys": "npm:8.1.0"
     debug: "npm:^4.3.4"
   peerDependencies:
-    eslint: ^8.56.0
+    eslint: ^8.57.0 || ^9.0.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/370e73fca4278091bc1b657f85e7d74cd52b24257ea20c927a8e17546107ce04fbf313fec99aed0cc2a145ddbae1d3b12e9cc2c1320117636dc1281bcfd08059
+  checksum: 10c0/b94b2d3ab5ca505484d100701fad6a04a5dc8d595029bac1b9f5b8a4a91d80fd605b0f65d230b36a97ab7e5d55eeb0c28af2ab63929a3e4ab8fdefd2a548c36b
   languageName: node
   linkType: hard
 
@@ -4165,30 +4188,28 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/scope-manager@npm:7.18.0":
-  version: 7.18.0
-  resolution: "@typescript-eslint/scope-manager@npm:7.18.0"
+"@typescript-eslint/scope-manager@npm:8.1.0":
+  version: 8.1.0
+  resolution: "@typescript-eslint/scope-manager@npm:8.1.0"
   dependencies:
-    "@typescript-eslint/types": "npm:7.18.0"
-    "@typescript-eslint/visitor-keys": "npm:7.18.0"
-  checksum: 10c0/038cd58c2271de146b3a594afe2c99290034033326d57ff1f902976022c8b0138ffd3cb893ae439ae41003b5e4bcc00cabf6b244ce40e8668f9412cc96d97b8e
+    "@typescript-eslint/types": "npm:8.1.0"
+    "@typescript-eslint/visitor-keys": "npm:8.1.0"
+  checksum: 10c0/2bcf8cd176a1819bddcae16c572e7da8fba821b995a91cd53d64d8d6b85a17f5a895522f281ba57e34929574bddd4d6684ee3e545ec4e8096be4c3198e253a9a
   languageName: node
   linkType: hard
 
-"@typescript-eslint/type-utils@npm:7.18.0":
-  version: 7.18.0
-  resolution: "@typescript-eslint/type-utils@npm:7.18.0"
+"@typescript-eslint/type-utils@npm:8.1.0":
+  version: 8.1.0
+  resolution: "@typescript-eslint/type-utils@npm:8.1.0"
   dependencies:
-    "@typescript-eslint/typescript-estree": "npm:7.18.0"
-    "@typescript-eslint/utils": "npm:7.18.0"
+    "@typescript-eslint/typescript-estree": "npm:8.1.0"
+    "@typescript-eslint/utils": "npm:8.1.0"
     debug: "npm:^4.3.4"
     ts-api-utils: "npm:^1.3.0"
-  peerDependencies:
-    eslint: ^8.56.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/ad92a38007be620f3f7036f10e234abdc2fdc518787b5a7227e55fd12896dacf56e8b34578723fbf9bea8128df2510ba8eb6739439a3879eda9519476d5783fd
+  checksum: 10c0/62753941c4136e8d2daa72fe0410dea48e5317a6f12ece6382ca85e29912bd1b3f739b61d1060fc0a1f8c488dfc905beab4c8b8497951a21c3138a659c7271ec
   languageName: node
   linkType: hard
 
@@ -4199,10 +4220,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/types@npm:7.18.0":
-  version: 7.18.0
-  resolution: "@typescript-eslint/types@npm:7.18.0"
-  checksum: 10c0/eb7371ac55ca77db8e59ba0310b41a74523f17e06f485a0ef819491bc3dd8909bb930120ff7d30aaf54e888167e0005aa1337011f3663dc90fb19203ce478054
+"@typescript-eslint/types@npm:8.1.0":
+  version: 8.1.0
+  resolution: "@typescript-eslint/types@npm:8.1.0"
+  checksum: 10c0/ceade44455f45974e68956016c4d1c6626580732f7f9675e14ffa63db80b551752b0df596b20473dae9f0dc6ed966e17417dc2cf36e1a82b6ab0edc97c5eaa50
   languageName: node
   linkType: hard
 
@@ -4225,12 +4246,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/typescript-estree@npm:7.18.0":
-  version: 7.18.0
-  resolution: "@typescript-eslint/typescript-estree@npm:7.18.0"
+"@typescript-eslint/typescript-estree@npm:8.1.0":
+  version: 8.1.0
+  resolution: "@typescript-eslint/typescript-estree@npm:8.1.0"
   dependencies:
-    "@typescript-eslint/types": "npm:7.18.0"
-    "@typescript-eslint/visitor-keys": "npm:7.18.0"
+    "@typescript-eslint/types": "npm:8.1.0"
+    "@typescript-eslint/visitor-keys": "npm:8.1.0"
     debug: "npm:^4.3.4"
     globby: "npm:^11.1.0"
     is-glob: "npm:^4.0.3"
@@ -4240,21 +4261,21 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/0c7f109a2e460ec8a1524339479cf78ff17814d23c83aa5112c77fb345e87b3642616291908dcddea1e671da63686403dfb712e4a4435104f92abdfddf9aba81
+  checksum: 10c0/a7bc8275df1c79c4cb14ef086c56674316dd4907efec53eddca35d0b5220428b69c82178ce2d95138da2e398269c8bd0764cae8020a36417e411e35c3c47bc4b
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:7.18.0":
-  version: 7.18.0
-  resolution: "@typescript-eslint/utils@npm:7.18.0"
+"@typescript-eslint/utils@npm:8.1.0":
+  version: 8.1.0
+  resolution: "@typescript-eslint/utils@npm:8.1.0"
   dependencies:
     "@eslint-community/eslint-utils": "npm:^4.4.0"
-    "@typescript-eslint/scope-manager": "npm:7.18.0"
-    "@typescript-eslint/types": "npm:7.18.0"
-    "@typescript-eslint/typescript-estree": "npm:7.18.0"
+    "@typescript-eslint/scope-manager": "npm:8.1.0"
+    "@typescript-eslint/types": "npm:8.1.0"
+    "@typescript-eslint/typescript-estree": "npm:8.1.0"
   peerDependencies:
-    eslint: ^8.56.0
-  checksum: 10c0/a25a6d50eb45c514469a01ff01f215115a4725fb18401055a847ddf20d1b681409c4027f349033a95c4ff7138d28c3b0a70253dfe8262eb732df4b87c547bd1e
+    eslint: ^8.57.0 || ^9.0.0
+  checksum: 10c0/c95503a6bdcd98b1ff04d1adbf46377b2036b1c510d90a4a056401f996f775f06c3108c95fb81cd6babc9c97b73b91b8e848f0337bc508de8a49c993582f0e75
   languageName: node
   linkType: hard
 
@@ -4285,13 +4306,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/visitor-keys@npm:7.18.0":
-  version: 7.18.0
-  resolution: "@typescript-eslint/visitor-keys@npm:7.18.0"
+"@typescript-eslint/visitor-keys@npm:8.1.0":
+  version: 8.1.0
+  resolution: "@typescript-eslint/visitor-keys@npm:8.1.0"
   dependencies:
-    "@typescript-eslint/types": "npm:7.18.0"
+    "@typescript-eslint/types": "npm:8.1.0"
     eslint-visitor-keys: "npm:^3.4.3"
-  checksum: 10c0/538b645f8ff1d9debf264865c69a317074eaff0255e63d7407046176b0f6a6beba34a6c51d511f12444bae12a98c69891eb6f403c9f54c6c2e2849d1c1cb73c0
+  checksum: 10c0/b7544dbb0eec1ddbfcd95c04b51b9a739c2e768c16d1c88508f976a2b0d1bc02fefb7491930e06e48073a5c07c6f488cd8403bba3a8b918888b93a88d5ac3869
   languageName: node
   linkType: hard
 

From 7d53ca56d200dc0c3c8c6491d9e25aa8965e8d52 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 12 Sep 2024 09:54:53 +0200
Subject: [PATCH 74/91] New Crowdin Translations (automated) (#31878)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/cy.json    | 5 ++++-
 app/javascript/mastodon/locales/es-AR.json | 1 +
 config/locales/activerecord.cy.yml         | 6 ++++++
 config/locales/activerecord.es-AR.yml      | 6 ++++++
 config/locales/activerecord.sq.yml         | 6 ++++++
 config/locales/activerecord.sv.yml         | 4 ++++
 config/locales/cy.yml                      | 8 ++++++++
 config/locales/es-AR.yml                   | 7 +++++++
 config/locales/simple_form.cy.yml          | 2 ++
 config/locales/simple_form.es-AR.yml       | 2 ++
 config/locales/simple_form.sq.yml          | 2 ++
 config/locales/sq.yml                      | 7 +++++++
 config/locales/sv.yml                      | 9 +++++++++
 13 files changed, 64 insertions(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index d34b7a970..7f058d891 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -97,7 +97,7 @@
   "block_modal.title": "Blocio defnyddiwr?",
   "block_modal.you_wont_see_mentions": "Fyddwch chi ddim yn gweld postiadau sy'n sôn amdanyn nhw.",
   "boost_modal.combo": "Mae modd pwyso {combo} er mwyn hepgor hyn tro nesa",
-  "boost_modal.reblog": "Hybu postiad",
+  "boost_modal.reblog": "Hybu postiad?",
   "boost_modal.undo_reblog": "Dad-hybu postiad?",
   "bundle_column_error.copy_stacktrace": "Copïo'r adroddiad gwall",
   "bundle_column_error.error.body": "Nid oedd modd cynhyrchu'r dudalen honno. Gall fod oherwydd gwall yn ein cod neu fater cydnawsedd porwr.",
@@ -457,6 +457,7 @@
   "lists.subheading": "Eich rhestrau",
   "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
   "loading_indicator.label": "Yn llwytho…",
+  "media_gallery.hide": "Cuddio",
   "moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau",
   "mute_modal.hide_options": "Cuddio'r dewis",
@@ -779,6 +780,7 @@
   "status.bookmark": "Llyfrnodi",
   "status.cancel_reblog_private": "Dadhybu",
   "status.cannot_reblog": "Nid oes modd hybu'r postiad hwn",
+  "status.continued_thread": "Edefyn parhaus",
   "status.copy": "Copïo dolen i'r post",
   "status.delete": "Dileu",
   "status.detailed_status": "Golwg manwl o'r sgwrs",
@@ -812,6 +814,7 @@
   "status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
   "status.redraft": "Dileu ac ailddrafftio",
   "status.remove_bookmark": "Tynnu nod tudalen",
+  "status.replied_in_thread": "Atebodd mewn edefyn",
   "status.replied_to": "Wedi ateb {name}",
   "status.reply": "Ateb",
   "status.replyAll": "Ateb i edefyn",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index a0f91c476..dfba0fe5d 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -457,6 +457,7 @@
   "lists.subheading": "Tus listas",
   "load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}",
   "loading_indicator.label": "Cargando…",
+  "media_gallery.hide": "Ocultar",
   "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.",
   "mute_modal.hide_from_notifications": "Ocultar en las notificaciones",
   "mute_modal.hide_options": "Ocultar opciones",
diff --git a/config/locales/activerecord.cy.yml b/config/locales/activerecord.cy.yml
index 73b54d554..0ad257db5 100644
--- a/config/locales/activerecord.cy.yml
+++ b/config/locales/activerecord.cy.yml
@@ -15,6 +15,12 @@ cy:
       user/invite_request:
         text: Rheswm
     errors:
+      attributes:
+        domain:
+          invalid: "- nid yw'n enw parth dilys"
+      messages:
+        invalid_domain_on_line: Nid yw %{value} yn enw parth dilys
+        too_many_lines: "- dros y terfyn o %{limit} llinell"
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.es-AR.yml b/config/locales/activerecord.es-AR.yml
index 71b7f9732..ba4d148c8 100644
--- a/config/locales/activerecord.es-AR.yml
+++ b/config/locales/activerecord.es-AR.yml
@@ -15,6 +15,12 @@ es-AR:
       user/invite_request:
         text: Motivo
     errors:
+      attributes:
+        domain:
+          invalid: no es un nombre de dominio válido
+      messages:
+        invalid_domain_on_line: "%{value} no es un nombre de dominio válido"
+        too_many_lines: está por encima del límite de %{limit} líneas
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.sq.yml b/config/locales/activerecord.sq.yml
index 9c548bda0..888a17a1c 100644
--- a/config/locales/activerecord.sq.yml
+++ b/config/locales/activerecord.sq.yml
@@ -15,6 +15,12 @@ sq:
       user/invite_request:
         text: Arsye
     errors:
+      attributes:
+        domain:
+          invalid: s’është emër i vlefshëm përkatësie
+      messages:
+        invalid_domain_on_line: "%{value} s’është emër i vlefshëm përkatësie"
+        too_many_lines: është tej kufirit prej %{limit} rreshta
       models:
         account:
           attributes:
diff --git a/config/locales/activerecord.sv.yml b/config/locales/activerecord.sv.yml
index 1679dae46..6ac96d9ea 100644
--- a/config/locales/activerecord.sv.yml
+++ b/config/locales/activerecord.sv.yml
@@ -15,8 +15,12 @@ sv:
       user/invite_request:
         text: Anledning
     errors:
+      attributes:
+        domain:
+          invalid: är inte ett giltigt domännamn
       messages:
         invalid_domain_on_line: "%{value} Är inte ett giltigt domännamn"
+        too_many_lines: överskrider gränsen på %{limit} rader
       models:
         account:
           attributes:
diff --git a/config/locales/cy.yml b/config/locales/cy.yml
index d317efad3..2e425bb49 100644
--- a/config/locales/cy.yml
+++ b/config/locales/cy.yml
@@ -33,6 +33,7 @@ cy:
   admin:
     account_actions:
       action: Cyflawni gweithred
+      already_silenced: Mae'r cyfrif hwn eisoes wedi'i gyfyngu.
       already_suspended: Mae'r cyfrif hwn eisoes wedi'i atal.
       title: Cyflawni gweithred cymedroli ar %{acct}
     account_moderation_notes:
@@ -1232,6 +1233,12 @@ cy:
       view_strikes: Gweld rybuddion y gorffennol yn erbyn eich cyfrif
     too_fast: Cafodd y ffurflen ei chyflwyno'n rhy gyflym, ceisiwch eto.
     use_security_key: Defnyddiwch allwedd diogelwch
+  author_attribution:
+    example_title: Testun enghreifftiol
+    hint_html: Rheolwch sut rydych chi'n cael eich canmol pan fydd dolenni'n cael eu rhannu ar Mastodon.
+    more_from_html: Mwy gan %{name}
+    s_blog: Blog %{name}
+    title: Priodoliad awdur
   challenge:
     confirm: Parhau
     hint_html: "<strong>Awgrym:</strong> Fyddwn ni ddim yn gofyn i chi am eich cyfrinair eto am yr awr nesaf."
@@ -2080,6 +2087,7 @@ cy:
     instructions_html: Copïwch a gludo'r cod isod i HTML eich gwefan. Yna ychwanegwch gyfeiriad eich gwefan i un o'r meysydd ychwanegol ar eich proffil o'r tab "Golygu proffil" a chadw'r newidiadau.
     verification: Dilysu
     verified_links: Eich dolenni wedi'u dilysu
+    website_verification: Gwirio gwefan
   webauthn_credentials:
     add: Ychwanegu allwedd ddiogelwch newydd
     create:
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index f4d88d732..63d2adc47 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -1161,6 +1161,12 @@ es-AR:
       view_strikes: Ver incumplimientos pasados contra tu cuenta
     too_fast: Formulario enviado demasiado rápido, probá de nuevo.
     use_security_key: Usar la llave de seguridad
+  author_attribution:
+    example_title: Texto de ejemplo
+    hint_html: Controlá cómo se te da crédito cuando los enlaces son compartidos en Mastodon.
+    more_from_html: Más de %{name}
+    s_blog: Blog de %{name}
+    title: Atribución del autor
   challenge:
     confirm: Continuar
     hint_html: "<strong>Dato:</strong> No volveremos a preguntarte por la contraseña durante la siguiente hora."
@@ -1949,6 +1955,7 @@ es-AR:
     instructions_html: Copiá y pegá el siguiente código en el HTML de tu sitio web. Luego, agregá la dirección de tu sitio web en uno de los campos extras de tu perfil desde la pestaña "Editar perfil" y guardá los cambios.
     verification: Verificación
     verified_links: Tus enlaces verificados
+    website_verification: Verificación del sitio web
   webauthn_credentials:
     add: Agregar nueva llave de seguridad
     create:
diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml
index 56586ecc9..56d1f873d 100644
--- a/config/locales/simple_form.cy.yml
+++ b/config/locales/simple_form.cy.yml
@@ -3,6 +3,7 @@ cy:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Yn amddiffyn rhag priodoliadau ffug.
         discoverable: Mae'n bosibl y bydd eich postiadau cyhoeddus a'ch proffil yn cael sylw neu'n cael eu hargymell mewn gwahanol feysydd o Mastodon ac efallai y bydd eich proffil yn cael ei awgrymu i ddefnyddwyr eraill.
         display_name: Eich enw llawn neu'ch enw hwyl.
         fields: Eich tudalen cartref, rhagenwau, oed, neu unrhyw beth.
@@ -143,6 +144,7 @@ cy:
         url: I ble bydd digwyddiadau'n cael eu hanfon
     labels:
       account:
+        attribution_domains_as_text: Dim ond yn caniatáu gwefannau penodol
         discoverable: Proffil nodwedd a phostiadau mewn algorithmau darganfod
         fields:
           name: Label
diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml
index 70573c75f..d06d09761 100644
--- a/config/locales/simple_form.es-AR.yml
+++ b/config/locales/simple_form.es-AR.yml
@@ -3,6 +3,7 @@ es-AR:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Protege de atribuciones falsas.
         discoverable: Tu perfil y publicaciones pueden ser destacadas o recomendadas en varias áreas de Mastodon, y tu perfil puede ser sugerido a otros usuarios.
         display_name: Tu nombre completo o tu pseudónimo.
         fields: Tu sitio web, pronombres, edad, o lo que quieras.
@@ -143,6 +144,7 @@ es-AR:
         url: Adónde serán enviados los eventos
     labels:
       account:
+        attribution_domains_as_text: Solo permitir sitios web específicos
         discoverable: Destacar perfil y mensajes en algoritmos de descubrimiento
         fields:
           name: Nombre de campo
diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml
index 3d8655728..169f4a02d 100644
--- a/config/locales/simple_form.sq.yml
+++ b/config/locales/simple_form.sq.yml
@@ -3,6 +3,7 @@ sq:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Mbron nga atribuime të rreme.
         discoverable: Postimet dhe profili juaj publik mund të shfaqen, ose rekomandohen në zona të ndryshme të Mastodon-it dhe profili juaj mund të sugjerohet përdoruesve të tjerë.
         display_name: Emri juaj i plotë, ose emri juaj lojcak.
         fields: Faqja juaj hyrëse, përemra, moshë, ç’të keni qejf.
@@ -143,6 +144,7 @@ sq:
         url: Ku do të dërgohen aktet
     labels:
       account:
+        attribution_domains_as_text: Lejo vetëm sajte specifikë
         discoverable: Profilin dhe postimet bëji objekt të algoritmeve të zbulimit
         fields:
           name: Etiketë
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index dcc6ff7fb..241dc08b2 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -1153,6 +1153,12 @@ sq:
       view_strikes: Shihni paralajmërime të dikurshme kundër llogarisë tuaj
     too_fast: Formulari u parashtrua shumë shpejt, riprovoni.
     use_security_key: Përdor kyç sigurie
+  author_attribution:
+    example_title: Tekst shembull
+    hint_html: Kontrolloni se si vlerësoheni, kur ndahen lidhje me të tjerë në Mastodon.
+    more_from_html: Më tepër nga %{name}
+    s_blog: Blogu i %{name}
+    title: Atribuim autorësh
   challenge:
     confirm: Vazhdo
     hint_html: "<strong>Ndihmëz:</strong> S’do t’ju pyesim për fjalëkalimin tuaj sërish, për një orë."
@@ -1941,6 +1947,7 @@ sq:
     instructions_html: Kopjoni dhe ngjitni në HTML-në e sajtit tuaj kodin më poshtë. Mandej shtoni adresën e sajtit tuaj te një nga fushat shtesë në profilin tuaj, që nga skeda “Përpunoni profil” dhe ruani ndryshimet.
     verification: Verifikim
     verified_links: Lidhjet tuaja të verifikuara
+    website_verification: Verifikim sajti
   webauthn_credentials:
     add: Shtoni kyç të ri sigurie
     create:
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 7b45f28c9..c8ddc346a 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -180,6 +180,7 @@ sv:
         confirm_user: Bekräfta användare
         create_account_warning: Skapa varning
         create_announcement: Skapa kungörelse
+        create_canonical_email_block: Skapa E-post block
         create_custom_emoji: Skapa egen emoji
         create_domain_allow: Skapa tillåten domän
         create_domain_block: Skapa blockerad domän
@@ -239,17 +240,21 @@ sv:
         confirm_user_html: "%{name} bekräftad e-post adress av användare %{target}"
         create_account_warning_html: "%{name} skickade en varning till %{target}"
         create_announcement_html: "%{name} skapade kungörelsen %{target}"
+        create_canonical_email_block_html: "%{name} blockade e-posten med %{target}"
         create_custom_emoji_html: "%{name} laddade upp ny emoji %{target}"
         create_domain_allow_html: "%{name} vitlistade domän %{target}"
         create_domain_block_html: "%{name} blockerade domänen %{target}"
+        create_email_domain_block_html: "%{name} blockerade e-post domänet%{target}"
         create_ip_block_html: "%{name} skapade regel för IP %{target}"
         create_unavailable_domain_html: "%{name} stoppade leverans till domänen %{target}"
         create_user_role_html: "%{name} skapade rollen %{target}"
         demote_user_html: "%{name} nedgraderade användare %{target}"
         destroy_announcement_html: "%{name} raderade kungörelsen %{target}"
+        destroy_canonical_email_block_html: "%{name} avblockerade e-post med hash%{target}"
         destroy_custom_emoji_html: "%{name} raderade emoji %{target}"
         destroy_domain_allow_html: "%{name} raderade domän %{target} från vitlistan"
         destroy_domain_block_html: "%{name} avblockerade domänen %{target}"
+        destroy_email_domain_block_html: "%{name} avblockerade e-post domänet %{target}"
         destroy_instance_html: "%{name} rensade domän %{target}"
         destroy_ip_block_html: "%{name} tog bort regel för IP %{target}"
         destroy_status_html: "%{name} tog bort inlägget av %{target}"
@@ -870,7 +875,9 @@ sv:
         message_html: "<strong>Din objektlagring är felkonfigurerad. Sekretessen för dina användare är i riskzonen.</strong>"
     tags:
       moderation:
+        reviewed: Granskat
         title: Status
+        trendable:
       name: Namn
       reset: Återställ
       review: Granskningsstatus
@@ -1112,6 +1119,8 @@ sv:
       view_strikes: Visa tidigare prickar på ditt konto
     too_fast: Formuläret har skickats för snabbt, försök igen.
     use_security_key: Använd säkerhetsnyckel
+  author_attribution:
+    example_title: Exempeltext
   challenge:
     confirm: Fortsätt
     hint_html: "<strong>Tips:</strong> Vi frågar dig inte efter ditt lösenord igen under nästkommande timme."

From f2a92c2d22345568ca7f47ee1d1d70de53eb547d Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 12 Sep 2024 10:16:07 +0200
Subject: [PATCH 75/91] Fix notifications re-rendering spuriously in web UI
 (#31879)

---
 .../components/notification_mention.tsx       |  4 ++-
 .../features/notifications_v2/index.tsx       |  3 ++-
 .../features/ui/containers/modal_container.js |  4 ++-
 .../ui/containers/notifications_container.js  | 19 +++-----------
 app/javascript/mastodon/selectors/index.js    | 25 ++++++++++++++-----
 5 files changed, 30 insertions(+), 25 deletions(-)

diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_mention.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_mention.tsx
index 1929446bb..d53cb37a8 100644
--- a/app/javascript/mastodon/features/notifications_v2/components/notification_mention.tsx
+++ b/app/javascript/mastodon/features/notifications_v2/components/notification_mention.tsx
@@ -1,5 +1,7 @@
 import { FormattedMessage } from 'react-intl';
 
+import { isEqual } from 'lodash';
+
 import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
 import { me } from 'mastodon/initial_state';
@@ -47,7 +49,7 @@ export const NotificationMention: React.FC<{
       status.get('visibility') === 'direct',
       status.get('in_reply_to_account_id') === me,
     ] as const;
-  });
+  }, isEqual);
 
   let labelRenderer = mentionLabelRenderer;
 
diff --git a/app/javascript/mastodon/features/notifications_v2/index.tsx b/app/javascript/mastodon/features/notifications_v2/index.tsx
index 29c49e05c..730d95bcd 100644
--- a/app/javascript/mastodon/features/notifications_v2/index.tsx
+++ b/app/javascript/mastodon/features/notifications_v2/index.tsx
@@ -4,6 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
 
+import { isEqual } from 'lodash';
 import { useDebouncedCallback } from 'use-debounce';
 
 import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
@@ -62,7 +63,7 @@ export const Notifications: React.FC<{
   multiColumn?: boolean;
 }> = ({ columnId, multiColumn }) => {
   const intl = useIntl();
-  const notifications = useAppSelector(selectNotificationGroups);
+  const notifications = useAppSelector(selectNotificationGroups, isEqual);
   const dispatch = useAppDispatch();
   const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
   const hasMore = notifications.at(-1)?.type === 'gap';
diff --git a/app/javascript/mastodon/features/ui/containers/modal_container.js b/app/javascript/mastodon/features/ui/containers/modal_container.js
index 1c3872cd5..63c568f84 100644
--- a/app/javascript/mastodon/features/ui/containers/modal_container.js
+++ b/app/javascript/mastodon/features/ui/containers/modal_container.js
@@ -3,10 +3,12 @@ import { connect } from 'react-redux';
 import { openModal, closeModal } from '../../../actions/modal';
 import ModalRoot from '../components/modal_root';
 
+const defaultProps = {};
+
 const mapStateToProps = state => ({
   ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
   type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
-  props: state.getIn(['modal', 'stack', 0, 'modalProps'], {}),
+  props: state.getIn(['modal', 'stack', 0, 'modalProps'], defaultProps),
 });
 
 const mapDispatchToProps = dispatch => ({
diff --git a/app/javascript/mastodon/features/ui/containers/notifications_container.js b/app/javascript/mastodon/features/ui/containers/notifications_container.js
index 3d60cfdad..b8aa9bc46 100644
--- a/app/javascript/mastodon/features/ui/containers/notifications_container.js
+++ b/app/javascript/mastodon/features/ui/containers/notifications_container.js
@@ -4,24 +4,11 @@ import { connect } from 'react-redux';
 
 import { NotificationStack } from 'react-notification';
 
-import { dismissAlert } from '../../../actions/alerts';
-import { getAlerts } from '../../../selectors';
-
-const formatIfNeeded = (intl, message, values) => {
-  if (typeof message === 'object') {
-    return intl.formatMessage(message, values);
-  }
-
-  return message;
-};
+import { dismissAlert } from 'mastodon/actions/alerts';
+import { getAlerts } from 'mastodon/selectors';
 
 const mapStateToProps = (state, { intl }) => ({
-  notifications: getAlerts(state).map(alert => ({
-    ...alert,
-    action: formatIfNeeded(intl, alert.action, alert.values),
-    title: formatIfNeeded(intl, alert.title, alert.values),
-    message: formatIfNeeded(intl, alert.message, alert.values),
-  })),
+  notifications: getAlerts(state, { intl }),
 });
 
 const mapDispatchToProps = (dispatch) => ({
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index bd9b53919..10e1b167c 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -7,14 +7,16 @@ import { me } from '../initial_state';
 
 export { makeGetAccount } from "./accounts";
 
-const getFilters = (state, { contextType }) => {
-  if (!contextType) return null;
+const getFilters = createSelector([state => state.get('filters'), (_, { contextType }) => contextType], (filters, contextType) => {
+  if (!contextType) {
+    return null;
+  }
 
-  const serverSideType = toServerSideType(contextType);
   const now = new Date();
+  const serverSideType = toServerSideType(contextType);
 
-  return state.get('filters').filter((filter) => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now));
-};
+  return filters.filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now));
+});
 
 export const makeGetStatus = () => {
   return createSelector(
@@ -73,10 +75,21 @@ const ALERT_DEFAULTS = {
   style: false,
 };
 
-export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
+const formatIfNeeded = (intl, message, values) => {
+  if (typeof message === 'object') {
+    return intl.formatMessage(message, values);
+  }
+
+  return message;
+};
+
+export const getAlerts = createSelector([state => state.get('alerts'), (_, { intl }) => intl], (alerts, intl) =>
   alerts.map(item => ({
     ...ALERT_DEFAULTS,
     ...item,
+    action: formatIfNeeded(intl, item.action, item.values),
+    title: formatIfNeeded(intl, item.title, item.values),
+    message: formatIfNeeded(intl, item.message, item.values),
   })).toArray());
 
 export const makeGetNotification = () => createSelector([

From 3d46f478174403a64bd194e8c60e11b07bbd5d2d Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 12 Sep 2024 11:41:19 +0200
Subject: [PATCH 76/91] Change embedded posts to use web UI (#31766)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 app/helpers/accounts_helper.rb                |   8 -
 app/helpers/media_component_helper.rb         |  36 --
 app/javascript/entrypoints/embed.tsx          |  74 ++++
 app/javascript/entrypoints/public.tsx         |  37 --
 app/javascript/hooks/useRenderSignal.ts       |  32 ++
 app/javascript/mastodon/actions/statuses.js   |   6 +-
 app/javascript/mastodon/components/logo.tsx   |   7 +
 .../mastodon/components/more_from_author.jsx  |   6 +-
 .../features/standalone/status/index.tsx      |  87 ++++
 .../status/components/detailed_status.jsx     | 322 ---------------
 .../status/components/detailed_status.tsx     | 390 ++++++++++++++++++
 .../containers/detailed_status_container.js   | 140 -------
 .../mastodon/features/status/index.jsx        |   2 +-
 app/javascript/styles/application.scss        |   1 -
 .../styles/mastodon/components.scss           |  31 +-
 app/javascript/styles/mastodon/statuses.scss  | 152 -------
 app/serializers/oembed_serializer.rb          |  20 +-
 app/views/layouts/embedded.html.haml          |   2 +-
 app/views/statuses/_detailed_status.html.haml |  80 ----
 app/views/statuses/_poll.html.haml            |  36 --
 app/views/statuses/_simple_status.html.haml   |  70 ----
 app/views/statuses/_status.html.haml          |   2 -
 app/views/statuses/embed.html.haml            |   3 +-
 config/locales/af.yml                         |   1 -
 config/locales/an.yml                         |  12 -
 config/locales/ar.yml                         |  20 -
 config/locales/ast.yml                        |   9 -
 config/locales/be.yml                         |  16 -
 config/locales/bg.yml                         |  12 -
 config/locales/bn.yml                         |   1 -
 config/locales/br.yml                         |   4 -
 config/locales/ca.yml                         |  12 -
 config/locales/ckb.yml                        |  12 -
 config/locales/co.yml                         |  12 -
 config/locales/cs.yml                         |  16 -
 config/locales/cy.yml                         |  20 -
 config/locales/da.yml                         |  12 -
 config/locales/de.yml                         |  12 -
 config/locales/el.yml                         |  12 -
 config/locales/en-GB.yml                      |  12 -
 config/locales/en.yml                         |  12 -
 config/locales/eo.yml                         |  12 -
 config/locales/es-AR.yml                      |  12 -
 config/locales/es-MX.yml                      |  12 -
 config/locales/es.yml                         |  12 -
 config/locales/et.yml                         |  12 -
 config/locales/eu.yml                         |  12 -
 config/locales/fa.yml                         |  12 -
 config/locales/fi.yml                         |  12 -
 config/locales/fo.yml                         |  12 -
 config/locales/fr-CA.yml                      |  12 -
 config/locales/fr.yml                         |  12 -
 config/locales/fy.yml                         |  12 -
 config/locales/ga.yml                         |  18 -
 config/locales/gd.yml                         |  16 -
 config/locales/gl.yml                         |  12 -
 config/locales/he.yml                         |  16 -
 config/locales/hi.yml                         |   1 -
 config/locales/hr.yml                         |  14 -
 config/locales/hu.yml                         |  12 -
 config/locales/hy.yml                         |  12 -
 config/locales/ia.yml                         |  12 -
 config/locales/id.yml                         |  10 -
 config/locales/ie.yml                         |  12 -
 config/locales/io.yml                         |  12 -
 config/locales/is.yml                         |  12 -
 config/locales/it.yml                         |  12 -
 config/locales/ja.yml                         |  10 -
 config/locales/ka.yml                         |   3 -
 config/locales/kab.yml                        |  12 -
 config/locales/kk.yml                         |  12 -
 config/locales/ko.yml                         |  10 -
 config/locales/ku.yml                         |  12 -
 config/locales/la.yml                         |   1 -
 config/locales/lad.yml                        |  12 -
 config/locales/lt.yml                         |   6 -
 config/locales/lv.yml                         |  14 -
 config/locales/ml.yml                         |   1 -
 config/locales/ms.yml                         |  10 -
 config/locales/my.yml                         |  10 -
 config/locales/nl.yml                         |  12 -
 config/locales/nn.yml                         |  12 -
 config/locales/no.yml                         |  12 -
 config/locales/oc.yml                         |  12 -
 config/locales/pa.yml                         |   1 -
 config/locales/pl.yml                         |  16 -
 config/locales/pt-BR.yml                      |  12 -
 config/locales/pt-PT.yml                      |  12 -
 config/locales/ro.yml                         |  14 -
 config/locales/ru.yml                         |  16 -
 config/locales/ry.yml                         |   1 -
 config/locales/sc.yml                         |  12 -
 config/locales/sco.yml                        |  12 -
 config/locales/si.yml                         |  12 -
 config/locales/sk.yml                         |  16 -
 config/locales/sl.yml                         |  16 -
 config/locales/sq.yml                         |  12 -
 config/locales/sr-Latn.yml                    |  14 -
 config/locales/sr.yml                         |  14 -
 config/locales/sv.yml                         |  12 -
 config/locales/ta.yml                         |   2 -
 config/locales/te.yml                         |   1 -
 config/locales/th.yml                         |  10 -
 config/locales/tr.yml                         |  12 -
 config/locales/tt.yml                         |   7 -
 config/locales/uk.yml                         |  16 -
 config/locales/uz.yml                         |   2 -
 config/locales/vi.yml                         |  10 -
 config/locales/zgh.yml                        |   1 -
 config/locales/zh-CN.yml                      |  10 -
 config/locales/zh-HK.yml                      |  10 -
 config/locales/zh-TW.yml                      |  10 -
 public/embed.js                               | 105 +++--
 spec/controllers/statuses_controller_spec.rb  |   1 -
 spec/helpers/media_component_helper_spec.rb   |  22 -
 115 files changed, 710 insertions(+), 1928 deletions(-)
 create mode 100644 app/javascript/entrypoints/embed.tsx
 create mode 100644 app/javascript/hooks/useRenderSignal.ts
 create mode 100644 app/javascript/mastodon/features/standalone/status/index.tsx
 delete mode 100644 app/javascript/mastodon/features/status/components/detailed_status.jsx
 create mode 100644 app/javascript/mastodon/features/status/components/detailed_status.tsx
 delete mode 100644 app/javascript/mastodon/features/status/containers/detailed_status_container.js
 delete mode 100644 app/javascript/styles/mastodon/statuses.scss
 delete mode 100644 app/views/statuses/_detailed_status.html.haml
 delete mode 100644 app/views/statuses/_poll.html.haml
 delete mode 100644 app/views/statuses/_simple_status.html.haml
 delete mode 100644 app/views/statuses/_status.html.haml

diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
index 158a0815e..d804566c9 100644
--- a/app/helpers/accounts_helper.rb
+++ b/app/helpers/accounts_helper.rb
@@ -19,14 +19,6 @@ module AccountsHelper
     end
   end
 
-  def account_action_button(account)
-    return if account.memorial? || account.moved?
-
-    link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
-      safe_join([logo_as_symbol, t('accounts.follow')])
-    end
-  end
-
   def account_formatted_stat(value)
     number_to_human(value, precision: 3, strip_insignificant_zeros: true)
   end
diff --git a/app/helpers/media_component_helper.rb b/app/helpers/media_component_helper.rb
index fa8f34fb4..60ccdd083 100644
--- a/app/helpers/media_component_helper.rb
+++ b/app/helpers/media_component_helper.rb
@@ -57,26 +57,6 @@ module MediaComponentHelper
     end
   end
 
-  def render_card_component(status, **options)
-    component_params = {
-      sensitive: sensitive_viewer?(status, current_account),
-      card: serialize_status_card(status).as_json,
-    }.merge(**options)
-
-    react_component :card, component_params
-  end
-
-  def render_poll_component(status, **options)
-    component_params = {
-      disabled: true,
-      poll: serialize_status_poll(status).as_json,
-    }.merge(**options)
-
-    react_component :poll, component_params do
-      render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? }
-    end
-  end
-
   private
 
   def serialize_media_attachment(attachment)
@@ -86,22 +66,6 @@ module MediaComponentHelper
     )
   end
 
-  def serialize_status_card(status)
-    ActiveModelSerializers::SerializableResource.new(
-      status.preview_card,
-      serializer: REST::PreviewCardSerializer
-    )
-  end
-
-  def serialize_status_poll(status)
-    ActiveModelSerializers::SerializableResource.new(
-      status.preloadable_poll,
-      serializer: REST::PollSerializer,
-      scope: current_user,
-      scope_name: :current_user
-    )
-  end
-
   def sensitive_viewer?(status, account)
     if !account.nil? && account.id == status.account_id
       status.sensitive
diff --git a/app/javascript/entrypoints/embed.tsx b/app/javascript/entrypoints/embed.tsx
new file mode 100644
index 000000000..f8c824d28
--- /dev/null
+++ b/app/javascript/entrypoints/embed.tsx
@@ -0,0 +1,74 @@
+import './public-path';
+import { createRoot } from 'react-dom/client';
+
+import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
+
+import { start } from '../mastodon/common';
+import { Status } from '../mastodon/features/standalone/status';
+import { loadPolyfills } from '../mastodon/polyfills';
+import ready from '../mastodon/ready';
+
+start();
+
+function loaded() {
+  const mountNode = document.getElementById('mastodon-status');
+
+  if (mountNode) {
+    const attr = mountNode.getAttribute('data-props');
+
+    if (!attr) return;
+
+    const props = JSON.parse(attr) as { id: string; locale: string };
+    const root = createRoot(mountNode);
+
+    root.render(<Status {...props} />);
+  }
+}
+
+function main() {
+  ready(loaded).catch((error: unknown) => {
+    console.error(error);
+  });
+}
+
+loadPolyfills()
+  .then(main)
+  .catch((error: unknown) => {
+    console.error(error);
+  });
+
+interface SetHeightMessage {
+  type: 'setHeight';
+  id: string;
+  height: number;
+}
+
+function isSetHeightMessage(data: unknown): data is SetHeightMessage {
+  if (
+    data &&
+    typeof data === 'object' &&
+    'type' in data &&
+    data.type === 'setHeight'
+  )
+    return true;
+  else return false;
+}
+
+window.addEventListener('message', (e) => {
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
+  if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
+
+  const data = e.data;
+
+  // We use a timeout to allow for the React page to render before calculating the height
+  afterInitialRender(() => {
+    window.parent.postMessage(
+      {
+        type: 'setHeight',
+        id: data.id,
+        height: document.getElementsByTagName('html')[0]?.scrollHeight,
+      },
+      '*',
+    );
+  });
+});
diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx
index b06675c2e..d33e00d5d 100644
--- a/app/javascript/entrypoints/public.tsx
+++ b/app/javascript/entrypoints/public.tsx
@@ -37,43 +37,6 @@ const messages = defineMessages({
   },
 });
 
-interface SetHeightMessage {
-  type: 'setHeight';
-  id: string;
-  height: number;
-}
-
-function isSetHeightMessage(data: unknown): data is SetHeightMessage {
-  if (
-    data &&
-    typeof data === 'object' &&
-    'type' in data &&
-    data.type === 'setHeight'
-  )
-    return true;
-  else return false;
-}
-
-window.addEventListener('message', (e) => {
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
-  if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
-
-  const data = e.data;
-
-  ready(() => {
-    window.parent.postMessage(
-      {
-        type: 'setHeight',
-        id: data.id,
-        height: document.getElementsByTagName('html')[0]?.scrollHeight,
-      },
-      '*',
-    );
-  }).catch((e: unknown) => {
-    console.error('Error in setHeightMessage postMessage', e);
-  });
-});
-
 function loaded() {
   const { messages: localeData } = getLocale();
 
diff --git a/app/javascript/hooks/useRenderSignal.ts b/app/javascript/hooks/useRenderSignal.ts
new file mode 100644
index 000000000..740df4a35
--- /dev/null
+++ b/app/javascript/hooks/useRenderSignal.ts
@@ -0,0 +1,32 @@
+// This hook allows a component to signal that it's done rendering in a way that
+// can be used by e.g. our embed code to determine correct iframe height
+
+let renderSignalReceived = false;
+
+type Callback = () => void;
+
+let onInitialRender: Callback;
+
+export const afterInitialRender = (callback: Callback) => {
+  if (renderSignalReceived) {
+    callback();
+  } else {
+    onInitialRender = callback;
+  }
+};
+
+export const useRenderSignal = () => {
+  return () => {
+    if (renderSignalReceived) {
+      return;
+    }
+
+    renderSignalReceived = true;
+
+    if (typeof onInitialRender !== 'undefined') {
+      window.requestAnimationFrame(() => {
+        onInitialRender();
+      });
+    }
+  };
+};
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 340cee802..1e4e545d8 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -49,11 +49,13 @@ export function fetchStatusRequest(id, skipLoading) {
   };
 }
 
-export function fetchStatus(id, forceFetch = false) {
+export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
   return (dispatch, getState) => {
     const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
 
-    dispatch(fetchContext(id));
+    if (alsoFetchContext) {
+      dispatch(fetchContext(id));
+    }
 
     if (skipLoading) {
       return;
diff --git a/app/javascript/mastodon/components/logo.tsx b/app/javascript/mastodon/components/logo.tsx
index b7f8bd669..fe9680d0e 100644
--- a/app/javascript/mastodon/components/logo.tsx
+++ b/app/javascript/mastodon/components/logo.tsx
@@ -7,6 +7,13 @@ export const WordmarkLogo: React.FC = () => (
   </svg>
 );
 
+export const IconLogo: React.FC = () => (
+  <svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
+    <title>Mastodon</title>
+    <use xlinkHref='#logo-symbol-icon' />
+  </svg>
+);
+
 export const SymbolLogo: React.FC = () => (
   <img src={logo} alt='Mastodon' className='logo logo--icon' />
 );
diff --git a/app/javascript/mastodon/components/more_from_author.jsx b/app/javascript/mastodon/components/more_from_author.jsx
index c20e76ac4..719f4dda8 100644
--- a/app/javascript/mastodon/components/more_from_author.jsx
+++ b/app/javascript/mastodon/components/more_from_author.jsx
@@ -2,14 +2,12 @@ import PropTypes from 'prop-types';
 
 import { FormattedMessage } from 'react-intl';
 
+import { IconLogo } from 'mastodon/components/logo';
 import { AuthorLink } from 'mastodon/features/explore/components/author_link';
 
 export const MoreFromAuthor = ({ accountId }) => (
   <div className='more-from-author'>
-    <svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
-      <use xlinkHref='#logo-symbol-icon' />
-    </svg>
-
+    <IconLogo />
     <FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
   </div>
 );
diff --git a/app/javascript/mastodon/features/standalone/status/index.tsx b/app/javascript/mastodon/features/standalone/status/index.tsx
new file mode 100644
index 000000000..d5cb7e7f4
--- /dev/null
+++ b/app/javascript/mastodon/features/standalone/status/index.tsx
@@ -0,0 +1,87 @@
+/* eslint-disable @typescript-eslint/no-unsafe-return,
+                  @typescript-eslint/no-explicit-any,
+                  @typescript-eslint/no-unsafe-assignment */
+
+import { useEffect, useCallback } from 'react';
+
+import { Provider } from 'react-redux';
+
+import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
+import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
+import { hydrateStore } from 'mastodon/actions/store';
+import { Router } from 'mastodon/components/router';
+import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
+import initialState from 'mastodon/initial_state';
+import { IntlProvider } from 'mastodon/locales';
+import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
+import { store, useAppSelector, useAppDispatch } from 'mastodon/store';
+
+const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
+const getPictureInPicture = makeGetPictureInPicture() as unknown as (
+  arg0: any,
+  arg1: any,
+) => any;
+
+const Embed: React.FC<{ id: string }> = ({ id }) => {
+  const status = useAppSelector((state) => getStatus(state, { id }));
+  const pictureInPicture = useAppSelector((state) =>
+    getPictureInPicture(state, { id }),
+  );
+  const domain = useAppSelector((state) => state.meta.get('domain'));
+  const dispatch = useAppDispatch();
+  const dispatchRenderSignal = useRenderSignal();
+
+  useEffect(() => {
+    dispatch(fetchStatus(id, false, false));
+  }, [dispatch, id]);
+
+  const handleToggleHidden = useCallback(() => {
+    dispatch(toggleStatusSpoilers(id));
+  }, [dispatch, id]);
+
+  // This allows us to calculate the correct page height for embeds
+  if (status) {
+    dispatchRenderSignal();
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
+  const permalink = status?.get('url') as string;
+
+  return (
+    <div className='embed'>
+      <DetailedStatus
+        status={status}
+        domain={domain}
+        pictureInPicture={pictureInPicture}
+        onToggleHidden={handleToggleHidden}
+        withLogo
+      />
+
+      <a
+        className='embed__overlay'
+        href={permalink}
+        target='_blank'
+        rel='noreferrer noopener'
+        aria-label=''
+      />
+    </div>
+  );
+};
+
+export const Status: React.FC<{ id: string }> = ({ id }) => {
+  useEffect(() => {
+    if (initialState) {
+      store.dispatch(hydrateStore(initialState));
+    }
+  }, []);
+
+  return (
+    <IntlProvider>
+      <Provider store={store}>
+        <Router>
+          <Embed id={id} />
+        </Router>
+      </Provider>
+    </IntlProvider>
+  );
+};
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx
deleted file mode 100644
index 8ee1ec9b9..000000000
--- a/app/javascript/mastodon/features/status/components/detailed_status.jsx
+++ /dev/null
@@ -1,322 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { FormattedDate, FormattedMessage } from 'react-intl';
-
-import classNames from 'classnames';
-import { Link, withRouter } from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
-import { AnimatedNumber } from 'mastodon/components/animated_number';
-import { ContentWarning } from 'mastodon/components/content_warning';
-import EditedTimestamp from 'mastodon/components/edited_timestamp';
-import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
-import { Icon }  from 'mastodon/components/icon';
-import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
-import { VisibilityIcon } from 'mastodon/components/visibility_icon';
-import { WithRouterPropTypes } from 'mastodon/utils/react_router';
-
-import { Avatar } from '../../../components/avatar';
-import { DisplayName } from '../../../components/display_name';
-import MediaGallery from '../../../components/media_gallery';
-import StatusContent from '../../../components/status_content';
-import Audio from '../../audio';
-import scheduleIdleTask from '../../ui/util/schedule_idle_task';
-import Video from '../../video';
-
-import Card from './card';
-
-class DetailedStatus extends ImmutablePureComponent {
-
-  static propTypes = {
-    status: ImmutablePropTypes.map,
-    onOpenMedia: PropTypes.func.isRequired,
-    onOpenVideo: PropTypes.func.isRequired,
-    onToggleHidden: PropTypes.func.isRequired,
-    onTranslate: PropTypes.func.isRequired,
-    measureHeight: PropTypes.bool,
-    onHeightChange: PropTypes.func,
-    domain: PropTypes.string.isRequired,
-    compact: PropTypes.bool,
-    showMedia: PropTypes.bool,
-    pictureInPicture: ImmutablePropTypes.contains({
-      inUse: PropTypes.bool,
-      available: PropTypes.bool,
-    }),
-    onToggleMediaVisibility: PropTypes.func,
-    ...WithRouterPropTypes,
-  };
-
-  state = {
-    height: null,
-  };
-
-  handleAccountClick = (e) => {
-    if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.props.history) {
-      e.preventDefault();
-      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
-    }
-
-    e.stopPropagation();
-  };
-
-  handleOpenVideo = (options) => {
-    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
-  };
-
-  handleExpandedToggle = () => {
-    this.props.onToggleHidden(this.props.status);
-  };
-
-  _measureHeight (heightJustChanged) {
-    if (this.props.measureHeight && this.node) {
-      scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
-
-      if (this.props.onHeightChange && heightJustChanged) {
-        this.props.onHeightChange();
-      }
-    }
-  }
-
-  setRef = c => {
-    this.node = c;
-    this._measureHeight();
-  };
-
-  componentDidUpdate (prevProps, prevState) {
-    this._measureHeight(prevState.height !== this.state.height);
-  }
-
-  handleModalLink = e => {
-    e.preventDefault();
-
-    let href;
-
-    if (e.target.nodeName !== 'A') {
-      href = e.target.parentNode.href;
-    } else {
-      href = e.target.href;
-    }
-
-    window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
-  };
-
-  handleTranslate = () => {
-    const { onTranslate, status } = this.props;
-    onTranslate(status);
-  };
-
-  _properStatus () {
-    const { status } = this.props;
-
-    if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
-      return status.get('reblog');
-    } else {
-      return status;
-    }
-  }
-
-  getAttachmentAspectRatio () {
-    const attachments = this._properStatus().get('media_attachments');
-
-    if (attachments.getIn([0, 'type']) === 'video') {
-      return `${attachments.getIn([0, 'meta', 'original', 'width'])} / ${attachments.getIn([0, 'meta', 'original', 'height'])}`;
-    } else if (attachments.getIn([0, 'type']) === 'audio') {
-      return '16 / 9';
-    } else {
-      return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2';
-    }
-  }
-
-  render () {
-    const status = this._properStatus();
-    const outerStyle = { boxSizing: 'border-box' };
-    const { compact, pictureInPicture } = this.props;
-
-    if (!status) {
-      return null;
-    }
-
-    let media           = '';
-    let applicationLink = '';
-    let reblogLink = '';
-    let favouriteLink = '';
-
-    if (this.props.measureHeight) {
-      outerStyle.height = `${this.state.height}px`;
-    }
-
-    const language = status.getIn(['translation', 'language']) || status.get('language');
-
-    if (pictureInPicture.get('inUse')) {
-      media = <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
-    } else if (status.get('media_attachments').size > 0) {
-      if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
-        const attachment = status.getIn(['media_attachments', 0]);
-        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
-
-        media = (
-          <Audio
-            src={attachment.get('url')}
-            alt={description}
-            lang={language}
-            duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
-            poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
-            backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
-            foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
-            accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
-            sensitive={status.get('sensitive')}
-            visible={this.props.showMedia}
-            blurhash={attachment.get('blurhash')}
-            height={150}
-            onToggleVisibility={this.props.onToggleMediaVisibility}
-          />
-        );
-      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
-        const attachment = status.getIn(['media_attachments', 0]);
-        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
-
-        media = (
-          <Video
-            preview={attachment.get('preview_url')}
-            frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
-            aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
-            blurhash={attachment.get('blurhash')}
-            src={attachment.get('url')}
-            alt={description}
-            lang={language}
-            width={300}
-            height={150}
-            onOpenVideo={this.handleOpenVideo}
-            sensitive={status.get('sensitive')}
-            visible={this.props.showMedia}
-            onToggleVisibility={this.props.onToggleMediaVisibility}
-          />
-        );
-      } else {
-        media = (
-          <MediaGallery
-            standalone
-            sensitive={status.get('sensitive')}
-            media={status.get('media_attachments')}
-            lang={language}
-            height={300}
-            onOpenMedia={this.props.onOpenMedia}
-            visible={this.props.showMedia}
-            onToggleVisibility={this.props.onToggleMediaVisibility}
-          />
-        );
-      }
-    } else if (status.get('spoiler_text').length === 0) {
-      media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
-    }
-
-    if (status.get('application')) {
-      applicationLink = <>·<a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
-    }
-
-    const visibilityLink = <>·<VisibilityIcon visibility={status.get('visibility')} /></>;
-
-    if (['private', 'direct'].includes(status.get('visibility'))) {
-      reblogLink = '';
-    } else if (this.props.history) {
-      reblogLink = (
-        <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
-          <span className='detailed-status__reblogs'>
-            <AnimatedNumber value={status.get('reblogs_count')} />
-          </span>
-          <FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
-        </Link>
-      );
-    } else {
-      reblogLink = (
-        <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
-          <span className='detailed-status__reblogs'>
-            <AnimatedNumber value={status.get('reblogs_count')} />
-          </span>
-          <FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
-        </a>
-      );
-    }
-
-    if (this.props.history) {
-      favouriteLink = (
-        <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
-          <span className='detailed-status__favorites'>
-            <AnimatedNumber value={status.get('favourites_count')} />
-          </span>
-          <FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
-        </Link>
-      );
-    } else {
-      favouriteLink = (
-        <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
-          <span className='detailed-status__favorites'>
-            <AnimatedNumber value={status.get('favourites_count')} />
-          </span>
-          <FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
-        </a>
-      );
-    }
-
-    const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
-    const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
-
-    return (
-      <div style={outerStyle}>
-        <div ref={this.setRef} className={classNames('detailed-status', { compact })}>
-          {status.get('visibility') === 'direct' && (
-            <div className='status__prepend'>
-              <div className='status__prepend-icon-wrapper'><Icon id='at' icon={AlternateEmailIcon} className='status__prepend-icon' /></div>
-              <FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
-            </div>
-          )}
-          <a href={`/@${status.getIn(['account', 'acct'])}`} data-hover-card-account={status.getIn(['account', 'id'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
-            <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
-            <DisplayName account={status.get('account')} localDomain={this.props.domain} />
-          </a>
-
-          {status.get('spoiler_text').length > 0 && <ContentWarning text={status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml')} expanded={expanded} onClick={this.handleExpandedToggle} />}
-
-          {expanded && (
-            <>
-              <StatusContent
-                status={status}
-                onTranslate={this.handleTranslate}
-                {...statusContentProps}
-              />
-
-              {media}
-              {hashtagBar}
-            </>
-          )}
-
-          <div className='detailed-status__meta'>
-            <div className='detailed-status__meta__line'>
-              <a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
-                <FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
-              </a>
-
-              {visibilityLink}
-
-              {applicationLink}
-            </div>
-
-            {status.get('edited_at') && <div className='detailed-status__meta__line'><EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /></div>}
-
-            <div className='detailed-status__meta__line'>
-              {reblogLink}
-              {reblogLink && <>·</>}
-              {favouriteLink}
-            </div>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-export default withRouter(DetailedStatus);
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx
new file mode 100644
index 000000000..fa843122f
--- /dev/null
+++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx
@@ -0,0 +1,390 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access,
+                  @typescript-eslint/no-unsafe-call,
+                  @typescript-eslint/no-explicit-any,
+                  @typescript-eslint/no-unsafe-assignment */
+
+import type { CSSProperties } from 'react';
+import { useState, useRef, useCallback } from 'react';
+
+import { FormattedDate, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
+import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
+import { AnimatedNumber } from 'mastodon/components/animated_number';
+import { ContentWarning } from 'mastodon/components/content_warning';
+import EditedTimestamp from 'mastodon/components/edited_timestamp';
+import type { StatusLike } from 'mastodon/components/hashtag_bar';
+import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
+import { Icon } from 'mastodon/components/icon';
+import { IconLogo } from 'mastodon/components/logo';
+import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
+import { VisibilityIcon } from 'mastodon/components/visibility_icon';
+
+import { Avatar } from '../../../components/avatar';
+import { DisplayName } from '../../../components/display_name';
+import MediaGallery from '../../../components/media_gallery';
+import StatusContent from '../../../components/status_content';
+import Audio from '../../audio';
+import scheduleIdleTask from '../../ui/util/schedule_idle_task';
+import Video from '../../video';
+
+import Card from './card';
+
+interface VideoModalOptions {
+  startTime: number;
+  autoPlay?: boolean;
+  defaultVolume: number;
+  componentIndex: number;
+}
+
+export const DetailedStatus: React.FC<{
+  status: any;
+  onOpenMedia?: (status: any, index: number, lang: string) => void;
+  onOpenVideo?: (status: any, lang: string, options: VideoModalOptions) => void;
+  onTranslate?: (status: any) => void;
+  measureHeight?: boolean;
+  onHeightChange?: () => void;
+  domain: string;
+  showMedia?: boolean;
+  withLogo?: boolean;
+  pictureInPicture: any;
+  onToggleHidden?: (status: any) => void;
+  onToggleMediaVisibility?: () => void;
+}> = ({
+  status,
+  onOpenMedia,
+  onOpenVideo,
+  onTranslate,
+  measureHeight,
+  onHeightChange,
+  domain,
+  showMedia,
+  withLogo,
+  pictureInPicture,
+  onToggleMediaVisibility,
+  onToggleHidden,
+}) => {
+  const properStatus = status?.get('reblog') ?? status;
+  const [height, setHeight] = useState(0);
+  const nodeRef = useRef<HTMLDivElement>();
+
+  const handleOpenVideo = useCallback(
+    (options: VideoModalOptions) => {
+      const lang = (status.getIn(['translation', 'language']) ||
+        status.get('language')) as string;
+      if (onOpenVideo)
+        onOpenVideo(status.getIn(['media_attachments', 0]), lang, options);
+    },
+    [onOpenVideo, status],
+  );
+
+  const handleExpandedToggle = useCallback(() => {
+    if (onToggleHidden) onToggleHidden(status);
+  }, [onToggleHidden, status]);
+
+  const _measureHeight = useCallback(
+    (heightJustChanged?: boolean) => {
+      if (measureHeight && nodeRef.current) {
+        scheduleIdleTask(() => {
+          if (nodeRef.current)
+            setHeight(Math.ceil(nodeRef.current.scrollHeight) + 1);
+        });
+
+        if (onHeightChange && heightJustChanged) {
+          onHeightChange();
+        }
+      }
+    },
+    [onHeightChange, measureHeight, setHeight],
+  );
+
+  const handleRef = useCallback(
+    (c: HTMLDivElement) => {
+      nodeRef.current = c;
+      _measureHeight();
+    },
+    [_measureHeight],
+  );
+
+  const handleTranslate = useCallback(() => {
+    if (onTranslate) onTranslate(status);
+  }, [onTranslate, status]);
+
+  if (!properStatus) {
+    return null;
+  }
+
+  let media;
+  let applicationLink;
+  let reblogLink;
+  let attachmentAspectRatio;
+
+  if (properStatus.get('media_attachments').getIn([0, 'type']) === 'video') {
+    attachmentAspectRatio = `${properStatus.get('media_attachments').getIn([0, 'meta', 'original', 'width'])} / ${properStatus.get('media_attachments').getIn([0, 'meta', 'original', 'height'])}`;
+  } else if (
+    properStatus.get('media_attachments').getIn([0, 'type']) === 'audio'
+  ) {
+    attachmentAspectRatio = '16 / 9';
+  } else {
+    attachmentAspectRatio =
+      properStatus.get('media_attachments').size === 1 &&
+      properStatus
+        .get('media_attachments')
+        .getIn([0, 'meta', 'small', 'aspect'])
+        ? properStatus
+            .get('media_attachments')
+            .getIn([0, 'meta', 'small', 'aspect'])
+        : '3 / 2';
+  }
+
+  const outerStyle = { boxSizing: 'border-box' } as CSSProperties;
+
+  if (measureHeight) {
+    outerStyle.height = height;
+  }
+
+  const language =
+    status.getIn(['translation', 'language']) || status.get('language');
+
+  if (pictureInPicture.get('inUse')) {
+    media = <PictureInPicturePlaceholder aspectRatio={attachmentAspectRatio} />;
+  } else if (status.get('media_attachments').size > 0) {
+    if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
+      const attachment = status.getIn(['media_attachments', 0]);
+      const description =
+        attachment.getIn(['translation', 'description']) ||
+        attachment.get('description');
+
+      media = (
+        <Audio
+          src={attachment.get('url')}
+          alt={description}
+          lang={language}
+          duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
+          poster={
+            attachment.get('preview_url') ||
+            status.getIn(['account', 'avatar_static'])
+          }
+          backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
+          foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
+          accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
+          sensitive={status.get('sensitive')}
+          visible={showMedia}
+          blurhash={attachment.get('blurhash')}
+          height={150}
+          onToggleVisibility={onToggleMediaVisibility}
+        />
+      );
+    } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+      const attachment = status.getIn(['media_attachments', 0]);
+      const description =
+        attachment.getIn(['translation', 'description']) ||
+        attachment.get('description');
+
+      media = (
+        <Video
+          preview={attachment.get('preview_url')}
+          frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
+          aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
+          blurhash={attachment.get('blurhash')}
+          src={attachment.get('url')}
+          alt={description}
+          lang={language}
+          width={300}
+          height={150}
+          onOpenVideo={handleOpenVideo}
+          sensitive={status.get('sensitive')}
+          visible={showMedia}
+          onToggleVisibility={onToggleMediaVisibility}
+        />
+      );
+    } else {
+      media = (
+        <MediaGallery
+          standalone
+          sensitive={status.get('sensitive')}
+          media={status.get('media_attachments')}
+          lang={language}
+          height={300}
+          onOpenMedia={onOpenMedia}
+          visible={showMedia}
+          onToggleVisibility={onToggleMediaVisibility}
+        />
+      );
+    }
+  } else if (status.get('spoiler_text').length === 0) {
+    media = (
+      <Card
+        sensitive={status.get('sensitive')}
+        onOpenMedia={onOpenMedia}
+        card={status.get('card', null)}
+      />
+    );
+  }
+
+  if (status.get('application')) {
+    applicationLink = (
+      <>
+        ·
+        <a
+          className='detailed-status__application'
+          href={status.getIn(['application', 'website'])}
+          target='_blank'
+          rel='noopener noreferrer'
+        >
+          {status.getIn(['application', 'name'])}
+        </a>
+      </>
+    );
+  }
+
+  const visibilityLink = (
+    <>
+      ·<VisibilityIcon visibility={status.get('visibility')} />
+    </>
+  );
+
+  if (['private', 'direct'].includes(status.get('visibility') as string)) {
+    reblogLink = '';
+  } else {
+    reblogLink = (
+      <Link
+        to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`}
+        className='detailed-status__link'
+      >
+        <span className='detailed-status__reblogs'>
+          <AnimatedNumber value={status.get('reblogs_count')} />
+        </span>
+        <FormattedMessage
+          id='status.reblogs'
+          defaultMessage='{count, plural, one {boost} other {boosts}}'
+          values={{ count: status.get('reblogs_count') }}
+        />
+      </Link>
+    );
+  }
+
+  const favouriteLink = (
+    <Link
+      to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`}
+      className='detailed-status__link'
+    >
+      <span className='detailed-status__favorites'>
+        <AnimatedNumber value={status.get('favourites_count')} />
+      </span>
+      <FormattedMessage
+        id='status.favourites'
+        defaultMessage='{count, plural, one {favorite} other {favorites}}'
+        values={{ count: status.get('favourites_count') }}
+      />
+    </Link>
+  );
+
+  const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
+    status as StatusLike,
+  );
+  const expanded =
+    !status.get('hidden') || status.get('spoiler_text').length === 0;
+
+  return (
+    <div style={outerStyle}>
+      <div ref={handleRef} className={classNames('detailed-status')}>
+        {status.get('visibility') === 'direct' && (
+          <div className='status__prepend'>
+            <div className='status__prepend-icon-wrapper'>
+              <Icon
+                id='at'
+                icon={AlternateEmailIcon}
+                className='status__prepend-icon'
+              />
+            </div>
+            <FormattedMessage
+              id='status.direct_indicator'
+              defaultMessage='Private mention'
+            />
+          </div>
+        )}
+        <Link
+          to={`/@${status.getIn(['account', 'acct'])}`}
+          data-hover-card-account={status.getIn(['account', 'id'])}
+          className='detailed-status__display-name'
+        >
+          <div className='detailed-status__display-avatar'>
+            <Avatar account={status.get('account')} size={46} />
+          </div>
+          <DisplayName account={status.get('account')} localDomain={domain} />
+          {withLogo && (
+            <>
+              <div className='spacer' />
+              <IconLogo />
+            </>
+          )}
+        </Link>
+
+        {status.get('spoiler_text').length > 0 && (
+          <ContentWarning
+            text={
+              status.getIn(['translation', 'spoilerHtml']) ||
+              status.get('spoilerHtml')
+            }
+            expanded={expanded}
+            onClick={handleExpandedToggle}
+          />
+        )}
+
+        {expanded && (
+          <>
+            <StatusContent
+              status={status}
+              onTranslate={handleTranslate}
+              {...(statusContentProps as any)}
+            />
+
+            {media}
+            {hashtagBar}
+          </>
+        )}
+
+        <div className='detailed-status__meta'>
+          <div className='detailed-status__meta__line'>
+            <a
+              className='detailed-status__datetime'
+              href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`}
+              target='_blank'
+              rel='noopener noreferrer'
+            >
+              <FormattedDate
+                value={new Date(status.get('created_at') as string)}
+                year='numeric'
+                month='short'
+                day='2-digit'
+                hour='2-digit'
+                minute='2-digit'
+              />
+            </a>
+
+            {visibilityLink}
+            {applicationLink}
+          </div>
+
+          {status.get('edited_at') && (
+            <div className='detailed-status__meta__line'>
+              <EditedTimestamp
+                statusId={status.get('id')}
+                timestamp={status.get('edited_at')}
+              />
+            </div>
+          )}
+
+          <div className='detailed-status__meta__line'>
+            {reblogLink}
+            {reblogLink && <>·</>}
+            {favouriteLink}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
deleted file mode 100644
index 0e73697fe..000000000
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import { injectIntl } from 'react-intl';
-
-import { connect } from 'react-redux';
-
-import { showAlertForError } from '../../../actions/alerts';
-import { initBlockModal } from '../../../actions/blocks';
-import {
-  replyCompose,
-  mentionCompose,
-  directCompose,
-} from '../../../actions/compose';
-import {
-  toggleReblog,
-  toggleFavourite,
-  pin,
-  unpin,
-} from '../../../actions/interactions';
-import { openModal } from '../../../actions/modal';
-import { initMuteModal } from '../../../actions/mutes';
-import { initReport } from '../../../actions/reports';
-import {
-  muteStatus,
-  unmuteStatus,
-  deleteStatus,
-  toggleStatusSpoilers,
-} from '../../../actions/statuses';
-import { deleteModal } from '../../../initial_state';
-import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
-import DetailedStatus from '../components/detailed_status';
-
-const makeMapStateToProps = () => {
-  const getStatus = makeGetStatus();
-  const getPictureInPicture = makeGetPictureInPicture();
-
-  const mapStateToProps = (state, props) => ({
-    status: getStatus(state, props),
-    domain: state.getIn(['meta', 'domain']),
-    pictureInPicture: getPictureInPicture(state, props),
-  });
-
-  return mapStateToProps;
-};
-
-const mapDispatchToProps = (dispatch) => ({
-
-  onReply (status) {
-    dispatch((_, getState) => {
-      let state = getState();
-      if (state.getIn(['compose', 'text']).trim().length !== 0) {
-        dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
-      } else {
-        dispatch(replyCompose(status));
-      }
-    });
-  },
-
-  onReblog (status, e) {
-    dispatch(toggleReblog(status.get('id'), e.shiftKey));
-  },
-
-  onFavourite (status) {
-    dispatch(toggleFavourite(status.get('id')));
-  },
-
-  onPin (status) {
-    if (status.get('pinned')) {
-      dispatch(unpin(status));
-    } else {
-      dispatch(pin(status));
-    }
-  },
-
-  onEmbed (status) {
-    dispatch(openModal({
-      modalType: 'EMBED',
-      modalProps: {
-        id: status.get('id'),
-        onError: error => dispatch(showAlertForError(error)),
-      },
-    }));
-  },
-
-  onDelete (status, withRedraft = false) {
-    if (!deleteModal) {
-      dispatch(deleteStatus(status.get('id'), withRedraft));
-    } else {
-      dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
-    }
-  },
-
-  onDirect (account) {
-    dispatch(directCompose(account));
-  },
-
-  onMention (account) {
-    dispatch(mentionCompose(account));
-  },
-
-  onOpenMedia (media, index, lang) {
-    dispatch(openModal({
-      modalType: 'MEDIA',
-      modalProps: { media, index, lang },
-    }));
-  },
-
-  onOpenVideo (media, lang, options) {
-    dispatch(openModal({
-      modalType: 'VIDEO',
-      modalProps: { media, lang, options },
-    }));
-  },
-
-  onBlock (status) {
-    const account = status.get('account');
-    dispatch(initBlockModal(account));
-  },
-
-  onReport (status) {
-    dispatch(initReport(status.get('account'), status));
-  },
-
-  onMute (account) {
-    dispatch(initMuteModal(account));
-  },
-
-  onMuteConversation (status) {
-    if (status.get('muted')) {
-      dispatch(unmuteStatus(status.get('id')));
-    } else {
-      dispatch(muteStatus(status.get('id')));
-    }
-  },
-
-  onToggleHidden (status) {
-    dispatch(toggleStatusSpoilers(status.get('id')));
-  },
-
-});
-
-export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index 5f325fe7b..c115f7775 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -69,7 +69,7 @@ import Column from '../ui/components/column';
 import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
 
 import ActionBar from './components/action_bar';
-import DetailedStatus from './components/detailed_status';
+import { DetailedStatus } from './components/detailed_status';
 
 
 const messages = defineMessages({
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index 0dd573da9..465b74807 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -11,7 +11,6 @@
 @import 'mastodon/widgets';
 @import 'mastodon/forms';
 @import 'mastodon/accounts';
-@import 'mastodon/statuses';
 @import 'mastodon/components';
 @import 'mastodon/polls';
 @import 'mastodon/modal';
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 5a8fa3e5c..c6ce8f55a 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1677,18 +1677,6 @@ body > [data-popper-placement] {
   padding: 16px;
   border-top: 1px solid var(--background-border-color);
 
-  &--flex {
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: space-between;
-    align-items: flex-start;
-
-    .status__content,
-    .detailed-status__meta {
-      flex: 100%;
-    }
-  }
-
   .status__content {
     font-size: 19px;
     line-height: 24px;
@@ -1723,6 +1711,25 @@ body > [data-popper-placement] {
       margin-bottom: 0;
     }
   }
+
+  .logo {
+    width: 40px;
+    height: 40px;
+    color: $dark-text-color;
+  }
+}
+
+.embed {
+  position: relative;
+
+  &__overlay {
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+  }
 }
 
 .scrollable > div:first-child .detailed-status {
diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss
deleted file mode 100644
index b6d4f98cc..000000000
--- a/app/javascript/styles/mastodon/statuses.scss
+++ /dev/null
@@ -1,152 +0,0 @@
-.activity-stream {
-  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-  border-radius: 4px;
-  overflow: hidden;
-  margin-bottom: 10px;
-
-  &--under-tabs {
-    border-radius: 0 0 4px 4px;
-  }
-
-  @media screen and (max-width: $no-gap-breakpoint) {
-    margin-bottom: 0;
-    border-radius: 0;
-    box-shadow: none;
-  }
-
-  &--headless {
-    border-radius: 0;
-    margin: 0;
-    box-shadow: none;
-
-    .detailed-status,
-    .status {
-      border-radius: 0 !important;
-    }
-  }
-
-  div[data-component] {
-    width: 100%;
-  }
-
-  .entry {
-    background: $ui-base-color;
-
-    .detailed-status,
-    .status,
-    .load-more {
-      animation: none;
-    }
-
-    &:last-child {
-      .detailed-status,
-      .status,
-      .load-more {
-        border-bottom: 0;
-        border-radius: 0 0 4px 4px;
-      }
-    }
-
-    &:first-child {
-      .detailed-status,
-      .status,
-      .load-more {
-        border-radius: 4px 4px 0 0;
-      }
-
-      &:last-child {
-        .detailed-status,
-        .status,
-        .load-more {
-          border-radius: 4px;
-        }
-      }
-    }
-
-    @media screen and (width <= 740px) {
-      .detailed-status,
-      .status,
-      .load-more {
-        border-radius: 0 !important;
-      }
-    }
-  }
-
-  &--highlighted .entry {
-    background: lighten($ui-base-color, 8%);
-  }
-}
-
-.button.logo-button svg {
-  width: 20px;
-  height: auto;
-  vertical-align: middle;
-  margin-inline-end: 5px;
-  fill: $primary-text-color;
-
-  @media screen and (max-width: $no-gap-breakpoint) {
-    display: none;
-  }
-}
-
-.embed {
-  .status__content[data-spoiler='folded'] {
-    .e-content {
-      display: none;
-    }
-
-    p:first-child {
-      margin-bottom: 0;
-    }
-  }
-
-  .detailed-status {
-    padding: 15px;
-
-    .detailed-status__display-avatar .account__avatar {
-      width: 48px;
-      height: 48px;
-    }
-  }
-
-  .status {
-    padding: 15px 15px 15px (48px + 15px * 2);
-    min-height: 48px + 2px;
-
-    &__avatar {
-      inset-inline-start: 15px;
-      top: 17px;
-
-      .account__avatar {
-        width: 48px;
-        height: 48px;
-      }
-    }
-
-    &__content {
-      padding-top: 5px;
-    }
-
-    &__prepend {
-      margin-inline-start: 48px + 15px * 2;
-      padding-top: 15px;
-    }
-
-    &__prepend-icon-wrapper {
-      inset-inline-start: -32px;
-    }
-
-    .media-gallery,
-    &__action-bar,
-    .video-player {
-      margin-top: 10px;
-    }
-
-    &__action-bar-button {
-      font-size: 18px;
-      width: 23.1429px;
-      height: 23.1429px;
-      line-height: 23.15px;
-    }
-  }
-}
diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb
index d6261d724..3882b0e30 100644
--- a/app/serializers/oembed_serializer.rb
+++ b/app/serializers/oembed_serializer.rb
@@ -37,16 +37,16 @@ class OEmbedSerializer < ActiveModel::Serializer
   end
 
   def html
-    attributes = {
-      src: embed_short_account_status_url(object.account, object),
-      class: 'mastodon-embed',
-      style: 'max-width: 100%; border: 0',
-      width: width,
-      height: height,
-      allowfullscreen: true,
-    }
-
-    content_tag(:iframe, nil, attributes) + content_tag(:script, nil, src: full_asset_url('embed.js', skip_pipeline: true), async: true)
+    <<~HTML.squish
+      <blockquote class="mastodon-embed" data-embed-url="#{embed_short_account_status_url(object.account, object)}" style="max-width: 540px; min-width: 270px; background:#FCF8FF; border: 1px solid #C9C4DA; border-radius: 8px; overflow: hidden; margin: 0; padding: 0;">
+        <a href="#{short_account_status_url(object.account, object)}" target="_blank" style="color: #1C1A25; text-decoration: none; display: flex; align-items: center; justify-content: center; flex-direction: column; padding: 24px; font-size: 14px; line-height: 20px; letter-spacing: 0.25px; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif;">
+          <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></svg>
+          <div style="margin-top: 16px; color: #787588;">Post by @#{object.account.pretty_acct}@#{provider_name}</div>
+          <div style="font-weight: 500;">View on Mastodon</div>
+        </a>
+      </blockquote>
+      <script data-allowed-prefixes="#{root_url}" src="#{full_asset_url('embed.js', skip_pipeline: true)}" async="true"></script>
+    HTML
   end
 
   def width
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
index 9258e8083..0237e0451 100644
--- a/app/views/layouts/embedded.html.haml
+++ b/app/views/layouts/embedded.html.haml
@@ -15,7 +15,7 @@
     = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
     = preload_pack_asset "locale/#{I18n.locale}-json.js"
     = render_initial_state
-    = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
+    = javascript_pack_tag 'embed', integrity: true, crossorigin: 'anonymous'
   %body.embed
     = yield
 
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
deleted file mode 100644
index 6cd240bbb..000000000
--- a/app/views/statuses/_detailed_status.html.haml
+++ /dev/null
@@ -1,80 +0,0 @@
-.detailed-status.detailed-status--flex{ class: "detailed-status-#{status.visibility}" }
-  .p-author.h-card
-    = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do
-      .detailed-status__display-avatar
-        - if prefers_autoplay?
-          = image_tag status.account.avatar_original_url, alt: '', class: 'account__avatar u-photo'
-        - else
-          = image_tag status.account.avatar_static_url, alt: '', class: 'account__avatar u-photo'
-      %span.display-name
-        %bdi
-          %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: prefers_autoplay?)
-        %span.display-name__account
-          = acct(status.account)
-          = material_symbol('lock') if status.account.locked?
-
-  = account_action_button(status.account)
-
-  .status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
-    - if status.spoiler_text?
-      %p<
-        %span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}&nbsp;
-        %button.status__content__spoiler-link= t('statuses.show_more')
-    .e-content{ lang: status.language }
-      = prerender_custom_emojis(status_content_format(status), status.emojis)
-
-      - if status.preloadable_poll
-        = render_poll_component(status)
-
-  - if !status.ordered_media_attachments.empty?
-    - if status.ordered_media_attachments.first.video?
-      = render_video_component(status, width: 670, height: 380, detailed: true)
-    - elsif status.ordered_media_attachments.first.audio?
-      = render_audio_component(status, width: 670, height: 380)
-    - else
-      = render_media_gallery_component(status, height: 380, standalone: true)
-  - elsif status.preview_card
-    = render_card_component(status)
-
-  .detailed-status__meta
-    %data.dt-published{ value: status.created_at.to_time.iso8601 }
-    - if status.edited?
-      %data.dt-updated{ value: status.edited_at.to_time.iso8601 }
-
-    = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
-      %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
-    ·
-    - if status.edited?
-      = t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted'))
-      ·
-    %span.detailed-status__visibility-icon
-      = visibility_icon status
-    ·
-    - if status.application && status.account.user&.setting_show_application
-      - if status.application.website.blank?
-        %strong.detailed-status__application= status.application.name
-      - else
-        = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer'
-      ·
-    %span.detailed-status__link
-      - if status.in_reply_to_id.nil?
-        = material_symbol('reply')
-      - else
-        = material_symbol('reply_all')
-      %span.detailed-status__reblogs>= friendly_number_to_human status.replies_count
-      &nbsp;
-    ·
-    - if status.public_visibility? || status.unlisted_visibility?
-      %span.detailed-status__link
-        = material_symbol('repeat')
-        %span.detailed-status__reblogs>= friendly_number_to_human status.reblogs_count
-        &nbsp;
-      ·
-    %span.detailed-status__link
-      = material_symbol('star')
-      %span.detailed-status__favorites>= friendly_number_to_human status.favourites_count
-      &nbsp;
-
-    - if user_signed_in?
-      ·
-      = link_to t('statuses.open_in_web'), web_url("@#{status.account.pretty_acct}/#{status.id}"), class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml
deleted file mode 100644
index 62416a44d..000000000
--- a/app/views/statuses/_poll.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-:ruby
-  show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired?
-  total_votes_count = poll.voters_count || poll.votes_count
-
-.poll
-  %ul
-    - poll.loaded_options.each do |option|
-      %li
-        - if show_results
-          - percent = total_votes_count.positive? ? 100 * option.votes_count / total_votes_count : 0
-          %label.poll__option><
-            %span.poll__number><
-              #{percent.round}%
-            %span.poll__option__text
-              = prerender_custom_emojis(h(option.title), status.emojis)
-
-          %progress{ max: 100, value: [percent, 1].max, 'aria-hidden': 'true' }
-            %span.poll__chart
-        - else
-          %label.poll__option><
-            %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil }><
-            %span.poll__option__text
-              = prerender_custom_emojis(h(option.title), status.emojis)
-  .poll__footer
-    - unless show_results
-      %button.button.button-secondary{ disabled: true }
-        = t('statuses.poll.vote')
-
-    - if poll.voters_count.nil?
-      %span= t('statuses.poll.total_votes', count: poll.votes_count)
-    - else
-      %span= t('statuses.poll.total_people', count: poll.voters_count)
-
-    - unless poll.expires_at.nil?
-      ·
-      %span= l poll.expires_at
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
deleted file mode 100644
index ee7900fbf..000000000
--- a/app/views/statuses/_simple_status.html.haml
+++ /dev/null
@@ -1,70 +0,0 @@
-:ruby
-  hide_show_thread ||= false
-
-.status{ class: "status-#{status.visibility}" }
-  .status__info
-    = link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
-      %span.status__visibility-icon><
-        = visibility_icon status
-      %time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
-      - if status.edited?
-        %abbr{ title: t('statuses.edited_at_html', date: l(status.edited_at.to_date)) }
-          *
-    %data.dt-published{ value: status.created_at.to_time.iso8601 }
-
-    .p-author.h-card
-      = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do
-        .status__avatar
-          %div
-            - if prefers_autoplay?
-              = image_tag status.account.avatar_original_url, alt: '', class: 'u-photo account__avatar'
-            - else
-              = image_tag status.account.avatar_static_url, alt: '', class: 'u-photo account__avatar'
-        %span.display-name
-          %bdi
-            %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: prefers_autoplay?)
-          &nbsp;
-          %span.display-name__account
-            = acct(status.account)
-            = material_symbol('lock') if status.account.locked?
-  .status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
-    - if status.spoiler_text?
-      %p<
-        %span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}&nbsp;
-        %button.status__content__spoiler-link= t('statuses.show_more')
-    .e-content{ lang: status.language }
-      = prerender_custom_emojis(status_content_format(status), status.emojis)
-
-      - if status.preloadable_poll
-        = render_poll_component(status)
-
-  - if !status.ordered_media_attachments.empty?
-    - if status.ordered_media_attachments.first.video?
-      = render_video_component(status, width: 610, height: 343)
-    - elsif status.ordered_media_attachments.first.audio?
-      = render_audio_component(status, width: 610, height: 343)
-    - else
-      = render_media_gallery_component(status, height: 343)
-  - elsif status.preview_card
-    = render_card_component(status)
-
-  - if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id && !hide_show_thread
-    = link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do
-      = t 'statuses.show_thread'
-
-  .status__action-bar
-    %span.status__action-bar-button.icon-button.icon-button--with-counter
-      - if status.in_reply_to_id.nil?
-        = material_symbol 'reply'
-      - else
-        = material_symbol 'reply_all'
-      %span.icon-button__counter= obscured_counter status.replies_count
-    %span.status__action-bar-button.icon-button
-      - if status.distributable?
-        = material_symbol 'repeat'
-      - elsif status.private_visibility? || status.limited_visibility?
-        = material_symbol 'lock'
-      - else
-        = material_symbol 'alternate_email'
-    %span.status__action-bar-button.icon-button
-      = material_symbol 'star'
diff --git a/app/views/statuses/_status.html.haml b/app/views/statuses/_status.html.haml
deleted file mode 100644
index bf51b5ff7..000000000
--- a/app/views/statuses/_status.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.entry
-  = render (centered ? 'statuses/detailed_status' : 'statuses/simple_status'), status: status.proper, hide_show_thread: false
diff --git a/app/views/statuses/embed.html.haml b/app/views/statuses/embed.html.haml
index 18d62fd8e..09d0792ea 100644
--- a/app/views/statuses/embed.html.haml
+++ b/app/views/statuses/embed.html.haml
@@ -1,2 +1 @@
-.activity-stream.activity-stream--headless
-  = render 'status', status: @status, centered: true
+#mastodon-status{ data: { props: Oj.dump(default_props.merge(id: @status.id.to_s)) } }
diff --git a/config/locales/af.yml b/config/locales/af.yml
index 648ec6091..89ede096e 100644
--- a/config/locales/af.yml
+++ b/config/locales/af.yml
@@ -6,7 +6,6 @@ af:
     hosted_on: Mastodon gehuisves op %{domain}
     title: Aangaande
   accounts:
-    follow: Volg
     followers:
       one: Volgeling
       other: Volgelinge
diff --git a/config/locales/an.yml b/config/locales/an.yml
index 41eeee461..589bb3983 100644
--- a/config/locales/an.yml
+++ b/config/locales/an.yml
@@ -7,7 +7,6 @@ an:
     hosted_on: Mastodon alochau en %{domain}
     title: Sobre
   accounts:
-    follow: Seguir
     followers:
       one: Seguidor
       other: Seguidores
@@ -1410,23 +1409,12 @@ an:
     edited_at_html: Editau %{date}
     errors:
       in_reply_not_found: Lo estau a lo qual intentas responder no existe.
-    open_in_web: Ubrir en web
     over_character_limit: Limite de caracters de %{max} superau
     pin_errors:
       direct: Las publicacions que son visibles solo pa los usuarios mencionaus no pueden fixar-se
       limit: Ya has fixau lo numero maximo de publicacions
       ownership: La publicación d'unatra persona no puede fixar-se
       reblog: Un boost no puede fixar-se
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} chent"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Vota
-    show_more: Amostrar mas
-    show_thread: Amostrar discusión
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: Directa
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index 06cea7ecb..480feeba2 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -7,7 +7,6 @@ ar:
     hosted_on: ماستدون مُستضاف على %{domain}
     title: عن
   accounts:
-    follow: متابَعة
     followers:
       few: متابِعون
       many: متابِعون
@@ -1772,31 +1771,12 @@ ar:
     edited_at_html: عُدّل في %{date}
     errors:
       in_reply_not_found: إنّ المنشور الذي تحاول الرد عليه غير موجود على ما يبدو.
-    open_in_web: افتح في الويب
     over_character_limit: تم تجاوز حد الـ %{max} حرف المسموح بها
     pin_errors:
       direct: لا يمكن تثبيت المنشورات التي يراها فقط المتسخدمون المشار إليهم
       limit: لقد بلغت الحد الأقصى للمنشورات المثبتة
       ownership: لا يمكن تثبيت منشور نشره شخص آخر
       reblog: لا يمكن تثبيت إعادة نشر
-    poll:
-      total_people:
-        few: "%{count} أشخاص"
-        many: "%{count} أشخاص"
-        one: "%{count} شخص واحد"
-        other: "%{count} شخصا"
-        two: "%{count} شخصين"
-        zero: "%{count} شخص"
-      total_votes:
-        few: "%{count} أصوات"
-        many: "%{count} أصوات"
-        one: صوت واحد %{count}
-        other: "%{count} صوتا"
-        two: صوتين %{count}
-        zero: بدون صوت %{count}
-      vote: صوّت
-    show_more: أظهر المزيد
-    show_thread: اعرض خيط المحادثة
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: مباشرة
diff --git a/config/locales/ast.yml b/config/locales/ast.yml
index 70a0ad3bd..be3441507 100644
--- a/config/locales/ast.yml
+++ b/config/locales/ast.yml
@@ -800,20 +800,11 @@ ast:
     default_language: La mesma que la de la interfaz
     errors:
       in_reply_not_found: L'artículu al que tentes de responder paez que nun esiste.
-    open_in_web: Abrir na web
     pin_errors:
       direct: Nun se puen fixar los artículos que son visibles namás pa los usuarios mentaos
       limit: Yá fixesti'l númberu máximu d'artículos
       ownership: Nun se pue fixar l'artículu d'otru perfil
       reblog: Nun se pue fixar un artículu compartíu
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} persones"
-      total_votes:
-        one: "%{count} votu"
-        other: "%{count} votos"
-    show_more: Amosar más
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: Mensaxe direutu
diff --git a/config/locales/be.yml b/config/locales/be.yml
index 31a31e9e6..48ca5751c 100644
--- a/config/locales/be.yml
+++ b/config/locales/be.yml
@@ -7,7 +7,6 @@ be:
     hosted_on: Mastodon месціцца на %{domain}
     title: Пра нас
   accounts:
-    follow: Падпісацца
     followers:
       few: Падпісчыка
       many: Падпісчыкаў
@@ -1778,27 +1777,12 @@ be:
     edited_at_html: Адрэдагавана %{date}
     errors:
       in_reply_not_found: Здаецца, допіс, на які вы спрабуеце адказаць, не існуе.
-    open_in_web: Адчыніць у вэб-версіі
     over_character_limit: перавышаная колькасць сімвалаў у %{max}
     pin_errors:
       direct: Допісы, бачныя толькі згаданым карыстальнікам, не могуць быць замацаваныя
       limit: Вы ўжо замацавалі максімальную колькасць допісаў
       ownership: Немагчыма замацаваць чужы допіс
       reblog: Немагчыма замацаваць пашырэнне
-    poll:
-      total_people:
-        few: "%{count} чалавекі"
-        many: "%{count} чалавек"
-        one: "%{count} чалавек"
-        other: "%{count} чалавека"
-      total_votes:
-        few: "%{count} галасы"
-        many: "%{count} галасоў"
-        one: "%{count} голас"
-        other: "%{count} голасу"
-      vote: Прагаласаваць
-    show_more: Паказаць больш
-    show_thread: Паказаць ланцуг
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Асабіста
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 42a626c69..604eeca48 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -7,7 +7,6 @@ bg:
     hosted_on: Mastodon е разположен на хост %{domain}
     title: Относно
   accounts:
-    follow: Последване
     followers:
       one: Последовател
       other: Последователи
@@ -1664,23 +1663,12 @@ bg:
     edited_at_html: Редактирано на %{date}
     errors:
       in_reply_not_found: Изглежда, че публикацията, на която се опитвате да отговорите, не съществува.
-    open_in_web: Отвори в уеб
     over_character_limit: прехвърлен лимит от %{max} символа
     pin_errors:
       direct: Публикациите, които са видими само за потребители споменати в тях, не могат да бъдат закачани
       limit: Вече сте закачили максималния брой публикации
       ownership: Публикация на някого другиго не може да бъде закачена
       reblog: Раздуване не може да бъде закачано
-    poll:
-      total_people:
-        one: "%{count} човек"
-        other: "%{count} души"
-      total_votes:
-        one: "%{count} глас"
-        other: "%{count} гласа"
-      vote: Гласуване
-    show_more: Покажи повече
-    show_thread: Показване на нишката
     title: "%{name}: „%{quote}“"
     visibilities:
       direct: Директно
diff --git a/config/locales/bn.yml b/config/locales/bn.yml
index edbef73ae..74ff25d75 100644
--- a/config/locales/bn.yml
+++ b/config/locales/bn.yml
@@ -7,7 +7,6 @@ bn:
     hosted_on: এই মাস্টাডনটি আছে %{domain} এ
     title: পরিচিতি
   accounts:
-    follow: যুক্ত
     followers:
       one: যুক্ত আছে
       other: যারা যুক্ত হয়েছে
diff --git a/config/locales/br.yml b/config/locales/br.yml
index 4ef8fa1a1..f9fbd34ad 100644
--- a/config/locales/br.yml
+++ b/config/locales/br.yml
@@ -6,7 +6,6 @@ br:
     hosted_on: Servijer Mastodon herberc'hiet war %{domain}
     title: Diwar-benn
   accounts:
-    follow: Heuliañ
     followers:
       few: Heulier·ez
       many: Heulier·ez
@@ -519,9 +518,6 @@ br:
         two: "%{count} skeudenn"
     pin_errors:
       ownership: N'hallit ket spilhennañ embannadurioù ar re all
-    poll:
-      vote: Mouezhiañ
-    show_more: Diskouez muioc'h
     visibilities:
       direct: War-eeun
       public: Publik
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 63654ae70..d985a2ac4 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -7,7 +7,6 @@ ca:
     hosted_on: Mastodon allotjat a %{domain}
     title: Quant a
   accounts:
-    follow: Segueix
     followers:
       one: Seguidor
       other: Seguidors
@@ -1734,23 +1733,12 @@ ca:
     edited_at_html: Editat %{date}
     errors:
       in_reply_not_found: El tut al qual intentes respondre sembla que no existeix.
-    open_in_web: Obre en la web
     over_character_limit: Límit de caràcters de %{max} superat
     pin_errors:
       direct: Els tuts que només són visibles per als usuaris mencionats no poden ser fixats
       limit: Ja has fixat el màxim nombre de tuts
       ownership: No es pot fixar el tut d'algú altre
       reblog: No es pot fixar un impuls
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} persones"
-      total_votes:
-        one: "%{count} vot"
-        other: "%{count} vots"
-      vote: Vota
-    show_more: Mostra'n més
-    show_thread: Mostra el fil
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Directe
diff --git a/config/locales/ckb.yml b/config/locales/ckb.yml
index 3ecef4bb4..8af3d8638 100644
--- a/config/locales/ckb.yml
+++ b/config/locales/ckb.yml
@@ -7,7 +7,6 @@ ckb:
     hosted_on: مەستودۆن میوانداری کراوە لە %{domain}
     title: دەربارە
   accounts:
-    follow: شوێن کەوە
     followers:
       one: شوێنکەوتوو
       other: شوێن‌کەوتووان
@@ -938,22 +937,11 @@ ckb:
       other: 'هاشتاگەکانی ڕێگەپێنەدراوەی تێدابوو: %{tags}'
     errors:
       in_reply_not_found: ئەو دۆخەی کە تۆ هەوڵی وەڵامدانەوەی دەدەیت وادەرناکەوێت کە هەبێت.
-    open_in_web: کردنەوە لە وێب
     over_character_limit: سنووری نووسەی %{max} تێپەڕێنرا
     pin_errors:
       limit: تۆ پێشتر زۆرترین ژمارەی توتتی چەسپیوەت هەیە
       ownership: نووسراوەکانی تر ناتوانرێ بسەلمێت
       reblog: بەهێزکردن ناتوانرێت بچەسپێ
-    poll:
-      total_people:
-        one: "%{count} کەس"
-        other: "%{count} خەڵک"
-      total_votes:
-        one: "%{count} دەنگ"
-        other: "%{count} دەنگەکان"
-      vote: دەنگ
-    show_more: زیاتر پیشان بدە
-    show_thread: نیشاندانی ڕشتە
     visibilities:
       private: شوێنکەوتوانی تەنها
       private_long: تەنها بۆ شوێنکەوتوانی پیشان بدە
diff --git a/config/locales/co.yml b/config/locales/co.yml
index 7c0695a77..b072e5e4e 100644
--- a/config/locales/co.yml
+++ b/config/locales/co.yml
@@ -6,7 +6,6 @@ co:
     contact_unavailable: Micca dispunibule
     hosted_on: Mastodon allughjatu nant’à %{domain}
   accounts:
-    follow: Siguità
     followers:
       one: Abbunatu·a
       other: Abbunati
@@ -922,22 +921,11 @@ co:
       other: 'cuntene l’hashtag disattivati: %{tags}'
     errors:
       in_reply_not_found: U statutu à quellu avete pruvatu di risponde ùn sembra micca esiste.
-    open_in_web: Apre nant’à u web
     over_character_limit: site sopr’à a limita di %{max} caratteri
     pin_errors:
       limit: Avete digià puntarulatu u numeru massimale di statuti
       ownership: Pudete puntarulà solu unu di i vostri propii statuti
       reblog: Ùn pudete micca puntarulà una spartera
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} persone"
-      total_votes:
-        one: "%{count} votu"
-        other: "%{count} voti"
-      vote: Vutà
-    show_more: Vede di più
-    show_thread: Vede u filu
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direttu
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 7d4d2296c..100044287 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -7,7 +7,6 @@ cs:
     hosted_on: Mastodon na doméně %{domain}
     title: O aplikaci
   accounts:
-    follow: Sledovat
     followers:
       few: Sledující
       many: Sledujících
@@ -1721,27 +1720,12 @@ cs:
     edited_at_html: Upraven %{date}
     errors:
       in_reply_not_found: Příspěvek, na který se pokoušíte odpovědět, neexistuje.
-    open_in_web: Otevřít na webu
     over_character_limit: byl překročen limit %{max} znaků
     pin_errors:
       direct: Příspěvky viditelné pouze zmíněným uživatelům nelze připnout
       limit: Už jste si připnuli maximální počet příspěvků
       ownership: Nelze připnout příspěvek někoho jiného
       reblog: Boosty nelze připnout
-    poll:
-      total_people:
-        few: "%{count} lidé"
-        many: "%{count} lidí"
-        one: "%{count} člověk"
-        other: "%{count} lidí"
-      total_votes:
-        few: "%{count} hlasy"
-        many: "%{count} hlasů"
-        one: "%{count} hlas"
-        other: "%{count} hlasů"
-      vote: Hlasovat
-    show_more: Zobrazit více
-    show_thread: Zobrazit vlákno
     title: "%{name}: „%{quote}“"
     visibilities:
       direct: Přímé
diff --git a/config/locales/cy.yml b/config/locales/cy.yml
index 2e425bb49..9d3c0c82f 100644
--- a/config/locales/cy.yml
+++ b/config/locales/cy.yml
@@ -7,7 +7,6 @@ cy:
     hosted_on: Mastodon wedi ei weinyddu ar %{domain}
     title: Ynghylch
   accounts:
-    follow: Dilyn
     followers:
       few: Dilynwyr
       many: Dilynwyr
@@ -1860,31 +1859,12 @@ cy:
     edited_at_html: Wedi'i olygu %{date}
     errors:
       in_reply_not_found: Nid yw'n ymddangos bod y postiad rydych chi'n ceisio ei ateb yn bodoli.
-    open_in_web: Agor yn y we
     over_character_limit: wedi mynd y tu hwnt i'r terfyn nodau o %{max}
     pin_errors:
       direct: Nid oes modd pinio postiadau sy'n weladwy i ddefnyddwyr a grybwyllwyd yn unig
       limit: Rydych chi eisoes wedi pinio uchafswm nifer y postiadau
       ownership: Nid oes modd pinio postiad rhywun arall
       reblog: Nid oes modd pinio hwb
-    poll:
-      total_people:
-        few: "%{count} person"
-        many: "%{count} person"
-        one: "%{count} berson"
-        other: "%{count} person"
-        two: "%{count} person"
-        zero: "%{count} o bersonau"
-      total_votes:
-        few: "%{count} pleidlais"
-        many: "%{count} pleidlais"
-        one: "%{count} bleidlais"
-        other: "%{count} pleidlais"
-        two: "%{count} pleidlais"
-        zero: "%{count} o bleidleisiau"
-      vote: Pleidlais
-    show_more: Dangos mwy
-    show_thread: Dangos edefyn
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Uniongyrchol
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 731c1f0b4..6f781742a 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -7,7 +7,6 @@ da:
     hosted_on: Mastodon hostet på %{domain}
     title: Om
   accounts:
-    follow: Følg
     followers:
       one: Følger
       other: tilhængere
@@ -1740,23 +1739,12 @@ da:
     edited_at_html: Redigeret %{date}
     errors:
       in_reply_not_found: Indlægget, der forsøges besvaret, ser ikke ud til at eksistere.
-    open_in_web: Åbn i webbrowser
     over_character_limit: grænsen på %{max} tegn overskredet
     pin_errors:
       direct: Indlæg, som kun kan ses af omtalte brugere, kan ikke fastgøres
       limit: Maksimalt antal indlæg allerede fastgjort
       ownership: Andres indlæg kan ikke fastgøres
       reblog: Et boost kan ikke fastgøres
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} personer"
-      total_votes:
-        one: "%{count} stemme"
-        other: "%{count} stemmer"
-      vote: Stem
-    show_more: Vis flere
-    show_thread: Vis tråd
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direkte
diff --git a/config/locales/de.yml b/config/locales/de.yml
index cc049c590..040ddaaf6 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -7,7 +7,6 @@ de:
     hosted_on: Mastodon, gehostet auf %{domain}
     title: Über
   accounts:
-    follow: Folgen
     followers:
       one: Follower
       other: Follower
@@ -1740,23 +1739,12 @@ de:
     edited_at_html: 'Bearbeitet: %{date}'
     errors:
       in_reply_not_found: Der Beitrag, auf den du antworten möchtest, scheint nicht zu existieren.
-    open_in_web: Im Webinterface öffnen
     over_character_limit: Begrenzung von %{max} Zeichen überschritten
     pin_errors:
       direct: Beiträge, die nur für erwähnte Profile sichtbar sind, können nicht angeheftet werden
       limit: Du hast bereits die maximale Anzahl an Beiträgen angeheftet
       ownership: Du kannst nur eigene Beiträge anheften
       reblog: Du kannst keine geteilten Beiträge anheften
-    poll:
-      total_people:
-        one: "%{count} Stimme"
-        other: "%{count} Stimmen"
-      total_votes:
-        one: "%{count} Stimme"
-        other: "%{count} Stimmen"
-      vote: Abstimmen
-    show_more: Mehr anzeigen
-    show_thread: Thread anzeigen
     title: "%{name}: „%{quote}“"
     visibilities:
       direct: Direktnachricht
diff --git a/config/locales/el.yml b/config/locales/el.yml
index 3cb9075c3..1f408e26e 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -7,7 +7,6 @@ el:
     hosted_on: Το Mastodon φιλοξενείται στο %{domain}
     title: Σχετικά
   accounts:
-    follow: Ακολούθησε
     followers:
       one: Ακόλουθος
       other: Ακόλουθοι
@@ -1636,23 +1635,12 @@ el:
     edited_at_html: Επεξεργάστηκε στις %{date}
     errors:
       in_reply_not_found: Η ανάρτηση στην οποία προσπαθείς να απαντήσεις δεν φαίνεται να υπάρχει.
-    open_in_web: Άνοιγμα στο διαδίκτυο
     over_character_limit: υπέρβαση μέγιστου ορίου %{max} χαρακτήρων
     pin_errors:
       direct: Αναρτήσεις που είναι ορατές μόνο στους αναφερόμενους χρήστες δεν μπορούν να καρφιτσωθούν
       limit: Έχεις ήδη καρφιτσώσει το μέγιστο αριθμό επιτρεπτών αναρτήσεων
       ownership: Δεν μπορείς να καρφιτσώσεις ανάρτηση κάποιου άλλου
       reblog: Οι ενισχύσεις δεν καρφιτσώνονται
-    poll:
-      total_people:
-        one: "%{count} άτομο"
-        other: "%{count} άτομα"
-      total_votes:
-        one: "%{count} ψήφος"
-        other: "%{count} ψήφοι"
-      vote: Ψήφισε
-    show_more: Δείξε περισσότερα
-    show_thread: Εμφάνιση νήματος
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Άμεση
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml
index dc07dcff0..56255f5d7 100644
--- a/config/locales/en-GB.yml
+++ b/config/locales/en-GB.yml
@@ -7,7 +7,6 @@ en-GB:
     hosted_on: Mastodon hosted on %{domain}
     title: About
   accounts:
-    follow: Follow
     followers:
       one: Follower
       other: Followers
@@ -1740,23 +1739,12 @@ en-GB:
     edited_at_html: Edited %{date}
     errors:
       in_reply_not_found: The post you are trying to reply to does not appear to exist.
-    open_in_web: Open in web
     over_character_limit: character limit of %{max} exceeded
     pin_errors:
       direct: Posts that are only visible to mentioned users cannot be pinned
       limit: You have already pinned the maximum number of posts
       ownership: Someone else's post cannot be pinned
       reblog: A boost cannot be pinned
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} people"
-      total_votes:
-        one: "%{count} vote"
-        other: "%{count} votes"
-      vote: Vote
-    show_more: Show more
-    show_thread: Show thread
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direct
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 05300acea..b1c100da0 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -7,7 +7,6 @@ en:
     hosted_on: Mastodon hosted on %{domain}
     title: About
   accounts:
-    follow: Follow
     followers:
       one: Follower
       other: Followers
@@ -1741,23 +1740,12 @@ en:
     edited_at_html: Edited %{date}
     errors:
       in_reply_not_found: The post you are trying to reply to does not appear to exist.
-    open_in_web: Open in web
     over_character_limit: character limit of %{max} exceeded
     pin_errors:
       direct: Posts that are only visible to mentioned users cannot be pinned
       limit: You have already pinned the maximum number of posts
       ownership: Someone else's post cannot be pinned
       reblog: A boost cannot be pinned
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} people"
-      total_votes:
-        one: "%{count} vote"
-        other: "%{count} votes"
-      vote: Vote
-    show_more: Show more
-    show_thread: Show thread
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direct
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index c1873c2f2..46c6cbcf8 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -7,7 +7,6 @@ eo:
     hosted_on: "%{domain} estas nodo de Mastodon"
     title: Pri
   accounts:
-    follow: Sekvi
     followers:
       one: Sekvanto
       other: Sekvantoj
@@ -1553,23 +1552,12 @@ eo:
     edited_at_html: Redaktis je %{date}
     errors:
       in_reply_not_found: Mesaĝo kiun vi provas respondi ŝajnas ne ekzisti.
-    open_in_web: Malfermi retumile
     over_character_limit: limo de %{max} signoj transpasita
     pin_errors:
       direct: Mesaĝoj kiu videbla nun al la uzantoj ne povas alpinglitis
       limit: Vi jam atingis la maksimuman nombron de alpinglitaj mesaĝoj
       ownership: Mesaĝo de iu alia ne povas esti alpinglita
       reblog: Diskonigo ne povas esti alpinglita
-    poll:
-      total_people:
-        one: "%{count} persono"
-        other: "%{count} personoj"
-      total_votes:
-        one: "%{count} voĉdono"
-        other: "%{count} voĉdonoj"
-      vote: Voĉdoni
-    show_more: Montri pli
-    show_thread: Montri la mesaĝaron
     title: "%{name}: “%{quote}”"
     visibilities:
       direct: Rekta
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index 63d2adc47..4d60d080a 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -7,7 +7,6 @@ es-AR:
     hosted_on: Mastodon alojado en %{domain}
     title: Información
   accounts:
-    follow: Seguir
     followers:
       one: Seguidor
       other: Seguidores
@@ -1740,23 +1739,12 @@ es-AR:
     edited_at_html: Editado el %{date}
     errors:
       in_reply_not_found: El mensaje al que intentás responder no existe.
-    open_in_web: Abrir en la web
     over_character_limit: se excedió el límite de %{max} caracteres
     pin_errors:
       direct: Los mensajes que sólo son visibles para los usuarios mencionados no pueden ser fijados
       limit: Ya fijaste el número máximo de mensajes
       ownership: No se puede fijar el mensaje de otra cuenta
       reblog: No se puede fijar una adhesión
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} personas"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Votar
-    show_more: Mostrar más
-    show_thread: Mostrar hilo
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Directo
diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml
index 84663aa89..050388c18 100644
--- a/config/locales/es-MX.yml
+++ b/config/locales/es-MX.yml
@@ -7,7 +7,6 @@ es-MX:
     hosted_on: Mastodon alojado en %{domain}
     title: Acerca de
   accounts:
-    follow: Seguir
     followers:
       one: Seguidor
       other: Seguidores
@@ -1730,23 +1729,12 @@ es-MX:
     edited_at_html: Editado %{date}
     errors:
       in_reply_not_found: El estado al que intentas responder no existe.
-    open_in_web: Abrir en web
     over_character_limit: Límite de caracteres de %{max} superado
     pin_errors:
       direct: Las publicaciones que son visibles solo para los usuarios mencionados no pueden fijarse
       limit: Ya has fijado el número máximo de publicaciones
       ownership: El toot de alguien más no puede fijarse
       reblog: Un boost no puede fijarse
-    poll:
-      total_people:
-        one: persona %{count}
-        other: "%{count} gente"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Vota
-    show_more: Mostrar más
-    show_thread: Mostrar discusión
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: Directa
diff --git a/config/locales/es.yml b/config/locales/es.yml
index e245dde5d..81a547ad8 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -7,7 +7,6 @@ es:
     hosted_on: Mastodon alojado en %{domain}
     title: Acerca de
   accounts:
-    follow: Seguir
     followers:
       one: Seguidor
       other: Seguidores
@@ -1730,23 +1729,12 @@ es:
     edited_at_html: Editado %{date}
     errors:
       in_reply_not_found: La publicación a la que intentas responder no existe.
-    open_in_web: Abrir en web
     over_character_limit: Límite de caracteres de %{max} superado
     pin_errors:
       direct: Las publicaciones que son visibles solo para los usuarios mencionados no pueden fijarse
       limit: Ya has fijado el número máximo de publicaciones
       ownership: La publicación de otra persona no puede fijarse
       reblog: Un boost no puede fijarse
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} personas"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Vota
-    show_more: Mostrar más
-    show_thread: Mostrar discusión
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: Directa
diff --git a/config/locales/et.yml b/config/locales/et.yml
index 88d48fefc..d8cdbf414 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -7,7 +7,6 @@ et:
     hosted_on: Mastodon majutatud %{domain}-is
     title: Teave
   accounts:
-    follow: Jälgi
     followers:
       one: Jälgija
       other: Jälgijaid
@@ -1701,23 +1700,12 @@ et:
     edited_at_html: Muudetud %{date}
     errors:
       in_reply_not_found: Postitus, millele üritad vastata, ei näi enam eksisteerivat.
-    open_in_web: Ava veebis
     over_character_limit: tähtmärkide limiit %{max} ületatud
     pin_errors:
       direct: Ei saa kinnitada postitusi, mis on nähtavad vaid mainitud kasutajatele
       limit: Kinnitatud on juba maksimaalne arv postitusi
       ownership: Kellegi teise postitust ei saa kinnitada
       reblog: Jagamist ei saa kinnitada
-    poll:
-      total_people:
-        one: "%{count} inimene"
-        other: "%{count} inimest"
-      total_votes:
-        one: "%{count} hääl"
-        other: "%{count} häält"
-      vote: Hääleta
-    show_more: Näita rohkem
-    show_thread: Kuva lõim
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Otsene
diff --git a/config/locales/eu.yml b/config/locales/eu.yml
index e5ae0ab79..626003399 100644
--- a/config/locales/eu.yml
+++ b/config/locales/eu.yml
@@ -7,7 +7,6 @@ eu:
     hosted_on: Mastodon %{domain} domeinuan ostatatua
     title: Honi buruz
   accounts:
-    follow: Jarraitu
     followers:
       one: Jarraitzaile
       other: jarraitzaile
@@ -1639,23 +1638,12 @@ eu:
     edited_at_html: Editatua %{date}
     errors:
       in_reply_not_found: Erantzuten saiatu zaren bidalketa antza ez da existitzen.
-    open_in_web: Ireki web-ean
     over_character_limit: "%{max}eko karaktere muga gaindituta"
     pin_errors:
       direct: Aipatutako erabiltzaileentzat soilik ikusgai dauden bidalketak ezin dira finkatu
       limit: Gehienez finkatu daitekeen bidalketa kopurua finkatu duzu jada
       ownership: Ezin duzu beste norbaiten bidalketa bat finkatu
       reblog: Bultzada bat ezin da finkatu
-    poll:
-      total_people:
-        one: pertsona %{count}
-        other: "%{count} pertsona"
-      total_votes:
-        one: Boto %{count}
-        other: "%{count} boto"
-      vote: Bozkatu
-    show_more: Erakutsi gehiago
-    show_thread: Erakutsi haria
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Zuzena
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index ce8a61e3f..f2fe134e3 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -7,7 +7,6 @@ fa:
     hosted_on: ماستودون، میزبانی‌شده روی %{domain}
     title: درباره
   accounts:
-    follow: پیگیری
     followers:
       one: پیگیر
       other: پیگیر
@@ -1404,23 +1403,12 @@ fa:
     edited_at_html: ویراسته در %{date}
     errors:
       in_reply_not_found: به نظر نمی‌رسد وضعیتی که می‌خواهید به آن پاسخ دهید، وجود داشته باشد.
-    open_in_web: گشودن در وب
     over_character_limit: از حد مجاز %{max} حرف فراتر رفتید
     pin_errors:
       direct: فرسته‌هایی که فقط برای کاربران اشاره شده نمایانند نمی‌توانند سنجاق شوند
       limit: از این بیشتر نمی‌شود نوشته‌های ثابت داشت
       ownership: نوشته‌های دیگران را نمی‌توان ثابت کرد
       reblog: تقویت نمی‌تواند سنجاق شود
-    poll:
-      total_people:
-        one: "%{count} نفر"
-        other: "%{count} نفر"
-      total_votes:
-        one: "%{count} رأی"
-        other: "%{count} رأی"
-      vote: رأی
-    show_more: نمایش
-    show_thread: نمایش رشته
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: مستقیم
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 5c39346a9..30837b600 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -7,7 +7,6 @@ fi:
     hosted_on: Mastodon palvelimella %{domain}
     title: Tietoja
   accounts:
-    follow: Seuraa
     followers:
       one: seuraaja
       other: seuraajaa
@@ -1738,23 +1737,12 @@ fi:
     edited_at_html: Muokattu %{date}
     errors:
       in_reply_not_found: Julkaisua, johon yrität vastata, ei näytä olevan olemassa.
-    open_in_web: Avaa selaimessa
     over_character_limit: merkkimäärän rajoitus %{max} ylitetty
     pin_errors:
       direct: Vain mainituille käyttäjille näkyviä julkaisuja ei voi kiinnittää
       limit: Olet jo kiinnittänyt enimmäismäärän julkaisuja
       ownership: Muiden julkaisuja ei voi kiinnittää
       reblog: Tehostusta ei voi kiinnittää
-    poll:
-      total_people:
-        one: "%{count} käyttäjä"
-        other: "%{count} käyttäjää"
-      total_votes:
-        one: "%{count} ääni"
-        other: "%{count} ääntä"
-      vote: Äänestä
-    show_more: Näytä lisää
-    show_thread: Näytä ketju
     title: "%{name}: ”%{quote}”"
     visibilities:
       direct: Suoraan
diff --git a/config/locales/fo.yml b/config/locales/fo.yml
index 6d7b38e18..d5127b4ad 100644
--- a/config/locales/fo.yml
+++ b/config/locales/fo.yml
@@ -7,7 +7,6 @@ fo:
     hosted_on: Mastodon hýst á %{domain}
     title: Um
   accounts:
-    follow: Fylg
     followers:
       one: Fylgjari
       other: Fylgjarar
@@ -1740,23 +1739,12 @@ fo:
     edited_at_html: Rættað %{date}
     errors:
       in_reply_not_found: Posturin, sum tú roynir at svara, sýnist ikki at finnast.
-    open_in_web: Lat upp á vevinum
     over_character_limit: mesta tal av teknum, %{max}, rokkið
     pin_errors:
       direct: Postar, sum einans eru sjónligir hjá nevndum brúkarum, kunnu ikki festast
       limit: Tú hevur longu fest loyvda talið av postum
       ownership: Postar hjá øðrum kunnu ikki festast
       reblog: Ein stimbran kann ikki festast
-    poll:
-      total_people:
-        one: "%{count} fólk"
-        other: "%{count} fólk"
-      total_votes:
-        one: "%{count} atkvøða"
-        other: "%{count} atkvøður"
-      vote: Atkvøð
-    show_more: Vís meira
-    show_thread: Vís tráð
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Beinleiðis
diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml
index 27e09d1f9..a47d7447a 100644
--- a/config/locales/fr-CA.yml
+++ b/config/locales/fr-CA.yml
@@ -7,7 +7,6 @@ fr-CA:
     hosted_on: Serveur Mastodon hébergé sur %{domain}
     title: À propos
   accounts:
-    follow: Suivre
     followers:
       one: Abonné·e
       other: Abonné·e·s
@@ -1711,23 +1710,12 @@ fr-CA:
     edited_at_html: Édité le %{date}
     errors:
       in_reply_not_found: Le message auquel vous essayez de répondre ne semble pas exister.
-    open_in_web: Ouvrir sur le web
     over_character_limit: limite de %{max} caractères dépassée
     pin_errors:
       direct: Les messages qui ne sont visibles que pour les utilisateur·rice·s mentionné·e·s ne peuvent pas être épinglés
       limit: Vous avez déjà épinglé le nombre maximum de messages
       ownership: Vous ne pouvez pas épingler un message ne vous appartenant pas
       reblog: Un partage ne peut pas être épinglé
-    poll:
-      total_people:
-        one: "%{count} personne"
-        other: "%{count} personnes"
-      total_votes:
-        one: "%{count} vote"
-        other: "%{count} votes"
-      vote: Voter
-    show_more: Déplier
-    show_thread: Afficher le fil de discussion
     title: "%{name} : « %{quote} »"
     visibilities:
       direct: Direct
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 055b50900..b2c692ea6 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -7,7 +7,6 @@ fr:
     hosted_on: Serveur Mastodon hébergé sur %{domain}
     title: À propos
   accounts:
-    follow: Suivre
     followers:
       one: Abonné·e
       other: Abonné·e·s
@@ -1711,23 +1710,12 @@ fr:
     edited_at_html: Modifié le %{date}
     errors:
       in_reply_not_found: Le message auquel vous essayez de répondre ne semble pas exister.
-    open_in_web: Ouvrir sur le web
     over_character_limit: limite de %{max} caractères dépassée
     pin_errors:
       direct: Les messages qui ne sont visibles que pour les utilisateur·rice·s mentionné·e·s ne peuvent pas être épinglés
       limit: Vous avez déjà épinglé le nombre maximum de messages
       ownership: Vous ne pouvez pas épingler un message ne vous appartenant pas
       reblog: Un partage ne peut pas être épinglé
-    poll:
-      total_people:
-        one: "%{count} personne"
-        other: "%{count} personnes"
-      total_votes:
-        one: "%{count} vote"
-        other: "%{count} votes"
-      vote: Voter
-    show_more: Déplier
-    show_thread: Afficher le fil de discussion
     title: "%{name} : « %{quote} »"
     visibilities:
       direct: Direct
diff --git a/config/locales/fy.yml b/config/locales/fy.yml
index 27fcaf3af..6afdecd55 100644
--- a/config/locales/fy.yml
+++ b/config/locales/fy.yml
@@ -7,7 +7,6 @@ fy:
     hosted_on: Mastodon op %{domain}
     title: Oer
   accounts:
-    follow: Folgje
     followers:
       one: Folger
       other: Folgers
@@ -1730,23 +1729,12 @@ fy:
     edited_at_html: Bewurke op %{date}
     errors:
       in_reply_not_found: It berjocht wêrop jo probearje te reagearjen liket net te bestean.
-    open_in_web: Yn de webapp iepenje
     over_character_limit: Oer de limyt fan %{max} tekens
     pin_errors:
       direct: Berjochten dy’t allinnich sichtber binne foar fermelde brûkers kinne net fêstset wurde
       limit: Jo hawwe it maksimaal tal berjochten al fêstmakke
       ownership: In berjocht fan in oar kin net fêstmakke wurde
       reblog: In boost kin net fêstset wurde
-    poll:
-      total_people:
-        one: "%{count} persoan"
-        other: "%{count} persoanen"
-      total_votes:
-        one: "%{count} stim"
-        other: "%{count} stimmen"
-      vote: Stimme
-    show_more: Mear toane
-    show_thread: Petear toane
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direkt
diff --git a/config/locales/ga.yml b/config/locales/ga.yml
index 870f79cba..09d5f7ae1 100644
--- a/config/locales/ga.yml
+++ b/config/locales/ga.yml
@@ -7,7 +7,6 @@ ga:
     hosted_on: Mastodon arna óstáil ar %{domain}
     title: Maidir le
   accounts:
-    follow: Lean
     followers:
       few: Leantóirí
       many: Leantóirí
@@ -1823,29 +1822,12 @@ ga:
     edited_at_html: "%{date} curtha in eagar"
     errors:
       in_reply_not_found: Is cosúil nach ann don phostáil a bhfuil tú ag iarraidh freagra a thabhairt air.
-    open_in_web: Oscail i ngréasán
     over_character_limit: teorainn carachtar %{max} sáraithe
     pin_errors:
       direct: Ní féidir postálacha nach bhfuil le feiceáil ach ag úsáideoirí luaite a phinnáil
       limit: Tá uaslíon na bpostálacha pinn agat cheana féin
       ownership: Ní féidir postáil duine éigin eile a phionnáil
       reblog: Ní féidir treisiú a phinnáil
-    poll:
-      total_people:
-        few: "%{count} daoine"
-        many: "%{count} daoine"
-        one: "%{count} duine"
-        other: "%{count} daoine"
-        two: "%{count} daoine"
-      total_votes:
-        few: "%{count} vótaí"
-        many: "%{count} vótaí"
-        one: "%{count} vóta"
-        other: "%{count} vótaí"
-        two: "%{count} vótaí"
-      vote: Vótáil
-    show_more: Taispeáin níos mó
-    show_thread: Taispeáin snáithe
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Díreach
diff --git a/config/locales/gd.yml b/config/locales/gd.yml
index dda918d15..3f30d3782 100644
--- a/config/locales/gd.yml
+++ b/config/locales/gd.yml
@@ -7,7 +7,6 @@ gd:
     hosted_on: Mastodon ’ga òstadh air %{domain}
     title: Mu dhèidhinn
   accounts:
-    follow: Lean
     followers:
       few: Luchd-leantainn
       one: Neach-leantainn
@@ -1793,27 +1792,12 @@ gd:
     edited_at_html: Air a dheasachadh %{date}
     errors:
       in_reply_not_found: Tha coltas nach eil am post dhan a tha thu airson freagairt ann.
-    open_in_web: Fosgail air an lìon
     over_character_limit: chaidh thu thar crìoch charactaran de %{max}
     pin_errors:
       direct: Chan urrainn dhut post a phrìneachadh nach fhaic ach na cleachdaichean le iomradh orra
       limit: Tha an àireamh as motha de phostaichean prìnichte agad a tha ceadaichte
       ownership: Chan urrainn dhut post càich a phrìneachadh
       reblog: Chan urrainn dhut brosnachadh a phrìneachadh
-    poll:
-      total_people:
-        few: "%{count} daoine"
-        one: "%{count} neach"
-        other: "%{count} duine"
-        two: "%{count} neach"
-      total_votes:
-        few: "%{count} bhòtaichean"
-        one: "%{count} bhòt"
-        other: "%{count} bhòt"
-        two: "%{count} bhòt"
-      vote: Bhòt
-    show_more: Seall barrachd dheth
-    show_thread: Seall an snàithlean
     title: "%{name}: “%{quote}”"
     visibilities:
       direct: Dìreach
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index d275b844f..58fd2d9ba 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -7,7 +7,6 @@ gl:
     hosted_on: Mastodon aloxado en %{domain}
     title: Sobre
   accounts:
-    follow: Seguir
     followers:
       one: Seguidora
       other: Seguidoras
@@ -1740,23 +1739,12 @@ gl:
     edited_at_html: Editado %{date}
     errors:
       in_reply_not_found: A publicación á que tentas responder semella que non existe.
-    open_in_web: Abrir na web
     over_character_limit: Excedeu o límite de caracteres %{max}
     pin_errors:
       direct: As publicacións que só son visibles para as usuarias mencionadas non se poden fixar
       limit: Xa fixaches o número máximo permitido de publicacións
       ownership: Non podes fixar a publicación doutra usuaria
       reblog: Non se poden fixar as mensaxes promovidas
-    poll:
-      total_people:
-        one: "%{count} persoa"
-        other: "%{count} persoas"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Votar
-    show_more: Mostrar máis
-    show_thread: Amosar fío
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Directa
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 341e2bf02..7a2d0a1d9 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -7,7 +7,6 @@ he:
     hosted_on: מסטודון שיושב בכתובת %{domain}
     title: אודות
   accounts:
-    follow: לעקוב
     followers:
       many: עוקבים
       one: עוקב
@@ -1800,27 +1799,12 @@ he:
     edited_at_html: נערך ב-%{date}
     errors:
       in_reply_not_found: נראה שההודעה שאת/ה מנסה להגיב לה לא קיימת.
-    open_in_web: פתח ברשת
     over_character_limit: חריגה מגבול התווים של %{max}
     pin_errors:
       direct: לא ניתן לקבע הודעות שנראותן מוגבלת למכותבים בלבד
       limit: הגעת למספר המירבי של ההודעות המוצמדות
       ownership: הודעות של אחרים לא יכולות להיות מוצמדות
       reblog: אין אפשרות להצמיד הדהודים
-    poll:
-      total_people:
-        many: "%{count} אנשים"
-        one: איש/ה %{count}
-        other: "%{count} אנשים"
-        two: "%{count} אנשים"
-      total_votes:
-        many: "%{count} קולות"
-        one: קול %{count}
-        other: "%{count} קולות"
-        two: "%{count} קולות"
-      vote: הצבעה
-    show_more: עוד
-    show_thread: הצג שרשור
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: ישיר
diff --git a/config/locales/hi.yml b/config/locales/hi.yml
index 60b500c7e..37df2afe1 100644
--- a/config/locales/hi.yml
+++ b/config/locales/hi.yml
@@ -5,7 +5,6 @@ hi:
     contact_unavailable: लागू नहीं है
     title: के बारे में
   accounts:
-    follow: अनुसरे
     following: फ़ॉलो कर रहे हैं
     instance_actor_flash: यह खाता आभासी है जो सर्वर को दिखाने के लिये है और ये किसी व्यक्तिका प्रतिनिधित्व नहि करता। यह सिर्फ देखरेख के हेतु से कार्यरत है और इसको निलंबित करने कि आवश्यकता नहि है।
     last_active: आखिरि बार इस वक्त सक्रिय थे
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index 6a67ea012..7dacf2007 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -5,7 +5,6 @@ hr:
     contact_missing: Nije postavljeno
     title: O aplikaciji
   accounts:
-    follow: Prati
     following: Praćenih
     last_active: posljednja aktivnost
     nothing_here: Ovdje nema ničeg!
@@ -215,20 +214,7 @@ hr:
     statuses_cleanup: Automatsko brisanje postova
     two_factor_authentication: Dvofaktorska autentifikacija
   statuses:
-    open_in_web: Otvori na webu
     over_character_limit: prijeđeno je ograničenje od %{max} znakova
-    poll:
-      total_people:
-        few: "%{count} osobe"
-        one: "%{count} osoba"
-        other: "%{count} ljudi"
-      total_votes:
-        few: "%{count} glasa"
-        one: "%{count} glas"
-        other: "%{count} glasova"
-      vote: Glasaj
-    show_more: Prikaži više
-    show_thread: Prikaži nit
     visibilities:
       private: Samo pratitelji
       public: Javno
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index e29472f8d..10c7506b0 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -7,7 +7,6 @@ hu:
     hosted_on: "%{domain} Mastodon-kiszolgáló"
     title: Névjegy
   accounts:
-    follow: Követés
     followers:
       one: Követő
       other: Követő
@@ -1740,23 +1739,12 @@ hu:
     edited_at_html: 'Szerkesztve: %{date}'
     errors:
       in_reply_not_found: Már nem létezik az a bejegyzés, melyre válaszolni szeretnél.
-    open_in_web: Megnyitás a weben
     over_character_limit: túllépted a maximális %{max} karakteres keretet
     pin_errors:
       direct: A csak a megemlített felhasználók számára látható bejegyzések nem tűzhetők ki
       limit: Elérted a kitűzhető bejegyzések maximális számát
       ownership: Nem tűzheted ki valaki más bejegyzését
       reblog: Megtolt bejegyzést nem tudsz kitűzni
-    poll:
-      total_people:
-        one: "%{count} személy"
-        other: "%{count} személy"
-      total_votes:
-        one: "%{count} szavazat"
-        other: "%{count} szavazat"
-      vote: Szavazás
-    show_more: Több megjelenítése
-    show_thread: Szál mutatása
     title: "%{name}: „%{quote}”"
     visibilities:
       direct: Közvetlen
diff --git a/config/locales/hy.yml b/config/locales/hy.yml
index c7128a2a4..80dbc7799 100644
--- a/config/locales/hy.yml
+++ b/config/locales/hy.yml
@@ -7,7 +7,6 @@ hy:
     hosted_on: Մաստոդոնը տեղակայուած է %{domain}ում
     title: Մասին
   accounts:
-    follow: Հետևել
     followers:
       one: Հետեւորդ
       other: Հետևորդներ
@@ -782,18 +781,7 @@ hy:
         other: "%{count} վիդեո"
     content_warning: Նախազգուշացում։ %{warning}
     edited_at_html: Խմբագրուած՝ %{date}
-    open_in_web: Բացել վէբում
     over_character_limit: "%{max} նիշի սահմանը գերազանցուած է"
-    poll:
-      total_people:
-        one: "%{count} մարդ"
-        other: "%{count} մարդիկ"
-      total_votes:
-        one: "%{count} ձայն"
-        other: "%{count} ձայներ"
-      vote: Քուէարկել
-    show_more: Աւելին
-    show_thread: Բացել շղթան
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Հասցէագրուած
diff --git a/config/locales/ia.yml b/config/locales/ia.yml
index 87789562f..957bae399 100644
--- a/config/locales/ia.yml
+++ b/config/locales/ia.yml
@@ -7,7 +7,6 @@ ia:
     hosted_on: Mastodon albergate sur %{domain}
     title: A proposito
   accounts:
-    follow: Sequer
     followers:
       one: Sequitor
       other: Sequitores
@@ -1723,23 +1722,12 @@ ia:
     edited_at_html: Modificate le %{date}
     errors:
       in_reply_not_found: Le message a que tu tenta responder non pare exister.
-    open_in_web: Aperir sur le web
     over_character_limit: limite de characteres de %{max} excedite
     pin_errors:
       direct: Messages que es solo visibile a usatores mentionate non pote esser appunctate
       limit: Tu ha jam appunctate le maxime numero de messages
       ownership: Le message de alcuno altere non pote esser appunctate
       reblog: Un impulso non pote esser affixate
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} personas"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Votar
-    show_more: Monstrar plus
-    show_thread: Monstrar discussion
     title: "%{name}: “%{quote}”"
     visibilities:
       direct: Directe
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 575daddca..222d2b568 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -7,7 +7,6 @@ id:
     hosted_on: Mastodon dihosting di %{domain}
     title: Tentang
   accounts:
-    follow: Ikuti
     followers:
       other: Pengikut
     following: Mengikuti
@@ -1378,21 +1377,12 @@ id:
     edited_at_html: Diedit %{date}
     errors:
       in_reply_not_found: Status yang ingin Anda balas sudah tidak ada.
-    open_in_web: Buka di web
     over_character_limit: melebihi %{max} karakter
     pin_errors:
       direct: Kiriman yang hanya terlihat oleh pengguna yang disebutkan tidak dapat disematkan
       limit: Anda sudah mencapai jumlah maksimum kiriman yang dapat disematkan
       ownership: Kiriman orang lain tidak bisa disematkan
       reblog: Boost tidak bisa disematkan
-    poll:
-      total_people:
-        other: "%{count} orang"
-      total_votes:
-        other: "%{count} memilih"
-      vote: Pilih
-    show_more: Tampilkan selengkapnya
-    show_thread: Tampilkan utas
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Langsung
diff --git a/config/locales/ie.yml b/config/locales/ie.yml
index 1529ea04b..6a79686f4 100644
--- a/config/locales/ie.yml
+++ b/config/locales/ie.yml
@@ -7,7 +7,6 @@ ie:
     hosted_on: Mastodon logiat che %{domain}
     title: Pri
   accounts:
-    follow: Sequer
     followers:
       one: Sequitor
       other: Sequitores
@@ -1637,23 +1636,12 @@ ie:
     edited_at_html: Modificat ye %{date}
     errors:
       in_reply_not_found: Li posta a quel tu prova responder ne sembla exister.
-    open_in_web: Aperter in web
     over_character_limit: límite de carácteres de %{max} transpassat
     pin_errors:
       direct: On ne posse pinglar postas queles es visibil solmen a mentionat usatores
       limit: Tu ja ha pinglat li maxim númere de postas
       ownership: On ne posse pinglar li posta de un altri person
       reblog: On ne posse pinglar un boost
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} persones"
-      total_votes:
-        one: "%{count} vote"
-        other: "%{count} votes"
-      vote: Votar
-    show_more: Monstrar plu
-    show_thread: Monstrar fil
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: Direct
diff --git a/config/locales/io.yml b/config/locales/io.yml
index d1b9aef3e..dbbe228e7 100644
--- a/config/locales/io.yml
+++ b/config/locales/io.yml
@@ -7,7 +7,6 @@ io:
     hosted_on: Mastodon hostigesas che %{domain}
     title: Pri co
   accounts:
-    follow: Sequar
     followers:
       one: Sequanto
       other: Sequanti
@@ -1590,23 +1589,12 @@ io:
     edited_at_html: Modifikesis ye %{date}
     errors:
       in_reply_not_found: Posto quon vu probas respondar semblas ne existas.
-    open_in_web: Apertar retnavigile
     over_character_limit: limito de %{max} signi ecesita
     pin_errors:
       direct: Posti quo povas videsar nur mencionita uzanti ne povas pinglagesar
       limit: Vu ja pinglagis maxima posti
       ownership: Posto di altra persono ne povas pinglagesar
       reblog: Repeto ne povas pinglizesar
-    poll:
-      total_people:
-        one: "%{count} persono"
-        other: "%{count} personi"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} voti"
-      vote: Votez
-    show_more: Montrar plue
-    show_thread: Montrez postaro
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direta
diff --git a/config/locales/is.yml b/config/locales/is.yml
index 5fa147e8d..78dfd6048 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -7,7 +7,6 @@ is:
     hosted_on: Mastodon hýst á %{domain}
     title: Um hugbúnaðinn
   accounts:
-    follow: Fylgjast með
     followers:
       one: fylgjandi
       other: fylgjendur
@@ -1744,23 +1743,12 @@ is:
     edited_at_html: Breytt %{date}
     errors:
       in_reply_not_found: Færslan sem þú ert að reyna að svara að er líklega ekki til.
-    open_in_web: Opna í vafra
     over_character_limit: hámarksfjölda stafa (%{max}) náð
     pin_errors:
       direct: Ekki er hægt að festa færslur sem einungis eru sýnilegar þeim notendum sem minnst er á
       limit: Þú hefur þegar fest leyfilegan hámarksfjölda færslna
       ownership: Færslur frá einhverjum öðrum er ekki hægt að festa
       reblog: Ekki er hægt að festa endurbirtingu
-    poll:
-      total_people:
-        one: "%{count} aðili"
-        other: "%{count} aðilar"
-      total_votes:
-        one: "%{count} atkvæði"
-        other: "%{count} atkvæði"
-      vote: Greiða atkvæði
-    show_more: Sýna meira
-    show_thread: Birta þráð
     title: "%{name}: „%{quote}‟"
     visibilities:
       direct: Beint
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 7de24fe25..792add14e 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -7,7 +7,6 @@ it:
     hosted_on: Mastodon ospitato su %{domain}
     title: Info
   accounts:
-    follow: Segui
     followers:
       one: Seguace
       other: Seguaci
@@ -1742,23 +1741,12 @@ it:
     edited_at_html: Modificato il %{date}
     errors:
       in_reply_not_found: Il post a cui stai tentando di rispondere non sembra esistere.
-    open_in_web: Apri sul Web
     over_character_limit: Limite caratteri superato di %{max}
     pin_errors:
       direct: I messaggi visibili solo agli utenti citati non possono essere fissati in cima
       limit: Hai già fissato in cima il massimo numero di post
       ownership: Non puoi fissare in cima un post di qualcun altro
       reblog: Un toot condiviso non può essere fissato in cima
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} persone"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} voti"
-      vote: Vota
-    show_more: Mostra di più
-    show_thread: Mostra thread
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Diretto
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 3f50b0d7a..13f59e981 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -7,7 +7,6 @@ ja:
     hosted_on: Mastodon hosted on %{domain}
     title: このサーバーについて
   accounts:
-    follow: フォロー
     followers:
       other: フォロワー
     following: フォロー中
@@ -1700,21 +1699,12 @@ ja:
     edited_at_html: "%{date} 編集済み"
     errors:
       in_reply_not_found: あなたが返信しようとしている投稿は存在しないようです。
-    open_in_web: Webで開く
     over_character_limit: 上限は%{max}文字です
     pin_errors:
       direct: 返信したユーザーのみに表示される投稿はピン留めできません
       limit: 固定できる投稿数の上限に達しました
       ownership: 他人の投稿を固定することはできません
       reblog: ブーストを固定することはできません
-    poll:
-      total_people:
-        other: "%{count}人"
-      total_votes:
-        other: "%{count}票"
-      vote: 投票
-    show_more: もっと見る
-    show_thread: スレッドを表示
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: ダイレクト
diff --git a/config/locales/ka.yml b/config/locales/ka.yml
index 97b56ea35..576937507 100644
--- a/config/locales/ka.yml
+++ b/config/locales/ka.yml
@@ -6,7 +6,6 @@ ka:
     contact_unavailable: მიუწ.
     hosted_on: მასტოდონს მასპინძლობს %{domain}
   accounts:
-    follow: გაყევი
     following: მიჰყვება
     nothing_here: აქ არაფერია!
     pin_errors:
@@ -430,13 +429,11 @@ ka:
     disallowed_hashtags:
       one: 'მოიცავდა აკრძალულ ჰეშტეგს: %{tags}'
       other: 'მოიცავს აკრძალულ ჰეშტეგს: %{tags}'
-    open_in_web: ვებში გახნსა
     over_character_limit: ნიშნების ლიმიტი გადასცდა %{max}-ს
     pin_errors:
       limit: ტუტების მაქსიმალური რაოდენობა უკვე აპინეთ
       ownership: სხვისი ტუტი ვერ აიპინება
       reblog: ბუსტი ვერ აიპინება
-    show_more: მეტის ჩვენება
     visibilities:
       private: მხოლოდ-მიმდევრები
       private_long: აჩვენე მხოლოდ მიმდევრებს
diff --git a/config/locales/kab.yml b/config/locales/kab.yml
index 3aed6c55e..7c3d52670 100644
--- a/config/locales/kab.yml
+++ b/config/locales/kab.yml
@@ -7,7 +7,6 @@ kab:
     hosted_on: Maṣṭudun yersen deg %{domain}
     title: Ɣef
   accounts:
-    follow: Ḍfeṛ
     followers:
       one: Umeḍfaṛ
       other: Imeḍfaṛen
@@ -798,17 +797,6 @@ kab:
         one: "%{count} n tbidyutt"
         other: "%{count} n tbidyutin"
     edited_at_html: Tettwaẓreg ass n %{date}
-    open_in_web: Ldi deg Web
-    poll:
-      total_people:
-        one: "%{count} n wemdan"
-        other: "%{count} n yemdanen"
-      total_votes:
-        one: "%{count} n wedɣar"
-        other: "%{count} n yedɣaren"
-      vote: Dɣeṛ
-    show_more: Ssken-d ugar
-    show_thread: Ssken-d lxiḍ
     title: '%{name} : "%{quote}"'
     visibilities:
       direct: Usrid
diff --git a/config/locales/kk.yml b/config/locales/kk.yml
index f89bdee62..67969d4d6 100644
--- a/config/locales/kk.yml
+++ b/config/locales/kk.yml
@@ -6,7 +6,6 @@ kk:
     contact_unavailable: Белгісіз
     hosted_on: Mastodon орнатылған %{domain} доменінде
   accounts:
-    follow: Жазылу
     followers:
       one: Оқырман
       other: Оқырман
@@ -647,22 +646,11 @@ kk:
     disallowed_hashtags:
       one: 'рұқсат етілмеген хэштег: %{tags}'
       other: 'рұқсат етілмеген хэштегтер: %{tags}'
-    open_in_web: Вебте ашу
     over_character_limit: "%{max} максимум таңбадан асып кетті"
     pin_errors:
       limit: Жабыстырылатын жазба саны максимумынан асты
       ownership: Біреудің жазбасы жабыстырылмайды
       reblog: Бөлісілген жазба жабыстырылмайды
-    poll:
-      total_people:
-        one: "%{count} адам"
-        other: "%{count} адам"
-      total_votes:
-        one: "%{count} дауыс"
-        other: "%{count} дауыс"
-      vote: Дауыс беру
-    show_more: Тағы әкел
-    show_thread: Тақырыпты көрсет
     visibilities:
       private: Тек оқырмандарға
       private_long: Тек оқырмандарға ғана көрінеді
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index ab377df30..cbcc09a4d 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -7,7 +7,6 @@ ko:
     hosted_on: "%{domain}에서 호스팅 되는 마스토돈"
     title: 정보
   accounts:
-    follow: 팔로우
     followers:
       other: 팔로워
     following: 팔로잉
@@ -1705,21 +1704,12 @@ ko:
     edited_at_html: "%{date}에 편집됨"
     errors:
       in_reply_not_found: 답장하려는 게시물이 존재하지 않습니다.
-    open_in_web: Web으로 열기
     over_character_limit: 최대 %{max}자까지 입력할 수 있습니다
     pin_errors:
       direct: 멘션된 사용자들에게만 보이는 게시물은 고정될 수 없습니다
       limit: 이미 너무 많은 게시물을 고정했습니다
       ownership: 다른 사람의 게시물은 고정될 수 없습니다
       reblog: 부스트는 고정될 수 없습니다
-    poll:
-      total_people:
-        other: "%{count}명"
-      total_votes:
-        other: "%{count} 명 투표함"
-      vote: 투표
-    show_more: 더 보기
-    show_thread: 글타래 보기
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: 다이렉트
diff --git a/config/locales/ku.yml b/config/locales/ku.yml
index 20fe6cf6d..6b80e32ba 100644
--- a/config/locales/ku.yml
+++ b/config/locales/ku.yml
@@ -7,7 +7,6 @@ ku:
     hosted_on: Mastodon li ser %{domain} tê pêşkêşkirin
     title: Derbar
   accounts:
-    follow: Bişopîne
     followers:
       one: Şopîner
       other: Şopîner
@@ -1404,23 +1403,12 @@ ku:
     edited_at_html: Di %{date} de hate serrastkirin
     errors:
       in_reply_not_found: Ew şandiya ku tu dikî nakî bersivê bide xuya nake an jî hatiye jêbirin.
-    open_in_web: Di tevnê de veke
     over_character_limit: sînorê karakterê %{max} derbas kir
     pin_errors:
       direct: Şandiyên ku tenê ji bikarhênerên qalkirî re têne xuyangkirin, nayê derzîkirin
       limit: Jixwe te mezintirîn hejmara şandîyên xwe derzî kir
       ownership: Şandiya kesekî din nay derzî kirin
       reblog: Ev şandî nayê derzî kirin
-    poll:
-      total_people:
-        one: "%{count} kes"
-        other: "%{count} kes"
-      total_votes:
-        one: "%{count} deng"
-        other: "%{count} deng"
-      vote: Deng bide
-    show_more: Bêtir nîşan bide
-    show_thread: Mijarê nîşan bide
     title: "%{name}%{quote}"
     visibilities:
       direct: Rasterast
diff --git a/config/locales/la.yml b/config/locales/la.yml
index d3733df93..cc92bf6d2 100644
--- a/config/locales/la.yml
+++ b/config/locales/la.yml
@@ -7,7 +7,6 @@ la:
     hosted_on: Mastodon in %{domain} hospitātum
     title: De
   accounts:
-    follow: Sequere
     followers:
       one: Sectātor
       other: Sectātōrēs
diff --git a/config/locales/lad.yml b/config/locales/lad.yml
index 164967159..2f5eb1553 100644
--- a/config/locales/lad.yml
+++ b/config/locales/lad.yml
@@ -7,7 +7,6 @@ lad:
     hosted_on: Mastodon balabayado en %{domain}
     title: Sovre mozotros
   accounts:
-    follow: Sige
     followers:
       one: Suivante
       other: Suivantes
@@ -1677,23 +1676,12 @@ lad:
     edited_at_html: Editado %{date}
     errors:
       in_reply_not_found: La publikasion a la ke aprovas arispondir no egziste.
-    open_in_web: Avre en web
     over_character_limit: limito de karakteres de %{max} superado
     pin_errors:
       direct: Las publikasyones ke son vizivles solo para los utilizadores enmentados no pueden fiksarse
       limit: Ya tienes fiksado el numero maksimo de publikasyones
       ownership: La publikasyon de otra persona no puede fiksarse
       reblog: No se puede fixar una repartajasyon
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} personas"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Vota
-    show_more: Amostra mas
-    show_thread: Amostra diskusyon
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direkto
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 07e8ba75a..42495053e 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -7,7 +7,6 @@ lt:
     hosted_on: Mastodon talpinamas %{domain}
     title: Apie
   accounts:
-    follow: Sekti
     followers:
       few: Sekėjai
       many: Sekėjo
@@ -1107,16 +1106,11 @@ lt:
         other: "%{count} vaizdų"
     boosted_from_html: Pakelta iš %{acct_link}
     content_warning: 'Turinio įspėjimas: %{warning}'
-    open_in_web: Atidaryti naudojan Web
     over_character_limit: pasiektas %{max} simbolių limitas
     pin_errors:
       limit: Jūs jau prisegėte maksimalų toot'ų skaičų
       ownership: Kitų vartotojų toot'ai negali būti prisegti
       reblog: Pakeltos žinutės negali būti prisegtos
-    poll:
-      vote: Balsuoti
-    show_more: Rodyti daugiau
-    show_thread: Rodyti giją
     visibilities:
       private: Tik sekėjams
       private_long: rodyti tik sekėjams
diff --git a/config/locales/lv.yml b/config/locales/lv.yml
index 5dd6ff9e1..09e6b9ba0 100644
--- a/config/locales/lv.yml
+++ b/config/locales/lv.yml
@@ -7,7 +7,6 @@ lv:
     hosted_on: Mastodon mitināts %{domain}
     title: Par
   accounts:
-    follow: Sekot
     followers:
       one: Sekotājs
       other: Sekotāji
@@ -1645,25 +1644,12 @@ lv:
     edited_at_html: Labots %{date}
     errors:
       in_reply_not_found: Šķiet, ka ziņa, uz kuru tu mēģini atbildēt, nepastāv.
-    open_in_web: Atvērt webā
     over_character_limit: pārsniegts %{max} rakstzīmju ierobežojums
     pin_errors:
       direct: Ziņojumus, kas ir redzami tikai minētajiem lietotājiem, nevar piespraust
       limit: Tu jau esi piespraudis maksimālo ziņu skaitu
       ownership: Kāda cita ierakstu nevar piespraust
       reblog: Izceltu ierakstu nevar piespraust
-    poll:
-      total_people:
-        one: "%{count} cilvēks"
-        other: "%{count} cilvēki"
-        zero: "%{count} cilvēku"
-      total_votes:
-        one: "%{count} balss"
-        other: "%{count} balsis"
-        zero: "%{count} balsu"
-      vote: Balsu skaits
-    show_more: Rādīt vairāk
-    show_thread: Rādīt tematu
     title: "%{name}: “%{quote}”"
     visibilities:
       direct: Tiešs
diff --git a/config/locales/ml.yml b/config/locales/ml.yml
index a4b9391c0..bdc0475a6 100644
--- a/config/locales/ml.yml
+++ b/config/locales/ml.yml
@@ -4,7 +4,6 @@ ml:
     contact_missing: സജ്ജമാക്കിയിട്ടില്ല
     contact_unavailable: ലഭ്യമല്ല
   accounts:
-    follow: പിന്തുടരുക
     following: പിന്തുടരുന്നു
     last_active: അവസാനം സജീവമായിരുന്നത്
     link_verified_on: സന്ധിയുടെ ഉടമസ്ഥാവസ്‌കാശം %{date} ൽ പരിശോധിക്കപ്പെട്ടു
diff --git a/config/locales/ms.yml b/config/locales/ms.yml
index c91a62423..39c695a53 100644
--- a/config/locales/ms.yml
+++ b/config/locales/ms.yml
@@ -7,7 +7,6 @@ ms:
     hosted_on: Mastodon dihoskan di %{domain}
     title: Perihal
   accounts:
-    follow: Ikut
     followers:
       other: Pengikut
     following: Mengikuti
@@ -1560,21 +1559,12 @@ ms:
     edited_at_html: Disunting %{date}
     errors:
       in_reply_not_found: Pos yang anda cuba balas nampaknya tidak wujud.
-    open_in_web: Buka dalam web
     over_character_limit: had aksara %{max} melebihi
     pin_errors:
       direct: Pos yang hanya boleh dilihat oleh pengguna yang disebut tidak boleh disematkan
       limit: Anda telah menyematkan bilangan maksimum pos
       ownership: Siaran orang lain tidak boleh disematkan
       reblog: Rangsangan tidak boleh disematkan
-    poll:
-      total_people:
-        other: "%{count} orang"
-      total_votes:
-        other: "%{count} undi"
-      vote: Undi
-    show_more: Tunjuk lebih banyak
-    show_thread: Tunjuk bebenang
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Terus
diff --git a/config/locales/my.yml b/config/locales/my.yml
index 771fbba57..92464523a 100644
--- a/config/locales/my.yml
+++ b/config/locales/my.yml
@@ -7,7 +7,6 @@ my:
     hosted_on: "%{domain} မှ လက်ခံဆောင်ရွက်ထားသော Mastodon"
     title: အကြောင်း
   accounts:
-    follow: စောင့်ကြည့်မယ်
     followers:
       other: စောင့်ကြည့်သူ
     following: စောင့်ကြည့်နေသည်
@@ -1560,21 +1559,12 @@ my:
     edited_at_html: "%{date} ကို ပြင်ဆင်ပြီးပါပြီ"
     errors:
       in_reply_not_found: သင် စာပြန်နေသည့်ပို့စ်မှာ မရှိတော့ပါ။
-    open_in_web: ဝဘ်တွင် ဖွင့်ပါ
     over_character_limit: စာလုံးကန့်သတ်ချက် %{max} ကို ကျော်လွန်သွားပါပြီ
     pin_errors:
       direct: အမည်ဖော်ပြထားသည့် ပို့စ်များကို ပင်တွဲ၍မရပါ
       limit: သင်သည် ပို့စ်အရေအတွက်အများဆုံးကို ပင်တွဲထားပြီးဖြစ်သည်
       ownership: အခြားသူ၏ပို့စ်ကို ပင်တွဲ၍မရပါ
       reblog: Boost လုပ်ထားသောပို့စ်ကို ပင်ထား၍မရပါ
-    poll:
-      total_people:
-        other: "%{count} ယောက်"
-      total_votes:
-        other: မဲအရေအတွက် %{count} မဲ
-      vote: မဲပေးမည်
-    show_more: ပိုမိုပြရန်
-    show_thread: Thread ကို ပြပါ
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: တိုက်ရိုက်
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 63fcf1c85..63656991a 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -7,7 +7,6 @@ nl:
     hosted_on: Mastodon op %{domain}
     title: Over
   accounts:
-    follow: Volgen
     followers:
       one: Volger
       other: Volgers
@@ -1740,23 +1739,12 @@ nl:
     edited_at_html: Bewerkt op %{date}
     errors:
       in_reply_not_found: Het bericht waarop je probeert te reageren lijkt niet te bestaan.
-    open_in_web: In de webapp openen
     over_character_limit: Limiet van %{max} tekens overschreden
     pin_errors:
       direct: Berichten die alleen zichtbaar zijn voor vermelde gebruikers, kunnen niet worden vastgezet
       limit: Je hebt het maximaal aantal bericht al vastgemaakt
       ownership: Een bericht van iemand anders kan niet worden vastgemaakt
       reblog: Een boost kan niet worden vastgezet
-    poll:
-      total_people:
-        one: "%{count} persoon"
-        other: "%{count} personen"
-      total_votes:
-        one: "%{count} stem"
-        other: "%{count} stemmen"
-      vote: Stemmen
-    show_more: Meer tonen
-    show_thread: Gesprek tonen
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Privébericht
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index 47dcc1ac8..b7beeb426 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -7,7 +7,6 @@ nn:
     hosted_on: "%{domain} er vert for Mastodon"
     title: Om
   accounts:
-    follow: Fylg
     followers:
       one: Fylgjar
       other: Fylgjarar
@@ -1740,23 +1739,12 @@ nn:
     edited_at_html: Redigert %{date}
     errors:
       in_reply_not_found: Det ser ut til at tutet du freistar å svara ikkje finst.
-    open_in_web: Opn på nett
     over_character_limit: øvregrensa for teikn, %{max}, er nådd
     pin_errors:
       direct: Innlegg som bare er synlige for nevnte brukere kan ikke festes
       limit: Du har allereie festa så mange tut som det går an å festa
       ownership: Du kan ikkje festa andre sine tut
       reblog: Ei framheving kan ikkje festast
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} folk"
-      total_votes:
-        one: "%{count} røyst"
-        other: "%{count} røyster"
-      vote: Røyst
-    show_more: Vis meir
-    show_thread: Vis tråden
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: Direkte
diff --git a/config/locales/no.yml b/config/locales/no.yml
index b3eebd8ec..635ceedde 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -7,7 +7,6 @@
     hosted_on: Mastodon driftet på %{domain}
     title: Om
   accounts:
-    follow: Følg
     followers:
       one: Følger
       other: Følgere
@@ -1619,23 +1618,12 @@
     edited_at_html: Redigert %{date}
     errors:
       in_reply_not_found: Posten du prøver å svare ser ikke ut til eksisterer.
-    open_in_web: Åpne i nettleser
     over_character_limit: grensen på %{max} tegn overskredet
     pin_errors:
       direct: Innlegg som bare er synlige for nevnte brukere kan ikke festes
       limit: Du har allerede festet det maksimale antall innlegg
       ownership: Kun egne innlegg kan festes
       reblog: En fremheving kan ikke festes
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} personer"
-      total_votes:
-        one: "%{count} stemme"
-        other: "%{count} stemmer"
-      vote: Stem
-    show_more: Vis mer
-    show_thread: Vis tråden
     title: "%{name}: «%{quote}»"
     visibilities:
       direct: Direkte
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index e88f8a3f6..5cdd9240b 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -7,7 +7,6 @@ oc:
     hosted_on: Mastodon albergat sus %{domain}
     title: A prepaus
   accounts:
-    follow: Sègre
     followers:
       one: Seguidor
       other: Seguidors
@@ -846,22 +845,11 @@ oc:
     edited_at_html: Modificat %{date}
     errors:
       in_reply_not_found: La publicacion que respondètz sembla pas mai exisitir.
-    open_in_web: Dobrir sul web
     over_character_limit: limit de %{max} caractèrs passat
     pin_errors:
       limit: Avètz ja lo maximum de tuts penjats
       ownership: Se pòt pas penjar lo tut de qualqu’un mai
       reblog: Se pòt pas penjar un tut partejat
-    poll:
-      total_people:
-        one: "%{count} persona"
-        other: "%{count} personas"
-      total_votes:
-        one: "%{count} vòte"
-        other: "%{count} vòtes"
-      vote: Votar
-    show_more: Ne veire mai
-    show_thread: Mostrar lo fil
     title: '%{name} : "%{quote}"'
     visibilities:
       direct: Dirècte
diff --git a/config/locales/pa.yml b/config/locales/pa.yml
index 7a34358dd..1899d7100 100644
--- a/config/locales/pa.yml
+++ b/config/locales/pa.yml
@@ -7,7 +7,6 @@ pa:
     hosted_on: "%{domain} ਉੱਤੇ ਹੋਸਟ ਕੀਤਾ ਮਸਟਾਡੋਨ"
     title: ਇਸ ਬਾਰੇ
   accounts:
-    follow: ਫ਼ਾਲੋ
     following: ਫ਼ਾਲੋ ਕੀਤੇ ਜਾ ਰਹੇ
     posts_tab_heading: ਪੋਸਟਾਂ
   admin:
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index b710df2e7..f0d09cb2d 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -7,7 +7,6 @@ pl:
     hosted_on: Mastodon prowadzony na %{domain}
     title: O nas
   accounts:
-    follow: Obserwuj
     followers:
       few: śledzących
       many: śledzących
@@ -1800,27 +1799,12 @@ pl:
     edited_at_html: Edytowane %{date}
     errors:
       in_reply_not_found: Post, na który próbujesz odpowiedzieć, nie istnieje.
-    open_in_web: Otwórz w przeglądarce
     over_character_limit: limit %{max} znaków przekroczony
     pin_errors:
       direct: Nie możesz przypiąć wpisu, który jest widoczny tylko dla wspomnianych użytkowników
       limit: Przekroczyłeś maksymalną liczbę przypiętych wpisów
       ownership: Nie możesz przypiąć cudzego wpisu
       reblog: Nie możesz przypiąć podbicia wpisu
-    poll:
-      total_people:
-        few: "%{count} osoby"
-        many: "%{count} osób"
-        one: "%{count} osoba"
-        other: "%{count} osoby"
-      total_votes:
-        few: "%{count} głosy"
-        many: "%{count} głosy"
-        one: "%{count} głos"
-        other: "%{count} głosy"
-      vote: Głosuj
-    show_more: Pokaż więcej
-    show_thread: Pokaż wątek
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Bezpośredni
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 864a8b4d9..d1140f364 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -7,7 +7,6 @@ pt-BR:
     hosted_on: Mastodon hospedado em %{domain}
     title: Sobre
   accounts:
-    follow: Seguir
     followers:
       one: Seguidor
       other: Seguidores
@@ -1740,23 +1739,12 @@ pt-BR:
     edited_at_html: Editado em %{date}
     errors:
       in_reply_not_found: A publicação que você quer responder parece não existir.
-    open_in_web: Abrir no navegador
     over_character_limit: limite de caracteres de %{max} excedido
     pin_errors:
       direct: Publicações visíveis apenas para usuários mencionados não podem ser fixadas
       limit: Você alcançou o número limite de publicações fixadas
       ownership: As publicações dos outros não podem ser fixadas
       reblog: Um impulso não pode ser fixado
-    poll:
-      total_people:
-        one: "%{count} pessoa"
-        other: "%{count} pessoas"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Votar
-    show_more: Mostrar mais
-    show_thread: Mostrar conversa
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direto
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index 0f0d6f36e..489fb2b89 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -7,7 +7,6 @@ pt-PT:
     hosted_on: Mastodon alojado em %{domain}
     title: Sobre
   accounts:
-    follow: Seguir
     followers:
       one: Seguidor
       other: Seguidores
@@ -1683,23 +1682,12 @@ pt-PT:
     edited_at_html: Editado em %{date}
     errors:
       in_reply_not_found: A publicação a que está a tentar responder parece não existir.
-    open_in_web: Abrir na web
     over_character_limit: limite de caracter excedeu %{max}
     pin_errors:
       direct: Publicações visíveis apenas para utilizadores mencionados não podem ser afixadas
       limit: Já afixaste a quantidade máxima de publicações
       ownership: Não podem ser afixadas publicações doutras pessoas
       reblog: Não pode afixar um reforço
-    poll:
-      total_people:
-        one: "%{count} pessoa"
-        other: "%{count} pessoas"
-      total_votes:
-        one: "%{count} voto"
-        other: "%{count} votos"
-      vote: Votar
-    show_more: Mostrar mais
-    show_thread: Mostrar conversa
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direto
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index 52982ead2..d4f202637 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -7,7 +7,6 @@ ro:
     hosted_on: Mastodon găzduit de %{domain}
     title: Despre
   accounts:
-    follow: Urmărește
     followers:
       few: Urmăritori
       one: Urmăritor
@@ -681,24 +680,11 @@ ro:
       other: 'conținea aceste hashtag-uri nepermise: %{tags}'
     errors:
       in_reply_not_found: Postarea la care încercați să răspundeți nu pare să existe.
-    open_in_web: Deschide pe web
     over_character_limit: s-a depășit limita de caracter %{max}
     pin_errors:
       limit: Deja ai fixat numărul maxim de postări
       ownership: Postarea altcuiva nu poate fi fixată
       reblog: Un impuls nu poate fi fixat
-    poll:
-      total_people:
-        few: "%{count} persoane"
-        one: "%{count} persoană"
-        other: "%{count} de persoane"
-      total_votes:
-        few: "%{count} voturi"
-        one: "%{count} vot"
-        other: "%{count} de voturi"
-      vote: Votează
-    show_more: Arată mai mult
-    show_thread: Arată discuția
     visibilities:
       private: Doar urmăritorii
       private_long: Arată doar urmăritorilor
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 9d6b2946a..d66dded89 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -7,7 +7,6 @@ ru:
     hosted_on: Вы получили это сообщение, так как зарегистрированы на %{domain}
     title: О проекте
   accounts:
-    follow: Подписаться
     followers:
       few: подписчика
       many: подписчиков
@@ -1692,27 +1691,12 @@ ru:
     edited_at_html: Редактировано %{date}
     errors:
       in_reply_not_found: Пост, на который вы пытаетесь ответить, не существует или удалён.
-    open_in_web: Открыть в веб-версии
     over_character_limit: превышен лимит символов (%{max})
     pin_errors:
       direct: Сообщения, видимые только упомянутым пользователям, не могут быть закреплены
       limit: Вы закрепили максимально возможное число постов
       ownership: Нельзя закрепить чужой пост
       reblog: Нельзя закрепить продвинутый пост
-    poll:
-      total_people:
-        few: "%{count} человека"
-        many: "%{count} человек"
-        one: "%{count} человек"
-        other: "%{count} человек"
-      total_votes:
-        few: "%{count} голоса"
-        many: "%{count} голосов"
-        one: "%{count} голос"
-        other: "%{count} голосов"
-      vote: Голосовать
-    show_more: Развернуть
-    show_thread: Открыть обсуждение
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Адресованный
diff --git a/config/locales/ry.yml b/config/locales/ry.yml
index e384b7f1b..dd1b78600 100644
--- a/config/locales/ry.yml
+++ b/config/locales/ry.yml
@@ -1,7 +1,6 @@
 ---
 ry:
   accounts:
-    follow: Пудписати ся
     following: Пудпискы
     posts:
       few: Публикації
diff --git a/config/locales/sc.yml b/config/locales/sc.yml
index fee79a132..435749f47 100644
--- a/config/locales/sc.yml
+++ b/config/locales/sc.yml
@@ -7,7 +7,6 @@ sc:
     hosted_on: Mastodon allogiadu in %{domain}
     title: Informatziones
   accounts:
-    follow: Sighi
     followers:
       one: Sighidura
       other: Sighiduras
@@ -1111,22 +1110,11 @@ sc:
       other: 'cuntenet is etichetas non permìtidas: %{tags}'
     errors:
       in_reply_not_found: Ses chirchende de rispòndere a unu tut chi no esistit prus.
-    open_in_web: Aberi in sa web
     over_character_limit: lìmite de caràteres de %{max} superadu
     pin_errors:
       limit: As giai apicadu su nùmeru màssimu de tuts
       ownership: Is tuts de àtere non podent èssere apicados
       reblog: Is cumpartziduras non podent èssere apicadas
-    poll:
-      total_people:
-        one: "%{count} persone"
-        other: "%{count} persones"
-      total_votes:
-        one: "%{count} votu"
-        other: "%{count} votos"
-      vote: Vota
-    show_more: Ammustra·nde prus
-    show_thread: Ammustra su tema
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Deretu
diff --git a/config/locales/sco.yml b/config/locales/sco.yml
index 967706f03..70143a968 100644
--- a/config/locales/sco.yml
+++ b/config/locales/sco.yml
@@ -7,7 +7,6 @@ sco:
     hosted_on: Mastodon hostit on %{domain}
     title: Aboot
   accounts:
-    follow: Follae
     followers:
       one: Follaer
       other: Follaers
@@ -1394,23 +1393,12 @@ sco:
     edited_at_html: Editit %{date}
     errors:
       in_reply_not_found: The post thit ye'r trying tae reply tae disnae appear tae exist.
-    open_in_web: Open in wab
     over_character_limit: chairacter limit o %{max} exceedit
     pin_errors:
       direct: Posts thit's ainly visible tae menshied uisers cannae be preent
       limit: Ye awriddy preent the maximum nummer o posts
       ownership: Somebody else's post cannae be preent
       reblog: A heeze cannae be preent
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} fowk"
-      total_votes:
-        one: "%{count} vote"
-        other: "%{count} votes"
-      vote: Vote
-    show_more: Shaw mair
-    show_thread: Shaw threid
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direck
diff --git a/config/locales/si.yml b/config/locales/si.yml
index c7968e247..135a99ceb 100644
--- a/config/locales/si.yml
+++ b/config/locales/si.yml
@@ -7,7 +7,6 @@ si:
     hosted_on: "%{domain} හරහා සත්කාරකත්‍වය ලබයි"
     title: පිළිබඳව
   accounts:
-    follow: අනුගමනය
     followers:
       one: අනුගාමිකයා
       other: අනුගාමිකයින්
@@ -1267,22 +1266,11 @@ si:
     edited_at_html: සංස්කරණය %{date}
     errors:
       in_reply_not_found: ඔබ පිළිතුරු දීමට තැත් කරන ලිපිය නොපවතින බව පෙනෙයි.
-    open_in_web: වෙබයේ විවෘත කරන්න
     over_character_limit: අක්ෂර සීමාව %{max} ඉක්මවා ඇත
     pin_errors:
       direct: සඳහන් කළ අයට පමණක් පෙනෙන ලිපි ඇමිණීමට නොහැකිය
       limit: දැනටමත් මුදුනට ඇමිණිමට හැකි ලිපි සීමාවට ළඟා වී ඇත
       ownership: වෙනත් අයගේ ලිපි ඇමිණීමට නොහැකිය
-    poll:
-      total_people:
-        one: පුද්ගලයින් %{count}
-        other: පුද්ගලයින් %{count}
-      total_votes:
-        one: ඡන්ද %{count} යි
-        other: ඡන්ද %{count} යි
-      vote: ඡන්දය
-    show_more: තව පෙන්වන්න
-    show_thread: නූල් පෙන්වන්න
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: සෘජු
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index c49da0fc3..d7eacb685 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -7,7 +7,6 @@ sk:
     hosted_on: Mastodon hostovaný na %{domain}
     title: Ohľadom
   accounts:
-    follow: Nasleduj
     followers:
       few: Sledovateľov
       many: Sledovateľov
@@ -1259,26 +1258,11 @@ sk:
     edited_at_html: Upravené %{date}
     errors:
       in_reply_not_found: Príspevok, na ktorý sa snažíš odpovedať, pravdepodobne neexistuje.
-    open_in_web: Otvor v okne na webe
     over_character_limit: limit %{max} znakov bol presiahnutý
     pin_errors:
       limit: Už si si pripol ten najvyšší možný počet hlášok
       ownership: Nieje možné pripnúť hlášku od niekoho iného
       reblog: Vyzdvihnutie sa nedá pripnúť
-    poll:
-      total_people:
-        few: "%{count} ľudí"
-        many: "%{count} ľudia"
-        one: "%{count} človek"
-        other: "%{count} ľudí"
-      total_votes:
-        few: "%{count} hlasov"
-        many: "%{count} hlasov"
-        one: "%{count} hlas"
-        other: "%{count} hlasy"
-      vote: Hlasuj
-    show_more: Ukáž viac
-    show_thread: Ukáž diskusné vlákno
     title: '%{name}: „%{quote}"'
     visibilities:
       direct: Súkromne
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index d0440abb0..ef6d00b8d 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -7,7 +7,6 @@ sl:
     hosted_on: Mastodon gostuje na %{domain}
     title: O programu
   accounts:
-    follow: Sledi
     followers:
       few: Sledilci
       one: Sledilec
@@ -1785,27 +1784,12 @@ sl:
     edited_at_html: Urejeno %{date}
     errors:
       in_reply_not_found: Objava, na katero želite odgovoriti, ne obstaja.
-    open_in_web: Odpri na spletu
     over_character_limit: omejitev %{max} znakov je presežena
     pin_errors:
       direct: Objav, ki so vidne samo omenjenum uporabnikom, ni mogoče pripenjati
       limit: Pripeli ste največje število objav
       ownership: Objava nekoga drugega ne more biti pripeta
       reblog: Izpostavitev ne more biti pripeta
-    poll:
-      total_people:
-        few: "%{count} osebe"
-        one: "%{count} Oseba"
-        other: "%{count} oseb"
-        two: "%{count} osebi"
-      total_votes:
-        few: "%{count} glasovi"
-        one: "%{count} glas"
-        other: "%{count} glasov"
-        two: "%{count} glasova"
-      vote: Glasuj
-    show_more: Pokaži več
-    show_thread: Pokaži nit
     title: "%{name}: »%{quote}«"
     visibilities:
       direct: Neposredno
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index 241dc08b2..70d20592a 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -7,7 +7,6 @@ sq:
     hosted_on: Server Mastodon i strehuar në %{domain}
     title: Mbi
   accounts:
-    follow: Ndiqeni
     followers:
       one: Ndjekës
       other: Ndjekës
@@ -1732,23 +1731,12 @@ sq:
     edited_at_html: Përpunuar më %{date}
     errors:
       in_reply_not_found: Gjendja të cilës po provoni t’i përgjigjeni s’duket se ekziston.
-    open_in_web: Hape në internet
     over_character_limit: u tejkalua kufi shenjash prej %{max}
     pin_errors:
       direct: Postimet që janë të dukshme vetëm për përdoruesit e përmendur s’mund të fiksohen
       limit: Keni fiksuar tashmë numrin maksimum të mesazheve
       ownership: S’mund të fiksohen mesazhet e të tjerëve
       reblog: S’mund të fiksohet një përforcim
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} vetë"
-      total_votes:
-        one: "%{count} votë"
-        other: "%{count} vota"
-      vote: Votë
-    show_more: Shfaq më tepër
-    show_thread: Shfaq rrjedhën
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: I drejtpërdrejtë
diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml
index 428b9cb08..91f093339 100644
--- a/config/locales/sr-Latn.yml
+++ b/config/locales/sr-Latn.yml
@@ -7,7 +7,6 @@ sr-Latn:
     hosted_on: Mastodon hostovan na %{domain}
     title: O instanci
   accounts:
-    follow: Zaprati
     followers:
       few: Pratioca
       one: Pratilac
@@ -1670,25 +1669,12 @@ sr-Latn:
     edited_at_html: Izmenjeno %{date}
     errors:
       in_reply_not_found: Objava na koju pokušavate da odgovorite naizgled ne postoji.
-    open_in_web: Otvori u vebu
     over_character_limit: ograničenje od %{max} karaktera prekoračeno
     pin_errors:
       direct: Objave koje su vidljive samo pomenutim korisnicima ne mogu biti prikačene
       limit: Već ste zakačili maksimalan broj objava
       ownership: Tuđa objava se ne može zakačiti
       reblog: Podrška ne može da se prikači
-    poll:
-      total_people:
-        few: "%{count} osobe"
-        one: "%{count} osoba"
-        other: "%{count} ljudi"
-      total_votes:
-        few: "%{count} glasa"
-        one: "%{count} glas"
-        other: "%{count} glasova"
-      vote: Glasajte
-    show_more: Prikaži još
-    show_thread: Prikaži niz
     title: "%{name}: „%{quote}”"
     visibilities:
       direct: Direktno
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 08fbf39fb..67aee931b 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -7,7 +7,6 @@ sr:
     hosted_on: Mastodon хостован на %{domain}
     title: О инстанци
   accounts:
-    follow: Запрати
     followers:
       few: Пратиоца
       one: Пратилац
@@ -1700,25 +1699,12 @@ sr:
     edited_at_html: Уређено %{date}
     errors:
       in_reply_not_found: Објава на коју покушавате да одговорите наизглед не постоји.
-    open_in_web: Отвори у вебу
     over_character_limit: ограничење од %{max} карактера прекорачено
     pin_errors:
       direct: Објаве које су видљиве само поменутим корисницима не могу бити прикачене
       limit: Већ сте закачили максималан број објава
       ownership: Туђа објава се не може закачити
       reblog: Подршка не може да се прикачи
-    poll:
-      total_people:
-        few: "%{count} особе"
-        one: "%{count} особа"
-        other: "%{count} људи"
-      total_votes:
-        few: "%{count} гласа"
-        one: "%{count} глас"
-        other: "%{count} гласова"
-      vote: Гласајте
-    show_more: Прикажи још
-    show_thread: Прикажи низ
     title: "%{name}: „%{quote}”"
     visibilities:
       direct: Директно
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index c8ddc346a..99b7ec9b3 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -7,7 +7,6 @@ sv:
     hosted_on: Mastodon-värd på %{domain}
     title: Om
   accounts:
-    follow: Följa
     followers:
       one: Följare
       other: Följare
@@ -1693,23 +1692,12 @@ sv:
     edited_at_html: 'Ändrad: %{date}'
     errors:
       in_reply_not_found: Inlägget du försöker svara på verkar inte existera.
-    open_in_web: Öppna på webben
     over_character_limit: teckengräns på %{max} har överskridits
     pin_errors:
       direct: Inlägg som endast är synliga för nämnda användare kan inte fästas
       limit: Du har redan fäst det maximala antalet inlägg
       ownership: Någon annans inlägg kan inte fästas
       reblog: En boost kan inte fästas
-    poll:
-      total_people:
-        one: "%{count} person"
-        other: "%{count} personer"
-      total_votes:
-        one: "%{count} röst"
-        other: "%{count} röster"
-      vote: Rösta
-    show_more: Visa mer
-    show_thread: Visa tråd
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Direkt
diff --git a/config/locales/ta.yml b/config/locales/ta.yml
index c73148eac..3a98b6a25 100644
--- a/config/locales/ta.yml
+++ b/config/locales/ta.yml
@@ -6,7 +6,6 @@ ta:
     contact_unavailable: பொ/இ
     hosted_on: மாஸ்டோடாண் %{domain} இனையத்தில் இயங்குகிறது
   accounts:
-    follow: பின்தொடர்
     followers:
       one: பின்தொடர்பவர்
       other: பின்தொடர்பவர்கள்
@@ -220,4 +219,3 @@ ta:
         other: "%{count} ஒலிகள்"
     errors:
       in_reply_not_found: நீங்கள் மறுமொழி அளிக்க முயலும் பதிவு இருப்பதுபோல் தெரியவில்லை.
-    show_thread: தொடரைக் காட்டு
diff --git a/config/locales/te.yml b/config/locales/te.yml
index a5eb8d779..84697a4ae 100644
--- a/config/locales/te.yml
+++ b/config/locales/te.yml
@@ -6,7 +6,6 @@ te:
     contact_unavailable: వర్తించదు
     hosted_on: మాస్టొడాన్ %{domain} లో హోస్టు చేయబడింది
   accounts:
-    follow: అనుసరించు
     followers:
       one: అనుచరి
       other: అనుచరులు
diff --git a/config/locales/th.yml b/config/locales/th.yml
index d1de9fd81..cbacdfac4 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -7,7 +7,6 @@ th:
     hosted_on: Mastodon ที่โฮสต์ที่ %{domain}
     title: เกี่ยวกับ
   accounts:
-    follow: ติดตาม
     followers:
       other: ผู้ติดตาม
     following: กำลังติดตาม
@@ -1703,21 +1702,12 @@ th:
     edited_at_html: แก้ไขเมื่อ %{date}
     errors:
       in_reply_not_found: ดูเหมือนว่าจะไม่มีโพสต์ที่คุณกำลังพยายามตอบกลับอยู่
-    open_in_web: เปิดในเว็บ
     over_character_limit: เกินขีดจำกัดตัวอักษรที่ %{max} แล้ว
     pin_errors:
       direct: ไม่สามารถปักหมุดโพสต์ที่ปรากฏแก่ผู้ใช้ที่กล่าวถึงเท่านั้น
       limit: คุณได้ปักหมุดโพสต์ถึงจำนวนสูงสุดไปแล้ว
       ownership: ไม่สามารถปักหมุดโพสต์ของคนอื่น
       reblog: ไม่สามารถปักหมุดการดัน
-    poll:
-      total_people:
-        other: "%{count} คน"
-      total_votes:
-        other: "%{count} การลงคะแนน"
-      vote: ลงคะแนน
-    show_more: แสดงเพิ่มเติม
-    show_thread: แสดงกระทู้
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: โดยตรง
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 785be3caf..d6ca6b427 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -7,7 +7,6 @@ tr:
     hosted_on: Mastodon %{domain} üzerinde barındırılıyor
     title: Hakkında
   accounts:
-    follow: Takip et
     followers:
       one: Takipçi
       other: Takipçiler
@@ -1740,23 +1739,12 @@ tr:
     edited_at_html: "%{date} tarihinde düzenlendi"
     errors:
       in_reply_not_found: Yanıtlamaya çalıştığınız durum yok gibi görünüyor.
-    open_in_web: Web sayfasında aç
     over_character_limit: "%{max} karakter limiti aşıldı"
     pin_errors:
       direct: Sadece değinilen kullanıcıların görebileceği gönderiler üstte tutulamaz
       limit: Halihazırda maksimum sayıda gönderi sabitlediniz
       ownership: Başkasının gönderisi sabitlenemez
       reblog: Bir gönderi sabitlenemez
-    poll:
-      total_people:
-        one: "%{count} kişi"
-        other: "%{count} kişiler"
-      total_votes:
-        one: "%{count} oy"
-        other: "%{count} oylar"
-      vote: Oy Ver
-    show_more: Daha fazlasını göster
-    show_thread: Konuyu göster
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Doğrudan
diff --git a/config/locales/tt.yml b/config/locales/tt.yml
index 3a0d9d9ce..7847d636e 100644
--- a/config/locales/tt.yml
+++ b/config/locales/tt.yml
@@ -4,7 +4,6 @@ tt:
     contact_unavailable: Юк
     title: Проект турында
   accounts:
-    follow: Языл
     followers:
       other: язылучы
     following: Язылгансыз
@@ -519,12 +518,6 @@ tt:
       video:
         other: "%{count} видео"
     edited_at_html: "%{date} көнне төзәтте"
-    open_in_web: Веб-та ачу
-    poll:
-      total_people:
-        other: "%{count} кеше"
-      vote: Тавыш бирү
-    show_more: Күбрәк күрсәтү
     title: '%{name}: "%{quote}"'
     visibilities:
       private: Ияртүчеләр генә
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index f5cd40bad..e8c4e6899 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -7,7 +7,6 @@ uk:
     hosted_on: Mastodon розміщено на %{domain}
     title: Про програму
   accounts:
-    follow: Підписатися
     followers:
       few: Підписники
       many: Підписників
@@ -1800,27 +1799,12 @@ uk:
     edited_at_html: Відредаговано %{date}
     errors:
       in_reply_not_found: Допису, на який ви намагаєтеся відповісти, не існує.
-    open_in_web: Відкрити у вебі
     over_character_limit: перевищено ліміт символів %{max}
     pin_errors:
       direct: Не можливо прикріпити дописи, які видимі лише згаданим користувачам
       limit: Ви вже закріпили максимальну кількість дописів
       ownership: Не можна закріпити чужий допис
       reblog: Не можна закріпити просунутий допис
-    poll:
-      total_people:
-        few: "%{count} людей"
-        many: "%{count} людей"
-        one: "%{count} людина"
-        other: "%{count} людей"
-      total_votes:
-        few: "%{count} голоса"
-        many: "%{count} голосів"
-        one: "%{count} голос"
-        other: "%{count} голоси"
-      vote: Проголосувати
-    show_more: Розгорнути
-    show_thread: Відкрити обговорення
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Особисто
diff --git a/config/locales/uz.yml b/config/locales/uz.yml
index 403ffd33c..9215a0f0e 100644
--- a/config/locales/uz.yml
+++ b/config/locales/uz.yml
@@ -2,8 +2,6 @@
 uz:
   about:
     title: Haqida
-  accounts:
-    follow: Obuna bo‘lish
   admin:
     accounts:
       display_name: Ko'rsatiladigan nom
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index dfb36c02d..a03b46c91 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -7,7 +7,6 @@ vi:
     hosted_on: "%{domain} vận hành nhờ Mastodon"
     title: Giới thiệu
   accounts:
-    follow: Theo dõi
     followers:
       other: Người theo dõi
     following: Theo dõi
@@ -1703,21 +1702,12 @@ vi:
     edited_at_html: Sửa %{date}
     errors:
       in_reply_not_found: Bạn đang trả lời một tút không còn tồn tại.
-    open_in_web: Xem trong web
     over_character_limit: vượt quá giới hạn %{max} ký tự
     pin_errors:
       direct: Không thể ghim những tút nhắn riêng
       limit: Bạn đã ghim quá số lượng tút cho phép
       ownership: Không thể ghim tút của người khác
       reblog: Không thể ghim tút đăng lại
-    poll:
-      total_people:
-        other: "%{count} người bình chọn"
-      total_votes:
-        other: "%{count} người bình chọn"
-      vote: Bình chọn
-    show_more: Đọc thêm
-    show_thread: Nội dung gốc
     title: '%{name}: "%{quote}"'
     visibilities:
       direct: Nhắn riêng
diff --git a/config/locales/zgh.yml b/config/locales/zgh.yml
index 180fcf2f1..cbd0bc961 100644
--- a/config/locales/zgh.yml
+++ b/config/locales/zgh.yml
@@ -1,7 +1,6 @@
 ---
 zgh:
   accounts:
-    follow: ⴹⴼⵕ
     followers:
       one: ⴰⵎⴹⴼⴰⵕ
       other: ⵉⵎⴹⴼⴰⵕⵏ
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 747dcf373..277785f68 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -7,7 +7,6 @@ zh-CN:
     hosted_on: 运行在 %{domain} 上的 Mastodon 实例
     title: 关于本站
   accounts:
-    follow: 关注
     followers:
       other: 关注者
     following: 正在关注
@@ -1710,21 +1709,12 @@ zh-CN:
     edited_at_html: 编辑于 %{date}
     errors:
       in_reply_not_found: 你回复的嘟文似乎不存在
-    open_in_web: 在站内打开
     over_character_limit: 超过了 %{max} 字的限制
     pin_errors:
       direct: 仅对被提及的用户可见的帖子不能被置顶
       limit: 你所固定的嘟文数量已达到上限
       ownership: 不能置顶别人的嘟文
       reblog: 不能置顶转嘟
-    poll:
-      total_people:
-        other: "%{count} 人"
-      total_votes:
-        other: "%{count} 票"
-      vote: 投票
-    show_more: 显示更多
-    show_thread: 显示全部对话
     title: "%{name}:“%{quote}”"
     visibilities:
       direct: 私信
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 90227b911..768271275 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -7,7 +7,6 @@ zh-HK:
     hosted_on: 在 %{domain} 運作的 Mastodon 伺服器
     title: 關於
   accounts:
-    follow: 關注
     followers:
       other: 關注者
     following: 正在關注
@@ -1607,21 +1606,12 @@ zh-HK:
     edited_at_html: 編輯於 %{date}
     errors:
       in_reply_not_found: 你所回覆的嘟文並不存在。
-    open_in_web: 開啟網頁
     over_character_limit: 超過了 %{max} 字的限制
     pin_errors:
       direct: 無法將只有被提及使用者可見的帖文置頂
       limit: 你所置頂的文章數量已經達到上限
       ownership: 不能置頂他人的文章
       reblog: 不能置頂轉推
-    poll:
-      total_people:
-        other: "%{count} 人"
-      total_votes:
-        other: "%{count} 票"
-      vote: 投票
-    show_more: 顯示更多
-    show_thread: 顯示討論串
     title: "%{name}:「%{quote}」"
     visibilities:
       direct: 私人訊息
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 8eab176d7..35f000b60 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -7,7 +7,6 @@ zh-TW:
     hosted_on: 於 %{domain} 託管之 Mastodon 站點
     title: 關於本站
   accounts:
-    follow: 跟隨
     followers:
       other: 跟隨者
     following: 正在跟隨
@@ -1712,21 +1711,12 @@ zh-TW:
     edited_at_html: 編輯於 %{date}
     errors:
       in_reply_not_found: 您嘗試回覆的嘟文看起來不存在。
-    open_in_web: 以網頁開啟
     over_character_limit: 已超過 %{max} 字的限制
     pin_errors:
       direct: 無法釘選只有僅提及使用者可見之嘟文
       limit: 釘選嘟文的數量已達上限
       ownership: 不能釘選他人的嘟文
       reblog: 不能釘選轉嘟
-    poll:
-      total_people:
-        other: "%{count} 個人"
-      total_votes:
-        other: "%{count} 票"
-      vote: 投票
-    show_more: 顯示更多
-    show_thread: 顯示討論串
     title: "%{name}:「%{quote}」"
     visibilities:
       direct: 私訊
diff --git a/public/embed.js b/public/embed.js
index f8e6a22db..3fb57469a 100644
--- a/public/embed.js
+++ b/public/embed.js
@@ -1,5 +1,7 @@
 // @ts-check
 
+const allowedPrefixes = (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT' && document.currentScript.dataset.allowedPrefixes) ? document.currentScript.dataset.allowedPrefixes.split(' ') : [];
+
 (function () {
   'use strict';
 
@@ -18,45 +20,71 @@
     }
   };
 
+  /**
+   * @param {Map} map
+   */
+  var generateId = function (map) {
+    var id = 0, failCount = 0, idBuffer = new Uint32Array(1);
+
+    while (id === 0 || map.has(id)) {
+      id = crypto.getRandomValues(idBuffer)[0];
+      failCount++;
+
+      if (failCount > 100) {
+        // give up and assign (easily guessable) unique number if getRandomValues is broken or no luck
+        id = -(map.size + 1);
+        break;
+      }
+    }
+
+    return id;
+  };
+
   ready(function () {
-    /** @type {Map<number, HTMLIFrameElement>} */
-    var iframes = new Map();
+    /** @type {Map<number, HTMLQuoteElement | HTMLIFrameElement>} */
+    var embeds = new Map();
 
     window.addEventListener('message', function (e) {
       var data = e.data || {};
 
-      if (typeof data !== 'object' || data.type !== 'setHeight' || !iframes.has(data.id)) {
+      if (typeof data !== 'object' || data.type !== 'setHeight' || !embeds.has(data.id)) {
         return;
       }
 
-      var iframe = iframes.get(data.id);
+      var embed = embeds.get(data.id);
 
-      if(!iframe) return;
-
-      if ('source' in e && iframe.contentWindow !== e.source) {
-        return;
+      if (embed instanceof HTMLIFrameElement) {
+        embed.height = data.height;
       }
 
-      iframe.height = data.height;
+      if (embed instanceof HTMLQuoteElement) {
+        var iframe = embed.querySelector('iframe');
+
+        if (!iframe || ('source' in e && iframe.contentWindow !== e.source)) {
+          return;
+        }
+
+        iframe.height = data.height;
+
+        var placeholder = embed.querySelector('a');
+
+        if (!placeholder) return;
+
+        embed.removeChild(placeholder);
+      }
     });
 
+    // Legacy embeds
     document.querySelectorAll('iframe.mastodon-embed').forEach(iframe => {
-      // select unique id for each iframe
-      var id = 0, failCount = 0, idBuffer = new Uint32Array(1);
-      while (id === 0 || iframes.has(id)) {
-        id = crypto.getRandomValues(idBuffer)[0];
-        failCount++;
-        if (failCount > 100) {
-          // give up and assign (easily guessable) unique number if getRandomValues is broken or no luck
-          id = -(iframes.size + 1);
-          break;
-        }
-      }
+      var id = generateId(embeds);
 
-      iframes.set(id, iframe);
+      embeds.set(id, iframe);
 
-      iframe.scrolling = 'no';
+      iframe.allow = 'fullscreen';
+      iframe.sandbox = 'allow-scripts allow-same-origin';
+      iframe.style.border = 0;
       iframe.style.overflow = 'hidden';
+      iframe.style.display = 'block';
 
       iframe.onload = function () {
         iframe.contentWindow.postMessage({
@@ -65,7 +93,38 @@
         }, '*');
       };
 
-      iframe.onload();
+      iframe.onload(); // In case the script is executing after the iframe has already loaded
+    });
+
+    // New generation of embeds
+    document.querySelectorAll('blockquote.mastodon-embed').forEach(container => {
+      var id = generateId(embeds);
+
+      embeds.set(id, container);
+
+      var iframe = document.createElement('iframe');
+      var embedUrl = new URL(container.getAttribute('data-embed-url'));
+
+      if (embedUrl.protocol !== 'https:' && embedUrl.protocol !== 'http:') return;
+      if (allowedPrefixes.every((allowedPrefix) => !embedUrl.toString().startsWith(allowedPrefix))) return;
+
+      iframe.src = embedUrl.toString();
+      iframe.width = container.clientWidth;
+      iframe.height = 0;
+      iframe.allow = 'fullscreen';
+      iframe.sandbox = 'allow-scripts allow-same-origin';
+      iframe.style.border = 0;
+      iframe.style.overflow = 'hidden';
+      iframe.style.display = 'block';
+
+      iframe.onload = function () {
+        iframe.contentWindow.postMessage({
+          type: 'setHeight',
+          id: id,
+        }, '*');
+      };
+
+      container.appendChild(iframe);
     });
   });
 })();
diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 2d5ff0135..5042523df 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -781,7 +781,6 @@ RSpec.describe StatusesController do
           'Cache-Control' => include('public'),
           'Link' => satisfy { |header| header.to_s.include?('activity+json') }
         )
-        expect(response.body).to include status.text
       end
     end
 
diff --git a/spec/helpers/media_component_helper_spec.rb b/spec/helpers/media_component_helper_spec.rb
index ec87a707c..a44b9b841 100644
--- a/spec/helpers/media_component_helper_spec.rb
+++ b/spec/helpers/media_component_helper_spec.rb
@@ -32,28 +32,6 @@ RSpec.describe MediaComponentHelper do
     end
   end
 
-  describe 'render_card_component' do
-    let(:status) { Fabricate(:status) }
-    let(:result) { helper.render_card_component(status) }
-
-    before do
-      PreviewCardsStatus.create(status: status, preview_card: Fabricate(:preview_card))
-    end
-
-    it 'returns the correct react component markup' do
-      expect(parsed_html.div['data-component']).to eq('Card')
-    end
-  end
-
-  describe 'render_poll_component' do
-    let(:status) { Fabricate(:status, poll: Fabricate(:poll)) }
-    let(:result) { helper.render_poll_component(status) }
-
-    it 'returns the correct react component markup' do
-      expect(parsed_html.div['data-component']).to eq('Poll')
-    end
-  end
-
   private
 
   def parsed_html

From ab763c493fd8d40db6005282c38bd2636120f273 Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Thu, 12 Sep 2024 13:14:42 +0200
Subject: [PATCH 77/91] Ignore `undefined` as canonical url (#31882)

---
 app/lib/link_details_extractor.rb       | 2 +-
 spec/lib/link_details_extractor_spec.rb | 8 ++++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb
index dff57f74f..e4e815c38 100644
--- a/app/lib/link_details_extractor.rb
+++ b/app/lib/link_details_extractor.rb
@@ -225,7 +225,7 @@ class LinkDetailsExtractor
   end
 
   def valid_url_or_nil(str, same_origin_only: false)
-    return if str.blank? || str == 'null'
+    return if str.blank? || str == 'null' || str == 'undefined'
 
     url = @original_url + Addressable::URI.parse(str)
 
diff --git a/spec/lib/link_details_extractor_spec.rb b/spec/lib/link_details_extractor_spec.rb
index b1e5cedce..d8d9db0ad 100644
--- a/spec/lib/link_details_extractor_spec.rb
+++ b/spec/lib/link_details_extractor_spec.rb
@@ -33,6 +33,14 @@ RSpec.describe LinkDetailsExtractor do
         expect(subject.canonical_url).to eq original_url
       end
     end
+
+    context 'when canonical URL is set to "undefined"' do
+      let(:url) { 'undefined' }
+
+      it 'ignores the canonical URLs' do
+        expect(subject.canonical_url).to eq original_url
+      end
+    end
   end
 
   context 'when only basic metadata is present' do

From 24ef8255b3f9b44cb54f49bc78fe3382a7070b1a Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 12 Sep 2024 14:54:16 +0200
Subject: [PATCH 78/91] Change design of embed modal in web UI (#31801)

---
 .../mastodon/components/copy_paste_text.tsx   |  90 ++++++++++++++
 .../mastodon/components/status_action_bar.jsx |   2 +-
 .../mastodon/containers/status_container.jsx  |   6 +-
 .../mastodon/features/onboarding/share.jsx    |  63 +---------
 .../features/status/components/action_bar.jsx |   2 +-
 .../features/ui/components/embed_modal.jsx    | 101 ---------------
 .../features/ui/components/embed_modal.tsx    | 116 ++++++++++++++++++
 app/javascript/mastodon/locales/en.json       |   2 +-
 .../styles/mastodon/components.scss           | 111 ++++++++---------
 app/serializers/oembed_serializer.rb          |  17 ++-
 app/views/layouts/embedded.html.haml          |   2 +-
 .../initializers/content_security_policy.rb   |  11 +-
 12 files changed, 278 insertions(+), 245 deletions(-)
 create mode 100644 app/javascript/mastodon/components/copy_paste_text.tsx
 delete mode 100644 app/javascript/mastodon/features/ui/components/embed_modal.jsx
 create mode 100644 app/javascript/mastodon/features/ui/components/embed_modal.tsx

diff --git a/app/javascript/mastodon/components/copy_paste_text.tsx b/app/javascript/mastodon/components/copy_paste_text.tsx
new file mode 100644
index 000000000..f888acd0f
--- /dev/null
+++ b/app/javascript/mastodon/components/copy_paste_text.tsx
@@ -0,0 +1,90 @@
+import { useRef, useState, useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+
+import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
+import { useTimeout } from 'mastodon/../hooks/useTimeout';
+import { Icon } from 'mastodon/components/icon';
+
+export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
+  const inputRef = useRef<HTMLTextAreaElement>(null);
+  const [copied, setCopied] = useState(false);
+  const [focused, setFocused] = useState(false);
+  const [setAnimationTimeout] = useTimeout();
+
+  const handleInputClick = useCallback(() => {
+    setCopied(false);
+
+    if (inputRef.current) {
+      inputRef.current.focus();
+      inputRef.current.select();
+      inputRef.current.setSelectionRange(0, value.length);
+    }
+  }, [setCopied, value]);
+
+  const handleButtonClick = useCallback(
+    (e: React.MouseEvent) => {
+      e.stopPropagation();
+      void navigator.clipboard.writeText(value);
+      inputRef.current?.blur();
+      setCopied(true);
+      setAnimationTimeout(() => {
+        setCopied(false);
+      }, 700);
+    },
+    [setCopied, setAnimationTimeout, value],
+  );
+
+  const handleKeyUp = useCallback(
+    (e: React.KeyboardEvent) => {
+      if (e.key !== ' ') return;
+      void navigator.clipboard.writeText(value);
+      setCopied(true);
+      setAnimationTimeout(() => {
+        setCopied(false);
+      }, 700);
+    },
+    [setCopied, setAnimationTimeout, value],
+  );
+
+  const handleFocus = useCallback(() => {
+    setFocused(true);
+  }, [setFocused]);
+
+  const handleBlur = useCallback(() => {
+    setFocused(false);
+  }, [setFocused]);
+
+  return (
+    <div
+      className={classNames('copy-paste-text', { copied, focused })}
+      tabIndex={0}
+      role='button'
+      onClick={handleInputClick}
+      onKeyUp={handleKeyUp}
+    >
+      <textarea
+        readOnly
+        value={value}
+        ref={inputRef}
+        onClick={handleInputClick}
+        onFocus={handleFocus}
+        onBlur={handleBlur}
+      />
+
+      <button className='button' onClick={handleButtonClick}>
+        <Icon id='copy' icon={ContentCopyIcon} />{' '}
+        {copied ? (
+          <FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
+        ) : (
+          <FormattedMessage
+            id='copypaste.copy_to_clipboard'
+            defaultMessage='Copy to clipboard'
+          />
+        )}
+      </button>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index f24f81e1b..165e81c7d 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -55,7 +55,7 @@ const messages = defineMessages({
   unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
   pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
   unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
-  embed: { id: 'status.embed', defaultMessage: 'Embed' },
+  embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
   admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
   admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
   admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx
index 58c5aac8f..0fb5f2556 100644
--- a/app/javascript/mastodon/containers/status_container.jsx
+++ b/app/javascript/mastodon/containers/status_container.jsx
@@ -6,7 +6,6 @@ import {
   unmuteAccount,
   unblockAccount,
 } from '../actions/accounts';
-import { showAlertForError } from '../actions/alerts';
 import { initBlockModal } from '../actions/blocks';
 import {
   replyCompose,
@@ -100,10 +99,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
   onEmbed (status) {
     dispatch(openModal({
       modalType: 'EMBED',
-      modalProps: {
-        id: status.get('id'),
-        onError: error => dispatch(showAlertForError(error)),
-      },
+      modalProps: { id: status.get('id') },
     }));
   },
 
diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx
index 32a86ab6c..9c720e907 100644
--- a/app/javascript/mastodon/features/onboarding/share.jsx
+++ b/app/javascript/mastodon/features/onboarding/share.jsx
@@ -10,8 +10,8 @@ import { Link } from 'react-router-dom';
 import SwipeableViews from 'react-swipeable-views';
 
 import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
-import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
 import { ColumnBackButton } from 'mastodon/components/column_back_button';
+import { CopyPasteText } from 'mastodon/components/copy_paste_text';
 import { Icon }  from 'mastodon/components/icon';
 import { me, domain } from 'mastodon/initial_state';
 import { useAppSelector } from 'mastodon/store';
@@ -20,67 +20,6 @@ const messages = defineMessages({
   shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
 });
 
-class CopyPasteText extends PureComponent {
-
-  static propTypes = {
-    value: PropTypes.string,
-  };
-
-  state = {
-    copied: false,
-    focused: false,
-  };
-
-  setRef = c => {
-    this.input = c;
-  };
-
-  handleInputClick = () => {
-    this.setState({ copied: false });
-    this.input.focus();
-    this.input.select();
-    this.input.setSelectionRange(0, this.props.value.length);
-  };
-
-  handleButtonClick = e => {
-    e.stopPropagation();
-
-    const { value } = this.props;
-    navigator.clipboard.writeText(value);
-    this.input.blur();
-    this.setState({ copied: true });
-    this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
-  };
-
-  handleFocus = () => {
-    this.setState({ focused: true });
-  };
-
-  handleBlur = () => {
-    this.setState({ focused: false });
-  };
-
-  componentWillUnmount () {
-    if (this.timeout) clearTimeout(this.timeout);
-  }
-
-  render () {
-    const { value } = this.props;
-    const { copied, focused } = this.state;
-
-    return (
-      <div className={classNames('copy-paste-text', { copied, focused })} tabIndex='0' role='button' onClick={this.handleInputClick}>
-        <textarea readOnly value={value} ref={this.setRef} onClick={this.handleInputClick} onFocus={this.handleFocus} onBlur={this.handleBlur} />
-
-        <button className='button' onClick={this.handleButtonClick}>
-          <Icon id='copy' icon={ContentCopyIcon} /> {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy_to_clipboard' defaultMessage='Copy to clipboard' />}
-        </button>
-      </div>
-    );
-  }
-
-}
-
 class TipCarousel extends PureComponent {
 
   static propTypes = {
diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx
index d90ca464a..8ba2db7d8 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.jsx
+++ b/app/javascript/mastodon/features/status/components/action_bar.jsx
@@ -49,7 +49,7 @@ const messages = defineMessages({
   share: { id: 'status.share', defaultMessage: 'Share' },
   pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
   unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
-  embed: { id: 'status.embed', defaultMessage: 'Embed' },
+  embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
   admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
   admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
   admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.jsx b/app/javascript/mastodon/features/ui/components/embed_modal.jsx
deleted file mode 100644
index a4e5fc9df..000000000
--- a/app/javascript/mastodon/features/ui/components/embed_modal.jsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-
-import ImmutablePureComponent from 'react-immutable-pure-component';
-
-import CloseIcon from '@/material-icons/400-24px/close.svg?react';
-import api from 'mastodon/api';
-import { IconButton } from 'mastodon/components/icon_button';
-
-const messages = defineMessages({
-  close: { id: 'lightbox.close', defaultMessage: 'Close' },
-});
-
-class EmbedModal extends ImmutablePureComponent {
-
-  static propTypes = {
-    id: PropTypes.string.isRequired,
-    onClose: PropTypes.func.isRequired,
-    onError: PropTypes.func.isRequired,
-    intl: PropTypes.object.isRequired,
-  };
-
-  state = {
-    loading: false,
-    oembed: null,
-  };
-
-  componentDidMount () {
-    const { id } = this.props;
-
-    this.setState({ loading: true });
-
-    api().get(`/api/web/embeds/${id}`).then(res => {
-      this.setState({ loading: false, oembed: res.data });
-
-      const iframeDocument = this.iframe.contentWindow.document;
-
-      iframeDocument.open();
-      iframeDocument.write(res.data.html);
-      iframeDocument.close();
-
-      iframeDocument.body.style.margin = 0;
-      this.iframe.width  = iframeDocument.body.scrollWidth;
-      this.iframe.height = iframeDocument.body.scrollHeight;
-    }).catch(error => {
-      this.props.onError(error);
-    });
-  }
-
-  setIframeRef = c =>  {
-    this.iframe = c;
-  };
-
-  handleTextareaClick = (e) => {
-    e.target.select();
-  };
-
-  render () {
-    const { intl, onClose } = this.props;
-    const { oembed } = this.state;
-
-    return (
-      <div className='modal-root__modal report-modal embed-modal'>
-        <div className='report-modal__target'>
-          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={16} />
-          <FormattedMessage id='status.embed' defaultMessage='Embed' />
-        </div>
-
-        <div className='report-modal__container embed-modal__container' style={{ display: 'block' }}>
-          <p className='hint'>
-            <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
-          </p>
-
-          <input
-            type='text'
-            className='embed-modal__html'
-            readOnly
-            value={oembed && oembed.html || ''}
-            onClick={this.handleTextareaClick}
-          />
-
-          <p className='hint'>
-            <FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' />
-          </p>
-
-          <iframe
-            className='embed-modal__iframe'
-            frameBorder='0'
-            ref={this.setIframeRef}
-            sandbox='allow-scripts allow-same-origin'
-            title='preview'
-          />
-        </div>
-      </div>
-    );
-  }
-
-}
-
-export default injectIntl(EmbedModal);
diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.tsx b/app/javascript/mastodon/features/ui/components/embed_modal.tsx
new file mode 100644
index 000000000..8f623e62b
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/embed_modal.tsx
@@ -0,0 +1,116 @@
+import { useRef, useState, useEffect } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { showAlertForError } from 'mastodon/actions/alerts';
+import api from 'mastodon/api';
+import { Button } from 'mastodon/components/button';
+import { CopyPasteText } from 'mastodon/components/copy_paste_text';
+import { useAppDispatch } from 'mastodon/store';
+
+interface OEmbedResponse {
+  html: string;
+}
+
+const EmbedModal: React.FC<{
+  id: string;
+  onClose: () => void;
+}> = ({ id, onClose }) => {
+  const iframeRef = useRef<HTMLIFrameElement>(null);
+  const intervalRef = useRef<ReturnType<typeof setInterval>>();
+  const [oembed, setOembed] = useState<OEmbedResponse | null>(null);
+  const dispatch = useAppDispatch();
+
+  useEffect(() => {
+    api()
+      .get(`/api/web/embeds/${id}`)
+      .then((res) => {
+        const data = res.data as OEmbedResponse;
+
+        setOembed(data);
+
+        const iframeDocument = iframeRef.current?.contentWindow?.document;
+
+        if (!iframeDocument) {
+          return '';
+        }
+
+        iframeDocument.open();
+        iframeDocument.write(data.html);
+        iframeDocument.close();
+
+        iframeDocument.body.style.margin = '0px';
+
+        // This is our best chance to ensure the parent iframe has the correct height...
+        intervalRef.current = setInterval(
+          () =>
+            window.requestAnimationFrame(() => {
+              if (iframeRef.current) {
+                iframeRef.current.width = `${iframeDocument.body.scrollWidth}px`;
+                iframeRef.current.height = `${iframeDocument.body.scrollHeight}px`;
+              }
+            }),
+          100,
+        );
+
+        return '';
+      })
+      .catch((error: unknown) => {
+        dispatch(showAlertForError(error));
+      });
+  }, [dispatch, id, setOembed]);
+
+  useEffect(
+    () => () => {
+      if (intervalRef.current) {
+        clearInterval(intervalRef.current);
+      }
+    },
+    [],
+  );
+
+  return (
+    <div className='modal-root__modal dialog-modal'>
+      <div className='dialog-modal__header'>
+        <Button onClick={onClose}>
+          <FormattedMessage id='report.close' defaultMessage='Done' />
+        </Button>
+        <span className='dialog-modal__header__title'>
+          <FormattedMessage id='status.embed' defaultMessage='Get embed code' />
+        </span>
+        <Button secondary onClick={onClose}>
+          <FormattedMessage
+            id='confirmation_modal.cancel'
+            defaultMessage='Cancel'
+          />
+        </Button>
+      </div>
+
+      <div className='dialog-modal__content'>
+        <div className='dialog-modal__content__form'>
+          <FormattedMessage
+            id='embed.instructions'
+            defaultMessage='Embed this status on your website by copying the code below.'
+          />
+
+          <CopyPasteText value={oembed?.html ?? ''} />
+
+          <FormattedMessage
+            id='embed.preview'
+            defaultMessage='Here is what it will look like:'
+          />
+
+          <iframe
+            frameBorder='0'
+            ref={iframeRef}
+            sandbox='allow-scripts allow-same-origin'
+            title='Preview'
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default EmbedModal;
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 39ee7b858..1de2dce44 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -789,7 +789,7 @@
   "status.edit": "Edit",
   "status.edited": "Last edited {date}",
   "status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
-  "status.embed": "Embed",
+  "status.embed": "Get embed code",
   "status.favourite": "Favorite",
   "status.favourites": "{count, plural, one {favorite} other {favorites}}",
   "status.filter": "Filter this post",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c6ce8f55a..8adad2441 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1730,6 +1730,10 @@ body > [data-popper-placement] {
     width: 100%;
     height: 100%;
   }
+
+  .detailed-status {
+    border-top: 0;
+  }
 }
 
 .scrollable > div:first-child .detailed-status {
@@ -6289,6 +6293,50 @@ a.status-card {
   }
 }
 
+.dialog-modal {
+  width: 588px;
+  max-height: 80vh;
+  flex-direction: column;
+  background: var(--modal-background-color);
+  backdrop-filter: var(--background-filter);
+  border: 1px solid var(--modal-border-color);
+  border-radius: 16px;
+
+  &__header {
+    border-bottom: 1px solid var(--modal-border-color);
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-direction: row-reverse;
+    padding: 12px 24px;
+
+    &__title {
+      font-size: 16px;
+      line-height: 24px;
+      font-weight: 500;
+      letter-spacing: 0.15px;
+    }
+  }
+
+  &__content {
+    font-size: 14px;
+    line-height: 20px;
+    letter-spacing: 0.25px;
+    overflow-y: auto;
+
+    &__form {
+      display: flex;
+      flex-direction: column;
+      gap: 16px;
+      padding: 24px;
+    }
+  }
+
+  .copy-paste-text {
+    margin-bottom: 0;
+  }
+}
+
 .hotkey-combination {
   display: inline-flex;
   align-items: center;
@@ -7737,69 +7785,6 @@ noscript {
   }
 }
 
-.embed-modal {
-  width: auto;
-  max-width: 80vw;
-  max-height: 80vh;
-
-  h4 {
-    padding: 30px;
-    font-weight: 500;
-    font-size: 16px;
-    text-align: center;
-  }
-
-  .embed-modal__container {
-    padding: 10px;
-
-    .hint {
-      margin-bottom: 15px;
-    }
-
-    .embed-modal__html {
-      outline: 0;
-      box-sizing: border-box;
-      display: block;
-      width: 100%;
-      border: 0;
-      padding: 10px;
-      font-family: $font-monospace, monospace;
-      background: $ui-base-color;
-      color: $primary-text-color;
-      font-size: 14px;
-      margin: 0;
-      margin-bottom: 15px;
-      border-radius: 4px;
-
-      &::-moz-focus-inner {
-        border: 0;
-      }
-
-      &::-moz-focus-inner,
-      &:focus,
-      &:active {
-        outline: 0 !important;
-      }
-
-      &:focus {
-        background: lighten($ui-base-color, 4%);
-      }
-
-      @media screen and (width <= 600px) {
-        font-size: 16px;
-      }
-    }
-
-    .embed-modal__iframe {
-      width: 400px;
-      max-width: 100%;
-      overflow: hidden;
-      border: 0;
-      border-radius: 4px;
-    }
-  }
-}
-
 .moved-account-banner,
 .follow-request-banner,
 .account-memorial-banner {
diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb
index 3882b0e30..19fa5ddec 100644
--- a/app/serializers/oembed_serializer.rb
+++ b/app/serializers/oembed_serializer.rb
@@ -1,6 +1,13 @@
 # frozen_string_literal: true
 
 class OEmbedSerializer < ActiveModel::Serializer
+  INLINE_STYLES = {
+    blockquote: 'max-width: 540px; min-width: 270px; background:#FCF8FF; border: 1px solid #C9C4DA; border-radius: 8px; overflow: hidden; margin: 0; padding: 0;',
+    a: "color: #1C1A25; text-decoration: none; display: flex; align-items: center; justify-content: center; flex-direction: column; padding: 24px; font-size: 14px; line-height: 20px; letter-spacing: 0.25px; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif;", # rubocop:disable Layout/LineLength
+    div0: 'margin-top: 16px; color: #787588;',
+    div1: 'font-weight: 500;',
+  }.freeze
+
   include RoutingHelper
   include ActionView::Helpers::TagHelper
 
@@ -38,14 +45,14 @@ class OEmbedSerializer < ActiveModel::Serializer
 
   def html
     <<~HTML.squish
-      <blockquote class="mastodon-embed" data-embed-url="#{embed_short_account_status_url(object.account, object)}" style="max-width: 540px; min-width: 270px; background:#FCF8FF; border: 1px solid #C9C4DA; border-radius: 8px; overflow: hidden; margin: 0; padding: 0;">
-        <a href="#{short_account_status_url(object.account, object)}" target="_blank" style="color: #1C1A25; text-decoration: none; display: flex; align-items: center; justify-content: center; flex-direction: column; padding: 24px; font-size: 14px; line-height: 20px; letter-spacing: 0.25px; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif;">
+      <blockquote class="mastodon-embed" data-embed-url="#{embed_short_account_status_url(object.account, object)}" style="#{INLINE_STYLES[:blockquote]}">
+        <a href="#{short_account_status_url(object.account, object)}" target="_blank" style="#{INLINE_STYLES[:a]}">
           <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></svg>
-          <div style="margin-top: 16px; color: #787588;">Post by @#{object.account.pretty_acct}@#{provider_name}</div>
-          <div style="font-weight: 500;">View on Mastodon</div>
+          <div style="#{INLINE_STYLES[:div0]}">Post by @#{object.account.pretty_acct}@#{provider_name}</div>
+          <div style="#{INLINE_STYLES[:div1]}">View on Mastodon</div>
         </a>
       </blockquote>
-      <script data-allowed-prefixes="#{root_url}" src="#{full_asset_url('embed.js', skip_pipeline: true)}" async="true"></script>
+      <script data-allowed-prefixes="#{root_url}" async src="#{full_asset_url('embed.js', skip_pipeline: true)}"></script>
     HTML
   end
 
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
index 0237e0451..c3de1bcd0 100644
--- a/app/views/layouts/embedded.html.haml
+++ b/app/views/layouts/embedded.html.haml
@@ -11,7 +11,7 @@
     - if storage_host?
       %link{ rel: 'dns-prefetch', href: storage_host }/
 
-    = theme_style_tags Setting.theme # Use the admin-configured theme here, even if logged in
+    = theme_style_tags 'mastodon-light'
     = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
     = preload_pack_asset "locale/#{I18n.locale}-json.js"
     = render_initial_state
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index e43e38786..7f34d93ee 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -38,17 +38,16 @@ Rails.application.config.content_security_policy do |p|
   p.img_src         :self, :data, :blob, *media_hosts
   p.style_src       :self, assets_host
   p.media_src       :self, :data, *media_hosts
-  p.frame_src       :self, :https
   p.manifest_src    :self, assets_host
 
   if sso_host.present?
-    p.form_action     :self, sso_host
+    p.form_action :self, sso_host
   else
-    p.form_action     :self
+    p.form_action :self
   end
 
-  p.child_src       :self, :blob, assets_host
-  p.worker_src      :self, :blob, assets_host
+  p.child_src  :self, :blob, assets_host
+  p.worker_src :self, :blob, assets_host
 
   if Rails.env.development?
     webpacker_public_host = ENV.fetch('WEBPACKER_DEV_SERVER_PUBLIC', Webpacker.config.dev_server[:public])
@@ -56,9 +55,11 @@ Rails.application.config.content_security_policy do |p|
 
     p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url, *front_end_build_urls
     p.script_src  :self, :unsafe_inline, :unsafe_eval, assets_host
+    p.frame_src   :self, :https, :http
   else
     p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url
     p.script_src  :self, assets_host, "'wasm-unsafe-eval'"
+    p.frame_src   :self, :https
   end
 end
 

From c35ea59ee6be05fbb7af57e339a493f363200103 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 12 Sep 2024 14:58:12 +0200
Subject: [PATCH 79/91] Fix security context sometimes not being added in
 LD-Signed activities (#31871)

---
 app/lib/activitypub/linked_data_signature.rb       | 9 ++++++++-
 spec/lib/activitypub/linked_data_signature_spec.rb | 9 ++-------
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb
index 9459fdd8b..c42313b05 100644
--- a/app/lib/activitypub/linked_data_signature.rb
+++ b/app/lib/activitypub/linked_data_signature.rb
@@ -4,6 +4,7 @@ class ActivityPub::LinkedDataSignature
   include JsonLdHelper
 
   CONTEXT = 'https://w3id.org/identity/v1'
+  SIGNATURE_CONTEXT = 'https://w3id.org/security/v1'
 
   def initialize(json)
     @json = json.with_indifferent_access
@@ -46,7 +47,13 @@ class ActivityPub::LinkedDataSignature
 
     signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), to_be_signed))
 
-    @json.merge('signature' => options.merge('signatureValue' => signature))
+    # Mastodon's context is either an array or a single URL
+    context_with_security = Array(@json['@context'])
+    context_with_security << 'https://w3id.org/security/v1'
+    context_with_security.uniq!
+    context_with_security = context_with_security.first if context_with_security.size == 1
+
+    @json.merge('signature' => options.merge('signatureValue' => signature), '@context' => context_with_security)
   end
 
   private
diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb
index 1af45673c..b1a8dc5c4 100644
--- a/spec/lib/activitypub/linked_data_signature_spec.rb
+++ b/spec/lib/activitypub/linked_data_signature_spec.rb
@@ -95,16 +95,11 @@ RSpec.describe ActivityPub::LinkedDataSignature do
   describe '#sign!' do
     subject { described_class.new(raw_json).sign!(sender) }
 
-    it 'returns a hash' do
+    it 'returns a hash with a signature, the expected context, and the signature can be verified', :aggregate_failures do
       expect(subject).to be_a Hash
-    end
-
-    it 'contains signature' do
       expect(subject['signature']).to be_a Hash
       expect(subject['signature']['signatureValue']).to be_present
-    end
-
-    it 'can be verified again' do
+      expect(Array(subject['@context'])).to include('https://w3id.org/security/v1')
       expect(described_class.new(subject).verify_actor!).to eq sender
     end
   end

From 5f782f9629de25a2029fbc75d79316d583adc9a0 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 12 Sep 2024 15:15:05 +0200
Subject: [PATCH 80/91] Autofocus primary button in modals (#31883)

---
 .../mastodon/features/ui/components/block_modal.jsx          | 2 +-
 .../ui/components/confirmation_modals/confirmation_modal.tsx | 5 ++++-
 .../mastodon/features/ui/components/domain_block_modal.jsx   | 2 +-
 .../mastodon/features/ui/components/mute_modal.jsx           | 2 +-
 4 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/app/javascript/mastodon/features/ui/components/block_modal.jsx b/app/javascript/mastodon/features/ui/components/block_modal.jsx
index fc9233a9c..d6fc6c415 100644
--- a/app/javascript/mastodon/features/ui/components/block_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/block_modal.jsx
@@ -99,7 +99,7 @@ export const BlockModal = ({ accountId, acct }) => {
             <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
           </button>
 
-          <Button onClick={handleClick}>
+          <Button onClick={handleClick} autoFocus>
             <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
           </Button>
         </div>
diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx
index c3a0c0aa7..ab567c697 100644
--- a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx
+++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx
@@ -71,7 +71,10 @@ export const ConfirmationModal: React.FC<
             />
           </button>
 
-          <Button onClick={handleClick}>{confirm}</Button>
+          {/* eslint-disable-next-line jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
+          <Button onClick={handleClick} autoFocus>
+            {confirm}
+          </Button>
         </div>
       </div>
     </div>
diff --git a/app/javascript/mastodon/features/ui/components/domain_block_modal.jsx b/app/javascript/mastodon/features/ui/components/domain_block_modal.jsx
index e69db6348..78d5cbb13 100644
--- a/app/javascript/mastodon/features/ui/components/domain_block_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/domain_block_modal.jsx
@@ -88,7 +88,7 @@ export const DomainBlockModal = ({ domain, accountId, acct }) => {
             <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
           </button>
 
-          <Button onClick={handleClick}>
+          <Button onClick={handleClick} autoFocus>
             <FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
           </Button>
         </div>
diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.jsx b/app/javascript/mastodon/features/ui/components/mute_modal.jsx
index df466cfac..70d95b593 100644
--- a/app/javascript/mastodon/features/ui/components/mute_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/mute_modal.jsx
@@ -137,7 +137,7 @@ export const MuteModal = ({ accountId, acct }) => {
             <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
           </button>
 
-          <Button onClick={handleClick}>
+          <Button onClick={handleClick} autoFocus>
             <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
           </Button>
         </div>

From a496aeabcb28b7cc7d8a9e69bf47543c2be038c2 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 12 Sep 2024 15:24:19 +0200
Subject: [PATCH 81/91] Change form-action Content-Security-Policy directive to
 be more restrictive (#26897)

---
 .../concerns/web_app_controller_concern.rb    | 10 +++++++++
 app/lib/content_security_policy.rb            | 16 ++++++++++++++
 .../initializers/content_security_policy.rb   | 22 ++-----------------
 spec/requests/content_security_policy_spec.rb |  2 +-
 4 files changed, 29 insertions(+), 21 deletions(-)

diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb
index b8c909877..e1f599dcb 100644
--- a/app/controllers/concerns/web_app_controller_concern.rb
+++ b/app/controllers/concerns/web_app_controller_concern.rb
@@ -8,6 +8,16 @@ module WebAppControllerConcern
 
     before_action :redirect_unauthenticated_to_permalinks!
     before_action :set_app_body_class
+
+    content_security_policy do |p|
+      policy = ContentSecurityPolicy.new
+
+      if policy.sso_host.present?
+        p.form_action policy.sso_host
+      else
+        p.form_action :none
+      end
+    end
   end
 
   def skip_csrf_meta_tags?
diff --git a/app/lib/content_security_policy.rb b/app/lib/content_security_policy.rb
index 210f37cea..0b60b0d98 100644
--- a/app/lib/content_security_policy.rb
+++ b/app/lib/content_security_policy.rb
@@ -13,6 +13,22 @@ class ContentSecurityPolicy
     [assets_host, cdn_host_value, paperclip_root_url].compact
   end
 
+  def sso_host
+    return unless ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1
+
+    provider = Devise.omniauth_configs[Devise.omniauth_providers[0]]
+    @sso_host ||= begin
+      case provider.provider
+      when :cas
+        provider.cas_url
+      when :saml
+        provider.options[:idp_sso_target_url]
+      when :openid_connect
+        provider.options.dig(:client_options, :authorization_endpoint) || OpenIDConnect::Discovery::Provider::Config.discover!(provider.options[:issuer]).authorization_endpoint
+      end
+    end
+  end
+
   private
 
   def url_from_configured_asset_host
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 7f34d93ee..9f4a41e3a 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -12,24 +12,6 @@ policy = ContentSecurityPolicy.new
 assets_host = policy.assets_host
 media_hosts = policy.media_hosts
 
-def sso_host
-  return unless ENV['ONE_CLICK_SSO_LOGIN'] == 'true'
-  return unless ENV['OMNIAUTH_ONLY'] == 'true'
-  return unless Devise.omniauth_providers.length == 1
-
-  provider = Devise.omniauth_configs[Devise.omniauth_providers[0]]
-  @sso_host ||= begin
-    case provider.provider
-    when :cas
-      provider.cas_url
-    when :saml
-      provider.options[:idp_sso_target_url]
-    when :openid_connect
-      provider.options.dig(:client_options, :authorization_endpoint) || OpenIDConnect::Discovery::Provider::Config.discover!(provider.options[:issuer]).authorization_endpoint
-    end
-  end
-end
-
 Rails.application.config.content_security_policy do |p|
   p.base_uri        :none
   p.default_src     :none
@@ -40,8 +22,8 @@ Rails.application.config.content_security_policy do |p|
   p.media_src       :self, :data, *media_hosts
   p.manifest_src    :self, assets_host
 
-  if sso_host.present?
-    p.form_action :self, sso_host
+  if policy.sso_host.present?
+    p.form_action :self, policy.sso_host
   else
     p.form_action :self
   end
diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb
index 7520ecb0d..2bbbdd841 100644
--- a/spec/requests/content_security_policy_spec.rb
+++ b/spec/requests/content_security_policy_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Content-Security-Policy' do
       connect-src 'self' data: blob: https://cb6e6126.ngrok.io #{Rails.configuration.x.streaming_api_base_url}
       default-src 'none'
       font-src 'self' https://cb6e6126.ngrok.io
-      form-action 'self'
+      form-action 'none'
       frame-ancestors 'none'
       frame-src 'self' https:
       img-src 'self' data: blob: https://cb6e6126.ngrok.io

From 17c57c46e7f54f32ad7b17b86c7b936c789ca799 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 12 Sep 2024 09:25:23 -0400
Subject: [PATCH 82/91] Add coverage for title/limit validations in `List`
 model (#31869)

---
 app/models/list.rb       | 10 ++++++----
 spec/models/list_spec.rb | 27 +++++++++++++++++++++++++++
 2 files changed, 33 insertions(+), 4 deletions(-)
 create mode 100644 spec/models/list_spec.rb

diff --git a/app/models/list.rb b/app/models/list.rb
index b45bd057b..d4915f56f 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -20,21 +20,23 @@ class List < ApplicationRecord
 
   enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show
 
-  belongs_to :account, optional: true
+  belongs_to :account
 
   has_many :list_accounts, inverse_of: :list, dependent: :destroy
   has_many :accounts, through: :list_accounts
 
   validates :title, presence: true
 
-  validates_each :account_id, on: :create do |record, _attr, value|
-    record.errors.add(:base, I18n.t('lists.errors.limit')) if List.where(account_id: value).count >= PER_ACCOUNT_LIMIT
-  end
+  validate :validate_account_lists_limit, on: :create
 
   before_destroy :clean_feed_manager
 
   private
 
+  def validate_account_lists_limit
+    errors.add(:base, I18n.t('lists.errors.limit')) if account.lists.count >= PER_ACCOUNT_LIMIT
+  end
+
   def clean_feed_manager
     FeedManager.instance.clean_feeds!(:list, [id])
   end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
new file mode 100644
index 000000000..62a84dfeb
--- /dev/null
+++ b/spec/models/list_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe List do
+  describe 'Validations' do
+    subject { Fabricate.build :list }
+
+    it { is_expected.to validate_presence_of(:title) }
+
+    context 'when account has hit max list limit' do
+      let(:account) { Fabricate :account }
+
+      before { stub_const 'List::PER_ACCOUNT_LIMIT', 0 }
+
+      context 'when creating a new list' do
+        it { is_expected.to_not allow_value(account).for(:account).against(:base).with_message(I18n.t('lists.errors.limit')) }
+      end
+
+      context 'when updating an existing list' do
+        before { subject.save(validate: false) }
+
+        it { is_expected.to allow_value(account).for(:account).against(:base) }
+      end
+    end
+  end
+end

From 8cdc1481679ef0c7a848d488dae1a265cdcfeac1 Mon Sep 17 00:00:00 2001
From: Christian Schmidt <github@chsc.dk>
Date: Thu, 12 Sep 2024 15:29:55 +0200
Subject: [PATCH 83/91] Handle invalid visibility (#31571)

---
 app/models/status.rb                      |  2 +-
 spec/services/post_status_service_spec.rb | 14 +++++++++++++-
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/app/models/status.rb b/app/models/status.rb
index 73f005267..e0630733d 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -52,7 +52,7 @@ class Status < ApplicationRecord
   update_index('statuses', :proper)
   update_index('public_statuses', :proper)
 
-  enum :visibility, { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 }, suffix: :visibility
+  enum :visibility, { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 }, suffix: :visibility, validate: true
 
   belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
 
diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb
index 7e4478962..26db398d5 100644
--- a/spec/services/post_status_service_spec.rb
+++ b/spec/services/post_status_service_spec.rb
@@ -68,7 +68,10 @@ RSpec.describe PostStatusService do
       it 'raises invalid record error' do
         expect do
           subject.call(account, text: 'Hi future!', scheduled_at: invalid_scheduled_time)
-        end.to raise_error(ActiveRecord::RecordInvalid)
+        end.to raise_error(
+          ActiveRecord::RecordInvalid,
+          'Validation failed: Scheduled at The scheduled date must be in the future'
+        )
       end
     end
   end
@@ -123,6 +126,15 @@ RSpec.describe PostStatusService do
     expect(status.visibility).to eq 'private'
   end
 
+  it 'raises on an invalid visibility' do
+    expect do
+      create_status_with_options(visibility: :xxx)
+    end.to raise_error(
+      ActiveRecord::RecordInvalid,
+      'Validation failed: Visibility is not included in the list'
+    )
+  end
+
   it 'creates a status with limited visibility for silenced users' do
     status = subject.call(Fabricate(:account, silenced: true), text: 'test', visibility: :public)
 

From 4aa600387e71bca60331d672dc5c15ba58886006 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 12 Sep 2024 09:31:50 -0400
Subject: [PATCH 84/91] Move redirect/base body class to view (#31796)

---
 app/controllers/redirect/base_controller.rb |  5 ---
 app/views/redirects/show.html.haml          |  2 ++
 spec/system/redirections_spec.rb            | 38 ++++++++++++---------
 3 files changed, 23 insertions(+), 22 deletions(-)

diff --git a/app/controllers/redirect/base_controller.rb b/app/controllers/redirect/base_controller.rb
index 90894ec1e..34558a412 100644
--- a/app/controllers/redirect/base_controller.rb
+++ b/app/controllers/redirect/base_controller.rb
@@ -4,7 +4,6 @@ class Redirect::BaseController < ApplicationController
   vary_by 'Accept-Language'
 
   before_action :set_resource
-  before_action :set_app_body_class
 
   def show
     @redirect_path = ActivityPub::TagManager.instance.url_for(@resource)
@@ -14,10 +13,6 @@ class Redirect::BaseController < ApplicationController
 
   private
 
-  def set_app_body_class
-    @body_classes = 'app-body'
-  end
-
   def set_resource
     raise NotImplementedError
   end
diff --git a/app/views/redirects/show.html.haml b/app/views/redirects/show.html.haml
index 64436e05d..aa0db350a 100644
--- a/app/views/redirects/show.html.haml
+++ b/app/views/redirects/show.html.haml
@@ -2,6 +2,8 @@
   %meta{ name: 'robots', content: 'noindex, noarchive' }/
   %link{ rel: 'canonical', href: @redirect_path }
 
+- content_for :body_classes, 'app-body'
+
 .redirect
   .redirect__logo
     = link_to render_logo, root_path
diff --git a/spec/system/redirections_spec.rb b/spec/system/redirections_spec.rb
index 860bbdd6b..eba034326 100644
--- a/spec/system/redirections_spec.rb
+++ b/spec/system/redirections_spec.rb
@@ -6,27 +6,31 @@ RSpec.describe 'redirection confirmations' do
   let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/foo', url: 'https://example.com/@foo') }
   let(:status)  { Fabricate(:status, account: account, uri: 'https://example.com/users/foo/statuses/1', url: 'https://example.com/@foo/1') }
 
-  context 'when a logged out user visits a local page for a remote account' do
-    it 'shows a confirmation page' do
-      visit "/@#{account.pretty_acct}"
+  context 'when logged out' do
+    describe 'a local page for a remote account' do
+      it 'shows a confirmation page with relevant content' do
+        visit "/@#{account.pretty_acct}"
 
-      # It explains about the redirect
-      expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io'))
+        expect(page)
+          .to have_content(redirect_title) # Redirect explanation
+          .and have_link(account.url, href: account.url) # Appropriate account link
+          .and have_css('body', class: 'app-body')
+      end
+    end
 
-      # It features an appropriate link
-      expect(page).to have_link(account.url, href: account.url)
+    describe 'a local page for a remote status' do
+      it 'shows a confirmation page with relevant content' do
+        visit "/@#{account.pretty_acct}/#{status.id}"
+
+        expect(page)
+          .to have_content(redirect_title) # Redirect explanation
+          .and have_link(status.url, href: status.url) # Appropriate status link
+          .and have_css('body', class: 'app-body')
+      end
     end
   end
 
-  context 'when a logged out user visits a local page for a remote status' do
-    it 'shows a confirmation page' do
-      visit "/@#{account.pretty_acct}/#{status.id}"
-
-      # It explains about the redirect
-      expect(page).to have_content(I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io'))
-
-      # It features an appropriate link
-      expect(page).to have_link(status.url, href: status.url)
-    end
+  def redirect_title
+    I18n.t('redirects.title', instance: 'cb6e6126.ngrok.io')
   end
 end

From f3c48745225fa4a0f21efbfa530e84e34bb56a97 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 12 Sep 2024 09:38:15 -0400
Subject: [PATCH 85/91] Remove unused `statuses#embed` body class assignment
 (#31787)

---
 app/controllers/statuses_controller.rb | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index a0885b469..341b0e647 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -11,7 +11,6 @@ class StatusesController < ApplicationController
   before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
   before_action :set_status
   before_action :redirect_to_original, only: :show
-  before_action :set_body_classes, only: :embed
 
   after_action :set_link_headers
 
@@ -51,10 +50,6 @@ class StatusesController < ApplicationController
 
   private
 
-  def set_body_classes
-    @body_classes = 'with-modals'
-  end
-
   def set_link_headers
     response.headers['Link'] = LinkHeader.new(
       [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]

From 1b6a82b7994e436d145b0e2282af07314fe54308 Mon Sep 17 00:00:00 2001
From: Taylor Chaparro <33099255+notchairmk@users.noreply.github.com>
Date: Thu, 12 Sep 2024 06:40:20 -0700
Subject: [PATCH 86/91] Fix invalid date searches returning 503 (#31526)

---
 app/lib/search_query_transformer.rb       | 17 +++++--
 lib/exceptions.rb                         |  1 +
 spec/lib/search_query_transformer_spec.rb | 57 +++++++++++++++++++++--
 3 files changed, 69 insertions(+), 6 deletions(-)

diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index 606819ed4..1306ed12e 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -168,15 +168,15 @@ class SearchQueryTransformer < Parslet::Transform
       when 'before'
         @filter = :created_at
         @type = :range
-        @term = { lt: term, time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
+        @term = { lt: TermValidator.validate_date!(term), time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
       when 'after'
         @filter = :created_at
         @type = :range
-        @term = { gt: term, time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
+        @term = { gt: TermValidator.validate_date!(term), time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
       when 'during'
         @filter = :created_at
         @type = :range
-        @term = { gte: term, lte: term, time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
+        @term = { gte: TermValidator.validate_date!(term), lte: TermValidator.validate_date!(term), time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
       when 'in'
         @operator = :flag
         @term = term
@@ -224,6 +224,17 @@ class SearchQueryTransformer < Parslet::Transform
     end
   end
 
+  class TermValidator
+    STRICT_DATE_REGEX = /\A\d{4}-\d{2}-\d{2}\z/ # yyyy-MM-dd
+    EPOCH_MILLIS_REGEX = /\A\d{1,19}\z/
+
+    def self.validate_date!(value)
+      return value if value.match?(STRICT_DATE_REGEX) || value.match?(EPOCH_MILLIS_REGEX)
+
+      raise Mastodon::FilterValidationError, "Invalid date #{value}"
+    end
+  end
+
   rule(clause: subtree(:clause)) do
     prefix   = clause[:prefix][:term].to_s.downcase if clause[:prefix]
     operator = clause[:operator]&.to_s
diff --git a/lib/exceptions.rb b/lib/exceptions.rb
index d3b92f4a0..c2ff162a6 100644
--- a/lib/exceptions.rb
+++ b/lib/exceptions.rb
@@ -8,6 +8,7 @@ module Mastodon
   class LengthValidationError < ValidationError; end
   class DimensionsValidationError < ValidationError; end
   class StreamValidationError < ValidationError; end
+  class FilterValidationError < ValidationError; end
   class RaceConditionError < Error; end
   class RateLimitExceededError < Error; end
   class SyntaxError < Error; end
diff --git a/spec/lib/search_query_transformer_spec.rb b/spec/lib/search_query_transformer_spec.rb
index 00220f84f..9399f3503 100644
--- a/spec/lib/search_query_transformer_spec.rb
+++ b/spec/lib/search_query_transformer_spec.rb
@@ -8,6 +8,37 @@ RSpec.describe SearchQueryTransformer do
   let(:account) { Fabricate(:account) }
   let(:parser) { SearchQueryParser.new.parse(query) }
 
+  shared_examples 'date operator' do |operator|
+    let(:statement_operations) { [] }
+
+    [
+      ['2022-01-01', '2022-01-01'],
+      ['"2022-01-01"', '2022-01-01'],
+      ['12345678', '12345678'],
+      ['"12345678"', '12345678'],
+    ].each do |value, parsed|
+      context "with #{operator}:#{value}" do
+        let(:query) { "#{operator}:#{value}" }
+
+        it 'transforms clauses' do
+          ops = statement_operations.index_with { |_op| parsed }
+
+          expect(subject.send(:must_clauses)).to be_empty
+          expect(subject.send(:must_not_clauses)).to be_empty
+          expect(subject.send(:filter_clauses).map(&:term)).to contain_exactly(**ops, time_zone: 'UTC')
+        end
+      end
+    end
+
+    context "with #{operator}:\"abc\"" do
+      let(:query) { "#{operator}:\"abc\"" }
+
+      it 'raises an exception' do
+        expect { subject }.to raise_error(Mastodon::FilterValidationError, 'Invalid date abc')
+      end
+    end
+  end
+
   context 'with "hello world"' do
     let(:query) { 'hello world' }
 
@@ -68,13 +99,33 @@ RSpec.describe SearchQueryTransformer do
     end
   end
 
-  context 'with \'before:"2022-01-01 23:00"\'' do
-    let(:query) { 'before:"2022-01-01 23:00"' }
+  context 'with \'is:"foo bar"\'' do
+    let(:query) { 'is:"foo bar"' }
 
     it 'transforms clauses' do
       expect(subject.send(:must_clauses)).to be_empty
       expect(subject.send(:must_not_clauses)).to be_empty
-      expect(subject.send(:filter_clauses).map(&:term)).to contain_exactly(lt: '2022-01-01 23:00', time_zone: 'UTC')
+      expect(subject.send(:filter_clauses).map(&:term)).to contain_exactly('foo bar')
+    end
+  end
+
+  context 'with date operators' do
+    context 'with "before"' do
+      it_behaves_like 'date operator', 'before' do
+        let(:statement_operations) { [:lt] }
+      end
+    end
+
+    context 'with "after"' do
+      it_behaves_like 'date operator', 'after' do
+        let(:statement_operations) { [:gt] }
+      end
+    end
+
+    context 'with "during"' do
+      it_behaves_like 'date operator', 'during' do
+        let(:statement_operations) { [:gte, :lte] }
+      end
     end
   end
 end

From 207c073bf87855c02a218526a3a389fc851e6c25 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 12 Sep 2024 10:04:46 -0400
Subject: [PATCH 87/91] Remove debug output in migration controller spec
 (#31886)

---
 spec/controllers/settings/migrations_controller_spec.rb | 1 -
 1 file changed, 1 deletion(-)

diff --git a/spec/controllers/settings/migrations_controller_spec.rb b/spec/controllers/settings/migrations_controller_spec.rb
index 67d5ab54f..93c5de089 100644
--- a/spec/controllers/settings/migrations_controller_spec.rb
+++ b/spec/controllers/settings/migrations_controller_spec.rb
@@ -95,7 +95,6 @@ RSpec.describe Settings::MigrationsController do
 
         before do
           moved_to = Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)])
-          p moved_to.acct
           user.account.migrations.create!(acct: moved_to.acct)
         end
 

From a269ff9253050fb9a0b2c063c253eacfad77b738 Mon Sep 17 00:00:00 2001
From: Michael Stanclift <mx@vmstan.com>
Date: Thu, 12 Sep 2024 09:18:43 -0500
Subject: [PATCH 88/91] Fix review history and action modal styling (#31864)

---
 .../styles/mastodon-light/diff.scss           | 27 -------------------
 .../styles/mastodon/components.scss           | 11 +++++---
 2 files changed, 8 insertions(+), 30 deletions(-)

diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index c0cabf837..45da56994 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -147,33 +147,6 @@
   border-top-color: lighten($ui-base-color, 4%);
 }
 
-// Change the background colors of modals
-.actions-modal,
-.boost-modal,
-.confirmation-modal,
-.mute-modal,
-.block-modal,
-.report-modal,
-.report-dialog-modal,
-.embed-modal,
-.error-modal,
-.onboarding-modal,
-.compare-history-modal,
-.report-modal__comment,
-.report-modal__comment,
-.announcements,
-.picture-in-picture__header,
-.picture-in-picture__footer,
-.reactions-bar__item {
-  background: $white;
-  border: 1px solid var(--background-border-color);
-}
-
-.setting-text__wrapper,
-.setting-text {
-  border: 1px solid var(--background-border-color);
-}
-
 .reactions-bar__item:hover,
 .reactions-bar__item:focus,
 .reactions-bar__item:active {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 8adad2441..a53eef52c 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6362,6 +6362,11 @@ a.status-card {
   width: 480px;
   position: relative;
   flex-direction: column;
+
+  @media screen and (max-width: $no-columns-breakpoint) {
+    border-bottom: 0;
+    border-radius: 4px 4px 0 0;
+  }
 }
 
 .boost-modal__container {
@@ -6759,7 +6764,7 @@ a.status-card {
 
     li:not(:empty) {
       a {
-        color: $inverted-text-color;
+        color: $primary-text-color;
         display: flex;
         padding: 12px 16px;
         font-size: 15px;
@@ -6839,7 +6844,7 @@ a.status-card {
 
 .compare-history-modal {
   .report-modal__target {
-    border-bottom: 1px solid $ui-secondary-color;
+    border-bottom: 1px solid var(--background-border-color);
   }
 
   &__container {
@@ -6849,7 +6854,7 @@ a.status-card {
   }
 
   .status__content {
-    color: $inverted-text-color;
+    color: $secondary-text-color;
     font-size: 19px;
     line-height: 24px;
 

From cc3cf9c4656460dbf3b93de0e95c4dc29e454cb2 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 12 Sep 2024 14:20:44 +0000
Subject: [PATCH 89/91] Update dependency aws-sdk-s3 to v1.162.0 (#31875)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 Gemfile.lock | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 1564c267b..206178a53 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -100,17 +100,17 @@ GEM
     attr_required (1.0.2)
     awrence (1.2.1)
     aws-eventstream (1.3.0)
-    aws-partitions (1.973.0)
-    aws-sdk-core (3.204.0)
+    aws-partitions (1.974.0)
+    aws-sdk-core (3.205.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.9)
       jmespath (~> 1, >= 1.6.1)
-    aws-sdk-kms (1.90.0)
-      aws-sdk-core (~> 3, >= 3.203.0)
+    aws-sdk-kms (1.91.0)
+      aws-sdk-core (~> 3, >= 3.205.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.161.0)
-      aws-sdk-core (~> 3, >= 3.203.0)
+    aws-sdk-s3 (1.162.0)
+      aws-sdk-core (~> 3, >= 3.205.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
     aws-sigv4 (1.9.1)

From 0226bbe5165a53658b29e46ddbef6a10507fdc8c Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 12 Sep 2024 14:21:06 +0000
Subject: [PATCH 90/91] Update dependency express to v4.21.0 (#31877)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 91 ++++++++++++-------------------------------------------
 1 file changed, 19 insertions(+), 72 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index f498a5560..50443c6db 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -82,19 +82,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/generator@npm:^7.25.0, @babel/generator@npm:^7.7.2":
-  version: 7.25.0
-  resolution: "@babel/generator@npm:7.25.0"
-  dependencies:
-    "@babel/types": "npm:^7.25.0"
-    "@jridgewell/gen-mapping": "npm:^0.3.5"
-    "@jridgewell/trace-mapping": "npm:^0.3.25"
-    jsesc: "npm:^2.5.1"
-  checksum: 10c0/d0e2dfcdc8bdbb5dded34b705ceebf2e0bc1b06795a1530e64fb6a3ccf313c189db7f60c1616effae48114e1a25adc75855bc4496f3779a396b3377bae718ce7
-  languageName: node
-  linkType: hard
-
-"@babel/generator@npm:^7.25.4":
+"@babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.4, @babel/generator@npm:^7.7.2":
   version: 7.25.4
   resolution: "@babel/generator@npm:7.25.4"
   dependencies:
@@ -1533,18 +1521,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
-  version: 7.25.2
-  resolution: "@babel/types@npm:7.25.2"
-  dependencies:
-    "@babel/helper-string-parser": "npm:^7.24.8"
-    "@babel/helper-validator-identifier": "npm:^7.24.7"
-    to-fast-properties: "npm:^2.0.0"
-  checksum: 10c0/e489435856be239f8cc1120c90a197e4c2865385121908e5edb7223cfdff3768cba18f489adfe0c26955d9e7bbb1fb10625bc2517505908ceb0af848989bd864
-  languageName: node
-  linkType: hard
-
-"@babel/types@npm:^7.25.4":
+"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
   version: 7.25.4
   resolution: "@babel/types@npm:7.25.4"
   dependencies:
@@ -8364,8 +8341,8 @@ __metadata:
   linkType: hard
 
 "express@npm:^4.17.1, express@npm:^4.18.2":
-  version: 4.20.0
-  resolution: "express@npm:4.20.0"
+  version: 4.21.0
+  resolution: "express@npm:4.21.0"
   dependencies:
     accepts: "npm:~1.3.8"
     array-flatten: "npm:1.1.1"
@@ -8379,7 +8356,7 @@ __metadata:
     encodeurl: "npm:~2.0.0"
     escape-html: "npm:~1.0.3"
     etag: "npm:~1.8.1"
-    finalhandler: "npm:1.2.0"
+    finalhandler: "npm:1.3.1"
     fresh: "npm:0.5.2"
     http-errors: "npm:2.0.0"
     merge-descriptors: "npm:1.0.3"
@@ -8388,17 +8365,17 @@ __metadata:
     parseurl: "npm:~1.3.3"
     path-to-regexp: "npm:0.1.10"
     proxy-addr: "npm:~2.0.7"
-    qs: "npm:6.11.0"
+    qs: "npm:6.13.0"
     range-parser: "npm:~1.2.1"
     safe-buffer: "npm:5.2.1"
     send: "npm:0.19.0"
-    serve-static: "npm:1.16.0"
+    serve-static: "npm:1.16.2"
     setprototypeof: "npm:1.2.0"
     statuses: "npm:2.0.1"
     type-is: "npm:~1.6.18"
     utils-merge: "npm:1.0.1"
     vary: "npm:~1.1.2"
-  checksum: 10c0/626e440e9feffa3f82ebce5e7dc0ad7a74fa96079994f30048cce450f4855a258abbcabf021f691aeb72154867f0d28440a8498c62888805faf667a829fb65aa
+  checksum: 10c0/4cf7ca328f3fdeb720f30ccb2ea7708bfa7d345f9cc460b64a82bf1b2c91e5b5852ba15a9a11b2a165d6089acf83457fc477dc904d59cd71ed34c7a91762c6cc
   languageName: node
   linkType: hard
 
@@ -8600,18 +8577,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"finalhandler@npm:1.2.0":
-  version: 1.2.0
-  resolution: "finalhandler@npm:1.2.0"
+"finalhandler@npm:1.3.1":
+  version: 1.3.1
+  resolution: "finalhandler@npm:1.3.1"
   dependencies:
     debug: "npm:2.6.9"
-    encodeurl: "npm:~1.0.2"
+    encodeurl: "npm:~2.0.0"
     escape-html: "npm:~1.0.3"
     on-finished: "npm:2.4.1"
     parseurl: "npm:~1.3.3"
     statuses: "npm:2.0.1"
     unpipe: "npm:~1.0.0"
-  checksum: 10c0/64b7e5ff2ad1fcb14931cd012651631b721ce657da24aedb5650ddde9378bf8e95daa451da43398123f5de161a81e79ff5affe4f9f2a6d2df4a813d6d3e254b7
+  checksum: 10c0/d38035831865a49b5610206a3a9a9aae4e8523cbbcd01175d0480ffbf1278c47f11d89be3ca7f617ae6d94f29cf797546a4619cd84dd109009ef33f12f69019f
   languageName: node
   linkType: hard
 
@@ -14354,15 +14331,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"qs@npm:6.11.0":
-  version: 6.11.0
-  resolution: "qs@npm:6.11.0"
-  dependencies:
-    side-channel: "npm:^1.0.4"
-  checksum: 10c0/4e4875e4d7c7c31c233d07a448e7e4650f456178b9dd3766b7cfa13158fdb24ecb8c4f059fa91e820dc6ab9f2d243721d071c9c0378892dcdad86e9e9a27c68f
-  languageName: node
-  linkType: hard
-
 "qs@npm:6.13.0, qs@npm:^6.11.0":
   version: 6.13.0
   resolution: "qs@npm:6.13.0"
@@ -15621,27 +15589,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"send@npm:0.18.0":
-  version: 0.18.0
-  resolution: "send@npm:0.18.0"
-  dependencies:
-    debug: "npm:2.6.9"
-    depd: "npm:2.0.0"
-    destroy: "npm:1.2.0"
-    encodeurl: "npm:~1.0.2"
-    escape-html: "npm:~1.0.3"
-    etag: "npm:~1.8.1"
-    fresh: "npm:0.5.2"
-    http-errors: "npm:2.0.0"
-    mime: "npm:1.6.0"
-    ms: "npm:2.1.3"
-    on-finished: "npm:2.4.1"
-    range-parser: "npm:~1.2.1"
-    statuses: "npm:2.0.1"
-  checksum: 10c0/0eb134d6a51fc13bbcb976a1f4214ea1e33f242fae046efc311e80aff66c7a43603e26a79d9d06670283a13000e51be6e0a2cb80ff0942eaf9f1cd30b7ae736a
-  languageName: node
-  linkType: hard
-
 "send@npm:0.19.0":
   version: 0.19.0
   resolution: "send@npm:0.19.0"
@@ -15696,15 +15643,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"serve-static@npm:1.16.0":
-  version: 1.16.0
-  resolution: "serve-static@npm:1.16.0"
+"serve-static@npm:1.16.2":
+  version: 1.16.2
+  resolution: "serve-static@npm:1.16.2"
   dependencies:
-    encodeurl: "npm:~1.0.2"
+    encodeurl: "npm:~2.0.0"
     escape-html: "npm:~1.0.3"
     parseurl: "npm:~1.3.3"
-    send: "npm:0.18.0"
-  checksum: 10c0/d7a5beca08cc55f92998d8b87c111dd842d642404231c90c11f504f9650935da4599c13256747b0a988442a59851343271fe8e1946e03e92cd79c447b5f3ae01
+    send: "npm:0.19.0"
+  checksum: 10c0/528fff6f5e12d0c5a391229ad893910709bc51b5705962b09404a1d813857578149b8815f35d3ee5752f44cd378d0f31669d4b1d7e2d11f41e08283d5134bd1f
   languageName: node
   linkType: hard
 

From 202077517c298c918782b764c7073215856c70db Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 12 Sep 2024 16:09:55 -0400
Subject: [PATCH 91/91] Add "search" group for chewy classes in simplecov
 config (#31890)

---
 spec/rails_helper.rb | 1 +
 1 file changed, 1 insertion(+)

diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 0b24f68f7..ee03b49bc 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -21,6 +21,7 @@ unless ENV['DISABLE_SIMPLECOV'] == 'true'
     add_group 'Libraries', 'lib'
     add_group 'Policies', 'app/policies'
     add_group 'Presenters', 'app/presenters'
+    add_group 'Search', 'app/chewy'
     add_group 'Serializers', 'app/serializers'
     add_group 'Services', 'app/services'
     add_group 'Validators', 'app/validators'