about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2018-05-20 03:01:44 -0500
committerGitHub <noreply@github.com>2018-05-20 03:01:44 -0500
commit56a3d2b64bb096bc771c1d3c043a9d44d41c3291 (patch)
tree6917de2e59e69f5ad0dc9b1471a7c7c8659c1af8 /app
parent625c4f36ef394215e65e19157bfaf60e7de94b5f (diff)
parentb481e4fac1c564b8008f6f1d0eea1727ec9faa08 (diff)
Merge pull request #494 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app')
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb5
-rw-r--r--app/controllers/oauth/tokens_controller.rb14
-rw-r--r--app/helpers/stream_entries_helper.rb6
-rw-r--r--app/javascript/mastodon/actions/notifications.js1
-rw-r--r--app/javascript/mastodon/actions/push_notifications/registerer.js7
-rw-r--r--app/javascript/mastodon/features/compose/containers/warning_container.js10
-rw-r--r--app/javascript/mastodon/locales/ar.json2
-rw-r--r--app/javascript/mastodon/locales/bg.json2
-rw-r--r--app/javascript/mastodon/locales/ca.json4
-rw-r--r--app/javascript/mastodon/locales/co.json2
-rw-r--r--app/javascript/mastodon/locales/de.json2
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json10
-rw-r--r--app/javascript/mastodon/locales/el.json2
-rw-r--r--app/javascript/mastodon/locales/en.json4
-rw-r--r--app/javascript/mastodon/locales/eo.json2
-rw-r--r--app/javascript/mastodon/locales/es.json2
-rw-r--r--app/javascript/mastodon/locales/eu.json2
-rw-r--r--app/javascript/mastodon/locales/fa.json2
-rw-r--r--app/javascript/mastodon/locales/fi.json2
-rw-r--r--app/javascript/mastodon/locales/fr.json4
-rw-r--r--app/javascript/mastodon/locales/gl.json70
-rw-r--r--app/javascript/mastodon/locales/he.json2
-rw-r--r--app/javascript/mastodon/locales/hr.json2
-rw-r--r--app/javascript/mastodon/locales/hu.json2
-rw-r--r--app/javascript/mastodon/locales/hy.json2
-rw-r--r--app/javascript/mastodon/locales/id.json2
-rw-r--r--app/javascript/mastodon/locales/io.json2
-rw-r--r--app/javascript/mastodon/locales/it.json72
-rw-r--r--app/javascript/mastodon/locales/ja.json6
-rw-r--r--app/javascript/mastodon/locales/ko.json2
-rw-r--r--app/javascript/mastodon/locales/nl.json4
-rw-r--r--app/javascript/mastodon/locales/no.json2
-rw-r--r--app/javascript/mastodon/locales/oc.json2
-rw-r--r--app/javascript/mastodon/locales/pl.json2
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json8
-rw-r--r--app/javascript/mastodon/locales/pt.json2
-rw-r--r--app/javascript/mastodon/locales/ru.json2
-rw-r--r--app/javascript/mastodon/locales/sk.json4
-rw-r--r--app/javascript/mastodon/locales/sl.json2
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json2
-rw-r--r--app/javascript/mastodon/locales/sr.json2
-rw-r--r--app/javascript/mastodon/locales/sv.json2
-rw-r--r--app/javascript/mastodon/locales/te.json2
-rw-r--r--app/javascript/mastodon/locales/th.json2
-rw-r--r--app/javascript/mastodon/locales/tr.json2
-rw-r--r--app/javascript/mastodon/locales/uk.json2
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json2
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json2
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json2
-rw-r--r--app/javascript/mastodon/reducers/timelines.js5
-rw-r--r--app/javascript/mastodon/service_worker/entry.js4
-rw-r--r--app/javascript/mastodon/service_worker/web_push_locales.js30
-rw-r--r--app/javascript/mastodon/service_worker/web_push_notifications.js178
-rw-r--r--app/javascript/mastodon/storage/modifier.js10
-rw-r--r--app/javascript/styles/mastodon/components.scss1
-rw-r--r--app/javascript/styles/mastodon/footer.scss8
-rw-r--r--app/models/web/push_subscription.rb30
-rw-r--r--app/serializers/web/notification_serializer.rb163
-rw-r--r--app/views/layouts/public.html.haml2
-rw-r--r--app/views/remote_follow/new.html.haml2
-rw-r--r--app/workers/activitypub/delivery_worker.rb15
61 files changed, 415 insertions, 328 deletions
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index f95d672ec..1e420b3e7 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -9,6 +9,11 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
 
   include Localized
 
+  def destroy
+    Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
+    super
+  end
+
   private
 
   def store_current_location
diff --git a/app/controllers/oauth/tokens_controller.rb b/app/controllers/oauth/tokens_controller.rb
new file mode 100644
index 000000000..fa6d58f25
--- /dev/null
+++ b/app/controllers/oauth/tokens_controller.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class Oauth::TokensController < Doorkeeper::TokensController
+  def revoke
+    unsubscribe_for_token if authorized? && token.accessible?
+    super
+  end
+
+  private
+
+  def unsubscribe_for_token
+    Web::PushSubscription.where(access_token_id: token.id).delete_all
+  end
+end
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 707c8e26c..a91a28935 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -5,7 +5,11 @@ module StreamEntriesHelper
   EMBEDDED_ACTION = 'embed'
 
   def display_name(account, **options)
-    Formatter.instance.format_display_name(account, options)
+    if options[:custom_emojify]
+      Formatter.instance.format_display_name(account, options)
+    else
+      account.display_name.presence || account.username
+    end
   end
 
   def account_description(account)
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 641ad0e14..3f95f6667 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -22,6 +22,7 @@ export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
 
 defineMessages({
   mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
+  group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
 });
 
 const fetchRelatedRelationships = (dispatch, notifications) => {
diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js
index 82fe4519a..b0f42b6a2 100644
--- a/app/javascript/mastodon/actions/push_notifications/registerer.js
+++ b/app/javascript/mastodon/actions/push_notifications/registerer.js
@@ -51,13 +51,6 @@ export function register () {
   return (dispatch, getState) => {
     dispatch(setBrowserSupport(supportsPushNotifications));
 
-    if (me && !pushNotificationsSetting.get(me)) {
-      const alerts = getState().getIn(['push_notifications', 'alerts']);
-      if (alerts) {
-        pushNotificationsSetting.set(me, { alerts: alerts });
-      }
-    }
-
     if (supportsPushNotifications) {
       if (!getApplicationServerKey()) {
         console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.');
diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js
index 88f816da4..8200a319f 100644
--- a/app/javascript/mastodon/features/compose/containers/warning_container.js
+++ b/app/javascript/mastodon/features/compose/containers/warning_container.js
@@ -17,11 +17,19 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
   if (needsLockWarning) {
     return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
   }
+
   if (hashtagWarning) {
     return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag." />} />;
   }
+
   if (directMessageWarning) {
-    return <Warning message={<FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This toot will only be sent to all the mentioned users. However, the operators of your instance and any receiving instances may see this message.' />} />;
+    const message = (
+      <span>
+        <FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This toot will only be sent to all the mentioned users.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>
+      </span>
+    );
+
+    return <Warning message={message} />;
   }
 
   return null;
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 1199c9535..9827ac5f0 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "التصفح",
   "column_subheading.settings": "الإعدادات",
   "compose_form.direct_message_warning": "لن يَظهر هذا التبويق إلا للمستخدمين المذكورين.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "هذا التبويق لن يُدرَج تحت أي وسم كان بما أنه غير مُدرَج. لا يُسمح بالبحث إلّا عن التبويقات العمومية عن طريق الوسوم.",
   "compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.",
   "compose_form.lock_disclaimer.lock": "مقفل",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "الترقيّات:",
   "notifications.column_settings.show": "إعرِضها في عمود",
   "notifications.column_settings.sound": "أصدر صوتا",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "تم",
   "onboarding.next": "التالي",
   "onboarding.page_five.public_timelines": "تُعرَض في الخيط الزمني المحلي المشاركات العامة المحررة من طرف جميع المسجلين في {domain}. أما في الخيط الزمني الموحد ، فإنه يتم عرض جميع المشاركات العامة المنشورة من طرف جميع الأشخاص المتابَعين من طرف أعضاء {domain}. هذه هي الخيوط الزمنية العامة، وهي طريقة رائعة للتعرف أشخاص جدد.",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index dd747ab3b..caba7703d 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Settings",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
   "compose_form.lock_disclaimer.lock": "locked",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Споделяния:",
   "notifications.column_settings.show": "Покажи в колона",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 68fc4a44c..7181589ec 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -60,7 +60,8 @@
   "column_header.unpin": "No fixis",
   "column_subheading.navigation": "Navegació",
   "column_subheading.settings": "Configuració",
-  "compose_form.direct_message_warning": "Aquest toot només serà visible per a tots els usuaris esmentats.",
+  "compose_form.direct_message_warning": "Aquest toot només serà enviat als usuaris esmentats. De totes maneres, els operadors de la teva o de qualsevol de les instàncies receptores poden inspeccionar aquest missatge.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Aquest toot no es mostrarà en cap etiqueta ja que no està llistat. Només els toots públics poden ser cercats per etiqueta.",
   "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",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Impulsos:",
   "notifications.column_settings.show": "Mostrar en la columna",
   "notifications.column_settings.sound": "Reproduïr so",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Fet",
   "onboarding.next": "Següent",
   "onboarding.page_five.public_timelines": "La línia de temps local mostra missatges públics de tothom de {domain}. La línia de temps federada mostra els missatges públics de tothom que la gent de {domain} segueix. Aquests són les línies de temps Públiques, una bona manera de descobrir noves persones.",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index 9d8d950a8..c6de9d016 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigazione",
   "column_subheading.settings": "Parametri",
   "compose_form.direct_message_warning": "Solu l'utilizatori mintuvati puderenu vede stu statutu.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Stu statutu ùn hè \"Micca listatu\" è ùn sarà micca listatu indè e circate da hashtag. Per esse vistu in quesse, u statutu deve esse \"Pubblicu\".",
   "compose_form.lock_disclaimer": "U vostru contu ùn hè micca {locked}. Tuttu u mondu pò seguitavi è vede i vostri statuti privati.",
   "compose_form.lock_disclaimer.lock": "privatu",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Spartere:",
   "notifications.column_settings.show": "Mustrà indè a colonna",
   "notifications.column_settings.sound": "Sunà",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Fatta",
   "onboarding.next": "Siguente",
   "onboarding.page_five.public_timelines": "A linea pubblica lucale mostra statuti pubblichi da tuttu u mondu nant'à {domain}. A linea pubblica glubale mostra ancu quelli di a ghjente seguitata da l'utilizatori di {domain}. Quesse sò una bona manera d'incuntrà nove parsone.",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index fe5bc4443..4d89ea704 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Einstellungen",
   "compose_form.direct_message_warning": "Dieser Beitrag wird nur für die erwähnten Nutzer sichtbar sein.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Dieser Beitrag wird nicht unter einen dieser Hashtags sichtbar sein, solange er ungelistet ist. Bei einer Suche kann er nicht gefunden werden.",
   "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Wer dir folgen will, kann das jederzeit tun und dann auch deine privaten Beiträge sehen.",
   "compose_form.lock_disclaimer.lock": "gesperrt",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Geteilte Beiträge:",
   "notifications.column_settings.show": "In der Spalte anzeigen",
   "notifications.column_settings.sound": "Ton abspielen",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Fertig",
   "onboarding.next": "Weiter",
   "onboarding.page_five.public_timelines": "Die lokale Zeitleiste zeigt alle Beiträge von Leuten, die auch auf {domain} sind. Das gesamte bekannte Netz zeigt Beiträge von allen, denen von Leuten auf {domain} gefolgt wird. Zusammen sind sie die öffentlichen Zeitleisten, ein guter Weg, um neue Leute zu finden.",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 3253e21d3..1fec1249b 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -17,6 +17,10 @@
       {
         "defaultMessage": "{name} mentioned you",
         "id": "notification.mention"
+      },
+      {
+        "defaultMessage": "{count} notifications",
+        "id": "notifications.group"
       }
     ],
     "path": "app/javascript/mastodon/actions/notifications.json"
@@ -870,8 +874,12 @@
         "id": "compose_form.hashtag_warning"
       },
       {
-        "defaultMessage": "This toot will only be visible to all the mentioned users.",
+        "defaultMessage": "This toot will only be sent to all the mentioned users.",
         "id": "compose_form.direct_message_warning"
+      },
+      {
+        "defaultMessage": "Learn more",
+        "id": "compose_form.direct_message_warning_learn_more"
       }
     ],
     "path": "app/javascript/mastodon/features/compose/containers/warning_container.json"
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index d1421be76..c547b2e28 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Πλοήγηση",
   "column_subheading.settings": "Ρυθμίσεις",
   "compose_form.direct_message_warning": "Αυτό το τουτ θα εμφανίζεται μόνο σε όλους τους αναφερόμενους χρήστες.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Αυτό το τουτ δεν θα εμφανίζεται κάτω από καμία ταμπέλα καθώς είναι αφανές. Μόνο τα δημόσια τουτ μπορούν να αναζητηθούν ανά ταμπέλα.",
   "compose_form.lock_disclaimer": "Ο λογαριασμός σου δεν είναι {locked}. Οποιοσδήποτε μπορεί να σε ακολουθήσει για να δει τις δημοσιεύσεις σας προς τους ακολούθους σας.",
   "compose_form.lock_disclaimer.lock": "κλειδωμένος",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 8eeee7c3d..1d7960abe 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -63,7 +63,8 @@
   "column_subheading.lists": "Lists",
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Settings",
-  "compose_form.direct_message_warning": "This toot will only be sent to the mentioned users. However, the operators of yours and any of the receiving instances may inspect this message.",
+  "compose_form.direct_message_warning": "This toot will only be sent to the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
   "compose_form.lock_disclaimer.lock": "locked",
@@ -190,6 +191,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index d5ddc23d4..2715b80c9 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigado",
   "column_subheading.settings": "Agordado",
   "compose_form.direct_message_warning": "Tiu mesaĝo videblos nur por ĉiuj menciitaj uzantoj.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Ĉi tiu mesaĝo ne estos listigita per ajna kradvorto. Nur publikaj mesaĝoj estas serĉeblaj per kradvortoj.",
   "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",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Diskonigoj:",
   "notifications.column_settings.show": "Montri en kolumno",
   "notifications.column_settings.sound": "Eligi sonon",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Farita",
   "onboarding.next": "Sekva",
   "onboarding.page_five.public_timelines": "La loka tempolinio montras publikajn mesaĝojn de ĉiuj en {domain}. La fratara tempolinio montras publikajn mesaĝojn de ĉiuj, kiuj estas sekvataj de homoj en {domain}. Tio estas la publikaj tempolinioj, kio estas bona maniero por malkovri novajn homojn.",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 8aac2d77d..668f437b9 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navegación",
   "column_subheading.settings": "Ajustes",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Este toot no se mostrará bajo hashtags porque no es público. Sólo los toots públicos se pueden buscar por hashtag.",
   "compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.",
   "compose_form.lock_disclaimer.lock": "bloqueado",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Retoots:",
   "notifications.column_settings.show": "Mostrar en columna",
   "notifications.column_settings.sound": "Reproducir sonido",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Listo",
   "onboarding.next": "Siguiente",
   "onboarding.page_five.public_timelines": "La línea de tiempo local muestra toots públicos de todos en {domain}. La línea de tiempo federada muestra toots públicos de cualquiera a quien la gente de {domain} siga. Estas son las líneas de tiempo públicas, una buena forma de conocer gente nueva.",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index e927547e3..8ec140753 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Settings",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
   "compose_form.lock_disclaimer.lock": "locked",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index e19b1cd85..605961a01 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "گشت و گذار",
   "column_subheading.settings": "تنظیمات",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "حساب شما {locked} نیست. هر کسی می‌تواند پیگیر شما شود و نوشته‌های ویژهٔ پیگیران شما را ببیند.",
   "compose_form.lock_disclaimer.lock": "قفل",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "بازبوق‌ها:",
   "notifications.column_settings.show": "نمایش در ستون",
   "notifications.column_settings.sound": "پخش صدا",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "پایان",
   "onboarding.next": "بعدی",
   "onboarding.page_five.public_timelines": "نوشته‌های محلی یعنی نوشته‌های همهٔ کاربران {domain}. نوشته‌های همه‌جا یعنی نوشته‌های همهٔ کسانی که کاربران {domain} آن‌ها را پی می‌گیرند. این فهرست‌های عمومی راه خوبی برای یافتن کاربران تازه هستند.",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index cfe9c4f33..ead1eb8c4 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigaatio",
   "column_subheading.settings": "Asetukset",
   "compose_form.direct_message_warning": "Tämä tuuttaus näkyy vain mainituille käyttäjille.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Tämä tuuttaus ei näy hashtag-hauissa, koska se on listaamaton. Hashtagien avulla voi hakea vain julkisia tuuttauksia.",
   "compose_form.lock_disclaimer": "Tilisi ei ole {locked}. Kuka tahansa voi seurata tiliäsi ja nähdä vain seuraajille rajaamasi julkaisut.",
   "compose_form.lock_disclaimer.lock": "lukittu",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Buustit:",
   "notifications.column_settings.show": "Näytä sarakkeessa",
   "notifications.column_settings.sound": "Äänimerkki",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Valmis",
   "onboarding.next": "Seuraava",
   "onboarding.page_five.public_timelines": "Paikallisella aikajanalla näytetään instanssin {domain} kaikkien käyttäjien julkiset julkaisut. Yleisellä aikajanalla näytetään kaikkien instanssin {domain} käyttäjien seuraamien käyttäjien julkiset julkaisut. Nämä julkiset aikajanat ovat loistavia paikkoja löytää uusia ihmisiä.",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index c4a41583c..c05cb40c1 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Paramètres",
   "compose_form.direct_message_warning": "Ce pouet sera uniquement envoyé qu'aux personnes mentionnées. Cependant, l'administration de votre instance et des instances réceptrices pourront inspecter ce message.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur \"non-listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par hashtag.",
   "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
   "compose_form.lock_disclaimer.lock": "verrouillé",
@@ -181,11 +182,12 @@
   "notifications.column_settings.favourite": "Favoris :",
   "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e·s :",
   "notifications.column_settings.mention": "Mentions :",
-  "notifications.column_settings.push": "Notifications push",
+  "notifications.column_settings.push": "Notifications",
   "notifications.column_settings.push_meta": "Cet appareil",
   "notifications.column_settings.reblog": "Partages :",
   "notifications.column_settings.show": "Afficher dans la colonne",
   "notifications.column_settings.sound": "Émettre un son",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Effectué",
   "onboarding.next": "Suivant",
   "onboarding.page_five.public_timelines": "Le fil public global affiche les messages de toutes les personnes suivies par les membres de {domain}. Le fil public local est identique, mais se limite aux membres de {domain}.",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 1eaaebc53..90447ad4c 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -2,10 +2,10 @@
   "account.badges.bot": "Bot",
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Ocultar calquer contido de {domain}",
-  "account.blocked": "Blocked",
-  "account.direct": "Direct message @{name}",
+  "account.blocked": "Bloqueada",
+  "account.direct": "Mensaxe directa @{name}",
   "account.disclaimer_full": "A información inferior podería mostrar un perfil incompleto da usuaria.",
-  "account.domain_blocked": "Domain hidden",
+  "account.domain_blocked": "Dominio agochado",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidoras",
@@ -17,7 +17,7 @@
   "account.moved_to": "{name} marchou a:",
   "account.mute": "Acalar @{name}",
   "account.mute_notifications": "Acalar as notificacións de @{name}",
-  "account.muted": "Muted",
+  "account.muted": "Acalada",
   "account.posts": "Toots",
   "account.posts_with_replies": "Toots e respostas",
   "account.report": "Informar sobre @{name}",
@@ -30,8 +30,8 @@
   "account.unmute": "Non acalar @{name}",
   "account.unmute_notifications": "Desbloquear as notificacións de @{name}",
   "account.view_full_profile": "Ver o perfil completo",
-  "alert.unexpected.message": "An unexpected error occurred.",
-  "alert.unexpected.title": "Oops!",
+  "alert.unexpected.message": "Aconteceu un fallo non agardado.",
+  "alert.unexpected.title": "Vaia!",
   "boost_modal.combo": "Pulse {combo} para saltar esto a próxima vez",
   "bundle_column_error.body": "Houbo un fallo mentras se cargaba este compoñente.",
   "bundle_column_error.retry": "Inténteo de novo",
@@ -41,8 +41,8 @@
   "bundle_modal_error.retry": "Inténteo de novo",
   "column.blocks": "Usuarias bloqueadas",
   "column.community": "Liña temporal local",
-  "column.direct": "Direct messages",
-  "column.domain_blocks": "Hidden domains",
+  "column.direct": "Mensaxes directas",
+  "column.domain_blocks": "Dominios agochados",
   "column.favourites": "Favoritas",
   "column.follow_requests": "Peticións de seguimento",
   "column.home": "Inicio",
@@ -60,17 +60,18 @@
   "column_header.unpin": "Soltar",
   "column_subheading.navigation": "Navegación",
   "column_subheading.settings": "Axustes",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning": "Este toot enviarase só as usuarias mencionadas. Porén, a súa proveedora de internet e calquera das instancias receptoras poderían examinar esta mensaxe.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Esta mensaxe non será listada baixo ningunha etiqueta xa que está marcada como non listada. Só os toots públicos poden buscarse por etiquetas.",
   "compose_form.lock_disclaimer": "A súa conta non está {locked}. Calquera pode seguila para ver as súas mensaxes só-para-seguidoras.",
   "compose_form.lock_disclaimer.lock": "bloqueado",
   "compose_form.placeholder": "A qué andas?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Media is marked as sensitive",
-  "compose_form.sensitive.unmarked": "Media is not marked as sensitive",
-  "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.sensitive.marked": "Medios marcados como sensibles",
+  "compose_form.sensitive.unmarked": "Os medios non están marcados como sensibles",
+  "compose_form.spoiler.marked": "O texto está agochado tras un aviso",
+  "compose_form.spoiler.unmarked": "O texto non está agochado",
   "compose_form.spoiler_placeholder": "Escriba o aviso aquí",
   "confirmation_modal.cancel": "Cancelar",
   "confirmations.block.confirm": "Bloquear",
@@ -102,7 +103,7 @@
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viaxes e Lugares",
   "empty_column.community": "A liña temporal local está baldeira. Escriba algo de xeito público para que rule!",
-  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
+  "empty_column.direct": "Aínda non ten mensaxes directas. Cando envíe ou reciba unha, aparecerá aquí.",
   "empty_column.hashtag": "Aínda non hai nada con esta etiqueta.",
   "empty_column.home": "A súa liña temporal de inicio está baldeira! Visite {public} ou utilice a busca para atopar outras usuarias.",
   "empty_column.home.public_timeline": "a liña temporal pública",
@@ -136,7 +137,7 @@
   "keyboard_shortcuts.mention": "para mencionar o autor",
   "keyboard_shortcuts.reply": "para responder",
   "keyboard_shortcuts.search": "para centrar a busca",
-  "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
+  "keyboard_shortcuts.toggle_hidden": "mostrar/agochar un texto detrás do AC",
   "keyboard_shortcuts.toot": "escribir un toot novo",
   "keyboard_shortcuts.unfocus": "quitar o foco do área de escritura/busca",
   "keyboard_shortcuts.up": "ir hacia arriba na lista",
@@ -154,12 +155,12 @@
   "loading_indicator.label": "Cargando...",
   "media_gallery.toggle_visible": "Ocultar",
   "missing_indicator.label": "Non atopado",
-  "missing_indicator.sublabel": "This resource could not be found",
+  "missing_indicator.sublabel": "Non se puido atopar o recurso",
   "mute_modal.hide_notifications": "Esconder notificacións deste usuario?",
   "navigation_bar.blocks": "Usuarias bloqueadas",
   "navigation_bar.community_timeline": "Liña temporal local",
-  "navigation_bar.direct": "Direct messages",
-  "navigation_bar.domain_blocks": "Hidden domains",
+  "navigation_bar.direct": "Mensaxes directas",
+  "navigation_bar.domain_blocks": "Dominios agochados",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritas",
   "navigation_bar.follow_requests": "Peticións de seguimento",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Promocións:",
   "notifications.column_settings.show": "Mostrar en columna",
   "notifications.column_settings.sound": "Reproducir son",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Feito",
   "onboarding.next": "Seguinte",
   "onboarding.page_five.public_timelines": "A liña de tempo local mostra as publicacións públicas de todos en {domain}. A liña de tempo federada mostra as publicacións públicas de todos os que as persoas en {domain} seguen. Estas son as Liñas de tempo públicas, unha boa forma de descubrir novas persoas.",
@@ -216,37 +218,37 @@
   "privacy.public.short": "Pública",
   "privacy.unlisted.long": "Non publicar en liñas temporais públicas",
   "privacy.unlisted.short": "Non listada",
-  "regeneration_indicator.label": "Loading…",
-  "regeneration_indicator.sublabel": "Your home feed is being prepared!",
+  "regeneration_indicator.label": "Cargando…",
+  "regeneration_indicator.sublabel": "Estase a preparar a súa liña temporal de inicio!",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
   "relative_time.just_now": "agora",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
-  "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
+  "report.forward": "Reenviar a {target}",
+  "report.forward_hint": "A conta pertence a outro servidor. Enviar unha copia anónima do informe alí tamén?",
+  "report.hint": "O informe enviarase a moderación da súa instancia. Abaixo pode explicar a razón pola que está a información:",
   "report.placeholder": "Comentarios adicionais",
   "report.submit": "Enviar",
   "report.target": "Informar {target}",
   "search.placeholder": "Buscar",
   "search_popout.search_format": "Formato de busca avanzada",
-  "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
+  "search_popout.tips.full_text": "Texto simple devolve estados que vostede escribeu, promoveu, marcou favoritos, ou foi mencionada, así como nomes de usuaria coincidentes, nomes públicos e etiquetas.",
   "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "estado",
   "search_popout.tips.text": "Texto simple devolve coincidencias con nomes públicos, nomes de usuaria e etiquetas",
   "search_popout.tips.user": "usuaria",
-  "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
+  "search_results.accounts": "Xente",
+  "search_results.hashtags": "Etiquetas",
   "search_results.statuses": "Toots",
   "search_results.total": "{count, number} {count,plural,one {result} outros {results}}",
   "standalone.public_title": "Ollada dentro...",
   "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
+  "status.cancel_reblog_private": "Non promover",
   "status.cannot_reblog": "Esta mensaxe non pode ser promovida",
   "status.delete": "Eliminar",
-  "status.direct": "Direct message @{name}",
+  "status.direct": "Mensaxe directa @{name}",
   "status.embed": "Incrustar",
   "status.favourite": "Favorita",
   "status.load_more": "Cargar máis",
@@ -257,9 +259,9 @@
   "status.mute_conversation": "Acalar conversa",
   "status.open": "Expandir este estado",
   "status.pin": "Fixar no perfil",
-  "status.pinned": "Pinned toot",
+  "status.pinned": "Toot fixado",
   "status.reblog": "Promover",
-  "status.reblog_private": "Boost to original audience",
+  "status.reblog_private": "Promover a audiencia orixinal",
   "status.reblogged_by": "{name} promoveu",
   "status.reply": "Resposta",
   "status.replyAll": "Resposta a conversa",
@@ -268,21 +270,21 @@
   "status.sensitive_warning": "Contido sensible",
   "status.share": "Compartir",
   "status.show_less": "Mostrar menos",
-  "status.show_less_all": "Show less for all",
+  "status.show_less_all": "Mostrar menos para todas",
   "status.show_more": "Mostrar máis",
-  "status.show_more_all": "Show more for all",
+  "status.show_more_all": "Mostrar máis para todas",
   "status.unmute_conversation": "Non acalar a conversa",
   "status.unpin": "Despegar do perfil",
   "tabs_bar.federated_timeline": "Federado",
   "tabs_bar.home": "Inicio",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificacións",
-  "tabs_bar.search": "Search",
+  "tabs_bar.search": "Buscar",
   "ui.beforeunload": "O borrador perderase se sae de Mastodon.",
   "upload_area.title": "Arrastre e solte para subir",
   "upload_button.label": "Engadir medios",
   "upload_form.description": "Describa para deficientes visuais",
-  "upload_form.focus": "Crop",
+  "upload_form.focus": "Recortar",
   "upload_form.undo": "Eliminar",
   "upload_progress.label": "Subindo...",
   "video.close": "Pechar video",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index a4bcba8ff..5da3e9ebb 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "ניווט",
   "column_subheading.settings": "אפשרויות",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "חשבונך אינו {locked}. כל אחד יוכל לעקוב אחריך כדי לקרוא את הודעותיך המיועדות לעוקבים בלבד.",
   "compose_form.lock_disclaimer.lock": "נעול",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "הדהודים:",
   "notifications.column_settings.show": "הצגה בטור",
   "notifications.column_settings.sound": "שמע מופעל",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "יציאה",
   "onboarding.next": "הלאה",
   "onboarding.page_five.public_timelines": "ציר הזמן המקומי מראה הודעות פומביות מכל באי קהילת {domain}. ציר הזמן העולמי מראה הודעות פומביות מאת כי מי שבאי קהילת {domain} עוקבים אחריו. אלו צירי הזמן הפומביים, דרך נהדרת לגלות אנשים חדשים.",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index bd1dfac4c..6486284ec 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigacija",
   "column_subheading.settings": "Postavke",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Tvoj račun nije {locked}. Svatko te može slijediti kako bi vidio postove namijenjene samo tvojim sljedbenicima.",
   "compose_form.lock_disclaimer.lock": "zaključan",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boostovi:",
   "notifications.column_settings.show": "Prikaži u stupcu",
   "notifications.column_settings.sound": "Sviraj zvuk",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Učinjeno",
   "onboarding.next": "Sljedeće",
   "onboarding.page_five.public_timelines": "Lokalni timeline prikazuje javne postove sviju od svakog na {domain}. Federalni timeline prikazuje javne postove svakog koga ljudi na {domain} slijede. To su Javni Timelineovi, sjajan način za otkriti nove ljude.",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 5059a45d4..6c04eced6 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigáció",
   "column_subheading.settings": "Beállítások",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Ezen tülkölés nem fog megjelenni semmilyen hashtag alatt mivel listázatlan. Csak a publikus tülkölések kereshetőek hashtag-el.",
   "compose_form.lock_disclaimer": "Az ön fiókja nincs {locked}. Bárki követni tud, hogy megtekintse a kizárt követőknek szánt üzeneteid.",
   "compose_form.lock_disclaimer.lock": "lezárva",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Rebloggolások:",
   "notifications.column_settings.show": "Oszlopban mutatás",
   "notifications.column_settings.sound": "Hang lejátszása",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Befejezve",
   "onboarding.next": "Következő",
   "onboarding.page_five.public_timelines": "A helyi idővonal mindenkinek a publikus posztját mutatja a(z) {domain}-n. A federált idővonal mindenki publikus posztját mutatja akit {domain} felhasználói követnek. Ezek a publikus idővonalak, nagyszerű mód új emberek megismerésére.",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 7fe723892..f1f4df968 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Նավարկություն",
   "column_subheading.settings": "Կարգավորումներ",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։",
   "compose_form.lock_disclaimer": "Քո հաշիվը {locked} չէ։ Յուրաքանչյուր ոք կարող է հետեւել քեզ եւ տեսնել միայն հետեւողների համար նախատեսված գրառումները։",
   "compose_form.lock_disclaimer.lock": "փակ",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Տարածածներից՝",
   "notifications.column_settings.show": "Ցուցադրել սյունում",
   "notifications.column_settings.sound": "Ձայն հանել",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Պատրաստ է",
   "onboarding.next": "Հաջորդ",
   "onboarding.page_five.public_timelines": "Տեղական հոսքը ցույց է տալիս {domain} տիրույթից բոլորի հրապարակային թթերը։ Դաշնային հոսքը ցույց է տալիս հրապարակային թթերը բոլորից, ում {domain} տիրույթի մարդիկ հետեւում են։ Սրանք Հրապարակային հոսքերն են՝ նոր մարդկանց բացահայտելու հրաշալի միջոց։",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 07b26a0a5..e0fa959b0 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigasi",
   "column_subheading.settings": "Pengaturan",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Toot ini tidak akan ada dalam daftar tagar manapun karena telah di set sebagai tidak terdaftar. Hanya postingan publik yang bisa dicari dengan tagar.",
   "compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.",
   "compose_form.lock_disclaimer.lock": "terkunci",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boost:",
   "notifications.column_settings.show": "Tampilkan dalam kolom",
   "notifications.column_settings.sound": "Mainkan suara",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Selesei",
   "onboarding.next": "Selanjutnya",
   "onboarding.page_five.public_timelines": "Linimasa lokal menampilkan semua postingan publik dari semua orang di {domain}. Linimasa gabungan menampilkan postingan publik dari semua orang yang diikuti oleh {domain}. Ini semua adalah Linimasa Publik, cara terbaik untuk bertemu orang lain.",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index a8ab72339..616232c3e 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Settings",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
   "compose_form.lock_disclaimer.lock": "locked",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Repeti:",
   "notifications.column_settings.show": "Montrar en kolumno",
   "notifications.column_settings.sound": "Plear sono",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 8bd652864..6bf9c73a6 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -4,7 +4,7 @@
   "account.block_domain": "Nascondi tutto da {domain}",
   "account.blocked": "Bloccato",
   "account.direct": "Direct Message @{name}",
-  "account.disclaimer_full": "Il profilo dell'utente mostrato qui sotto potrebbe essere incompleto",
+  "account.disclaimer_full": "Il profilo dell'utente mostrato qui sotto potrebbe essere incompleto.",
   "account.domain_blocked": "Dominio nascosto",
   "account.edit_profile": "Modifica profilo",
   "account.follow": "Segui",
@@ -61,14 +61,15 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Impostazioni",
   "compose_form.direct_message_warning": "Questo toot sarà visibile solo a tutti gli utenti citati.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Questo toot non è listato, quindi non sarà trovato nelle ricerche per hashtag. Solo i toot pubblici possono essere cercati per hashtag.",
   "compose_form.lock_disclaimer": "Il tuo account non è {bloccato}. Chiunque può decidere di seguirti per vedere i tuoi post per soli seguaci.",
   "compose_form.lock_disclaimer.lock": "bloccato",
   "compose_form.placeholder": "A cosa stai pensando?",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
-  "compose_form.sensitive.marked": "Questo media è contrassegnato come sensibile.",
-  "compose_form.sensitive.unmarked": "Questo media non è contrassegnato come sensibile.",
+  "compose_form.sensitive.marked": "Questo media è contrassegnato come sensibile",
+  "compose_form.sensitive.unmarked": "Questo media non è contrassegnato come sensibile",
   "compose_form.spoiler.marked": "Il testo è nascosto dall'avviso",
   "compose_form.spoiler.unmarked": "Il testo non è nascosto",
   "compose_form.spoiler_placeholder": "Content warning",
@@ -108,7 +109,7 @@
   "empty_column.home.public_timeline": "la timeline pubblica",
   "empty_column.list": "Non c'è niente in questo elenco ancora. Quando i membri di questo elenco postano nuovi stati, questi appariranno qui.",
   "empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.",
-  "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.",
+  "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio",
   "follow_request.authorize": "Autorizza",
   "follow_request.reject": "Rifiuta",
   "getting_started.appsshort": "App",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Post condivisi:",
   "notifications.column_settings.show": "Mostra in colonna",
   "notifications.column_settings.sound": "Riproduci suono",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Fatto",
   "onboarding.next": "Prossimo",
   "onboarding.page_five.public_timelines": "La timeline locale mostra i post pubblici di tutti gli utenti di {domain}. La timeline federata mostra i post pubblici di tutti gli utenti seguiti da quelli di {domain}. Queste sono le timeline pubbliche, che vi danno grandi possibilità di scoprire nuovi utenti.",
@@ -224,8 +226,8 @@
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Annulla",
-  "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.forward": "Inoltra a {target}",
+  "report.forward_hint": "Questo account appartiene a un altro server. Mandare anche là una copia anonima del rapporto?",
   "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
   "report.placeholder": "Commenti aggiuntivi",
   "report.submit": "Invia",
@@ -238,60 +240,60 @@
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
   "search_popout.tips.user": "user",
   "search_results.accounts": "People",
-  "search_results.hashtags": "Hashtags",
-  "search_results.statuses": "Toots",
+  "search_results.hashtags": "Hashtag",
+  "search_results.statuses": "Toot",
   "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}",
-  "standalone.public_title": "A look inside...",
+  "standalone.public_title": "Un'occhiata all'interno...",
   "status.block": "Block @{name}",
-  "status.cancel_reblog_private": "Unboost",
-  "status.cannot_reblog": "This post cannot be boosted",
+  "status.cancel_reblog_private": "Annulla condivisione",
+  "status.cannot_reblog": "Questo post non può essere condiviso",
   "status.delete": "Elimina",
-  "status.direct": "Direct message @{name}",
-  "status.embed": "Embed",
+  "status.direct": "Messaggio diretto @{name}",
+  "status.embed": "Incorpora",
   "status.favourite": "Apprezzato",
   "status.load_more": "Mostra di più",
   "status.media_hidden": "Allegato nascosto",
   "status.mention": "Nomina @{name}",
-  "status.more": "More",
-  "status.mute": "Mute @{name}",
-  "status.mute_conversation": "Mute conversation",
+  "status.more": "Altro",
+  "status.mute": "Silenzia @{name}",
+  "status.mute_conversation": "Silenzia conversazione",
   "status.open": "Espandi questo post",
-  "status.pin": "Pin on profile",
-  "status.pinned": "Pinned toot",
+  "status.pin": "Fissa in cima sul profilo",
+  "status.pinned": "Toot fissato in cima",
   "status.reblog": "Condividi",
   "status.reblog_private": "Boost to original audience",
   "status.reblogged_by": "{name} ha condiviso",
   "status.reply": "Rispondi",
-  "status.replyAll": "Reply to thread",
+  "status.replyAll": "Rispondi alla conversazione",
   "status.report": "Segnala @{name}",
   "status.sensitive_toggle": "Clicca per vedere",
   "status.sensitive_warning": "Materiale sensibile",
-  "status.share": "Share",
+  "status.share": "Condividi",
   "status.show_less": "Mostra meno",
-  "status.show_less_all": "Show less for all",
+  "status.show_less_all": "Mostra meno per tutti",
   "status.show_more": "Mostra di più",
-  "status.show_more_all": "Show more for all",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
+  "status.show_more_all": "Mostra di più per tutti",
+  "status.unmute_conversation": "Annulla silenzia conversazione",
+  "status.unpin": "Non fissare in cima al profilo",
   "tabs_bar.federated_timeline": "Federazione",
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Locale",
   "tabs_bar.notifications": "Notifiche",
-  "tabs_bar.search": "Search",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "tabs_bar.search": "Cerca",
+  "ui.beforeunload": "La bozza andrà persa se esci da Mastodon.",
   "upload_area.title": "Trascina per caricare",
   "upload_button.label": "Aggiungi file multimediale",
-  "upload_form.description": "Describe for the visually impaired",
-  "upload_form.focus": "Crop",
+  "upload_form.description": "Descrizione per utenti con disabilità visive",
+  "upload_form.focus": "Rifila",
   "upload_form.undo": "Cancella",
   "upload_progress.label": "Sto caricando...",
-  "video.close": "Close video",
-  "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
+  "video.close": "Chiudi video",
+  "video.exit_fullscreen": "Esci da modalità a schermo intero",
+  "video.expand": "Espandi video",
   "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
+  "video.hide": "Nascondi video",
+  "video.mute": "Silenzia suono",
   "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.play": "Avvia",
+  "video.unmute": "Riattiva suono"
 }
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 8fbf19bec..0a6c5396e 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -63,7 +63,8 @@
   "column_subheading.lists": "リスト",
   "column_subheading.navigation": "ナビゲーション",
   "column_subheading.settings": "設定",
-  "compose_form.direct_message_warning": "このトゥートはメンションされた人だけが見ることができます。",
+  "compose_form.direct_message_warning": "このトゥートはメンションされた人にのみ送信されます。ただし送受信したインスタンスの管理者が検査するかもしれません。",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "このトゥートは未収載なのでハッシュタグの一覧に表示されません。公開トゥートだけがハッシュタグで検索できます。",
   "compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
   "compose_form.lock_disclaimer.lock": "非公開",
@@ -190,6 +191,7 @@
   "notifications.column_settings.reblog": "ブースト:",
   "notifications.column_settings.show": "カラムに表示",
   "notifications.column_settings.sound": "通知音を再生",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "完了",
   "onboarding.next": "次へ",
   "onboarding.page_five.public_timelines": "連合タイムラインでは{domain}の人がフォローしているMastodon全体での公開投稿を表示します。同じくローカルタイムラインでは{domain}のみの公開投稿を表示します。",
@@ -215,7 +217,7 @@
   "privacy.direct.long": "メンションしたユーザーだけに公開",
   "privacy.direct.short": "ダイレクト",
   "privacy.private.long": "フォロワーだけに公開",
-  "privacy.private.short": "非公開",
+  "privacy.private.short": "フォロワー限定",
   "privacy.public.long": "公開TLに投稿する",
   "privacy.public.short": "公開",
   "privacy.unlisted.long": "公開TLで表示しない",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index fc13a5b5b..66657abcd 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "내비게이션",
   "column_subheading.settings": "설정",
   "compose_form.direct_message_warning": "이 툿은 멘션 된 유저들에게만 보여집니다.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "이 툿은 어떤 해시태그로도 검색 되지 않습니다. 전체공개로 게시 된 툿만이 해시태그로 검색 될 수 있습니다.",
   "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
   "compose_form.lock_disclaimer.lock": "비공개",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "부스트:",
   "notifications.column_settings.show": "컬럼에 표시",
   "notifications.column_settings.sound": "효과음 재생",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "완료",
   "onboarding.next": "다음",
   "onboarding.page_five.public_timelines": "연합 타임라인에서는 {domain}의 사람들이 팔로우 중인 Mastodon 전체 인스턴스의 공개 포스트를 표시합니다. 로컬 타임라인에서는 {domain} 만의 공개 포스트를 표시합니다.",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 047583ce2..8ddf47f3f 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -60,7 +60,8 @@
   "column_header.unpin": "Losmaken",
   "column_subheading.navigation": "Navigatie",
   "column_subheading.settings": "Instellingen",
-  "compose_form.direct_message_warning": "Deze toot zal alleen zichtbaar zijn voor alle vermelde gebruikers.",
+  "compose_form.direct_message_warning": "Deze toot wordt alleen naar vermelde gebruikers verstuurd. Echter, de beheerders en moderatoren van jouw en de ontvangende Mastodonserver(s) kunnen dit bericht mogelijk wel bekijken.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Deze toot valt niet onder een hashtag te bekijken, omdat deze niet op openbare tijdlijnen wordt getoond. Alleen openbare toots kunnen via hashtags gevonden worden.",
   "compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en kan de toots zien die je alleen aan jouw volgers hebt gericht.",
   "compose_form.lock_disclaimer.lock": "besloten",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "In kolom tonen",
   "notifications.column_settings.sound": "Geluid afspelen",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Klaar",
   "onboarding.next": "Volgende",
   "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodonservers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te leren kennen.",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 05dae0630..ab755a3e8 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigasjon",
   "column_subheading.settings": "Innstillinger",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Denne tuten blir ikke listet under noen emneknagger da den er ulistet. Kun offentlige tuter kan søktes etter med emneknagg.",
   "compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.",
   "compose_form.lock_disclaimer.lock": "låst",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Fremhevet:",
   "notifications.column_settings.show": "Vis i kolonne",
   "notifications.column_settings.sound": "Spill lyd",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Ferdig",
   "onboarding.next": "Neste",
   "onboarding.page_five.public_timelines": "Den lokale tidslinjen viser offentlige poster fra alle på {domain}. Felles tidslinje viser offentlige poster fra alle som brukere på {domain} følger. Dette er de offentlige tidslinjene, et fint sted å oppdage nye brukere.",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 615af998f..9378cfa8f 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigacion",
   "column_subheading.settings": "Paramètres",
   "compose_form.direct_message_warning": "Aqueste tut serà pas que visibile pel monde mencionat.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Aqueste tut serà pas ligat a cap etiqueta estant qu’es pas listat. Òm pas cercar que los tuts publics per etiqueta.",
   "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
   "compose_form.lock_disclaimer.lock": "clavat",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Partatges :",
   "notifications.column_settings.show": "Mostrar dins la colomna",
   "notifications.column_settings.sound": "Emetre un son",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Sortir",
   "onboarding.next": "Seguent",
   "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 658b5887d..f3db8a125 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -64,6 +64,7 @@
   "column_subheading.navigation": "Nawigacja",
   "column_subheading.settings": "Ustawienia",
   "compose_form.direct_message_warning": "Ten wpis będzie widoczny tylko dla wszystkich wspomnianych użytkowników.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.",
   "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje wpisy przeznaczone tylko dla śledzących.",
   "compose_form.lock_disclaimer.lock": "zablokowane",
@@ -190,6 +191,7 @@
   "notifications.column_settings.reblog": "Podbicia:",
   "notifications.column_settings.show": "Pokaż w kolumnie",
   "notifications.column_settings.sound": "Odtwarzaj dźwięk",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Gotowe",
   "onboarding.next": "Dalej",
   "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Globalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 2360c41e7..82bbd17a3 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -1,5 +1,5 @@
 {
-  "account.badges.bot": "Bot",
+  "account.badges.bot": "Robô",
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo de {domain}",
   "account.blocked": "Bloqueado",
@@ -60,7 +60,8 @@
   "column_header.unpin": "Desafixar",
   "column_subheading.navigation": "Navegação",
   "column_subheading.settings": "Configurações",
-  "compose_form.direct_message_warning": "Este toot só será visível a todos os usuários mencionados.",
+  "compose_form.direct_message_warning": "Este toot só será enviado aos usuários mencionados. No entanto, a administração da sua instância e das instâncias dos usuários mencionados podem inspecionar essa mensagem.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Esse toot não será listado em nenhuma hashtag por ser não listado. Somente toots públicos podem ser pesquisados por hashtag.",
   "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",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Compartilhamento:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Pronto",
   "onboarding.next": "Próximo",
   "onboarding.page_five.public_timelines": "A timeline local mostra postagens públicas de todos os usuários no {domain}. A timeline federada mostra todas as postagens de todas as pessoas que pessoas no {domain} seguem. Estas são as timelines públicas, uma ótima maneira de conhecer novas pessoas.",
@@ -283,7 +285,7 @@
   "upload_button.label": "Adicionar mídia",
   "upload_form.description": "Descreva a imagem para deficientes visuais",
   "upload_form.focus": "Recortar",
-  "upload_form.undo": "Desfazer",
+  "upload_form.undo": "Remover",
   "upload_progress.label": "Salvando...",
   "video.close": "Fechar vídeo",
   "video.exit_fullscreen": "Sair da tela cheia",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 3ac92dd57..6463d001d 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navegação",
   "column_subheading.settings": "Preferências",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Esta pulbicacção não será listada em nenhuma hashtag por ser não listada. Somente publicações públicas podem ser pesquisadas por hashtag.",
   "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.",
   "compose_form.lock_disclaimer.lock": "bloqueada",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Partilhas:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Pronto",
   "onboarding.next": "Próximo",
   "onboarding.page_five.public_timelines": "A timeline local mostra as publicações de todos os utilizadores em {domain}. A timeline global mostra as publicações de todas as pessoas que pessoas em {domain} seguem. Estas são as timelines públicas, uma óptima forma de conhecer novas pessoas.",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 5cf983b83..6c7926ffe 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Навигация",
   "column_subheading.settings": "Настройки",
   "compose_form.direct_message_warning": "Этот статус будет виден только упомянутым пользователям.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Этот пост не будет показывается в поиске по хэштегу, т.к. он непубличный. Только публичные посты можно найти в поиске по хэштегу.",
   "compose_form.lock_disclaimer": "Ваш аккаунт не {locked}. Любой человек может подписаться на Вас и просматривать посты для подписчиков.",
   "compose_form.lock_disclaimer.lock": "закрыт",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Продвижения:",
   "notifications.column_settings.show": "Показывать в колонке",
   "notifications.column_settings.sound": "Проигрывать звук",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Готово",
   "onboarding.next": "Далее",
   "onboarding.page_five.public_timelines": "Локальная лента показывает публичные посты всех пользователей {domain}. Глобальная лента показывает публичные посты всех людей, на которых подписаны пользователи {domain}. Это - публичные ленты, отличный способ найти новые знакомства.",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 4f7969bd3..6ac646bee 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -60,7 +60,8 @@
   "column_header.unpin": "Odopnúť",
   "column_subheading.navigation": "Navigácia",
   "column_subheading.settings": "Nastavenia",
-  "compose_form.direct_message_warning": "Tento príspevok bude videný výhradne iba spomenutými užívateľmi.",
+  "compose_form.direct_message_warning": "Tento príspevok bude videný výhradne iba spomenutými užívateľmi. Ber ale na vedomie že správci tvojej a všetkých iných zahrnutých instancií majú možnosť skontrolovať túto správu.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Tento toot nebude zobrazený pod žiadným haštagom lebo nieje listovaný. Iba verejné tooty môžu byť nájdené podľa haštagu.",
   "compose_form.lock_disclaimer": "Váš účet nie je zamknutý. Ktokoľvek ťa môže nasledovať a vidieť tvoje správy pre sledujúcich.",
   "compose_form.lock_disclaimer.lock": "zamknutý",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boosty:",
   "notifications.column_settings.show": "Zobraziť v stĺpci",
   "notifications.column_settings.sound": "Prehrať zvuk",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Koniec",
   "onboarding.next": "Ďalej",
   "onboarding.page_five.public_timelines": "Lokálna časová os zobrazuje verejné správy od všetkých na {domain}. Federovaná časová os zobrazuje verejné správy od všetkých tých, čo následujú užívatrľov {domain} z iných serverov. Tieto sú takzvané Verejné Časové Osi, výborná možnosť ako nájsť a spoznať nových ľudí.",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index 69b416258..edd5ab62b 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigacija",
   "column_subheading.settings": "Nastavitve",
   "compose_form.direct_message_warning": "Ta tut bo viden le vsem omenjenim uporabnikom.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Ta tut ne bo naveden pod nobenim hashtagom, ker ni dodan hashtag. Samo javne tute lahko iščete pod hashtagom.",
   "compose_form.lock_disclaimer": "Vaš račun ni {locked}. Vsakdo vam lahko sledi in si ogleda objave, ki so namenjene samo sledilcem.",
   "compose_form.lock_disclaimer.lock": "zaklenjen",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 490b3a51a..2a9d3737d 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigacija",
   "column_subheading.settings": "Postavke",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Vaš nalog nije {locked}. Svako može da Vas zaprati i da vidi objave namenjene samo Vašim pratiocima.",
   "compose_form.lock_disclaimer.lock": "zaključan",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Podrški:",
   "notifications.column_settings.show": "Prikaži u koloni",
   "notifications.column_settings.sound": "Puštaj zvuk",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Gotovo",
   "onboarding.next": "Sledeće",
   "onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Federisana lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index b331f9f48..b53f1ecc9 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Навигација",
   "column_subheading.settings": "Поставке",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Ваш налог није {locked}. Свако може да Вас запрати и да види објаве намењене само Вашим пратиоцима.",
   "compose_form.lock_disclaimer.lock": "закључан",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Подршки:",
   "notifications.column_settings.show": "Прикажи у колони",
   "notifications.column_settings.sound": "Пуштај звук",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Готово",
   "onboarding.next": "Следеће",
   "onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Федерисана лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 36b200764..dbe2c182e 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Inställningar",
   "compose_form.direct_message_warning": "Denna toot kommer endast vara synlig för nämnda användare.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "Denna toot kommer inte att listas under någon hashtag eftersom den är onoterad. Endast offentliga toots kan sökas med hashtag.",
   "compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vemsomhelst kan följa dig och även se dina inlägg skrivna för endast dina följare.",
   "compose_form.lock_disclaimer.lock": "låst",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Knuffar:",
   "notifications.column_settings.show": "Visa i kolumnen",
   "notifications.column_settings.sound": "Spela upp ljud",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Klart",
   "onboarding.next": "Nästa",
   "onboarding.page_five.public_timelines": "Den lokala tidslinjen visar offentliga inlägg från alla på {domain}. Den förenade tidslinjen visar offentliga inlägg från alla personer på {domain} som följer. Dom här offentliga tidslinjerna är ett bra sätt att upptäcka nya människor.",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 038ac6abb..52bd41935 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Settings",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
   "compose_form.lock_disclaimer.lock": "locked",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 8d24395af..32aa24080 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Settings",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
   "compose_form.lock_disclaimer.lock": "locked",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index a377e7b74..b73157e27 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Navigasyon",
   "column_subheading.settings": "Ayarlar",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Hesabınız {locked} değil. Sadece takipçilerle paylaştığınız gönderileri görebilmek için sizi herhangi bir kullanıcı takip edebilir.",
   "compose_form.lock_disclaimer.lock": "kilitli",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Boost’lar:",
   "notifications.column_settings.show": "Bildirimlerde göster",
   "notifications.column_settings.sound": "Ses çal",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Tamam",
   "onboarding.next": "Sıradaki",
   "onboarding.page_five.public_timelines": "Yerel zaman tüneli, bu sunucudaki herkesten gelen gönderileri gösterir.Federe zaman tüneli, kullanıcıların diğer sunuculardan takip ettiği kişilerin herkese açık gönderilerini gösterir. Bunlar herkese açık zaman tünelleridir ve yeni insanlarla tanışmak  için harika yerlerdir. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new ",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 613d1a00d..b301c6827 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "Навігація",
   "column_subheading.settings": "Налаштування",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
   "compose_form.lock_disclaimer": "Ваш акаунт не {locked}. Кожен може підписатися на Вас та бачити Ваші приватні пости.",
   "compose_form.lock_disclaimer.lock": "приватний",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "Передмухи:",
   "notifications.column_settings.show": "Показати в колонці",
   "notifications.column_settings.sound": "Відтворювати звук",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Готово",
   "onboarding.next": "Далі",
   "onboarding.page_five.public_timelines": "Локальна стрічка показує публічні пости усіх користувачів {domain}. Глобальна стрічка показує публічні пости усіх людей, на яких підписані користувачі {domain}. Це публичні стрічки, відмінний спосіб знайти нових людей.",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 073dbe6cb..455f1f828 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "导航",
   "column_subheading.settings": "设置",
   "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "这条嘟文被设置为“不公开”,因此它不会出现在任何话题标签的列表下。只有公开的嘟文才能通过话题标签进行搜索。",
   "compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。",
   "compose_form.lock_disclaimer.lock": "开启保护",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "当有人转嘟了你的嘟文时:",
   "notifications.column_settings.show": "在通知栏显示",
   "notifications.column_settings.sound": "播放音效",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "出发!",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "“本站时间轴”显示的是由本站({domain})用户发布的所有公开嘟文。“跨站公共时间轴”显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 9334b5d20..18be17792 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "瀏覽",
   "column_subheading.settings": "設定",
   "compose_form.direct_message_warning": "這文章只有被提及的用戶才可以看到。",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "這文章因為不是公開,所以不會被標籤搜索。只有公開的文章才會被標籤搜索。",
   "compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。",
   "compose_form.lock_disclaimer.lock": "公共",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "轉推你的文章:",
   "notifications.column_settings.show": "在通知欄顯示",
   "notifications.column_settings.sound": "播放音效",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "開始使用",
   "onboarding.next": "繼續",
   "onboarding.page_five.public_timelines": "「本站時間軸」顯示在 {domain} 各用戶的公開文章。「跨站時間軸」顯示在 {domain} 各人關注的所有用戶(包括其他服務站)的公開文章。這些都是「公共時間軸」,是認識新朋友的好地方。",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 0f4d04947..3222c035a 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -61,6 +61,7 @@
   "column_subheading.navigation": "瀏覽",
   "column_subheading.settings": "設定",
   "compose_form.direct_message_warning": "此則推文只會被所有提到的使用者看見。",
+  "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.hashtag_warning": "此則推文將不會在任何主題標籤中看見,只有公開的推文可以用主題標籤來搜尋。",
   "compose_form.lock_disclaimer": "你的帳號沒有{locked}。任何人都可以關注你,看到發給關注者的貼文。",
   "compose_form.lock_disclaimer.lock": "上鎖",
@@ -186,6 +187,7 @@
   "notifications.column_settings.reblog": "轉推:",
   "notifications.column_settings.show": "顯示在欄位中",
   "notifications.column_settings.sound": "播放音效",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "完成",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "本地時間軸顯示 {domain} 上所有人的公開貼文。聯盟時間軸顯示 {domain} 上所有人關注的公開貼文。這就是公開時間軸,發現新朋友的好地方。",
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index b09d78b0f..916a091eb 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -33,6 +33,11 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial) =>
     if (!statuses.isEmpty()) {
       mMap.update('items', ImmutableList(), oldIds => {
         const newIds = statuses.map(status => status.get('id'));
+
+        if (timeline.indexOf(':pinned') !== -1) {
+          return newIds;
+        }
+
         const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
         const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);
 
diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js
index 5955e9146..ce42271a9 100644
--- a/app/javascript/mastodon/service_worker/entry.js
+++ b/app/javascript/mastodon/service_worker/entry.js
@@ -1,4 +1,4 @@
-import { freeStorage } from '../storage/modifier';
+import { freeStorage, storageFreeable } from '../storage/modifier';
 import './web_push_notifications';
 
 function openSystemCache() {
@@ -49,7 +49,7 @@ self.addEventListener('fetch', function(event) {
 
       return response;
     }));
-  } else if (process.env.CDN_HOST ? url.host === process.env.CDN_HOST : url.pathname.startsWith('/system/')) {
+  } else if (storageFreeable && process.env.CDN_HOST ? url.host === process.env.CDN_HOST : url.pathname.startsWith('/system/')) {
     event.respondWith(openSystemCache().then(cache => {
       return cache.match(event.request.url).then(cached => {
         if (cached === undefined) {
diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js
new file mode 100644
index 000000000..ce96ae297
--- /dev/null
+++ b/app/javascript/mastodon/service_worker/web_push_locales.js
@@ -0,0 +1,30 @@
+/* @preval */
+
+const fs   = require('fs');
+const path = require('path');
+
+const filtered  = {};
+const filenames = fs.readdirSync(path.resolve(__dirname, '../locales'));
+
+filenames.forEach(filename => {
+  if (!filename.match(/\.json$/) || filename.match(/defaultMessages|whitelist/)) return;
+
+  const content = fs.readFileSync(path.resolve(__dirname, `../locales/${filename}`), 'utf-8');
+  const full    = JSON.parse(content);
+  const locale  = filename.split('.')[0];
+
+  filtered[locale] = {
+    'notification.favourite': full['notification.favourite'] || '',
+    'notification.follow': full['notification.follow'] || '',
+    'notification.mention': full['notification.mention'] || '',
+    'notification.reblog': full['notification.reblog'] || '',
+
+    'status.show_more': full['status.show_more'] || '',
+    'status.reblog': full['status.reblog'] || '',
+    'status.favourite': full['status.favourite'] || '',
+
+    'notifications.group': full['notifications.group'] || '',
+  };
+});
+
+module.exports = JSON.parse(JSON.stringify(filtered));
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index f63cff335..16eaefcbb 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -1,36 +1,32 @@
+import IntlMessageFormat from 'intl-messageformat';
+import locales from './web_push_locales';
+
 const MAX_NOTIFICATIONS = 5;
 const GROUP_TAG = 'tag';
 
-// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker
-const formatGroupTitle = (message, count) => message.replace('%{count}', count);
-
 const notify = options =>
   self.registration.getNotifications().then(notifications => {
-    if (notifications.length === MAX_NOTIFICATIONS) {
-      // Reached the maximum number of notifications, proceed with grouping
+    if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping
       const group = {
-        title: formatGroupTitle(options.data.message, notifications.length + 1),
-        body: notifications
-          .sort((n1, n2) => n1.timestamp < n2.timestamp)
-          .map(notification => notification.title).join('\n'),
+        title: formatMessage('notifications.group', options.data.preferred_locale, { count: notifications.length + 1 }),
+        body: notifications.sort((n1, n2) => n1.timestamp < n2.timestamp).map(notification => notification.title).join('\n'),
         badge: '/badge.png',
         icon: '/android-chrome-192x192.png',
         tag: GROUP_TAG,
         data: {
           url: (new URL('/web/notifications', self.location)).href,
           count: notifications.length + 1,
-          message: options.data.message,
+          preferred_locale: options.data.preferred_locale,
         },
       };
 
       notifications.forEach(notification => notification.close());
 
       return self.registration.showNotification(group.title, group);
-    } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) {
-      // Already grouped, proceed with appending the notification to the group
-      const group = cloneNotification(notifications[0]);
+    } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { // Already grouped, proceed with appending the notification to the group
+      const group = { ...notifications[0] };
 
-      group.title = formatGroupTitle(group.data.message, group.data.count + 1);
+      group.title = formatMessage('notifications.group', options.data.preferred_locale, { count: group.data.count + 1 });
       group.body  = `${options.title}\n${group.body}`;
       group.data  = { ...group.data, count: group.data.count + 1 };
 
@@ -40,57 +36,87 @@ const notify = options =>
     return self.registration.showNotification(options.title, options);
   });
 
-const handlePush = (event) => {
-  const options = event.data.json();
-
-  options.body      = options.data.nsfw || options.data.content;
-  options.dir       = options.data.dir;
-  options.image     = options.image || undefined; // Null results in a network request (404)
-  options.timestamp = options.timestamp && new Date(options.timestamp);
-
-  const expandAction = options.data.actions.find(action => action.todo === 'expand');
+const fetchFromApi = (path, method, accessToken) => {
+  const url = (new URL(path, self.location)).href;
 
-  if (expandAction) {
-    options.actions          = [expandAction];
-    options.hiddenActions    = options.data.actions.filter(action => action !== expandAction);
-    options.data.hiddenImage = options.image;
-    options.image            = undefined;
-  } else {
-    options.actions = options.data.actions;
-  }
+  return fetch(url, {
+    headers: {
+      'Authorization': `Bearer ${accessToken}`,
+      'Content-Type': 'application/json',
+    },
 
-  event.waitUntil(notify(options));
+    method: method,
+    credentials: 'include',
+  }).then(res => {
+    if (res.ok) {
+      return res;
+    } else {
+      throw new Error(res.status);
+    }
+  }).then(res => res.json());
 };
 
-const cloneNotification = (notification) => {
-  const clone = {  };
+const formatMessage = (messageId, locale, values = {}) =>
+  (new IntlMessageFormat(locales[locale][messageId], locale)).format(values);
 
-  for(var k in notification) {
-    clone[k] = notification[k];
-  }
+const handlePush = (event) => {
+  const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
+
+  // Placeholder until more information can be loaded
+  event.waitUntil(
+    notify({
+      title,
+      body,
+      icon,
+      tag: notification_id,
+      timestamp: new Date(),
+      badge: '/badge.png',
+      data: { access_token, preferred_locale, url: '/web/notifications' },
+    }).then(() => fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token)).then(notification => {
+      const options = {};
+
+      options.title     = formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
+      options.body      = notification.status && notification.status.content;
+      options.icon      = notification.account.avatar_static;
+      options.timestamp = notification.created_at && new Date(notification.created_at);
+      options.tag       = notification.id;
+      options.badge     = '/badge.png';
+      options.image     = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined;
+      options.data      = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/statuses/${notification.status.id}` : `/web/accounts/${notification.account.id}` };
+
+      if (notification.status && notification.status.sensitive) {
+        options.data.hiddenBody  = notification.status.content;
+        options.data.hiddenImage = notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url;
+
+        options.body    = undefined;
+        options.image   = undefined;
+        options.actions = [actionExpand(preferred_locale)];
+      } else if (notification.status) {
+        options.actions = [actionReblog(preferred_locale), actionFavourite(preferred_locale)];
+      }
 
-  return clone;
+      return notify(options);
+    })
+  );
 };
 
-const expandNotification = (notification) => {
-  const nextNotification = cloneNotification(notification);
+const actionExpand = preferred_locale => ({
+  action: 'expand',
+  icon: '/web-push-icon_expand.png',
+  title: formatMessage('status.show_more', preferred_locale),
+});
 
-  nextNotification.body    = notification.data.content;
-  nextNotification.image   = notification.data.hiddenImage;
-  nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand');
+const actionReblog = preferred_locale => ({
+  action: 'reblog',
+  icon: '/web-push-icon_reblog.png',
+  title: formatMessage('status.reblog', preferred_locale),
+});
 
-  return self.registration.showNotification(nextNotification.title, nextNotification);
-};
-
-const makeRequest = (notification, action) =>
-  fetch(action.action, {
-    headers: {
-      'Authorization': `Bearer ${notification.data.access_token}`,
-      'Content-Type': 'application/json',
-    },
-    method: action.method,
-    credentials: 'include',
-  });
+const actionFavourite = preferred_locale => ({
+  action: 'favourite',
+  icon: '/web-push-icon_favourite.png',
+  title: formatMessage('status.favourite', preferred_locale),
+});
 
 const findBestClient = clients => {
   const focusedClient = clients.find(client => client.focused);
@@ -99,6 +125,24 @@ const findBestClient = clients => {
   return focusedClient || visibleClient || clients[0];
 };
 
+const expandNotification = notification => {
+  const newNotification = { ...notification };
+
+  newNotification.body    = newNotification.data.hiddenBody;
+  newNotification.image   = newNotification.data.hiddenImage;
+  newNotification.actions = [actionReblog(notification.data.preferred_locale), actionFavourite(notification.data.preferred_locale)];
+
+  return self.registration.showNotification(newNotification.title, newNotification);
+};
+
+const removeActionFromNotification = (notification, action) => {
+  const newNotification = { ...notification };
+
+  newNotification.actions = newNotification.actions.filter(item => item.action !== action);
+
+  return self.registration.showNotification(newNotification.title, newNotification);
+};
+
 const openUrl = url =>
   self.clients.matchAll({ type: 'window' }).then(clientList => {
     if (clientList.length !== 0) {
@@ -124,27 +168,19 @@ const openUrl = url =>
     return self.clients.openWindow(url);
   });
 
-const removeActionFromNotification = (notification, action) => {
-  const actions          = notification.actions.filter(act => act.action !== action.action);
-  const nextNotification = cloneNotification(notification);
-
-  nextNotification.actions = actions;
-
-  return self.registration.showNotification(nextNotification.title, nextNotification);
-};
-
 const handleNotificationClick = (event) => {
   const reactToNotificationClick = new Promise((resolve, reject) => {
     if (event.action) {
-      const action = event.notification.data.actions.find(({ action }) => action === event.action);
-
-      if (action.todo === 'expand') {
+      if (event.action === 'expand') {
         resolve(expandNotification(event.notification));
-      } else if (action.todo === 'request') {
-        resolve(makeRequest(event.notification, action)
-          .then(() => removeActionFromNotification(event.notification, action)));
+      } else if (event.action === 'reblog') {
+        const { data } = event.notification;
+        resolve(fetchFromApi(`/api/v1/statuses/${data.id}/reblog`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'reblog')));
+      } else if (event.action === 'favourite') {
+        const { data } = event.notification;
+        resolve(fetchFromApi(`/api/v1/statuses/${data.id}/favourite`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'favourite')));
       } else {
-        reject(`Unknown action: ${action.todo}`);
+        reject(`Unknown action: ${event.action}`);
       }
     } else {
       event.notification.close();
diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js
index a42b6ab12..9fadabef4 100644
--- a/app/javascript/mastodon/storage/modifier.js
+++ b/app/javascript/mastodon/storage/modifier.js
@@ -4,6 +4,11 @@ const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
 const storageMargin = 8388608;
 const storeLimit = 1024;
 
+// navigator.storage is not present on:
+// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
+// estimate method is not present on Chrome 57.0.2987.98 on Linux.
+export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage;
+
 function openCache() {
   // ServiceWorker and Cache API is not available on iOS 11
   // https://webkit.org/status/#specification-service-workers
@@ -182,10 +187,7 @@ export function putStatuses(records) {
 }
 
 export function freeStorage() {
-  // navigator.storage is not present on:
-  // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
-  // estimate method is not present on Chrome 57.0.2987.98 on Linux.
-  return 'storage' in navigator && 'estimate' in navigator.storage && navigator.storage.estimate().then(({ quota, usage }) => {
+  return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => {
     if (usage + storageMargin < quota) {
       return null;
     }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 9f0a7a058..4022885e1 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3181,6 +3181,7 @@ a.status-card {
   &.active {
     background: $ui-highlight-color;
     color: $primary-text-color;
+    outline: 0;
 
     .privacy-dropdown__option__content {
       color: $primary-text-color;
diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss
index dd3c1b688..fe2d40c0c 100644
--- a/app/javascript/styles/mastodon/footer.scss
+++ b/app/javascript/styles/mastodon/footer.scss
@@ -26,5 +26,13 @@
         text-decoration: none;
       }
     }
+
+    img {
+      margin: 0 4px;
+      position: relative;
+      bottom: -1px;
+      height: 18px;
+      vertical-align: top;
+    }
   }
 }
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index df549c6d3..867bc9519 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -21,8 +21,8 @@ class Web::PushSubscription < ApplicationRecord
   has_one :session_activation
 
   def push(notification)
-    I18n.with_locale(associated_user.locale || I18n.default_locale) do
-      push_payload(message_from(notification), 48.hours.seconds)
+    I18n.with_locale(associated_user&.locale || I18n.default_locale) do
+      push_payload(payload_for_notification(notification), 48.hours.seconds)
     end
   end
 
@@ -46,16 +46,22 @@ class Web::PushSubscription < ApplicationRecord
     @associated_access_token = if access_token_id.nil?
                                  find_or_create_access_token.token
                                else
-                                 access_token
+                                 access_token.token
                                end
   end
 
+  class << self
+    def unsubscribe_for(application_id, resource_owner)
+      access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil)
+                                                .pluck(:id)
+
+      where(access_token_id: access_token_ids).delete_all
+    end
+  end
+
   private
 
   def push_payload(message, ttl = 5.minutes.seconds)
-    # TODO: Make sure that the payload does not
-    # exceed 4KB - Webpush::PayloadTooLarge
-
     Webpush.payload_send(
       message: Oj.dump(message),
       endpoint: endpoint,
@@ -70,16 +76,20 @@ class Web::PushSubscription < ApplicationRecord
     )
   end
 
-  def message_from(notification)
-    serializable_resource = ActiveModelSerializers::SerializableResource.new(notification, serializer: Web::NotificationSerializer, scope: self, scope_name: :current_push_subscription)
-    serializable_resource.as_json
+  def payload_for_notification(notification)
+    ActiveModelSerializers::SerializableResource.new(
+      notification,
+      serializer: Web::NotificationSerializer,
+      scope: self,
+      scope_name: :current_push_subscription
+    ).as_json
   end
 
   def find_or_create_access_token
     Doorkeeper::AccessToken.find_or_create_for(
       Doorkeeper::Application.find_by(superapp: true),
       session_activation.user_id,
-      Doorkeeper::OAuth::Scopes.from_string('read write follow'),
+      Doorkeeper::OAuth::Scopes.from_string('read write follow push'),
       Doorkeeper.configuration.access_token_expires_in,
       Doorkeeper.configuration.refresh_token_enabled?
     )
diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb
index 31c703832..f3c4ffc47 100644
--- a/app/serializers/web/notification_serializer.rb
+++ b/app/serializers/web/notification_serializer.rb
@@ -2,168 +2,37 @@
 
 class Web::NotificationSerializer < ActiveModel::Serializer
   include RoutingHelper
-  include StreamEntriesHelper
+  include ActionView::Helpers::TextHelper
+  include ActionView::Helpers::SanitizeHelper
 
-  class DataSerializer < ActiveModel::Serializer
-    include RoutingHelper
-    include StreamEntriesHelper
-    include ActionView::Helpers::SanitizeHelper
+  attributes :access_token, :preferred_locale, :notification_id,
+             :notification_type, :icon, :title, :body
 
-    attributes :content, :nsfw, :url, :actions,
-               :access_token, :message, :dir
-
-    def content
-      decoder.decode(strip_tags(body))
-    end
-
-    def dir
-      rtl?(body) ? 'rtl' : 'ltr'
-    end
-
-    def nsfw
-      return if object.target_status.nil?
-      object.target_status.spoiler_text.presence
-    end
-
-    def url
-      case object.type
-      when :mention
-        web_url("statuses/#{object.target_status.id}")
-      when :follow
-        web_url("accounts/#{object.from_account.id}")
-      when :favourite
-        web_url("statuses/#{object.target_status.id}")
-      when :reblog
-        web_url("statuses/#{object.target_status.id}")
-      end
-    end
-
-    def actions
-      return @actions if defined?(@actions)
-
-      @actions = []
-
-      if object.type == :mention
-        @actions << expand_action if collapsed?
-        @actions << favourite_action
-        @actions << reblog_action if rebloggable?
-      end
-
-      @actions
-    end
-
-    def access_token
-      return if actions.empty?
-      current_push_subscription.associated_access_token
-    end
-
-    def message
-      I18n.t('push_notifications.group.title')
-    end
-
-    private
-
-    def body
-      case object.type
-      when :mention
-        object.target_status.text
-      when :follow
-        object.from_account.note
-      when :favourite
-        object.target_status.text
-      when :reblog
-        object.target_status.text
-      end
-    end
-
-    def decoder
-      @decoder ||= HTMLEntities.new
-    end
-
-    def expand_action
-      {
-        title: I18n.t('push_notifications.mention.action_expand'),
-        icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true),
-        todo: 'expand',
-        action: 'expand',
-      }
-    end
-
-    def favourite_action
-      {
-        title: I18n.t('push_notifications.mention.action_favourite'),
-        icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true),
-        todo: 'request',
-        method: 'POST',
-        action: "/api/v1/statuses/#{object.target_status.id}/favourite",
-      }
-    end
-
-    def reblog_action
-      {
-        title: I18n.t('push_notifications.mention.action_boost'),
-        icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true),
-        todo: 'request',
-        method: 'POST',
-        action: "/api/v1/statuses/#{object.target_status.id}/reblog",
-      }
-    end
-
-    def collapsed?
-      !object.target_status.nil? && (object.target_status.sensitive? || object.target_status.spoiler_text.present?)
-    end
-
-    def rebloggable?
-      !object.target_status.nil? && !object.target_status.hidden?
-    end
+  def access_token
+    current_push_subscription.associated_access_token
   end
 
-  attributes :title, :image, :badge, :tag,
-             :timestamp, :icon
-
-  has_one :data, serializer: DataSerializer
-
-  def title
-    case object.type
-    when :mention
-      I18n.t('push_notifications.mention.title', name: name)
-    when :follow
-      I18n.t('push_notifications.follow.title', name: name)
-    when :favourite
-      I18n.t('push_notifications.favourite.title', name: name)
-    when :reblog
-      I18n.t('push_notifications.reblog.title', name: name)
-    end
+  def preferred_locale
+    current_push_subscription.associated_user&.locale || I18n.default_locale
   end
 
-  def image
-    return if object.target_status.nil? || object.target_status.media_attachments.empty?
-    full_asset_url(object.target_status.media_attachments.first.file.url(:small))
-  end
-
-  def badge
-    full_asset_url('badge.png', skip_pipeline: true)
-  end
-
-  def tag
+  def notification_id
     object.id
   end
 
-  def timestamp
-    object.created_at
+  def notification_type
+    object.type
   end
 
   def icon
-    object.from_account.avatar_static_url
+    full_asset_url(object.from_account.avatar_static_url)
   end
 
-  def data
-    object
+  def title
+    I18n.t("notification_mailer.#{object.type}.subject", name: object.from_account.display_name.presence || object.from_account.username)
   end
 
-  private
-
-  def name
-    display_name(object.from_account)
+  def body
+    truncate(strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note), length: 140)
   end
 end
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 858d354fa..8bbd184bb 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -9,6 +9,6 @@
     - else
       %span.footer__domain= link_to site_hostname, root_path
     %span.powered-by
-      != t('generic.powered_by', link: link_to('Mastodon', 'https://joinmastodon.org'))
+      != t('generic.powered_by', link: link_to('https://joinmastodon.org') { image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' })
 
 = render template: 'layouts/application'
diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml
index fa48e5e62..fc5c4da20 100644
--- a/app/views/remote_follow/new.html.haml
+++ b/app/views/remote_follow/new.html.haml
@@ -7,7 +7,7 @@
   = simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f|
     = render 'shared/error_messages', object: @remote_follow
 
-    = f.input :acct, placeholder: t('remote_follow.acct')
+    = f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' }
 
     .actions
       = f.button :button, t('remote_follow.proceed'), type: :submit
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index adffd1d3b..323a9f85b 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -3,6 +3,9 @@
 class ActivityPub::DeliveryWorker
   include Sidekiq::Worker
 
+  STOPLIGHT_FAILURE_THRESHOLD = 10
+  STOPLIGHT_COOLDOWN = 60
+
   sidekiq_options queue: 'push', retry: 16, dead: false
 
   HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
@@ -31,15 +34,21 @@ class ActivityPub::DeliveryWorker
   def perform_request
     light = Stoplight(@inbox_url) do
       build_request.perform do |response|
-        raise Mastodon::UnexpectedResponseError, response unless response_successful?(response)
+        raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
       end
     end
 
-    light.run
+    light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD)
+         .with_cool_off_time(STOPLIGHT_COOLDOWN)
+         .run
   end
 
   def response_successful?(response)
-    response.code > 199 && response.code < 300
+    (200...300).cover?(response.code)
+  end
+
+  def response_error_unsalvageable?(response)
+    (400...500).cover?(response.code) && response.code != 429
   end
 
   def failure_tracker