about summary refs log tree commit diff
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2019-04-04 14:53:52 +0200
committerThibaut Girka <thib@sitedethib.com>2019-04-04 14:53:52 +0200
commit1682ac571763f473da54074b0608cd9ccb4b9ae5 (patch)
tree2cb3f134660b2d7d460772966f7eb595395611f7
parent3f5acc1ab3bc9c5a6c5805901a393ccd3457b909 (diff)
parent26bd9fa508ac155f996c2c0d9753a27d46158749 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- app/controllers/following_accounts_controller.rb
  Conflicts were due to glitch-soc's theming system.
- app/javascript/packs/public.js
  Some code has been change upstream, but it has been
  moved to app/javascript/core/settings.js in glitch-soc.
  Applied the changes there.
-rw-r--r--Gemfile.lock8
-rw-r--r--app/controllers/activitypub/collections_controller.rb16
-rw-r--r--app/controllers/activitypub/outboxes_controller.rb6
-rw-r--r--app/controllers/follower_accounts_controller.rb7
-rw-r--r--app/controllers/following_accounts_controller.rb8
-rw-r--r--app/javascript/core/settings.js8
-rw-r--r--app/javascript/mastodon/components/poll.js33
-rw-r--r--app/javascript/mastodon/components/relative_timestamp.js30
-rw-r--r--app/javascript/mastodon/locales/ca.json34
-rw-r--r--app/javascript/mastodon/locales/cs.json2
-rw-r--r--app/javascript/mastodon/locales/eo.json42
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json54
-rw-r--r--app/models/export.rb6
-rw-r--r--app/services/import_service.rb23
-rw-r--r--app/workers/import/relationship_worker.rb4
-rw-r--r--config/locales/activerecord.pt-BR.yml3
-rw-r--r--config/locales/ca.yml48
-rw-r--r--config/locales/cs.yml23
-rw-r--r--config/locales/devise.ko.yml76
-rw-r--r--config/locales/devise.pt-BR.yml3
-rw-r--r--config/locales/doorkeeper.ca.yml4
-rw-r--r--config/locales/eo.yml79
-rw-r--r--config/locales/pt-BR.yml105
-rw-r--r--config/locales/simple_form.cs.yml4
-rw-r--r--config/locales/simple_form.pt-BR.yml8
-rw-r--r--spec/controllers/settings/exports/muted_accounts_controller_spec.rb2
-rw-r--r--spec/fixtures/files/mute-imports.txt4
-rw-r--r--spec/fixtures/files/new-mute-imports.txt4
-rw-r--r--spec/models/export_spec.rb7
-rw-r--r--spec/services/import_service_spec.rb84
30 files changed, 577 insertions, 158 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 620651983..a4b76c796 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -127,7 +127,7 @@ GEM
       sshkit (~> 1.3)
     capistrano-yarn (2.0.2)
       capistrano (~> 3.0)
-    capybara (3.16.0)
+    capybara (3.16.1)
       addressable
       mini_mime (>= 0.1.3)
       nokogiri (~> 1.8)
@@ -254,7 +254,7 @@ GEM
     hashdiff (0.3.7)
     hashie (3.6.0)
     heapy (0.1.4)
-    highline (2.0.0)
+    highline (2.0.1)
     hiredis (0.6.3)
     hkdf (0.3.0)
     html2text (0.2.1)
@@ -274,7 +274,7 @@ GEM
       rainbow (>= 2.0.0)
     i18n (1.6.0)
       concurrent-ruby (~> 1.0)
-    i18n-tasks (0.9.28)
+    i18n-tasks (0.9.29)
       activesupport (>= 4.0.2)
       ast (>= 2.1.0)
       erubi
@@ -395,7 +395,7 @@ GEM
     parallel (1.14.0)
     parallel_tests (2.28.0)
       parallel
-    parser (2.6.0.0)
+    parser (2.6.2.0)
       ast (~> 2.4.0)
     pastel (0.7.2)
       equatable (~> 0.5.0)
diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index 995da9c55..853f4f907 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -6,13 +6,19 @@ class ActivityPub::CollectionsController < Api::BaseController
   before_action :set_account
   before_action :set_size
   before_action :set_statuses
+  before_action :set_cache_headers
 
   def show
-    render json: collection_presenter,
-           serializer: ActivityPub::CollectionSerializer,
-           adapter: ActivityPub::Adapter,
-           content_type: 'application/activity+json',
-           skip_activities: true
+    skip_session!
+
+    render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do
+      ActiveModelSerializers::SerializableResource.new(
+        collection_presenter,
+        serializer: ActivityPub::CollectionSerializer,
+        adapter: ActivityPub::Adapter,
+        skip_activities: true
+      )
+    end
   end
 
   private
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index be4289b21..438fa226e 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -7,8 +7,14 @@ class ActivityPub::OutboxesController < Api::BaseController
 
   before_action :set_account
   before_action :set_statuses
+  before_action :set_cache_headers
 
   def show
+    unless page_requested?
+      skip_session!
+      expires_in 1.minute, public: true
+    end
+
     render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
   end
 
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 213c209ab..1462b94fc 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -3,6 +3,8 @@
 class FollowerAccountsController < ApplicationController
   include AccountControllerConcern
 
+  before_action :set_cache_headers
+
   def index
     respond_to do |format|
       format.html do
@@ -18,6 +20,11 @@ class FollowerAccountsController < ApplicationController
       format.json do
         raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
 
+        if params[:page].blank?
+          skip_session!
+          expires_in 3.minutes, public: true
+        end
+
         render json: collection_presenter,
                serializer: ActivityPub::CollectionSerializer,
                adapter: ActivityPub::Adapter,
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 098b2a20c..181f85221 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -3,10 +3,13 @@
 class FollowingAccountsController < ApplicationController
   include AccountControllerConcern
 
+  before_action :set_cache_headers
+
   def index
     respond_to do |format|
       format.html do
         use_pack 'public'
+        mark_cacheable! unless user_signed_in?
 
         next if @account.user_hides_network?
 
@@ -17,6 +20,11 @@ class FollowingAccountsController < ApplicationController
       format.json do
         raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network?
 
+        if params[:page].blank?
+          skip_session!
+          expires_in 3.minutes, public: true
+        end
+
         render json: collection_presenter,
                serializer: ActivityPub::CollectionSerializer,
                adapter: ActivityPub::Adapter,
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
index af97fb25f..e0cb944e0 100644
--- a/app/javascript/core/settings.js
+++ b/app/javascript/core/settings.js
@@ -42,14 +42,20 @@ delegate(document, '#account_locked', 'change', ({ target }) => {
 });
 
 delegate(document, '.input-copy input', 'click', ({ target }) => {
+  target.focus();
   target.select();
+  target.setSelectionRange(0, target.value.length);
 });
 
 delegate(document, '.input-copy button', 'click', ({ target }) => {
   const input = target.parentNode.querySelector('.input-copy__wrapper input');
 
+  const oldReadOnly = input.readonly;
+
+  input.readonly = false;
   input.focus();
   input.select();
+  input.setSelectionRange(0, input.value.length);
 
   try {
     if (document.execCommand('copy')) {
@@ -63,4 +69,6 @@ delegate(document, '.input-copy button', 'click', ({ target }) => {
   } catch (err) {
     console.error(err);
   }
+
+  input.readonly = oldReadOnly;
 });
diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js
index 56331cb29..690f9ae5a 100644
--- a/app/javascript/mastodon/components/poll.js
+++ b/app/javascript/mastodon/components/poll.js
@@ -9,41 +9,12 @@ import Motion from 'mastodon/features/ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import escapeTextContentForBrowser from 'escape-html';
 import emojify from 'mastodon/features/emoji/emoji';
+import RelativeTimestamp from './relative_timestamp';
 
 const messages = defineMessages({
-  moments: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
-  seconds: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
-  minutes: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
-  hours: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
-  days: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
   closed: { id: 'poll.closed', defaultMessage: 'Closed' },
 });
 
-const SECOND = 1000;
-const MINUTE = 1000 * 60;
-const HOUR   = 1000 * 60 * 60;
-const DAY    = 1000 * 60 * 60 * 24;
-
-const timeRemainingString = (intl, date, now) => {
-  const delta = date.getTime() - now;
-
-  let relativeTime;
-
-  if (delta < 10 * SECOND) {
-    relativeTime = intl.formatMessage(messages.moments);
-  } else if (delta < MINUTE) {
-    relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
-  } else if (delta < HOUR) {
-    relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
-  } else if (delta < DAY) {
-    relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
-  } else {
-    relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
-  }
-
-  return relativeTime;
-};
-
 const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
   obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
   return obj;
@@ -146,7 +117,7 @@ class Poll extends ImmutablePureComponent {
       return null;
     }
 
-    const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : timeRemainingString(intl, new Date(poll.get('expires_at')), intl.now());
+    const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
     const showResults   = poll.get('voted') || poll.get('expired');
     const disabled      = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
 
diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js
index 57d99dd19..aa4b73cfe 100644
--- a/app/javascript/mastodon/components/relative_timestamp.js
+++ b/app/javascript/mastodon/components/relative_timestamp.js
@@ -8,6 +8,11 @@ const messages = defineMessages({
   minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
   hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
   days: { id: 'relative_time.days', defaultMessage: '{number}d' },
+  moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
+  seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
+  minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
+  hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
+  days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
 });
 
 const dateFormatOptions = {
@@ -86,6 +91,26 @@ export const timeAgoString = (intl, date, now, year) => {
   return relativeTime;
 };
 
+const timeRemainingString = (intl, date, now) => {
+  const delta = date.getTime() - now;
+
+  let relativeTime;
+
+  if (delta < 10 * SECOND) {
+    relativeTime = intl.formatMessage(messages.moments_remaining);
+  } else if (delta < MINUTE) {
+    relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
+  } else if (delta < HOUR) {
+    relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
+  } else if (delta < DAY) {
+    relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
+  } else {
+    relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
+  }
+
+  return relativeTime;
+};
+
 export default @injectIntl
 class RelativeTimestamp extends React.Component {
 
@@ -93,6 +118,7 @@ class RelativeTimestamp extends React.Component {
     intl: PropTypes.object.isRequired,
     timestamp: PropTypes.string.isRequired,
     year: PropTypes.number.isRequired,
+    futureDate: PropTypes.bool,
   };
 
   state = {
@@ -145,10 +171,10 @@ class RelativeTimestamp extends React.Component {
   }
 
   render () {
-    const { timestamp, intl, year } = this.props;
+    const { timestamp, intl, year, futureDate } = this.props;
 
     const date         = new Date(timestamp);
-    const relativeTime = timeAgoString(intl, date, this.state.now, year);
+    const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year);
 
     return (
       <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 6c98c6ae7..c6b71a4f8 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -71,10 +71,10 @@
   "compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.",
   "compose_form.lock_disclaimer.lock": "blocat",
   "compose_form.placeholder": "En què estàs pensant?",
-  "compose_form.poll.add_option": "Add a choice",
-  "compose_form.poll.duration": "Poll duration",
-  "compose_form.poll.option_placeholder": "Choice {number}",
-  "compose_form.poll.remove_option": "Remove this choice",
+  "compose_form.poll.add_option": "Afegeix una opció",
+  "compose_form.poll.duration": "Durada de l'enquesta",
+  "compose_form.poll.option_placeholder": "Opció {number}",
+  "compose_form.poll.remove_option": "Elimina aquesta opció",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Mèdia marcat com a sensible",
@@ -108,7 +108,7 @@
   "emoji_button.food": "Menjar i beure",
   "emoji_button.label": "Insereix un emoji",
   "emoji_button.nature": "Natura",
-  "emoji_button.not_found": "Emojos no!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Emojis no!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objectes",
   "emoji_button.people": "Gent",
   "emoji_button.recent": "Usats freqüentment",
@@ -154,8 +154,8 @@
   "home.column_settings.basic": "Bàsic",
   "home.column_settings.show_reblogs": "Mostrar impulsos",
   "home.column_settings.show_replies": "Mostrar respostes",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
+  "intervals.full.days": "{number, plural, one {# dia} other {# dies}}",
+  "intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
   "intervals.full.minutes": "{number, plural, one {# minut} other {# minuts}}",
   "introduction.federation.action": "Següent",
   "introduction.federation.federated.headline": "Federada",
@@ -179,14 +179,14 @@
   "keyboard_shortcuts.boost": "impulsar",
   "keyboard_shortcuts.column": "per centrar un estat en una de les columnes",
   "keyboard_shortcuts.compose": "per centrar l'area de composició de text",
-  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.description": "Descripció",
   "keyboard_shortcuts.direct": "per obrir la columna de missatges directes",
   "keyboard_shortcuts.down": "per baixar en la llista",
   "keyboard_shortcuts.enter": "ampliar estat",
   "keyboard_shortcuts.favourite": "afavorir",
   "keyboard_shortcuts.favourites": "per obrir la llista de favorits",
   "keyboard_shortcuts.federated": "per obrir la línia de temps federada",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.heading": "Dreçeres de teclat",
   "keyboard_shortcuts.home": "per obrir la línia de temps Inici",
   "keyboard_shortcuts.hotkey": "Tecla d'accés directe",
   "keyboard_shortcuts.legend": "per a mostrar aquesta llegenda",
@@ -247,7 +247,7 @@
   "notification.follow": "{name} et segueix",
   "notification.mention": "{name} t'ha esmentat",
   "notification.poll": "Ha finalitzat una enquesta en la que has votat",
-  "notification.reblog": "{name} ha retootejat el teu estat",
+  "notification.reblog": "{name} ha impulsat el teu estat",
   "notifications.clear": "Netejar notificacions",
   "notifications.clear_confirmation": "Estàs segur que vols esborrar permanenment totes les teves notificacions?",
   "notifications.column_settings.alert": "Notificacions d'escriptori",
@@ -258,7 +258,7 @@
   "notifications.column_settings.follow": "Nous seguidors:",
   "notifications.column_settings.mention": "Mencions:",
   "notifications.column_settings.poll": "Resultats de l’enquesta:",
-  "notifications.column_settings.push": "Push notificacions",
+  "notifications.column_settings.push": "Notificacions push",
   "notifications.column_settings.reblog": "Impulsos:",
   "notifications.column_settings.show": "Mostrar en la columna",
   "notifications.column_settings.sound": "Reproduïr so",
@@ -273,8 +273,8 @@
   "poll.refresh": "Actualitza",
   "poll.total_votes": "{count, plural, one {# vot} other {# vots}}",
   "poll.vote": "Vota",
-  "poll_button.add_poll": "Add a poll",
-  "poll_button.remove_poll": "Remove poll",
+  "poll_button.add_poll": "Afegeix una enquesta",
+  "poll_button.remove_poll": "Elimina l'enquesta",
   "privacy.change": "Ajusta l'estat de privacitat",
   "privacy.direct.long": "Publicar només per als usuaris esmentats",
   "privacy.direct.short": "Directe",
@@ -311,9 +311,9 @@
   "search_results.total": "{count, number} {count, plural, un {result} altres {results}}",
   "status.admin_account": "Obre l'interfície de moderació per a @{name}",
   "status.admin_status": "Obre aquest estat a la interfície de moderació",
-  "status.block": "Block @{name}",
+  "status.block": "Bloqueja @{name}",
   "status.cancel_reblog_private": "Desfer l'impuls",
-  "status.cannot_reblog": "Aquesta publicació no pot ser retootejada",
+  "status.cannot_reblog": "Aquesta publicació no pot ser impulsada",
   "status.copy": "Copia l'enllaç a l'estat",
   "status.delete": "Esborrar",
   "status.detailed_status": "Visualització detallada de la conversa",
@@ -333,7 +333,7 @@
   "status.read_more": "Llegir més",
   "status.reblog": "Impuls",
   "status.reblog_private": "Impulsar a l'audiència original",
-  "status.reblogged_by": "{name} ha retootejat",
+  "status.reblogged_by": "{name} ha impulsat",
   "status.reblogs.empty": "Encara ningú no ha impulsat aquest toot. Quan algú ho faci, apareixeran aquí.",
   "status.redraft": "Esborrar i reescriure",
   "status.reply": "Respondre",
@@ -366,7 +366,7 @@
   "upload_area.title": "Arrossega i deixa anar per carregar",
   "upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "S'ha superat el límit de càrrega d'arxius.",
-  "upload_error.poll": "File upload not allowed with polls.",
+  "upload_error.poll": "No es permet l'enviament de fitxers amb les enquestes.",
   "upload_form.description": "Descriure els problemes visuals",
   "upload_form.focus": "Modificar la previsualització",
   "upload_form.undo": "Esborra",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index e711961e7..9701bd695 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -318,7 +318,7 @@
   "status.delete": "Smazat",
   "status.detailed_status": "Detailní zobrazení konverzace",
   "status.direct": "Poslat přímou zprávu uživateli @{name}",
-  "status.embed": "Vložit",
+  "status.embed": "Vložit na web",
   "status.favourite": "Oblíbit",
   "status.filtered": "Filtrováno",
   "status.load_more": "Zobrazit více",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index dd2188d8e..ed0113650 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -71,10 +71,10 @@
   "compose_form.lock_disclaimer": "Via konta ne estas {locked}. Iu ajn povas sekvi vin por vidi viajn mesaĝojn, kiuj estas nur por sekvantoj.",
   "compose_form.lock_disclaimer.lock": "ŝlosita",
   "compose_form.placeholder": "Pri kio vi pensas?",
-  "compose_form.poll.add_option": "Add a choice",
-  "compose_form.poll.duration": "Poll duration",
-  "compose_form.poll.option_placeholder": "Choice {number}",
-  "compose_form.poll.remove_option": "Remove this choice",
+  "compose_form.poll.add_option": "Aldoni elekto",
+  "compose_form.poll.duration": "Balotenketo daŭro",
+  "compose_form.poll.option_placeholder": "elekto {number}",
+  "compose_form.poll.remove_option": "Forigi ĉi tiu elekton",
   "compose_form.publish": "Hup",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Aŭdovidaĵo markita tikla",
@@ -83,7 +83,7 @@
   "compose_form.spoiler.unmarked": "Teksto ne kaŝita",
   "compose_form.spoiler_placeholder": "Skribu vian averton ĉi tie",
   "confirmation_modal.cancel": "Nuligi",
-  "confirmations.block.block_and_report": "Block & Report",
+  "confirmations.block.block_and_report": "Bloki & Signali",
   "confirmations.block.confirm": "Bloki",
   "confirmations.block.message": "Ĉu vi certas, ke vi volas bloki {name}?",
   "confirmations.delete.confirm": "Forigi",
@@ -154,15 +154,15 @@
   "home.column_settings.basic": "Bazaj agordoj",
   "home.column_settings.show_reblogs": "Montri diskonigojn",
   "home.column_settings.show_replies": "Montri respondojn",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
+  "intervals.full.days": "{number, plural, one {# tago} other {# tagoj}}",
+  "intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}",
   "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}",
   "introduction.federation.action": "Sekva",
-  "introduction.federation.federated.headline": "Federated",
+  "introduction.federation.federated.headline": "Federacio",
   "introduction.federation.federated.text": "Publikaj mesaĝoj el aliaj serviloj de la Fediverse aperos en la fratara tempolinio.",
-  "introduction.federation.home.headline": "Home",
+  "introduction.federation.home.headline": "Heimo",
   "introduction.federation.home.text": "Mesaĝoj de homoj, kiujn vi sekvas, aperos en via hejma fluo. Vi povas sekvi iun ajn de ajna servilo!",
-  "introduction.federation.local.headline": "Local",
+  "introduction.federation.local.headline": "Loka",
   "introduction.federation.local.text": "Publikaj mesaĝoj de homoj de via servilo aperos en la loka tempolinio.",
   "introduction.interactions.action": "Fini la lernilon!",
   "introduction.interactions.favourite.headline": "Stelumi",
@@ -246,7 +246,7 @@
   "notification.favourite": "{name} stelumis vian mesaĝon",
   "notification.follow": "{name} eksekvis vin",
   "notification.mention": "{name} menciis vin",
-  "notification.poll": "A poll you have voted in has ended",
+  "notification.poll": "Balotenketo ke vi balotis estas finita",
   "notification.reblog": "{name} diskonigis vian mesaĝon",
   "notifications.clear": "Forviŝi sciigojn",
   "notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?",
@@ -257,7 +257,7 @@
   "notifications.column_settings.filter_bar.show": "Montri",
   "notifications.column_settings.follow": "Novaj sekvantoj:",
   "notifications.column_settings.mention": "Mencioj:",
-  "notifications.column_settings.poll": "Poll results:",
+  "notifications.column_settings.poll": "Balotenketo rezulto:",
   "notifications.column_settings.push": "Puŝsciigoj",
   "notifications.column_settings.reblog": "Diskonigoj:",
   "notifications.column_settings.show": "Montri en kolumno",
@@ -267,14 +267,14 @@
   "notifications.filter.favourites": "Stelumoj",
   "notifications.filter.follows": "Sekvoj",
   "notifications.filter.mentions": "Mencioj",
-  "notifications.filter.polls": "Poll results",
+  "notifications.filter.polls": "Balotenketoj rezultoj",
   "notifications.group": "{count} sciigoj",
   "poll.closed": "Finita",
   "poll.refresh": "Aktualigi",
   "poll.total_votes": "{count, plural, one {# voĉdono} other {# voĉdonoj}}",
   "poll.vote": "Voĉdoni",
-  "poll_button.add_poll": "Add a poll",
-  "poll_button.remove_poll": "Remove poll",
+  "poll_button.add_poll": "Aldoni balotenketon",
+  "poll_button.remove_poll": "Forigi balotenketon",
   "privacy.change": "Agordi mesaĝan privatecon",
   "privacy.direct.long": "Afiŝi nur al menciitaj uzantoj",
   "privacy.direct.short": "Rekta",
@@ -356,17 +356,17 @@
   "tabs_bar.local_timeline": "Loka tempolinio",
   "tabs_bar.notifications": "Sciigoj",
   "tabs_bar.search": "Serĉi",
-  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
-  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
-  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
-  "time_remaining.moments": "Moments remaining",
-  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
+  "time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restanta",
+  "time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restanta",
+  "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restanta",
+  "time_remaining.moments": "Momento restanta",
+  "time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundoj}} restanta",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {persono} other {personoj}} parolas",
   "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.",
   "upload_area.title": "Altreni kaj lasi por alŝuti",
   "upload_button.label": "Aldoni aŭdovidaĵon (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Limo de dosiera alŝutado transpasita.",
-  "upload_error.poll": "File upload not allowed with polls.",
+  "upload_error.poll": "Alŝuto de dosiero ne permisita kun balotenketo",
   "upload_form.description": "Priskribi por misvidantaj homoj",
   "upload_form.focus": "Antaŭvido de ŝanĝo",
   "upload_form.undo": "Forigi",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 7533a462b..c2bac0f07 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -71,10 +71,10 @@
   "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.",
   "compose_form.lock_disclaimer.lock": "trancada",
   "compose_form.placeholder": "No que você está pensando?",
-  "compose_form.poll.add_option": "Add a choice",
-  "compose_form.poll.duration": "Poll duration",
-  "compose_form.poll.option_placeholder": "Choice {number}",
-  "compose_form.poll.remove_option": "Remove this choice",
+  "compose_form.poll.add_option": "Adicionar uma opção",
+  "compose_form.poll.duration": "Duração da enquete",
+  "compose_form.poll.option_placeholder": "Opção {number}",
+  "compose_form.poll.remove_option": "Remover essa opção",
   "compose_form.publish": "Publicar",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Mídia está marcada como sensível",
@@ -83,7 +83,7 @@
   "compose_form.spoiler.unmarked": "O texto não está escondido",
   "compose_form.spoiler_placeholder": "Aviso de conteúdo",
   "confirmation_modal.cancel": "Cancelar",
-  "confirmations.block.block_and_report": "Block & Report",
+  "confirmations.block.block_and_report": "Bloquear e denunciar",
   "confirmations.block.confirm": "Bloquear",
   "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
   "confirmations.delete.confirm": "Excluir",
@@ -145,17 +145,17 @@
   "hashtag.column_header.tag_mode.all": "e {additional}",
   "hashtag.column_header.tag_mode.any": "ou {additional}",
   "hashtag.column_header.tag_mode.none": "sem {additional}",
-  "hashtag.column_settings.select.no_options_message": "No suggestions found",
-  "hashtag.column_settings.select.placeholder": "Enter hashtags…",
+  "hashtag.column_settings.select.no_options_message": "Nenhuma sugestão encontrada",
+  "hashtag.column_settings.select.placeholder": "Adicione as hashtags…",
   "hashtag.column_settings.tag_mode.all": "Todas essas",
   "hashtag.column_settings.tag_mode.any": "Qualquer uma dessas",
   "hashtag.column_settings.tag_mode.none": "Nenhuma dessas",
-  "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
+  "hashtag.column_settings.tag_toggle": "Incluir outras hashtags nessa coluna",
   "home.column_settings.basic": "Básico",
   "home.column_settings.show_reblogs": "Mostrar compartilhamentos",
   "home.column_settings.show_replies": "Mostrar as respostas",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
+  "intervals.full.days": "{number, plural, one {# dia} other {# dias}}",
+  "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
   "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
   "introduction.federation.action": "Próximo",
   "introduction.federation.federated.headline": "Federated",
@@ -212,7 +212,7 @@
   "lists.account.remove": "Remover da lista",
   "lists.delete": "Delete list",
   "lists.edit": "Editar lista",
-  "lists.edit.submit": "Change title",
+  "lists.edit.submit": "Mudar o título",
   "lists.new.create": "Adicionar lista",
   "lists.new.title_placeholder": "Novo título da lista",
   "lists.search": "Procurar entre as pessoas que você segue",
@@ -246,7 +246,7 @@
   "notification.favourite": "{name} adicionou a sua postagem aos favoritos",
   "notification.follow": "{name} te seguiu",
   "notification.mention": "{name} te mencionou",
-  "notification.poll": "A poll you have voted in has ended",
+  "notification.poll": "Uma enquete em que você votou chegou ao fim",
   "notification.reblog": "{name} compartilhou a sua postagem",
   "notifications.clear": "Limpar notificações",
   "notifications.clear_confirmation": "Você tem certeza de que quer limpar todas as suas notificações permanentemente?",
@@ -257,7 +257,7 @@
   "notifications.column_settings.filter_bar.show": "Mostrar",
   "notifications.column_settings.follow": "Novos seguidores:",
   "notifications.column_settings.mention": "Menções:",
-  "notifications.column_settings.poll": "Poll results:",
+  "notifications.column_settings.poll": "Resultados da enquete:",
   "notifications.column_settings.push": "Enviar notificações",
   "notifications.column_settings.reblog": "Compartilhamento:",
   "notifications.column_settings.show": "Mostrar nas colunas",
@@ -267,14 +267,14 @@
   "notifications.filter.favourites": "Favoritos",
   "notifications.filter.follows": "Seguidores",
   "notifications.filter.mentions": "Menções",
-  "notifications.filter.polls": "Poll results",
+  "notifications.filter.polls": "Resultados da enquete",
   "notifications.group": "{count} notificações",
-  "poll.closed": "Closed",
-  "poll.refresh": "Refresh",
-  "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
+  "poll.closed": "Fechada",
+  "poll.refresh": "Atualizar",
+  "poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
   "poll.vote": "Votar",
-  "poll_button.add_poll": "Add a poll",
-  "poll_button.remove_poll": "Remove poll",
+  "poll_button.add_poll": "Adicionar uma enquete",
+  "poll_button.remove_poll": "Remover enquete",
   "privacy.change": "Ajustar a privacidade da mensagem",
   "privacy.direct.long": "Apenas para usuários mencionados",
   "privacy.direct.short": "Direta",
@@ -314,7 +314,7 @@
   "status.block": "Block @{name}",
   "status.cancel_reblog_private": "Desfazer compartilhamento",
   "status.cannot_reblog": "Esta postagem não pode ser compartilhada",
-  "status.copy": "Copy link to status",
+  "status.copy": "Copiar o link para o status",
   "status.delete": "Excluir",
   "status.detailed_status": "Visão detalhada da conversa",
   "status.direct": "Enviar mensagem direta a @{name}",
@@ -356,17 +356,17 @@
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
   "tabs_bar.search": "Buscar",
-  "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
-  "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
-  "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
-  "time_remaining.moments": "Moments remaining",
-  "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
+  "time_remaining.days": "{number, plural, one {# dia restante} other {# dias restantes}}",
+  "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
+  "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",
+  "time_remaining.moments": "Momentos restantes",
+  "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}",
   "trends.count_by_accounts": "{count} {rawCount, plural, one {pessoa} other {pessoas}} falando sobre",
   "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia (JPEG, PNG, GIF, WebM, MP4, MOV)",
-  "upload_error.limit": "File upload limit exceeded.",
-  "upload_error.poll": "File upload not allowed with polls.",
+  "upload_error.limit": "Limite de envio de arquivos excedido.",
+  "upload_error.poll": "Envio de arquivos não é permitido com enquetes.",
   "upload_form.description": "Descreva a imagem para deficientes visuais",
   "upload_form.focus": "Ajustar foco",
   "upload_form.undo": "Remover",
diff --git a/app/models/export.rb b/app/models/export.rb
index 9bf866d35..b35632c60 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -14,7 +14,11 @@ class Export
   end
 
   def to_muted_accounts_csv
-    to_csv account.muting.select(:username, :domain)
+    CSV.generate(headers: ['Account address', 'Hide notifications'], write_headers: true) do |csv|
+      account.mute_relationships.includes(:target_account).reorder(id: :desc).each do |mute|
+        csv << [acct(mute.target_account), mute.hide_notifications]
+      end
+    end
   end
 
   def to_following_accounts_csv
diff --git a/app/services/import_service.rb b/app/services/import_service.rb
index 3f558626e..c1c88e0dd 100644
--- a/app/services/import_service.rb
+++ b/app/services/import_service.rb
@@ -8,7 +8,6 @@ class ImportService < BaseService
   def call(import)
     @import  = import
     @account = @import.account
-    @data    = CSV.new(import_data).reject(&:blank?)
 
     case @import.type
     when 'following'
@@ -25,19 +24,23 @@ class ImportService < BaseService
   private
 
   def import_follows!
+    parse_import_data!(['Account address'])
     import_relationships!('follow', 'unfollow', @account.following, follow_limit)
   end
 
   def import_blocks!
+    parse_import_data!(['Account address'])
     import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT)
   end
 
   def import_mutes!
+    parse_import_data!(['Account address'])
     import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT)
   end
 
   def import_domain_blocks!
-    items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row.first.strip }
+    parse_import_data!(['#domain'])
+    items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#domain'].strip }
 
     if @import.overwrite?
       presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true }
@@ -61,25 +64,33 @@ class ImportService < BaseService
   end
 
   def import_relationships!(action, undo_action, overwrite_scope, limit)
-    items = @data.take(limit).map { |row| row.first.strip }
+    items = @data.take(limit).map { |row| [row['Account address']&.strip, row['Hide notifications']&.strip] }.reject { |(id, _)| id.blank? }
 
     if @import.overwrite?
-      presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true }
+      presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
 
       overwrite_scope.find_each do |target_account|
         if presence_hash[target_account.acct]
           items.delete(target_account.acct)
+          extra = presence_hash[target_account.acct][1]
+          Import::RelationshipWorker.perform_async(@account.id, target_account.acct, action, ActiveModel::Type::Boolean.new.cast(extra))
         else
           Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
         end
       end
     end
 
-    Import::RelationshipWorker.push_bulk(items) do |acct|
-      [@account.id, acct, action]
+    Import::RelationshipWorker.push_bulk(items) do |acct, extra|
+      [@account.id, acct, action, ActiveModel::Type::Boolean.new.cast(extra)]
     end
   end
 
+  def parse_import_data!(default_headers)
+    data = CSV.parse(import_data, headers: true)
+    data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(' ')
+    @data = data.reject(&:blank?)
+  end
+
   def import_data
     Paperclip.io_adapters.for(@import.data).read
   end
diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb
index e9db20a46..43ec09ea2 100644
--- a/app/workers/import/relationship_worker.rb
+++ b/app/workers/import/relationship_worker.rb
@@ -5,7 +5,7 @@ class Import::RelationshipWorker
 
   sidekiq_options queue: 'pull', retry: 8, dead: false
 
-  def perform(account_id, target_account_uri, relationship)
+  def perform(account_id, target_account_uri, relationship, extra = nil)
     from_account   = Account.find(account_id)
     target_account = ResolveAccountService.new.call(target_account_uri)
 
@@ -21,7 +21,7 @@ class Import::RelationshipWorker
     when 'unblock'
       UnblockService.new.call(from_account, target_account)
     when 'mute'
-      MuteService.new.call(from_account, target_account)
+      MuteService.new.call(from_account, target_account, notifications: extra)
     when 'unmute'
       UnmuteService.new.call(from_account, target_account)
     end
diff --git a/config/locales/activerecord.pt-BR.yml b/config/locales/activerecord.pt-BR.yml
index d2519fe90..ddea7bbb4 100644
--- a/config/locales/activerecord.pt-BR.yml
+++ b/config/locales/activerecord.pt-BR.yml
@@ -1,6 +1,9 @@
 ---
 pt-BR:
   activerecord:
+    attributes:
+      status:
+        owned_poll: enquete
     errors:
       models:
         account:
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index c9e0e092a..c6ab35cb6 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -147,9 +147,9 @@ ca:
       remove_avatar: Eliminar avatar
       remove_header: Treu la capçalera
       resend_confirmation:
-        already_confirmed: Este usuario ya está confirmado
+        already_confirmed: Aquest usuari ja està confirmat
         send: Reenviar el correu electrònic de confirmació
-        success: "¡Correo electrónico de confirmación enviado con éxito!"
+        success: Correu electrònic de confirmació enviat amb èxit!
       reset: Reinicialitza
       reset_password: Restableix la contrasenya
       resubscribe: Torna a subscriure
@@ -245,6 +245,7 @@ ca:
       feature_profile_directory: Directori de perfils
       feature_registrations: Registres
       feature_relay: Relay de la Federació
+      feature_timeline_preview: Vista previa de línia de temps
       features: Característiques
       hidden_service: Federació amb serveis ocults
       open_reports: informes oberts
@@ -293,7 +294,7 @@ ca:
         undo: Desfés
       undo: Desfés el bloqueig del domini
     email_domain_blocks:
-      add_new: Afegeix
+      add_new: Afegir nou
       created_msg: S'ha creat el bloc de domini de correu electrònic
       delete: Suprimeix
       destroyed_msg: S'ha eliminat correctament el bloc del domini de correu
@@ -316,10 +317,10 @@ ca:
         limited: Limitades
         title: Moderació
       title: Federació
-      total_blocked_by_us: Bloquejades per nosaltres
-      total_followed_by_them: Seguides per ells
-      total_followed_by_us: Seguides per nosaltres
-      total_reported: Informes sobre elles
+      total_blocked_by_us: Bloquejats per nosaltres
+      total_followed_by_them: Seguits per ells
+      total_followed_by_us: Seguits per nosaltres
+      total_reported: Informes sobre ells
       total_storage: Adjunts multimèdia
     invites:
       deactivate_all: Desactiva-ho tot
@@ -336,7 +337,7 @@ ca:
       disable: Inhabilita
       disabled: Desactivat
       enable: Activat
-      enable_hint: Una vegada habilitat, el teu servidor es subscriurà a tots els toots públics d'aquest relay i començarà a enviar-hi tots els toots públics d'aquest servidor.
+      enable_hint: Una vegada habilitat el teu servidor es subscriurà a tots els toots públics d'aquest relay i començarà a enviar-hi tots els toots públics d'aquest servidor.
       enabled: Activat
       inbox_url: URL del Relay
       pending: S'està esperant l'aprovació del relay
@@ -385,7 +386,7 @@ ca:
         desc_html: Separa diversos noms d'usuari amb comes. Només funcionaran els comptes locals i desblocats. El valor predeterminat quan està buit és tots els administradors locals.
         title: El seguiment per defecte per als usuaris nous
       contact_information:
-        email: Introdueix una adreça de correu electrònic píblica
+        email: Introdueix una adreça de correu electrònic pública
         username: Nom d'usuari del contacte
       custom_css:
         desc_html: Modifica l'aspecte amb CSS carregat a cada pàgina
@@ -586,6 +587,9 @@ ca:
       content: Ho sentim, però alguna cosa ha fallat a la nostra banda.
       title: Aquesta pàgina no es correcta
     noscript_html: Per a utilitzar Mastodon, activa el JavaScript. També pots provar una de les <a href="%{apps_path}"> aplicacions natives</a> de Mastodon per a la vostra plataforma.
+  existing_username_validator:
+    not_found: no s'ha pogut trobar cap usuari local amb aquest nom d'usuari
+    not_found_multiple: no s'ha pogut trobar %{usernames}
   exports:
     archive_takeout:
       date: Data
@@ -629,10 +633,31 @@ ca:
     all: Tot
     changes_saved_msg: Els canvis s'han desat correctament!
     copy: Copia
+    order_by: Ordena per
     save_changes: Desa els canvis
     validation_errors:
       one: Alguna cosa no va bé! Si us plau, revisa l'error
       other: Alguna cosa no va bé! Si us plau, revisa %{count} errors més a baix
+  html_validator:
+    invalid_markup: 'conté HTML markup no vàlid: %{error}'
+  identity_proofs:
+    active: Actiu
+    authorize: Sí, autoritza
+    authorize_connection_prompt: Autoritzar aquesta connexió criptogràfica?
+    errors:
+      failed: Ha fallat la connexió criptogràfica. Torna-ho a provar des de %{provider}.
+      keybase:
+        invalid_token: Els tokens de Keybase són hashs de signatures i han de tenir 66 caràcters hexadecimals
+        verification_failed: Keybase no reconeix aquest token com a signatura del usuari de Keybase %{kb_username}. Si us plau prova des de Keybase.
+      wrong_user: No es pot crear una prova per a %{proving} mentre es connectava com a %{current}. Inicia sessió com a %{proving} i prova de nou.
+    explanation_html: Aquí pots connectar criptogràficament les teves altres identitats com ara el teu perfil de Keybase. Això permet que altres persones t'envïin missatges xifrats i continguts de confiança que els hi enviess.
+    i_am_html: Sóc %{username} a %{service}.
+    identity: Identitat
+    inactive: Inactiu
+    publicize_checkbox: 'I tooteja això:'
+    publicize_toot: 'Està provat! Sóc %{username} a %{service}: %{url}'
+    status: Estat de verificació
+    view_proof: Veure la prova
   imports:
     modes:
       merge: Fusionar
@@ -753,6 +778,8 @@ ca:
   relationships:
     activity: Activitat del compte
     dormant: Inactiu
+    last_active: Darrer actiu
+    most_recent: Més recent
     moved: Mogut
     mutual: Mútua
     primary: Primari
@@ -835,6 +862,7 @@ ca:
     edit_profile: Editar perfil
     export: Exportar informació
     featured_tags: Etiquetes destacades
+    identity_proofs: Proves d'identitat
     import: Importar
     migrate: Migració del compte
     notifications: Notificacions
@@ -862,7 +890,7 @@ ca:
     over_character_limit: Límit de caràcters de %{max} superat
     pin_errors:
       limit: Ja has fixat el màxim nombre de toots
-      ownership: El toot d'algú altre no es pot fixar
+      ownership: No es pot fixar el toot d'algú altre
       private: No es pot fixar el toot no públic
       reblog: No es pot fixar un impuls
     poll:
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 43d8764ff..96b31cab4 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -196,7 +196,7 @@ cs:
         destroy_domain_block: "%{name} odblokoval/a doménu %{target}"
         destroy_email_domain_block: "%{name} odebral/a e-mailovou doménu %{target} z černé listiny"
         destroy_status: "%{name} odstranil/a toot uživatele %{target}"
-        disable_2fa_user: "%{name} vypnul/a požadavek pro dvoufaktorovou autentikaci pro uživatele %{target}"
+        disable_2fa_user: "%{name} vypnul/a dvoufázové ověřování pro uživatele %{target}"
         disable_custom_emoji: "%{name} zakázal/a emoji %{target}"
         disable_user: "%{name} zakázal/a přihlašování pro uživatele %{target}"
         enable_custom_emoji: "%{name} povolil/a emoji %{target}"
@@ -640,6 +640,7 @@ cs:
     all: Všechny
     changes_saved_msg: Změny byly úspěšně uloženy!
     copy: Kopírovat
+    order_by: Seřadit od
     save_changes: Uložit změny
     validation_errors:
       few: Něco ještě není úplně v pořádku! Prosím zkontrolujte %{count} chyby níže
@@ -787,8 +788,8 @@ cs:
   relationships:
     activity: Aktivita účtu
     dormant: Nečinné
-    last_active: Naposledy aktivní
-    most_recent: Nedávno přidaní
+    last_active: Naposledy aktivních
+    most_recent: Naposledy přidaných
     moved: Přesunuté
     mutual: Vzájemné
     primary: Primární
@@ -878,7 +879,7 @@ cs:
     preferences: Předvolby
     relationships: Sledovaní a sledující
     settings: Nastavení
-    two_factor_authentication: Dvoufaktorové ověřování
+    two_factor_authentication: Dvoufázové ověřování
     your_apps: Vaše aplikace
   statuses:
     attached:
@@ -953,7 +954,7 @@ cs:
 
       <h3 id="protect">Jak vaše informace chráníme?</h3>
 
-      <p>Implenentujeme různá bezpečnostní opatření pro udržování bezpečnosti vašich osobních dat, když zadáváte, odesíláte, či přistupujete k vašim osobním datům. Mimo jiné je vaše relace v prohlížeči, jakož i provoz mezi vašimi aplikacemi a API, zabezpečena pomocí SSL, a vaše heslo je hashováno pomocí silného jednosměrného algoritmu. Pro větší zabezpečení vašeho účtu můžete povolit dvoufaktorovou autentikaci.</p>
+      <p>Implenentujeme různá bezpečnostní opatření pro udržování bezpečnosti vašich osobních dat, když zadáváte, odesíláte, či přistupujete k vašim osobním datům. Mimo jiné je vaše relace v prohlížeči, jakož i provoz mezi vašimi aplikacemi a API, zabezpečena pomocí SSL, a vaše heslo je hashováno pomocí silného jednosměrného algoritmu. Pro větší zabezpečení vašeho účtu můžete povolit dvoufázové ověřování.</p>
 
       <hr class="spacer" />
 
@@ -1017,18 +1018,18 @@ cs:
       default: "%d. %b %Y, %H:%M"
       month: "%b %Y"
   two_factor_authentication:
-    code_hint: Pro potvrzení zadejte kód vygenerovaný vaší autentikační aplikací
-    description_html: Povolíte-li <strong>dvoufaktorové ověřování</strong>, budete při přihlášení potřebovat telefon, který vám vygeneruje přístupové tokeny, které musíte zadat.
+    code_hint: Pro potvrzení zadejte kód vygenerovaný vaší ověřovací aplikací
+    description_html: Povolíte-li <strong>dvoufázové ověřování</strong>, budete při přihlášení potřebovat telefon, který vám vygeneruje přístupové tokeny, které musíte zadat.
     disable: Zakázat
     enable: Povolit
-    enabled: Dvoufaktorové ověřování je povoleno
-    enabled_success: Dvoufaktorové ověřování bylo úspěšně povoleno
+    enabled: Dvoufázové ověřování je povoleno
+    enabled_success: Dvoufázové ověřování bylo úspěšně povoleno
     generate_recovery_codes: Vygenerovat záložní kódy
     instructions_html: "<strong>Naskenujte tento QR kód Google Authenticatorem nebo jinou TOTP aplikací na vašem telefonu</strong>. Od teď bude tato aplikace generovat tokeny, které budete muset zadat při přihlášení."
     lost_recovery_codes: Záložní kódy vám dovolí dostat se k vašemu účtu, pokud ztratíte telefon. Ztratíte-li záložní kódy, můžete je zde znovu vygenerovat. Vaše staré záložní kódy budou zneplatněny.
     manual_instructions: 'Nemůžete-li oskenovat QR kód a je potřeba ho zadat ručně, zde je tajemství v prostém textu:'
     recovery_codes: Záložní kódy pro obnovu
-    recovery_codes_regenerated: Záložní kódy byly úspěšně znovu vygenerované
+    recovery_codes_regenerated: Záložní kódy byly úspěšně znovu vygenerovány
     recovery_instructions_html: Ztratíte-li někdy přístup k vašemu telefonu, můžete k získání přístupu k účtu použít jeden ze záložních kódů. <strong>Uchovávejte tyto kódy v bezpečí</strong>. Můžete si je například vytisknout a uložit je mezi jiné důležité dokumenty.
     setup: Nastavit
     wrong_code: Zadaný kód byl neplatný! Je čas na serveru a na zařízení správný?
@@ -1073,7 +1074,7 @@ cs:
   users:
     follow_limit_reached: Nemůžete sledovat více než %{limit} lidí
     invalid_email: E-mailová adresa je neplatná
-    invalid_otp_token: Neplatný kód pro dvoufaktorovou autentikaci
+    invalid_otp_token: Neplatný kód pro dvoufázové ověřování
     otp_lost_help_html: Pokud jste ztratil/a přístup k oběma, můžete se spojit %{email}
     seamless_external_login: Jste přihlášen/a přes externí službu, nastavení hesla a e-mailu proto nejsou dostupná.
     signed_in_as: 'Přihlášen/a jako:'
diff --git a/config/locales/devise.ko.yml b/config/locales/devise.ko.yml
new file mode 100644
index 000000000..33ca8f842
--- /dev/null
+++ b/config/locales/devise.ko.yml
@@ -0,0 +1,76 @@
+---
+ko:
+  devise:
+    confirmations:
+      confirmed: 이메일이 성공적으로 확인 되었습니다.
+      send_instructions: 몇 분 이내로 확인 이메일이 발송 됩니다. 이메일을 받지 못 한 경우, 스팸 폴더를 확인하세요.
+      send_paranoid_instructions: 당신의 이메일이 우리의 DB에 있을 경우 몇 분 이내로 확인 메일이 발송 됩니다. 이메일을 받지 못 한 경우, 스팸 폴더를 확인하세요.
+    failure:
+      already_authenticated: 이미 로그인 된 상태입니다.
+      inactive: 계정이 활성화 되지 않았습니다.
+      invalid: 올바르지 않은 %{authentication_keys} 혹은 패스워드입니다.
+      last_attempt: 계정이 잠기기까지 한 번의 시도가 남았습니다.
+      locked: 계정이 잠겼습니다.
+      not_found_in_database: 올바르지 않은 %{authentication_keys} 혹은 패스워드입니다.
+      pending: 계정이 아직 심사 중입니다.
+      timeout: 세션이 만료 되었습니다. 다시 로그인 해 주세요.
+      unauthenticated: 계속 하려면 로그인을 해야 합니다.
+      unconfirmed: 계속 하려면 이메일을 확인 받아야 합니다.
+    mailer:
+      confirmation_instructions:
+        action: 이메일 확인
+        action_with_app: 확인하고 %{app}으로 돌아가기
+        explanation: 당신은 %{host}에서 이 이메일로 가입하셨습니다. 클릭만 하시면 계정이 활성화 됩니다. 만약 당신이 가입한 게 아니라면 이 메일을 무시해 주세요.
+        explanation_when_pending: 당신은 %{host}에 가입 요청을 하셨습니다. 이 이메일이 확인 되면 우리가 가입 요청을 리뷰하고 승인할 수 있습니다. 그 전까지는 로그인을 할 수 없습니다. 당신의 가입 요청이 거부 될 경우 당신에 대한 정보는 모두 삭제 되며 따로 요청 할 필요는 없습니다. 만약 당신이 가입 요청을 한 게 아니라면 이 메일을 무시해 주세요.
+        extra_html: <a href="%{terms_path}">서버의 룰</a>과 <a href="%{policy_path}">이용 약관</a>도 확인해 주세요.
+        subject: '마스토돈: %{instance}에 대한 확인 메일'
+        title: 이메일 주소 확인
+      email_changed:
+        explanation: '당신의 계정에 대한 이메일이 다음과 같이 바뀌려고 합니다:'
+        extra: 만약 당신이 메일을 바꾸지 않았다면 누군가가 당신의 계정에 대한 접근 권한을 얻은 것입니다. 즉시 패스워드를 바꾼 후, 계정이 잠겼다면 서버의 관리자에게 연락 하세요.
+        subject: '마스토돈: 이메일이 변경 되었습니다'
+        title: 새 이메일 주소
+      password_change:
+        explanation: 당신의 계정 패스워드가 변경되었습니다.
+        extra: 만약 패스워드 변경을 하지 않았다면 누군가가 당신의 계정에 대한 접근 권한을 얻은 것입니다. 즉시 패스워드를 바꾼 후, 계정이 잠겼다면 서버의 관리자에게 연락 하세요.
+        subject: '마스토돈: 패스워드가 변경 되었습니다'
+        title: 패스워드가 변경 되었습니다
+      reconfirmation_instructions:
+        explanation: 이메일 주소를 바꾸려면 새 이메일 주소를 확인해야 합니다.
+        extra: 당신이 시도한 것이 아니라면 이 메일을 무시해 주세요. 위 링크를 클릭하지 않으면 이메일 변경은 일어나지 않습니다.
+        subject: '마스토돈: %{instance}에 대한 이메일 확인'
+        title: 이메일 주소 확인
+      reset_password_instructions:
+        action: 패스워드 변경
+        explanation: 계정에 대한 패스워드 변경을 요청하였습니다.
+        extra: 만약 당신이 시도한 것이 아니라면 이 메일을 무시해 주세요. 위 링크를 클릭해 패스워드를 새로 설정하기 전까지는 패스워드가 바뀌지 않습니다.
+        subject: '마스토돈: 패스워드 재설정 방법'
+        title: 패스워드 재설정
+      unlock_instructions:
+        subject: '마스토돈: 잠금 해제 방법'
+    omniauth_callbacks:
+      failure: '"%{reason}" 때문에 당신을 %{kind}에서 인증할 수 없습니다.'
+      success: 성공적으로 %{kind} 계정을 인증 했습니다.
+    passwords:
+      no_token: 패스워드 재설정 이메일을 거치지 않고는 여기에 올 수 없습니다. 만약 패스워드 재설정 메일에서 온 것이라면 URL이 맞는지 확인해 주세요.
+      send_instructions: 당신의 이메일 주소가 우리의 DB에 있아면 패스워드 복구 링크가 몇 분 이내에 메일로 발송 됩니다. 만약 메일을 받지 못 하신 경우 스팸 폴더를 확인해 주세요.
+      send_paranoid_instructions: 당신의 이메일 주소가 우리의 DB에 있아면 패스워드 복구 링크가 몇 분 이내에 메일로 발송 됩니다. 만약 메일을 받지 못 하신 경우 스팸 폴더를 확인해 주세요.
+      updated: 패스워드가 재설정 되었습니다. 로그인 되었습니다.
+      updated_not_active: 패스워드가 성공적으로 변경 되었습니다.
+    registrations:
+      destroyed: 안녕히 가세요! 계정이 성공적으로 제거되었습니다. 다시 만나기를 희망합니다.
+      signed_up: 안녕하세요! 성공적으로 가입했습니다.
+      signed_up_but_inactive: 성공적으로 가입 했습니다. 그러나, 계정이 활성화 되지 않았기 때문에 아직 로그인 할 수 없습니다.
+      signed_up_but_locked: 성공적으로 가입 했습니다. 그러나, 계정이 잠겨있기 때문에 아직 로그인 할 수 없습니다.
+      signed_up_but_pending: 확인 링크를 포함한 메일이 발송 되었습니다. 링크를 클릭한 이후, 우리가 당신의 신청양식을 검토합니다. 승인이 되면 알림을 발송합니다.
+      signed_up_but_unconfirmed: 확인 링크를 포함한 메일이 발송 되었습니다. 링크를 클릭해 계정을 활성화 하세요. 메일을 받지 못 하신 경우 스팸 폴더를 확인해 주세요.
+      update_needs_confirmation: 계정 정보를 업데이트 했습니다. 하지만 새 이메일 주소에 대한 확인이 필요합니다. 이메일을 확인 한 후 링크를 통해 새 이메일을 확인 하세요. 메일을 받지 못 하신 경우 스팸 폴더를 확인해 주세요.
+      updated: 계정 정보가 성공적으로 업데이트 되었습니다.
+    sessions:
+      already_signed_out: 로그아웃 되었습니다.
+      signed_in: 로그인 되었습니다.
+      signed_out: 로그아웃 되었습니다.
+    unlocks:
+      send_instructions: 몇 분 이내로 계정 잠금 해제에 대한 안내 메일이 발송 됩니다. 메일을 받지 못 하신 경우 스팸 폴더를 확인해 주세요.
+      send_paranoid_instructions: 계정이 존재한다면 몇 분 이내로 계정 잠금 해제에 대한 안내 메일이 발송 됩니다. 메일을 받지 못 하신 경우 스팸 폴더를 확인해 주세요.
+      unlocked: 계정이 성공적으로 잠금 해제 되었습니다. 계속 하려면 로그인 하세요.
diff --git a/config/locales/devise.pt-BR.yml b/config/locales/devise.pt-BR.yml
index ede004892..0b6d36187 100644
--- a/config/locales/devise.pt-BR.yml
+++ b/config/locales/devise.pt-BR.yml
@@ -12,6 +12,7 @@ pt-BR:
       last_attempt: Você tem apenas mais uma tentativa sobrando antes que a sua conta seja bloqueada.
       locked: A sua conta está bloqueada.
       not_found_in_database: "%{authentication_keys} ou senha inválida."
+      pending: Sua conta ainda está sendo revisada.
       timeout: A sua sessão expirou. Por favor, entre novamente para continuar.
       unauthenticated: Você precisa entrar ou cadastrar-se antes de continuar.
       unconfirmed: Você precisa confirmar o seu endereço de e-mail antes de continuar.
@@ -20,6 +21,7 @@ pt-BR:
         action: Verificar endereço de e-mail
         action_with_app: Confirmar e voltar para %{app}
         explanation: Você criou uma conta em %{host} com esse endereço de e-mail. Você está a um clique de ativá-la. Se não foi você, por favor ignore esse e-mail.
+        explanation_when_pending: Você pediu um convite para %{host} com esse endereço de email. Assim que você confirmar o seu endereço de e-mail, iremos revisar o seu pedido. Você não poderá fazer login até então. Se sua aplicação for rejeitada, seus dados serão removidos e nenhuma ação será necessária da sua parte. Se você não pediu por isso, por favor ignore esse e-mail.
         extra_html: Por favor confira também <a href="%{terms_path}">as regras da instância</a> e <a href="%{policy_path}">nossos termos de serviço</a>.
         subject: 'Mastodon: Instruções de confirmação para %{instance}'
         title: Verifique o endereço de e-mail
@@ -60,6 +62,7 @@ pt-BR:
       signed_up: Bem vindo! A sua conta foi registrada com sucesso.
       signed_up_but_inactive: A sua conta foi registrada. No entanto, não abrimos a sua sessão porque a sua conta ainda não foi ativada.
       signed_up_but_locked: A sua conta foi registrada. No entanto, não abrimos a sua sessão porque a sua conta está bloqueada.
+      signed_up_but_pending: Uma mensagem com um link de confirmação foi enviada ao seu endereço de e-mail. Depois que você clicar no link, revisaremos seu pedido. Você será notificado se seu pedido for aprovado.
       signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o seu endereço de e-mail. Por favor, siga o link para ativar a sua conta e, caso não tenha recebido esta mensagem, cheque a sua pasta de spam.
       update_needs_confirmation: Você mudou o seu endereço de e-mail ou a sua senha, mas é necessário confirmar a mudança. Por favor siga o link que foi enviado para o seu novo endereço de e-mail e, caso não tenha recebido esta mensagem, cheque a sua pasta de spam.
       updated: A sua conta foi alterada com sucesso.
diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml
index 56686e3e5..8366912dc 100644
--- a/config/locales/doorkeeper.ca.yml
+++ b/config/locales/doorkeeper.ca.yml
@@ -77,9 +77,9 @@ ca:
         title: Les teves aplicacions autoritzades
     errors:
       messages:
-        access_denied: El propietari del recurs o servidor de autorizació ha denegat la petició.
+        access_denied: El propietari del recurs o servidor d'autorizació ha denegat la petició.
         credential_flow_not_configured: Les credencials de contrasenya del propietari del recurs han fallat degut a que Doorkeeper.configure.resource_owner_from_credentials està sense configurar.
-        invalid_client: La autentificació del client falló perquè és un client desconegut o no està inclòs l'autentificació del client o el mètode d'autenticació no està confirmat.
+        invalid_client: La autentificació del client ha fallat perquè és un client desconegut o no està inclòs l'autentificació del client o el mètode d'autenticació no està confirmat.
         invalid_grant: La concessió d'autorizació oferida és invàlida, ha vençut, s'ha revocat, no coincideix amb l'URI de redirecció utilizada en la petició d'autorizació, o fou emesa per a un altre client.
         invalid_redirect_uri: L'URI de redirecció inclòs no és vàlid.
         invalid_request: En la petició manca un paràmetre necessari o inclou un valor de paràmetre no suportat o te un altre tipus de format incorrecte.
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index 4621b93fc..dcbf0065b 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -5,11 +5,13 @@ eo:
     about_mastodon_html: Mastodon estas socia reto bazita sur malfermitaj retaj protokoloj kaj sur libera malfermitkoda programo. Ĝi estas sencentra kiel retmesaĝoj.
     about_this: Pri
     active_count_after: aktiva
+    active_footnote: Monate Aktivaj Uzantoj (MAU)
     administered_by: 'Administrata de:'
     api: API
     apps: Poŝtelefonaj aplikaĵoj
     apps_platforms: Uzu Mastodon ĉe iOS, Android kaj aliajn platformojn
     browse_directory: Esplori profilujo kaj filtri per interesoj
+    browse_public_posts: Vidi vivantan fluon de publikaj mesaĝoj al Mastodon
     contact: Kontakti
     contact_missing: Ne elektita
     contact_unavailable: Ne disponebla
@@ -24,11 +26,14 @@ eo:
     hosted_on: "%{domain} estas nodo de Mastodon"
     learn_more: Lerni pli
     privacy_policy: Privateca politiko
+    see_whats_happening: Vidi kio okazas
+    server_stats: Servo statuso
     source_code: Fontkodo
     status_count_after:
       one: mesaĝo
       other: mesaĝoj
     status_count_before: Kie skribiĝis
+    tagline: Sekvi amikojn kaj trovi novan onin
     terms: Uzkondiĉoj
     user_count_after:
       one: uzanto
@@ -74,6 +79,7 @@ eo:
       delete: Forigi
       destroyed_msg: Kontrola noto sukcese detruita!
     accounts:
+      approve: Aprobi
       are_you_sure: Ĉu vi certas?
       avatar: Profilbildo
       by_domain: Domajno
@@ -119,6 +125,7 @@ eo:
       moderation:
         active: Aktivaj
         all: Ĉio
+        pending: Pritraktata
         silenced: Silentigitaj
         suspended: Haltigitaj
         title: Kontrolado
@@ -128,6 +135,7 @@ eo:
       no_limits_imposed: Neniu limito trudita
       not_subscribed: Ne abonita
       outbox_url: Elira URL
+      pending: Pritraktata recenzo
       perform_full_suspension: Haltigi
       profile_url: Profila URL
       promote: Plirangigi
@@ -135,6 +143,7 @@ eo:
       public: Publika
       push_subscription_expires: Eksvalidiĝo de la abono al PuSH
       redownload: Aktualigi profilon
+      reject: Malakcepti
       remove_avatar: Forigi profilbildon
       remove_header: Forigi kapan bildon
       resend_confirmation:
@@ -236,6 +245,7 @@ eo:
       feature_profile_directory: Profilujo
       feature_registrations: Registriĝoj
       feature_relay: Federacia ripetilo
+      feature_timeline_preview: Templinio antaŭvidi
       features: Funkcioj
       hidden_service: Federacio kun kaŝitaj servoj
       open_reports: nefermitaj raportoj
@@ -406,6 +416,12 @@ eo:
         min_invite_role:
           disabled: Neniu
           title: Permesi invitojn de
+      registrations_mode:
+        modes:
+          approved: Bezonas aprobi por aliĝi
+          none: Neniu povas aliĝi
+          open: Iu povas aliĝi
+        title: Registrado modo
       show_known_fediverse_at_about_page:
         desc_html: Kiam ŝaltita, ĝi montros mesaĝojn de la tuta konata fediverse antaŭvide. Aliokaze, ĝi montros nur lokajn mesaĝojn.
         title: Montri konatan fediverse en tempolinia antaŭvido
@@ -429,7 +445,7 @@ eo:
         desc_html: Uzata por antaŭvidoj per OpenGraph kaj per API. 1200x630px rekomendita
         title: Bildeto de la servilo
       timeline_preview:
-        desc_html: Montri publikan tempolinion en komenca paĝo
+        desc_html: Montri publikan templinion en komenca paĝo
         title: Tempolinia antaŭvido
       title: Retejaj agordoj
     statuses:
@@ -468,6 +484,9 @@ eo:
       edit_preset: Redakti avertan antaŭagordon
       title: Administri avertajn antaŭagordojn
   admin_mailer:
+    new_pending_account:
+      body: La detaloj de la nova konto estas sube. Vi povas aprobi aŭ Malakcepti ĉi kandidatiĝo.
+      subject: Nova konto atendas por recenzo en %{instance} (%{username})
     new_report:
       body: "%{reporter} signalis %{target}"
       body_remote: Iu de %{domain} signalis %{target}
@@ -489,7 +508,9 @@ eo:
     your_token: Via alira ĵetono
   auth:
     agreement_html: Klakante “Registriĝi” sube, vi konsentas kun <a href="%{rules_path}">la reguloj de la servilo</a> kaj <a href="%{terms_path}">niaj uzkondiĉoj</a>.
+    apply_for_account: Peti inviton
     change_password: Pasvorto
+    checkbox_agreement_html: Mi samopinii al la <a href="%{rules_path}" target="_blank">Servo reguloj</a> kaj <a href="%{terms_path}" target="_blank">kondiĉo al servadon</a>
     confirm_email: Konfirmi retadreson
     delete_account: Forigi konton
     delete_account_html: Se vi deziras forigi vian konton, vi povas <a href="%{path}">fari tion ĉi tie</a>. Vi bezonos konfirmi vian peton.
@@ -505,10 +526,12 @@ eo:
       cas: CAS
       saml: SAML
     register: Registriĝi
+    registration_closed: "%{instance} ne estas akcepti nova uzantojn"
     resend_confirmation: Resendi la instrukciojn por konfirmi
     reset_password: Ŝanĝi pasvorton
     security: Sekureco
     set_new_password: Elekti novan pasvorton
+    trouble_logging_in: Ĝeni ensaluti?
   authorize_follow:
     already_following: Vi jam sekvas tiun konton
     error: Bedaŭrinde, estis eraro en la serĉado de la fora konto
@@ -566,6 +589,9 @@ eo:
     noscript_html: |-
       Por uzi la retan aplikaĵon de Mastodon, bonvolu ebligi JavaScript. Alimaniere, provu unu el la
       <a href="%{apps_path}">operaciumaj aplikaĵoj</a> por Mastodon por via platformo.
+  existing_username_validator:
+    not_found: Ne povas trovi lokaj uzanto kun tiu uzantnomo
+    not_found_multiple: Ne povas trovi %{usernames}
   exports:
     archive_takeout:
       date: Dato
@@ -587,9 +613,9 @@ eo:
       limit: Vi jam elstarigis la maksimuman kvanton da kradvortoj
   filters:
     contexts:
-      home: Hejma tempolinio
+      home: Hejma templinio
       notifications: Sciigoj
-      public: Publikaj tempolinioj
+      public: Publika templinio
       thread: Konversacioj
     edit:
       title: Ŝanĝi filtrilojn
@@ -606,12 +632,34 @@ eo:
     more: Pli…
     resources: Rimedoj
   generic:
+    all: Ĉio
     changes_saved_msg: Ŝanĝoj sukcese konservitaj!
     copy: Kopii
+    order_by: Ordigi de
     save_changes: Konservi ŝanĝojn
     validation_errors:
       one: Io mise okazis! Bonvolu konsulti la suban erar-raporton
       other: Io mise okazis! Bonvolu konsulti la subajn %{count} erar-raportojn
+  html_validator:
+    invalid_markup: 'havas malvalida HTML markado: %{error}'
+  identity_proofs:
+    active: Aktiva
+    authorize: Jes, permesi
+    authorize_connection_prompt: Permesi ĉi tiu ĉifrikan conekton?
+    errors:
+      failed: La ĉifrika conekto nefaris. Peti provu denove el %{provider}.
+      keybase:
+        invalid_token: Keybase signo estas haŝoj de subskribo kaj devi 66 deksesuma leteroj
+        verification_failed: Keybase ne rekoni ĉi tiu signo kiel subskribo de Keybase uzanto %{kb_username}. Peti provu denove el Keybase.
+      wrong_user: Ne povas krei por %{proving} dum ensalutis kiel %{current}. Ensaluti kiel %{proving} kaj provu denove.
+    explanation_html: Ĉi tie vi povas ĉifrika konekti via alia identicoj, kiel Keybase profilon. ĉi tiu igi aliaj popoloj sendi al vi ĉifritaj mesaĝoj kaj fidi kontento vi sendi al ilin.
+    i_am_html: Mi estas %{username} en %{service}.
+    identity: Identeco
+    inactive: Malaktiva
+    publicize_checkbox: 'And toot this:'
+    publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}'
+    status: Confirmo statuso
+    view_proof: Vidi pruvo
   imports:
     modes:
       merge: Kunigi
@@ -729,6 +777,19 @@ eo:
     other: Aliaj aferoj
     publishing: Publikado
     web: Reto
+  relationships:
+    activity: Account activity
+    dormant: Dormant
+    last_active: Last active
+    most_recent: Most recent
+    moved: Moved
+    mutual: Mutual
+    primary: Primary
+    relationship: Relationship
+    remove_selected_domains: Remove all followers from the selected domains
+    remove_selected_followers: Remove selected followers
+    remove_selected_follows: Unfollow selected users
+    status: Account status
   remote_follow:
     acct: Enmetu vian uzantnomo@domajno de kie vi volas agi
     missing_resource: La URL de plusendado ne estis trovita
@@ -803,10 +864,12 @@ eo:
     edit_profile: Redakti profilon
     export: Eksporti datumojn
     featured_tags: Elstarigitaj kradvortoj
+    identity_proofs: Identity proofs
     import: Importi
     migrate: Konta migrado
     notifications: Sciigoj
     preferences: Preferoj
+    relationships: Follows and followers
     settings: Agordoj
     two_factor_authentication: Dufaktora aŭtentigo
     your_apps: Viaj aplikaĵoj
@@ -846,7 +909,7 @@ eo:
       public: Publika
       public_long: Ĉiuj povas vidi
       unlisted: Nelistigita
-      unlisted_long: Ĉiuj povas vidi, sed nelistigita en publikaj tempolinioj
+      unlisted_long: Ĉiuj povas vidi, sed nelistigita en publikaj templinioj
   stream_entries:
     pinned: Alpinglita
     reblogged: diskonigita
@@ -903,15 +966,15 @@ eo:
       edit_profile_step: Vi povas proprigi vian profilon per alŝuto de profilbildo, fonbildo, ŝanĝo de via afiŝita nomo kaj pli. Se vi ŝatus kontroli novajn sekvantojn antaŭ ol ili rajtas sekvi vin, vi povas ŝlosi vian konton.
       explanation: Jen kelkaj konsiloj por helpi vin komenci
       final_action: Ekmesaĝi
-      final_step: 'Ekmesaĝu! Eĉ sen sekvantoj, viaj publikaj mesaĝoj povas esti vidataj de aliaj, ekzemple en la loka tempolinio kaj en la kradvortoj. Eble vi ŝatus prezenti vin per la kradvorto #introductions.'
+      final_step: 'Ekmesaĝu! Eĉ sen sekvantoj, viaj publikaj mesaĝoj povas esti vidataj de aliaj, ekzemple en la loka templinio kaj en la kradvortoj. Eble vi ŝatus prezenti vin per la kradvorto #introductions.'
       full_handle: Via kompleta uzantnomo
       full_handle_hint: Jen kion vi dirus al viaj amikoj, por ke ili mesaĝu aŭ sekvu vin de alia servilo.
       review_preferences_action: Ŝanĝi preferojn
       review_preferences_step: Estu certa ke vi agordis viajn preferojn, kiel kiujn retmesaĝojn vi ŝatus ricevi, aŭ kiun dekomencan privatecan nivelon vi ŝatus ke viaj mesaĝoj havu. Se tio ne ĝenas vin, vi povas ebligi aŭtomatan ekigon de GIF-oj.
       subject: Bonvenon en Mastodon
-      tip_federated_timeline: La fratara tempolinio estas antaŭvido de la reto de Mastodon. Sed ĝi enhavas nur homojn, kiuj estas sekvataj de aliaj homoj de via nodo, do ĝi ne estas kompleta.
-      tip_following: Vi dekomence sekvas la administrantojn de via servilo. Por trovi pli da interesaj homoj, rigardu la lokan kaj frataran tempoliniojn.
-      tip_local_timeline: La loka tempolinio estas antaŭvido de la homoj en %{instance}. Ĉi tiuj estas viaj apudaj najbaroj!
+      tip_federated_timeline: La fratara templinio estas antaŭvido de la reto de Mastodon. Sed ĝi enhavas nur homojn, kiuj estas sekvataj de aliaj homoj de via nodo, do ĝi ne estas kompleta.
+      tip_following: Vi dekomence sekvas la administrantojn de via servilo. Por trovi pli da interesaj homoj, rigardu la lokan kaj frataran templiniojn.
+      tip_local_timeline: La loka templinio estas antaŭvido de la homoj en %{instance}. Ĉi tiuj estas viaj apudaj najbaroj!
       tip_mobile_webapp: Se via telefona retumilo proponas al vi aldoni Mastodon al via hejma ekrano, vi povas ricevi puŝsciigojn. Tio multmaniere funkcias kiel operaciuma aplikaĵo!
       tips: Konsiloj
       title: Bonvenon, %{name}!
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index be1ea6155..6a82a41b1 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -4,25 +4,36 @@ pt-BR:
     about_hashtag_html: Estes são toots públicos com a hashtag <strong>#%{hashtag}</strong>. Você pode interagir com eles se tiver uma conta em qualquer lugar no fediverso.
     about_mastodon_html: Mastodon é uma rede social baseada em protocolos abertos e software gratuito e de código aberto. É descentralizada como e-mail.
     about_this: Sobre
+    active_count_after: Ativo
+    active_footnote: Usuários ativos mensais (UAM)
     administered_by: 'Administrado por:'
     api: API
     apps: Apps
+    apps_platforms: Use o Mastodon a partir de iOS, Android e outras plataformas
+    browse_directory: Navegue pelo diretório de perfis e filtre por interesses
+    browse_public_posts: Navegue pelos posts públicos sendo postados ao vivo no Mastodon
     contact: Contato
     contact_missing: Não definido
     contact_unavailable: Não disponível
+    discover_users: Descubra usuários
     documentation: Documentação
     extended_description_html: |
       <h3>Um bom lugar para regras</h3>
       <p>A descrição da instância ainda não foi feita.</p>
+    federation_hint_html: Com uma conta em %{instance} você vai poder seguir pessoas em qualquer servidor Mastodon ou outros do fediverso.
     generic_description: "%{domain} é um servidor na rede"
+    get_apps: Experimente um aplicativo
     hosted_on: Mastodon hospedado em %{domain}
     learn_more: Saiba mais
     privacy_policy: Política de Privacidade
+    see_whats_happening: Veja o que está acontecendo
+    server_stats: 'Estatísticas do servidor:'
     source_code: Código-fonte
     status_count_after:
       one: status
       other: status
     status_count_before: Autores de
+    tagline: Siga amigos e encontre novos
     terms: Termos de serviço
     user_count_after:
       one: usuário
@@ -68,6 +79,7 @@ pt-BR:
       delete: Excluir
       destroyed_msg: Nota de moderação excluída com sucesso!
     accounts:
+      approve: Aprovar
       are_you_sure: Você tem certeza?
       avatar: Avatar
       by_domain: Domínio
@@ -113,6 +125,7 @@ pt-BR:
       moderation:
         active: Ativo
         all: Todos
+        pending: Pendente
         silenced: Silenciados
         suspended: Suspensos
         title: Moderação
@@ -122,6 +135,7 @@ pt-BR:
       no_limits_imposed: Nenhum limite imposto
       not_subscribed: Não está inscrito
       outbox_url: URL da caixa de saída
+      pending: Esperando revisão
       perform_full_suspension: Suspender
       profile_url: URL do perfil
       promote: Promover
@@ -129,6 +143,7 @@ pt-BR:
       public: Público
       push_subscription_expires: Inscrição PuSH expira
       redownload: Atualizar perfil
+      reject: Rejeitar
       remove_avatar: Remover avatar
       remove_header: Remover cabeçalho
       resend_confirmation:
@@ -230,6 +245,7 @@ pt-BR:
       feature_profile_directory: Diretório de perfis
       feature_registrations: Cadastros
       feature_relay: Repetidor da federação
+      feature_timeline_preview: pré-visualização da timeline
       features: Funcionalidades
       hidden_service: Federação com serviços onion
       open_reports: Denúncias em aberto
@@ -291,6 +307,7 @@ pt-BR:
       back_to_account: Voltar para a conta
       title: Pessoas que seguem %{acct}
     instances:
+      by_domain: Domínio
       delivery_available: Entrega está disponível
       known_accounts:
         one: "%{count} conta conhecida"
@@ -370,7 +387,7 @@ pt-BR:
         title: Usuários a serem seguidos por padrão por novas contas
       contact_information:
         email: E-mail
-        username: Contate usuário
+        username: Usuário de contato
       custom_css:
         desc_html: Modificar o visual com CSS que é carregado em todas as páginas
         title: CSS customizado
@@ -399,6 +416,12 @@ pt-BR:
         min_invite_role:
           disabled: Ninguém
           title: Permitir convites de
+      registrations_mode:
+        modes:
+          approved: Aprovação necessária para cadastro
+          none: Ninguém pode se cadastrar
+          open: Qualquer um pode se cadastrar
+        title: Modo de cadastro
       show_known_fediverse_at_about_page:
         desc_html: Quando ligado, vai mostrar toots de todo o fediverso conhecido na prévia da timeline. Senão, mostra somente toots locais.
         title: Mostrar fediverso conhecido na prévia da timeline
@@ -461,6 +484,9 @@ pt-BR:
       edit_preset: Editar o aviso pré-definido
       title: Gerenciar os avisos pré-definidos
   admin_mailer:
+    new_pending_account:
+      body: Os detalhes da nova conta estão abaixo. Você pode aprovar ou rejeitar essa aplicação.
+      subject: Nova conta para revisão em %{instance} (%{username})
     new_report:
       body: "%{reporter} denunciou %{target}"
       body_remote: Alguém da instância %{domain} reportou %{target}
@@ -482,7 +508,9 @@ pt-BR:
     your_token: Seu token de acesso
   auth:
     agreement_html: Ao se cadastrar você concorda em seguir <a href="%{rules_path}">as regras da instância</a> e <a href="%{terms_path}">os nossos termos de serviço</a>.
+    apply_for_account: Pedir um convite
     change_password: Senha
+    checkbox_agreement_html: Eu concordo com <a href="%{rules_path}" target="_blank">as regras do servidor</a> e com <a href="%{terms_path}" target="_blank">os termos de serviço</a>
     confirm_email: Confirmar e-mail
     delete_account: Excluir conta
     delete_account_html: Se você deseja excluir a sua conta, você pode <a href="%{path}">prosseguir para cá</a>. Uma confirmação será requisitada.
@@ -498,10 +526,12 @@ pt-BR:
       cas: CAS
       saml: SAML
     register: Cadastrar-se
+    registration_closed: "%{instance} não está aceitando novos membros"
     resend_confirmation: Reenviar instruções de confirmação
     reset_password: Redefinir senha
     security: Segurança
     set_new_password: Definir uma nova senha
+    trouble_logging_in: Problemas para se conectar?
   authorize_follow:
     already_following: Você já está seguindo esta conta
     error: Infelizmente, ocorreu um erro ao buscar a conta remota
@@ -537,8 +567,11 @@ pt-BR:
     warning_title: Disponibilidade de conteúdo disseminado
   directories:
     directory: Diretório de perfis
+    enabled: Você está na lista do diretório.
+    enabled_but_waiting: Você escolheu ser listado no diretório, mas você ainda não tem o mínimo de seguidores (%{min_followers}) para ser listado.
     explanation: Descobrir usuários baseado em seus interesses
     explore_mastodon: Explorar %{title}
+    how_to_enable: Você não se inscreveu no diretório. Você pode se inscrever abaixo. Use hashtags no texto da sua bio para ser listado em hashtags específicas!
     people:
       one: "%{count} pessoa"
       other: "%{count} pessoas"
@@ -554,6 +587,9 @@ pt-BR:
       content: Desculpe, algo deu errado.
       title: Esta página não está certa
     noscript_html: Para usar o aplicativo web do Mastodon, por favor ative o JavaScript. Ou, se quiser, experimente um dos <a href="%{apps_path}">apps nativos</a> para o Mastodon em sua plataforma.
+  existing_username_validator:
+    not_found: não foi possível encontrar um usuário local com esse nome de usuário
+    not_found_multiple: não foi possível encontrar %{usernames}
   exports:
     archive_takeout:
       date: Data
@@ -569,6 +605,10 @@ pt-BR:
     lists: Listas
     mutes: Você silenciou
     storage: Armazenamento de mídia
+  featured_tags:
+    add_new: Adicionar uma nova hashtag
+    errors:
+      limit: Você atingiu o limite de hashtags em destaque
   filters:
     contexts:
       home: Página inicial
@@ -590,17 +630,45 @@ pt-BR:
     more: Mais…
     resources: Recursos
   generic:
+    all: Tudo
     changes_saved_msg: Mudanças salvas com sucesso!
     copy: Copiar
+    order_by: Ordenar por
     save_changes: Salvar mudanças
     validation_errors:
       one: Algo não está certo! Por favor, reveja o erro abaixo
       other: Algo não está certo! Por favor, reveja os %{count} erros abaixo
+  html_validator:
+    invalid_markup: 'contém HTML inválido: %{error}'
+  identity_proofs:
+    active: Ativo
+    authorize: Sim, autorizar
+    authorize_connection_prompt: Autorizar essa conexão criptográfica?
+    errors:
+      failed: A conexão criptográfica falhou. Por favor tente novamente a partir de %{provider}.
+      keybase:
+        invalid_token: Tokens keybase são hashs de assinatura e devem conter 66 caracteres hexa
+        verification_failed: Keybase não reconhece esse token como uma assinatura do usuário keybase %{kb_username}. Por favor tente novamente a partir de Keybase.
+      wrong_user: Não é possível criar uma prova para %{proving} estando logado como %{current}. Faça login como %{proving} e tente novamente.
+    explanation_html: Você pode conectar criptograficamente suas outras identidades, tais quais seu perfil Keybase. Isso permite outras pessoas de lhe enviarem mensagens encriptadas e confiar no conteúdo que você as envia.
+    i_am_html: Eu sou %{username} em %{service}.
+    identity: Identidade
+    inactive: Inativo
+    publicize_checkbox: 'E publique isso:'
+    publicize_toot: 'Está provado! Eu sou %{username} no %{service}: %{url}'
+    status: Status da verificação
+    view_proof: Ver prova
   imports:
+    modes:
+      merge: Juntar
+      merge_long: Manter os registros existentes e adicionar os novos
+      overwrite: Sobreescrever
+      overwrite_long: Substituir os registros atuais com os novos
     preface: Você pode importar dados que você exportou de outra instância, como a lista de pessoas que você segue ou bloqueou.
     success: Os seus dados foram enviados com sucesso e serão processados em instantes
     types:
       blocking: Lista de bloqueio
+      domain_blocking: Lista de domínios bloqueados
       following: Pessoas que você segue
       muting: Lista de silêncio
     upload: Enviar
@@ -692,11 +760,34 @@ pt-BR:
     older: Mais antigo
     prev: Anterior
     truncate: "&hellip;"
+  polls:
+    errors:
+      already_voted: Você já votou nessa enquete
+      duplicate_options: contém itens duplicados
+      duration_too_long: está muito longe no futuro
+      duration_too_short: é curto demais
+      expired: A enquete já terminou
+      over_character_limit: não pode ter mais que %{max} caracteres em cada
+      too_few_options: deve ter mais que um item
+      too_many_options: não pode ter mais que %{max} itens
   preferences:
     languages: Idiomas
     other: Outro
     publishing: Publicação
     web: Web
+  relationships:
+    activity: Atividade da conta
+    dormant: Inativo
+    last_active: Ativo por último em
+    most_recent: Mais recente
+    moved: Mudou-se
+    mutual: Mútuo
+    primary: Primário
+    relationship: Relação
+    remove_selected_domains: Remover todos os seguidores dos domínios selecionados
+    remove_selected_followers: Remover os seguidores selecionados
+    remove_selected_follows: Deixar de seguir usuários selecionados
+    status: Status da conta
   remote_follow:
     acct: Insira o seu usuário@domínio a partir do qual você deseja agir
     missing_resource: Não foi possível encontrar a URL de direcionamento para a sua conta
@@ -770,10 +861,13 @@ pt-BR:
     development: Desenvolvimento
     edit_profile: Editar perfil
     export: Exportar dados
+    featured_tags: Hashtags em destaque
+    identity_proofs: Provas de identidade
     import: Importar
     migrate: Migração de conta
     notifications: Notificações
     preferences: Preferências
+    relationships: Seguindo e seguidores
     settings: Configurações
     two_factor_authentication: Autenticação em dois passos
     your_apps: Seus aplicativos
@@ -799,6 +893,11 @@ pt-BR:
       ownership: Toots de outras pessoas não podem ser fixados
       private: Toot não-público não pode ser fixado
       reblog: Um compartilhamento não pode ser fixado
+    poll:
+      total_votes:
+        one: "%{count} voto"
+        other: "%{count} votos"
+      vote: Votar
     show_more: Mostrar mais
     sign_in_to_participate: Entre para participar dessa conversa
     title: '%{name}: "%{quote}"'
@@ -897,8 +996,8 @@ pt-BR:
       <p>Adaptado originalmente a partir da <a href="https://github.com/discourse/discourse">política de privacidade Discourse</a>.</p>
     title: "%{instance} Termos de Serviço e Política de Privacidade"
   themes:
-    contrast: Alto contraste
-    default: Mastodon
+    contrast: Mastodon  (Alto contraste)
+    default: Mastodon (Escuro)
     mastodon-light: Mastodon (claro)
   time:
     formats:
diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml
index 21134d07c..579f627d7 100644
--- a/config/locales/simple_form.cs.yml
+++ b/config/locales/simple_form.cs.yml
@@ -42,7 +42,7 @@ cs:
       imports:
         data: Soubor CSV exportovaný z jiného serveru Mastodon
       sessions:
-        otp: 'Napište dvoufaktorový kód vygenerovaný vaší mobilní aplikací, nebo použijte jeden z vašich záložních kódů:'
+        otp: 'Napište dvoufázový kód vygenerovaný vaší mobilní aplikací, nebo použijte jeden z vašich záložních kódů:'
       user:
         chosen_languages: Je-li tohle zaškrtnuto, budou ve veřejných časových osách zobrazeny pouze tooty ve zvolených jazycích
     labels:
@@ -85,7 +85,7 @@ cs:
         max_uses: Maximální počet použití
         new_password: Nové heslo
         note: O vás
-        otp_attempt: Dvoufaktorový kód
+        otp_attempt: Dvoufázový kód
         password: Heslo
         phrase: Klíčové slovo či fráze
         setting_aggregate_reblogs: Seskupovat boosty v časových osách
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
index 664c07a46..6cd6c7c85 100644
--- a/config/locales/simple_form.pt-BR.yml
+++ b/config/locales/simple_form.pt-BR.yml
@@ -6,7 +6,7 @@ pt-BR:
         text: Você pode usar a sintaxe de um toot, como URLs, hashtags e menções
       admin_account_action:
         send_email_notification: O usuário vai receber uma explicação do que aconteceu com a sua conta
-        text_html: Opcional. Você pode usar a sintaxe de toots. Você pode <a href="%{path}">adicionar avisos pré-definidos</a> para ganhar tempo.
+        text_html: Opcional. Você pode usar a sintaxe de toots. Você pode <a href="%{path}">adicionar avisos pré-definidos</a> para ganhar tempo
         type_html: Escolha o que fazer com <strong>%{acct}</strong>
         warning_preset_id: Opcional. Você ainda pode adicionar texto customizado no fim do texto pré-definido
       defaults:
@@ -33,9 +33,12 @@ pt-BR:
         setting_display_media_show_all: Sempre mostrar mídia marcada como sensível
         setting_hide_network: Quem você segue e quem segue você não serão exibidos no seu perfil
         setting_noindex: Afeta seu perfil público e as páginas de suas postagens
+        setting_show_application: A aplicação que você usar para enviar seus toots vai aparecer na visão detalhada dos seus toots
         setting_theme: Afeta a aparência do Mastodon quando em sua conta em qualquer aparelho.
         username: Seu nome de usuário será único em %{domain}
         whole_word: Quando a palavra ou frase é inteiramente alfanumérica, ela será aplicada somente se corresponder a palavra inteira
+      featured_tag:
+        name: 'Você pode querer usar um destes:'
       imports:
         data: Arquivo CSV exportado de outra instância do Mastodon
       sessions:
@@ -100,6 +103,7 @@ pt-BR:
         setting_hide_network: Esconder as suas redes
         setting_noindex: Não quero ser indexado por mecanismos de busca
         setting_reduce_motion: Reduz movimento em animações
+        setting_show_application: Mostrar o nome da aplicação utilizada para enviar os toots
         setting_system_font_ui: Usar a fonte padrão de seu sistema
         setting_theme: Tema do site
         setting_unfollow_modal: Mostrar diálogo de confirmação antes de deixar de seguir alguém
@@ -108,6 +112,8 @@ pt-BR:
         username: Nome de usuário
         username_or_email: Nome de usuário ou e-mail
         whole_word: Palavra inteira
+      featured_tag:
+        name: Hashtag
       interactions:
         must_be_follower: Bloquear notificações de não-seguidores
         must_be_following: Bloquear notificações de pessoas que você não segue
diff --git a/spec/controllers/settings/exports/muted_accounts_controller_spec.rb b/spec/controllers/settings/exports/muted_accounts_controller_spec.rb
index f42d7881e..642f0a9b8 100644
--- a/spec/controllers/settings/exports/muted_accounts_controller_spec.rb
+++ b/spec/controllers/settings/exports/muted_accounts_controller_spec.rb
@@ -11,7 +11,7 @@ describe Settings::Exports::MutedAccountsController do
       sign_in user, scope: :user
       get :index, format: :csv
 
-      expect(response.body).to eq "username@domain\n"
+      expect(response.body).to eq "Account address,Hide notifications\nusername@domain,true\n"
     end
   end
 end
diff --git a/spec/fixtures/files/mute-imports.txt b/spec/fixtures/files/mute-imports.txt
new file mode 100644
index 000000000..125cbd384
--- /dev/null
+++ b/spec/fixtures/files/mute-imports.txt
@@ -0,0 +1,4 @@
+bob
+
+eve@example.com
+
diff --git a/spec/fixtures/files/new-mute-imports.txt b/spec/fixtures/files/new-mute-imports.txt
new file mode 100644
index 000000000..c1c9bca9b
--- /dev/null
+++ b/spec/fixtures/files/new-mute-imports.txt
@@ -0,0 +1,4 @@
+Account address,Hide notifications
+bob,true
+eve@example.com,false
+
diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb
index 277dcc526..0553f4098 100644
--- a/spec/models/export_spec.rb
+++ b/spec/models/export_spec.rb
@@ -21,10 +21,11 @@ describe Export do
       target_accounts.each(&account.method(:mute!))
 
       export = Export.new(account).to_muted_accounts_csv
-      results = export.strip.split
+      results = export.strip.split("\n")
 
-      expect(results.size).to eq 2
-      expect(results.first).to eq 'one@local.host'
+      expect(results.size).to eq 3
+      expect(results.first).to eq 'Account address,Hide notifications'
+      expect(results.second).to eq 'one@local.host,true'
     end
 
     it 'returns a csv of the following accounts' do
diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb
new file mode 100644
index 000000000..bd23781ce
--- /dev/null
+++ b/spec/services/import_service_spec.rb
@@ -0,0 +1,84 @@
+require 'rails_helper'
+
+RSpec.describe ImportService, type: :service do
+  let!(:account) { Fabricate(:account) }
+  let!(:bob)     { Fabricate(:account, username: 'bob') }
+  let!(:eve)     { Fabricate(:account, username: 'eve', domain: 'example.com') }
+
+  context 'import old-style list of muted users' do
+    subject { ImportService.new }
+
+    let(:csv) { attachment_fixture('mute-imports.txt') }
+
+    describe 'when no accounts are muted' do
+      let(:import) { Import.create(account: account, type: 'muting', data: csv) }
+      it 'mutes the listed accounts, including notifications' do
+        subject.call(import)
+        expect(account.muting.count).to eq 2
+        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
+      end
+    end
+
+    describe 'when some accounts are muted and overwrite is not set' do
+      let(:import) { Import.create(account: account, type: 'muting', data: csv) }
+
+      it 'mutes the listed accounts, including notifications' do
+        account.mute!(bob, notifications: false)
+        subject.call(import)
+        expect(account.muting.count).to eq 2
+        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
+      end
+    end
+
+    describe 'when some accounts are muted and overwrite is set' do
+      let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
+
+      it 'mutes the listed accounts, including notifications' do
+        account.mute!(bob, notifications: false)
+        subject.call(import)
+        expect(account.muting.count).to eq 2
+        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
+      end
+    end
+  end
+
+  context 'import new-style list of muted users' do
+    subject { ImportService.new }
+
+    let(:csv) { attachment_fixture('new-mute-imports.txt') }
+
+    describe 'when no accounts are muted' do
+      let(:import) { Import.create(account: account, type: 'muting', data: csv) }
+      it 'mutes the listed accounts, respecting notifications' do
+        subject.call(import)
+        expect(account.muting.count).to eq 2
+        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
+        expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
+      end
+    end
+
+    describe 'when some accounts are muted and overwrite is not set' do
+      let(:import) { Import.create(account: account, type: 'muting', data: csv) }
+
+      it 'mutes the listed accounts, respecting notifications' do
+        account.mute!(bob, notifications: true)
+        subject.call(import)
+        expect(account.muting.count).to eq 2
+        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
+        expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
+      end
+    end
+
+    describe 'when some accounts are muted and overwrite is set' do
+      let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
+
+      it 'mutes the listed accounts, respecting notifications' do
+        account.mute!(bob, notifications: true)
+        subject.call(import)
+        expect(account.muting.count).to eq 2
+        expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
+        expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
+      end
+    end
+  end
+end