diff --git a/package.json b/package.json
index 1ef35f4a..09ba18ac 100644
--- a/package.json
+++ b/package.json
@@ -21,10 +21,10 @@
     "@fortawesome/fontawesome-svg-core": "1.3.0",
     "@fortawesome/free-regular-svg-icons": "5.15.4",
     "@fortawesome/free-solid-svg-icons": "5.15.4",
-    "@fortawesome/vue-fontawesome": "3.0.0-5",
+    "@fortawesome/vue-fontawesome": "3.0.1",
     "@kazvmoe-infra/pinch-zoom-element": "1.2.0",
-    "@vuelidate/core": "2.0.0-alpha.41",
-    "@vuelidate/validators": "2.0.0-alpha.27",
+    "@vuelidate/core": "2.0.0-alpha.42",
+    "@vuelidate/validators": "2.0.0-alpha.30",
     "body-scroll-lock": "2.7.1",
     "chromatism": "3.0.0",
     "click-outside-vue3": "4.0.1",
@@ -86,7 +86,7 @@
     "html-webpack-plugin": "3.2.0",
     "http-proxy-middleware": "0.21.0",
     "inject-loader": "2.0.1",
-    "iso-639-1": "2.1.13",
+    "iso-639-1": "2.1.15",
     "isparta-loader": "2.0.0",
     "json-loader": "0.5.7",
     "karma": "6.3.17",
@@ -107,8 +107,8 @@
     "ora": "0.4.1",
     "postcss-loader": "3.0.0",
     "raw-loader": "0.5.1",
-    "sass": "1.20.1",
-    "sass-loader": "7.2.0",
+    "sass": "1.53.0",
+    "sass-loader": "7.3.1",
     "selenium-server": "2.53.1",
     "semver": "5.7.1",
     "serviceworker-webpack-plugin": "1.0.1",
@@ -123,7 +123,7 @@
     "vue-style-loader": "4.1.2",
     "webpack": "4.46.0",
     "webpack-dev-middleware": "3.7.3",
-    "webpack-hot-middleware": "2.24.3",
+    "webpack-hot-middleware": "2.25.1",
     "webpack-merge": "0.20.0"
   },
   "engines": {
diff --git a/src/App.scss b/src/App.scss
index 5cd0b96e..7e6d0dfc 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -310,7 +310,6 @@ nav {
       border-top-right-radius: 0;
     }
 
-    .underlay,
     #sidebar,
     #notifs-column {
       display: none;
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index f655c38f..b8bbbe38 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -370,6 +370,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
 
   // Start fetching things that don't need to block the UI
   store.dispatch('fetchMutes')
+  store.dispatch('startFetchingAnnouncements')
   getTOS({ store })
   getStickers({ store })
 
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 1ab8209d..00f553c2 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -23,6 +23,7 @@ import RemoteUserResolver from 'components/remote_user_resolver/remote_user_reso
 import Lists from 'components/lists/lists.vue'
 import ListTimeline from 'components/list_timeline/list_timeline.vue'
 import ListEdit from 'components/list_edit/list_edit.vue'
+import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
 
 export default (store) => {
   const validateAuthenticatedRoute = (to, from, next) => {
@@ -72,10 +73,11 @@ export default (store) => {
     { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
     { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
     { name: 'about', path: '/about', component: About },
-    { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile },
     { name: 'lists', path: '/lists', component: Lists },
     { name: 'list-timeline', path: '/lists/:id', component: ListTimeline },
-    { name: 'list-edit', path: '/lists/:id/edit', component: ListEdit }
+    { name: 'list-edit', path: '/lists/:id/edit', component: ListEdit },
+    { name: 'announcements', path: '/announcements', component: AnnouncementsPage },
+    { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }
   ]
 
   if (store.state.instance.pleromaChatMessagesAvailable) {
diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
new file mode 100644
index 00000000..993e3655
--- /dev/null
+++ b/src/components/announcement/announcement.js
@@ -0,0 +1,105 @@
+import { mapState } from 'vuex'
+import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
+import RichContent from '../rich_content/rich_content.jsx'
+import localeService from '../../services/locale/locale.service.js'
+
+const Announcement = {
+  components: {
+    AnnouncementEditor,
+    RichContent
+  },
+  data () {
+    return {
+      editing: false,
+      editedAnnouncement: {
+        content: '',
+        startsAt: undefined,
+        endsAt: undefined,
+        allDay: undefined
+      },
+      editError: ''
+    }
+  },
+  props: {
+    announcement: Object
+  },
+  computed: {
+    ...mapState({
+      currentUser: state => state.users.currentUser
+    }),
+    content () {
+      return this.announcement.content
+    },
+    isRead () {
+      return this.announcement.read
+    },
+    publishedAt () {
+      const time = this.announcement['published_at']
+      if (!time) {
+        return
+      }
+
+      return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+    },
+    startsAt () {
+      const time = this.announcement['starts_at']
+      if (!time) {
+        return
+      }
+
+      return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+    },
+    endsAt () {
+      const time = this.announcement['ends_at']
+      if (!time) {
+        return
+      }
+
+      return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
+    },
+    inactive () {
+      return this.announcement.inactive
+    }
+  },
+  methods: {
+    markAsRead () {
+      if (!this.isRead) {
+        return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
+      }
+    },
+    deleteAnnouncement () {
+      return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
+    },
+    formatTimeOrDate (time, locale) {
+      const d = new Date(time)
+      return this.announcement['all_day'] ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
+    },
+    enterEditMode () {
+      this.editedAnnouncement.content = this.announcement.pleroma['raw_content']
+      this.editedAnnouncement.startsAt = this.announcement['starts_at']
+      this.editedAnnouncement.endsAt = this.announcement['ends_at']
+      this.editedAnnouncement.allDay = this.announcement['all_day']
+      this.editing = true
+    },
+    submitEdit () {
+      this.$store.dispatch('editAnnouncement', {
+        id: this.announcement.id,
+        ...this.editedAnnouncement
+      })
+        .then(() => {
+          this.editing = false
+        })
+        .catch(error => {
+          this.editError = error.error
+        })
+    },
+    cancelEdit () {
+      this.editing = false
+    },
+    clearError () {
+      this.editError = undefined
+    }
+  }
+}
+
+export default Announcement
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
new file mode 100644
index 00000000..5f64232a
--- /dev/null
+++ b/src/components/announcement/announcement.vue
@@ -0,0 +1,136 @@
+<template>
+  <div class="announcement">
+    <div class="heading">
+      <h4>{{ $t('announcements.title') }}</h4>
+    </div>
+    <div class="body">
+      <rich-content
+        v-if="!editing"
+        :html="content"
+        :emoji="announcement.emojis"
+        :handle-links="true"
+      />
+      <announcement-editor
+        v-else
+        :announcement="editedAnnouncement"
+      />
+    </div>
+    <div class="footer">
+      <div
+        v-if="!editing"
+        class="times"
+      >
+        <span v-if="publishedAt">
+          {{ $t('announcements.published_time_display', { time: publishedAt }) }}
+        </span>
+        <span v-if="startsAt">
+          {{ $t('announcements.start_time_display', { time: startsAt }) }}
+        </span>
+        <span v-if="endsAt">
+          {{ $t('announcements.end_time_display', { time: endsAt }) }}
+        </span>
+      </div>
+      <div
+        v-if="!editing"
+        class="actions"
+      >
+        <button
+          v-if="currentUser"
+          class="btn button-default"
+          :class="{ toggled: isRead }"
+          :disabled="inactive"
+          :title="inactive ? $t('announcements.inactive_message') : ''"
+          @click="markAsRead"
+        >
+          {{ $t('announcements.mark_as_read_action') }}
+        </button>
+        <button
+          v-if="currentUser && currentUser.role === 'admin'"
+          class="btn button-default"
+          @click="enterEditMode"
+        >
+          {{ $t('announcements.edit_action') }}
+        </button>
+        <button
+          v-if="currentUser && currentUser.role === 'admin'"
+          class="btn button-default"
+          @click="deleteAnnouncement"
+        >
+          {{ $t('announcements.delete_action') }}
+        </button>
+      </div>
+      <div
+        v-else
+        class="actions"
+      >
+        <button
+          class="btn button-default"
+          @click="submitEdit"
+        >
+          {{ $t('announcements.submit_edit_action') }}
+        </button>
+        <button
+          class="btn button-default"
+          @click="cancelEdit"
+        >
+          {{ $t('announcements.cancel_edit_action') }}
+        </button>
+        <div
+          v-if="editing && editError"
+          class="alert error"
+        >
+          {{ $t('announcements.edit_error', { error }) }}
+          <button
+            class="button-unstyled"
+            @click="clearError"
+          >
+            <FAIcon
+              class="fa-scale-110 fa-old-padding"
+              icon="times"
+              :title="$t('announcements.close_error')"
+            />
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script src="./announcement.js"></script>
+
+<style lang="scss">
+@import "../../variables";
+
+.announcement {
+  border-bottom-width: 1px;
+  border-bottom-style: solid;
+  border-bottom-color: var(--border, $fallback--border);
+  border-radius: 0;
+  padding: var(--status-margin, $status-margin);
+
+  .heading, .body {
+    margin-bottom: var(--status-margin, $status-margin);
+  }
+
+  .footer {
+    display: flex;
+    flex-direction: column;
+    .times {
+      display: flex;
+      flex-direction: column;
+    }
+  }
+
+  .footer .actions {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-evenly;
+
+    .btn {
+      flex: 1;
+      margin: 1em;
+      max-width: 10em;
+    }
+  }
+}
+</style>
diff --git a/src/components/announcement_editor/announcement_editor.js b/src/components/announcement_editor/announcement_editor.js
new file mode 100644
index 00000000..79a03afe
--- /dev/null
+++ b/src/components/announcement_editor/announcement_editor.js
@@ -0,0 +1,13 @@
+import Checkbox from '../checkbox/checkbox.vue'
+
+const AnnouncementEditor = {
+  components: {
+    Checkbox
+  },
+  props: {
+    announcement: Object,
+    disabled: Boolean
+  }
+}
+
+export default AnnouncementEditor
diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue
new file mode 100644
index 00000000..0f29f9f7
--- /dev/null
+++ b/src/components/announcement_editor/announcement_editor.vue
@@ -0,0 +1,60 @@
+<template>
+  <div class="announcement-editor">
+    <textarea
+      ref="textarea"
+      v-model="announcement.content"
+      class="post-textarea"
+      rows="1"
+      cols="1"
+      :placeholder="$t('announcements.post_placeholder')"
+      :disabled="disabled"
+    />
+    <span class="announcement-metadata">
+      <label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
+      <input
+        id="announcement-start-time"
+        v-model="announcement.startsAt"
+        :type="announcement.allDay ? 'date' : 'datetime-local'"
+        :disabled="disabled"
+      >
+    </span>
+    <span class="announcement-metadata">
+      <label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
+      <input
+        id="announcement-end-time"
+        v-model="announcement.endsAt"
+        :type="announcement.allDay ? 'date' : 'datetime-local'"
+        :disabled="disabled"
+      >
+    </span>
+    <span class="announcement-metadata">
+      <Checkbox
+        id="announcement-all-day"
+        v-model="announcement.allDay"
+        :disabled="disabled"
+      />
+      <label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
+    </span>
+  </div>
+</template>
+
+<script src="./announcement_editor.js"></script>
+
+<style lang="scss">
+.announcement-editor {
+  display: flex;
+  align-items: stretch;
+  flex-direction: column;
+
+  .announcement-metadata {
+    margin-top: 0.5em;
+  }
+
+  .post-textarea {
+    resize: vertical;
+    height: 10em;
+    overflow: none;
+    box-sizing: content-box;
+  }
+}
+</style>
diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js
new file mode 100644
index 00000000..0bb4892e
--- /dev/null
+++ b/src/components/announcements_page/announcements_page.js
@@ -0,0 +1,55 @@
+import { mapState } from 'vuex'
+import Announcement from '../announcement/announcement.vue'
+import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
+
+const AnnouncementsPage = {
+  components: {
+    Announcement,
+    AnnouncementEditor
+  },
+  data () {
+    return {
+      newAnnouncement: {
+        content: '',
+        startsAt: undefined,
+        endsAt: undefined,
+        allDay: false
+      },
+      posting: false,
+      error: undefined
+    }
+  },
+  mounted () {
+    this.$store.dispatch('fetchAnnouncements')
+  },
+  computed: {
+    ...mapState({
+      currentUser: state => state.users.currentUser
+    }),
+    announcements () {
+      return this.$store.state.announcements.announcements
+    }
+  },
+  methods: {
+    postAnnouncement () {
+      this.posting = true
+      this.$store.dispatch('postAnnouncement', this.newAnnouncement)
+        .then(() => {
+          this.newAnnouncement.content = ''
+          this.startsAt = undefined
+          this.endsAt = undefined
+        })
+        .catch(error => {
+          this.error = error.error
+        })
+        .finally(() => {
+          this.posting = false
+        })
+    },
+    clearError () {
+      this.error = undefined
+    }
+  }
+}
+
+export default AnnouncementsPage
diff --git a/src/components/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue
new file mode 100644
index 00000000..b1489dec
--- /dev/null
+++ b/src/components/announcements_page/announcements_page.vue
@@ -0,0 +1,79 @@
+<template>
+  <div class="panel panel-default announcements-page">
+    <div class="panel-heading">
+      <span>
+        {{ $t('announcements.page_header') }}
+      </span>
+    </div>
+    <div class="panel-body">
+      <section
+        v-if="currentUser && currentUser.role === 'admin'"
+      >
+        <div class="post-form">
+          <div class="heading">
+            <h4>{{ $t('announcements.post_form_header') }}</h4>
+          </div>
+          <div class="body">
+            <announcement-editor
+              :announcement="newAnnouncement"
+              :disabled="posting"
+            />
+          </div>
+          <div class="footer">
+            <button
+              class="btn button-default post-button"
+              :disabled="posting"
+              @click.prevent="postAnnouncement"
+            >
+              {{ $t('announcements.post_action') }}
+            </button>
+            <div
+              v-if="error"
+              class="alert error"
+            >
+              {{ $t('announcements.post_error', { error }) }}
+              <button
+                class="button-unstyled"
+                @click="clearError"
+              >
+                <FAIcon
+                  class="fa-scale-110 fa-old-padding"
+                  icon="times"
+                  :title="$t('announcements.close_error')"
+                />
+              </button>
+            </div>
+          </div>
+        </div>
+      </section>
+      <section
+        v-for="announcement in announcements"
+        :key="announcement.id"
+      >
+        <announcement
+          :announcement="announcement"
+        />
+      </section>
+    </div>
+  </div>
+</template>
+
+<script src="./announcements_page.js"></script>
+
+<style lang="scss">
+@import "../../variables";
+
+.announcements-page {
+  .post-form {
+    padding: var(--status-margin, $status-margin);
+
+    .heading, .body {
+      margin-bottom: var(--status-margin, $status-margin);
+    }
+
+    .post-button {
+      min-width: 10em;
+    }
+  }
+}
+</style>
diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss
index 05cc0ceb..7fd2b26f 100644
--- a/src/components/desktop_nav/desktop_nav.scss
+++ b/src/components/desktop_nav/desktop_nav.scss
@@ -3,6 +3,10 @@
 .DesktopNav {
   width: 100%;
 
+  input {
+    color: var(--inputTopbarText, var(--inputText));
+  }
+
   a {
     color: var(--topBarLink, $fallback--link);
   }
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 6b589079..bd5c2e39 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -6,6 +6,7 @@ import {
   faStickyNote,
   faSmileBeam
 } from '@fortawesome/free-solid-svg-icons'
+import { trim } from 'lodash'
 
 library.add(
   faBoxOpen,
@@ -176,7 +177,7 @@ const EmojiPicker = {
     filteredEmoji () {
       return filterByKeyword(
         this.$store.state.instance.customEmoji || [],
-        this.keyword
+        trim(this.keyword)
       )
     },
     customEmojiBuffer () {
@@ -197,7 +198,7 @@ const EmojiPicker = {
           id: 'standard',
           text: this.$t('emoji.unicode'),
           icon: 'box-open',
-          emojis: filterByKeyword(standardEmojis, this.keyword)
+          emojis: filterByKeyword(standardEmojis, trim(this.keyword))
         }
       ]
     },
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 3262a3d9..a7269120 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -47,6 +47,7 @@
             type="text"
             class="form-control"
             :placeholder="$t('emoji.search_emoji')"
+            @input="$event.target.composing = false"
           >
         </div>
         <div
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index 877d52a9..6efffd13 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -47,7 +47,7 @@ const MobileNav = {
     isChat () {
       return this.$route.name === 'chat'
     },
-    ...mapGetters(['unreadChatCount'])
+    ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
   },
   methods: {
     toggleMobileSidebar () {
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index 8d63fe7f..50e71ffb 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -17,7 +17,7 @@
             icon="bars"
           />
           <div
-            v-if="unreadChatCount"
+            v-if="unreadChatCount || unreadAnnouncementCount"
             class="alert-dot"
           />
         </button>
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index f52fc677..1f188174 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -13,7 +13,8 @@ import {
   faBell,
   faInfoCircle,
   faStream,
-  faList
+  faList,
+  faBullhorn
 } from '@fortawesome/free-solid-svg-icons'
 
 library.add(
@@ -27,7 +28,8 @@ library.add(
   faBell,
   faInfoCircle,
   faStream,
-  faList
+  faList,
+  faBullhorn
 )
 
 const NavPanel = {
@@ -57,7 +59,7 @@ const NavPanel = {
       federating: state => state.instance.federating,
       pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
     }),
-    ...mapGetters(['unreadChatCount'])
+    ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
   }
 }
 
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index c139549d..15f8b227 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -97,6 +97,24 @@
             />{{ $t("nav.about") }}
           </router-link>
         </li>
+        <li v-if="currentUser">
+          <router-link
+            class="menu-item"
+            :to="{ name: 'announcements' }"
+          >
+            <FAIcon
+              fixed-width
+              class="fa-scale-110"
+              icon="bullhorn"
+            />{{ $t('nav.announcements') }}
+            <span
+              v-if="unreadAnnouncementCount > 0"
+              class="badge badge-notification"
+            >
+              {{ unreadAnnouncementCount }}
+            </span>
+          </router-link>
+        </li>
       </ul>
     </div>
   </div>
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 82aa1489..5f2a1251 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -60,7 +60,7 @@ const Notifications = {
       return this.unseenNotifications.length
     },
     unseenCountTitle () {
-      return this.unseenCount + (this.unreadChatCount)
+      return this.unseenCount + (this.unreadChatCount) + this.unreadAnnouncementCount
     },
     loading () {
       return this.$store.state.statuses.notifications.loading
@@ -80,7 +80,7 @@ const Notifications = {
     notificationsToDisplay () {
       return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
     },
-    ...mapGetters(['unreadChatCount'])
+    ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
   },
   watch: {
     unseenCountTitle (count) {
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js
index f4b736d2..4895733c 100644
--- a/src/components/settings_modal/tabs/data_import_export_tab.js
+++ b/src/components/settings_modal/tabs/data_import_export_tab.js
@@ -7,11 +7,16 @@ const DataImportExportTab = {
   data () {
     return {
       activeTab: 'profile',
-      newDomainToMute: ''
+      newDomainToMute: '',
+      listBackupsError: false,
+      addBackupError: false,
+      addedBackup: false,
+      backups: []
     }
   },
   created () {
     this.$store.dispatch('fetchTokens')
+    this.fetchBackups()
   },
   components: {
     Importer,
@@ -72,6 +77,28 @@ const DataImportExportTab = {
         }
         return user.screen_name
       }).join('\n')
+    },
+    addBackup () {
+      this.$store.state.api.backendInteractor.addBackup()
+        .then((res) => {
+          this.addedBackup = true
+          this.addBackupError = false
+        })
+        .catch((error) => {
+          this.addedBackup = false
+          this.addBackupError = error
+        })
+        .then(() => this.fetchBackups())
+    },
+    fetchBackups () {
+      this.$store.state.api.backendInteractor.listBackups()
+        .then((res) => {
+          this.backups = res
+          this.listBackupsError = false
+        })
+        .catch((error) => {
+          this.listBackupsError = error.error
+        })
     }
   }
 }
diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue
index a406077d..e3b7f407 100644
--- a/src/components/settings_modal/tabs/data_import_export_tab.vue
+++ b/src/components/settings_modal/tabs/data_import_export_tab.vue
@@ -53,6 +53,67 @@
         :export-button-label="$t('settings.mute_export_button')"
       />
     </div>
+    <div class="setting-item">
+      <h2>{{ $t('settings.account_backup') }}</h2>
+      <p>{{ $t('settings.account_backup_description') }}</p>
+      <table>
+        <thead>
+          <tr>
+            <th>{{ $t('settings.account_backup_table_head') }}</th>
+            <th />
+          </tr>
+        </thead>
+        <tbody>
+          <tr
+            v-for="backup in backups"
+            :key="backup.id"
+          >
+            <td>{{ backup.inserted_at }}</td>
+            <td class="actions">
+              <a
+                v-if="backup.processed"
+                target="_blank"
+                :href="backup.url"
+              >
+                {{ $t('settings.download_backup') }}
+              </a>
+              <span
+                v-else
+              >
+                {{ $t('settings.backup_not_ready') }}
+              </span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+      <div
+        v-if="listBackupsError"
+        class="alert error"
+      >
+        {{ $t('settings.list_backups_error', { error }) }}
+        <button
+          :title="$t('settings.hide_list_backups_error_action')"
+          @click="listBackupsError = false"
+        >
+          <FAIcon
+            class="fa-scale-110 fa-old-padding"
+            icon="times"
+          />
+        </button>
+      </div>
+      <button
+        class="btn button-default"
+        @click="addBackup"
+      >
+        {{ $t('settings.add_backup') }}
+      </button>
+      <p v-if="addedBackup">
+        {{ $t('settings.added_backup') }}
+      </p>
+      <template v-if="addBackupError !== false">
+        <p>{{ $t('settings.add_backup_error', { error: addBackupError }) }}</p>
+      </template>
+    </div>
   </div>
 </template>
 
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 78b7045b..c5c28698 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -88,7 +88,7 @@ const SideDrawer = {
     ...mapState({
       pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
     }),
-    ...mapGetters(['unreadChatCount'])
+    ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
   },
   methods: {
     toggleDrawer () {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 4696b488..0b6fb8d9 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -191,6 +191,26 @@
             /> {{ $t("nav.administration") }}
           </a>
         </li>
+        <li
+          v-if="currentUser"
+          @click="toggleDrawer"
+        >
+          <router-link
+            :to="{ name: 'announcements' }"
+          >
+            <FAIcon
+              fixed-width
+              class="fa-scale-110 fa-old-padding"
+              icon="bullhorn"
+            /> {{ $t("nav.announcements") }}
+            <span
+              v-if="unreadAnnouncementCount"
+              class="badge badge-notification"
+            >
+              {{ unreadAnnouncementCount }}
+            </span>
+          </router-link>
+        </li>
         <li
           v-if="currentUser"
           @click="toggleDrawer"
diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue
index bed29020..2b487dfd 100644
--- a/src/components/timeago/timeago.vue
+++ b/src/components/timeago/timeago.vue
@@ -3,7 +3,7 @@
     :datetime="time"
     :title="localeDateString"
   >
-    {{ $t(relativeTime.key, [relativeTime.num]) }}
+    {{ $tc(relativeTime.key, relativeTime.num, [relativeTime.num]) }}
   </time>
 </template>
 
diff --git a/src/i18n/ca.json b/src/i18n/ca.json
index 74260143..5f2795a8 100644
--- a/src/i18n/ca.json
+++ b/src/i18n/ca.json
@@ -621,7 +621,6 @@
       "disable_any_subscription": "Deshabilita completament seguir algú",
       "quarantine": "Deshabilita la federació a les entrades de les usuàries",
       "moderation": "Moderació",
-      "delete_user_confirmation": "Estàs completament segur/a? Aquesta acció no es pot desfer.",
       "revoke_admin": "Revoca l'Admin",
       "activate_account": "Activa el compte",
       "deactivate_account": "Desactiva el compte",
diff --git a/src/i18n/de.json b/src/i18n/de.json
index b6599594..4bf897ef 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -582,7 +582,6 @@
     "statuses": "Beiträge",
     "admin_menu": {
       "sandbox": "Erzwinge Beiträge nur für Follower sichtbar zu sein",
-      "delete_user_confirmation": "Achtung! Diese Entscheidung kann nicht rückgängig gemacht werden! Trotzdem durchführen?",
       "grant_admin": "Administratorprivilegien gewähren",
       "delete_user": "Nutzer löschen",
       "strip_media": "Medien von Beiträgen entfernen",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index db9625ad..f3f99001 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -32,6 +32,27 @@
     },
     "staff": "Staff"
   },
+  "announcements": {
+    "page_header": "Announcements",
+    "title": "Announcement",
+    "mark_as_read_action": "Mark as read",
+    "post_form_header": "Post announcement",
+    "post_placeholder": "Type your announcement content here...",
+    "post_action": "Post",
+    "post_error": "Error: {error}",
+    "close_error": "Close",
+    "delete_action": "Delete",
+    "start_time_prompt": "Start time: ",
+    "end_time_prompt": "End time: ",
+    "all_day_prompt": "This is an all-day event",
+    "published_time_display": "Published at {time}",
+    "start_time_display": "Starts at {time}",
+    "end_time_display": "Ends at {time}",
+    "edit_action": "Edit",
+    "submit_edit_action": "Submit",
+    "cancel_edit_action": "Cancel",
+    "inactive_message": "This announcement is inactive"
+  },
   "shoutbox": {
     "title": "Shoutbox"
   },
@@ -147,7 +168,8 @@
     "preferences": "Preferences",
     "timelines": "Timelines",
     "chats": "Chats",
-    "lists": "Lists"
+    "lists": "Lists",
+    "announcements": "Announcements"
   },
   "notifications": {
     "broken_favorite": "Unknown status, searching for it…",
@@ -322,6 +344,16 @@
     "mute_import_error": "Error importing mutes",
     "mutes_imported": "Mutes imported! Processing them will take a while.",
     "import_mutes_from_a_csv_file": "Import mutes from a csv file",
+    "account_backup": "Account backup",
+    "account_backup_description": "This allows you to download an archive of your account information and your posts, but they cannot yet be imported into a Pleroma account.",
+    "account_backup_table_head": "Backup",
+    "download_backup": "Download",
+    "backup_not_ready": "This backup is not ready yet.",
+    "remove_backup": "Remove",
+    "list_backups_error": "Error fetching backup list: {error}",
+    "add_backup": "Create a new backup",
+    "added_backup": "Added a new backup.",
+    "add_backup_error": "Error adding a new backup: {error}",
     "blocks_tab": "Blocks",
     "bot": "This is a bot account",
     "btnRadius": "Buttons",
@@ -718,38 +750,26 @@
     }
   },
   "time": {
-    "day": "{0} day",
-    "days": "{0} days",
-    "day_short": "{0}d",
-    "days_short": "{0}d",
-    "hour": "{0} hour",
-    "hours": "{0} hours",
-    "hour_short": "{0}h",
-    "hours_short": "{0}h",
+    "unit": {
+      "days": "{0} day | {0} days",
+      "days_short": "{0}d",
+      "hours": "{0} hour | {0} hours",
+      "hours_short": "{0}h",
+      "minutes": "{0} minute | {0} minutes",
+      "minutes_short": "{0}min",
+      "months": "{0} month | {0} months",
+      "months_short": "{0}mo",
+      "seconds": "{0} second | {0} seconds",
+      "seconds_short": "{0}s",
+      "weeks": "{0} week | {0} weeks",
+      "weeks_short": "{0}w",
+      "years": "{0} year | {0} years",
+      "years_short": "{0}y"
+    },
     "in_future": "in {0}",
     "in_past": "{0} ago",
-    "minute": "{0} minute",
-    "minutes": "{0} minutes",
-    "minute_short": "{0}min",
-    "minutes_short": "{0}min",
-    "month": "{0} month",
-    "months": "{0} months",
-    "month_short": "{0}mo",
-    "months_short": "{0}mo",
     "now": "just now",
-    "now_short": "now",
-    "second": "{0} second",
-    "seconds": "{0} seconds",
-    "second_short": "{0}s",
-    "seconds_short": "{0}s",
-    "week": "{0} week",
-    "weeks": "{0} weeks",
-    "week_short": "{0}w",
-    "weeks_short": "{0}w",
-    "year": "{0} year",
-    "years": "{0} years",
-    "year_short": "{0}y",
-    "years_short": "{0}y"
+    "now_short": "now"
   },
   "timeline": {
     "collapse": "Collapse",
@@ -878,7 +898,7 @@
       "disable_any_subscription": "Disallow following user at all",
       "quarantine": "Disallow user posts from federating",
       "delete_user": "Delete user",
-      "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone."
+      "delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?"
     },
     "highlight": {
       "disabled": "No highlight",
diff --git a/src/i18n/eo.json b/src/i18n/eo.json
index 659b5960..3c401b30 100644
--- a/src/i18n/eo.json
+++ b/src/i18n/eo.json
@@ -606,7 +606,6 @@
     "mention": "Mencio",
     "hidden": "Kaŝita",
     "admin_menu": {
-      "delete_user_confirmation": "Ĉu vi tute certas? Ĉi tiu ago ne estas malfarebla.",
       "delete_user": "Forigi uzanton",
       "quarantine": "Malpermesi federadon de afiŝoj de uzanto",
       "disable_any_subscription": "Malpermesi ĉian abonadon al uzanto",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index eb9fc0a5..9887f007 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -731,8 +731,7 @@
       "disable_remote_subscription": "No permitir que usuarios de instancias remotas te siga",
       "disable_any_subscription": "No permitir que ningún usuario te siga",
       "quarantine": "No permitir publicaciones de usuarios de instancias remotas",
-      "delete_user": "Eliminar usuario",
-      "delete_user_confirmation": "¿Estás completamente seguro? Esta acción no se puede deshacer."
+      "delete_user": "Eliminar usuario"
     },
     "show_repeats": "Mostrar repetidos",
     "hide_repeats": "Ocultar repetidos",
diff --git a/src/i18n/eu.json b/src/i18n/eu.json
index 539ee1bd..4e6ea550 100644
--- a/src/i18n/eu.json
+++ b/src/i18n/eu.json
@@ -609,8 +609,7 @@
       "disable_remote_subscription": "Ez utzi istantzia kanpoko erabiltzaileak zuri jarraitzea",
       "disable_any_subscription": "Ez utzi beste erabiltzaileak zuri jarraitzea",
       "quarantine": "Ez onartu mezuak beste instantzietatik",
-      "delete_user": "Erabiltzailea ezabatu",
-      "delete_user_confirmation": "Erabat ziur zaude? Ekintza hau ezin da desegin."
+      "delete_user": "Erabiltzailea ezabatu"
     }
   },
   "user_profile": {
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index 7b5244cb..f8c3b4ae 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -620,8 +620,7 @@
       "sandbox": "Pakota viestit vain seuraajille",
       "disable_remote_subscription": "Estä seuraaminen ulkopuolisilta sivuilta",
       "quarantine": "Estä käyttäjän viestin federoituminen",
-      "delete_user": "Poista käyttäjä",
-      "delete_user_confirmation": "Oletko aivan varma? Tätä ei voi kumota."
+      "delete_user": "Poista käyttäjä"
     },
     "favorites": "Tykkäykset",
     "mention": "Mainitse",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 6d3c75d1..fae7c7a2 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -659,8 +659,7 @@
       "disable_remote_subscription": "Interdir de s'abonner a l'utilisateur depuis l'instance distante",
       "disable_any_subscription": "Interdir de s'abonner à l'utilisateur tout court",
       "quarantine": "Interdir les statuts de l'utilisateur à fédérer",
-      "delete_user": "Supprimer l'utilisateur",
-      "delete_user_confirmation": "Êtes-vous absolument-sûr⋅e ? Cette action ne peut être annulée."
+      "delete_user": "Supprimer l'utilisateur"
     },
     "mention": "Mention",
     "hidden": "Caché",
diff --git a/src/i18n/he.json b/src/i18n/he.json
index b0c59a30..6c62acc4 100644
--- a/src/i18n/he.json
+++ b/src/i18n/he.json
@@ -347,8 +347,7 @@
       "disable_remote_subscription": "אל תאפשר עקיבה של המשתמש מאינסטנס אחר",
       "disable_any_subscription": "אל תאפשר עקיבה של המשתמש בכלל",
       "quarantine": "אל תאפשר פדרציה של ההודעות של המשתמש",
-      "delete_user": "מחק משתמש",
-      "delete_user_confirmation": "בטוח? פעולה זו הינה בלתי הפיכה."
+      "delete_user": "מחק משתמש"
     }
   },
   "user_profile": {
diff --git a/src/i18n/id.json b/src/i18n/id.json
index fd64b7ae..73cc2a71 100644
--- a/src/i18n/id.json
+++ b/src/i18n/id.json
@@ -327,8 +327,7 @@
       "delete_account": "Hapus akun",
       "force_nsfw": "Tandai semua postingan sebagai NSFW",
       "strip_media": "Hapus media dari postingan-postingan",
-      "delete_user": "Hapus pengguna",
-      "delete_user_confirmation": "Apakah Anda benar-benar yakin? Tindakan ini tidak dapat dibatalkan."
+      "delete_user": "Hapus pengguna"
     },
     "follow_unfollow": "Berhenti mengikuti",
     "followees": "Mengikuti",
diff --git a/src/i18n/it.json b/src/i18n/it.json
index a1ec37a2..c8c74b70 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -485,7 +485,6 @@
     "deny": "Nega",
     "remote_follow": "Segui da remoto",
     "admin_menu": {
-      "delete_user_confirmation": "Ne sei completamente sicuro? Non potrai tornare indietro.",
       "delete_user": "Elimina utente",
       "quarantine": "I messaggi non arriveranno alle altre stanze",
       "disable_any_subscription": "Rendi utente non seguibile",
diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json
index f64943d9..abca262b 100644
--- a/src/i18n/ja_easy.json
+++ b/src/i18n/ja_easy.json
@@ -608,8 +608,7 @@
       "disable_remote_subscription": "ほかのインスタンスからフォローされないようにする",
       "disable_any_subscription": "フォローされないようにする",
       "quarantine": "ほかのインスタンスのユーザーのとうこうをとめる",
-      "delete_user": "ユーザーをけす",
-      "delete_user_confirmation": "あなたは、ほんとうに、きはたしかですか? これは、とりけすことが、できません。"
+      "delete_user": "ユーザーをけす"
     }
   },
   "user_profile": {
diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json
index cb52dffa..77b270c9 100644
--- a/src/i18n/ja_pedantic.json
+++ b/src/i18n/ja_pedantic.json
@@ -731,8 +731,7 @@
       "disable_remote_subscription": "他のインスタンスからフォローされないようにする",
       "disable_any_subscription": "フォローされないようにする",
       "quarantine": "他のインスタンスからの投稿を止める",
-      "delete_user": "ユーザーを削除",
-      "delete_user_confirmation": "あなたの精神状態に何か問題はございませんか? この操作を取り消すことはできません。"
+      "delete_user": "ユーザーを削除"
     },
     "roles": {
       "moderator": "モデレーター",
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index 977b8eb3..18ed79b7 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -32,6 +32,7 @@ const loaders = {
   pt: () => import('./pt.json'),
   ro: () => import('./ro.json'),
   ru: () => import('./ru.json'),
+  sk: () => import('./sk.json'),
   te: () => import('./te.json'),
   uk: () => import('./uk.json'),
   zh: () => import('./zh.json'),
diff --git a/src/i18n/nb.json b/src/i18n/nb.json
index 5e3e8ef3..1c160afb 100644
--- a/src/i18n/nb.json
+++ b/src/i18n/nb.json
@@ -553,8 +553,7 @@
       "disable_remote_subscription": "Fjern mulighet til å følge brukeren fra andre instanser",
       "disable_any_subscription": "Fjern mulighet til å følge brukeren",
       "quarantine": "Gjør at statuser fra brukeren ikke kan sendes til andre instanser",
-      "delete_user": "Slett bruker",
-      "delete_user_confirmation": "Er du helt sikker? Denne handlingen kan ikke omgjøres."
+      "delete_user": "Slett bruker"
     }
   },
   "user_profile": {
diff --git a/src/i18n/nl.json b/src/i18n/nl.json
index b113ffe4..5c00efc4 100644
--- a/src/i18n/nl.json
+++ b/src/i18n/nl.json
@@ -580,7 +580,6 @@
     "remote_follow": "Volg vanop afstand",
     "statuses": "Statussen",
     "admin_menu": {
-      "delete_user_confirmation": "Weet je het heel zeker? Deze uitvoering kan niet ongedaan worden gemaakt.",
       "delete_user": "Gebruiker verwijderen",
       "quarantine": "Federeren van gebruikers berichten verbieden",
       "disable_any_subscription": "Volgen van gebruiker in zijn geheel verbieden",
diff --git a/src/i18n/oc.json b/src/i18n/oc.json
index 40f48149..556b3d0b 100644
--- a/src/i18n/oc.json
+++ b/src/i18n/oc.json
@@ -501,8 +501,7 @@
       "disable_remote_subscription": "Desactivar lo seguiment d’utilizaire d’instàncias alonhadas",
       "disable_any_subscription": "Desactivar tot seguiment",
       "quarantine": "Defendre la federacion de las publicacions de l’utilizaire",
-      "delete_user": "Suprimir l’utilizaire",
-      "delete_user_confirmation": "Volètz vertadièrament far aquò ? Aquesta accion se pòt pas anullar."
+      "delete_user": "Suprimir l’utilizaire"
     }
   },
   "user_profile": {
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index 304a0349..efebcc83 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -762,8 +762,7 @@
       "disable_remote_subscription": "Zakaż obserwowania użytkownika ze zdalnych instancji",
       "disable_any_subscription": "Zakaż całkowicie obserwowania użytkownika",
       "quarantine": "Zakaż federowania postów od tego użytkownika",
-      "delete_user": "Usuń użytkownika",
-      "delete_user_confirmation": "Czy jesteś absolutnie pewny(-a)? Ta operacja nie może być cofnięta."
+      "delete_user": "Usuń użytkownika"
     },
     "message": "Napisz",
     "edit_profile": "Edytuj profil",
diff --git a/src/i18n/pt.json b/src/i18n/pt.json
index e32a95e4..b997701c 100644
--- a/src/i18n/pt.json
+++ b/src/i18n/pt.json
@@ -594,7 +594,6 @@
     "unmute_progress": "A retirar silêncio…",
     "mute_progress": "A silenciar…",
     "admin_menu": {
-      "delete_user_confirmation": "Tens a certeza? Esta ação não pode ser revertida.",
       "delete_user": "Eliminar utilizador",
       "quarantine": "Não permitir publicações de utilizadores de instâncias remotas",
       "disable_any_subscription": "Não permitir que nenhum utilizador te siga",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index ba0cec28..7e6ff3f5 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -576,8 +576,7 @@
       "disable_remote_subscription": "Запретить читать с других узлов",
       "disable_any_subscription": "Запретить читать пользователя",
       "quarantine": "Не федерировать статусы пользователя",
-      "delete_user": "Удалить пользователя",
-      "delete_user_confirmation": "Вы уверены? Это действие нельзя отменить."
+      "delete_user": "Удалить пользователя"
     },
     "media": "С вложениями",
     "mention": "Упомянуть",
diff --git a/src/i18n/service_worker_messages.js b/src/i18n/service_worker_messages.js
index 270ed043..f691f1c4 100644
--- a/src/i18n/service_worker_messages.js
+++ b/src/i18n/service_worker_messages.js
@@ -27,6 +27,7 @@ const messages = {
   pt: require('../lib/notification-i18n-loader.js!./pt.json'),
   ro: require('../lib/notification-i18n-loader.js!./ro.json'),
   ru: require('../lib/notification-i18n-loader.js!./ru.json'),
+  sk: require('../lib/notification-i18n-loader.js!./sk.json'),
   te: require('../lib/notification-i18n-loader.js!./te.json'),
   zh: require('../lib/notification-i18n-loader.js!./zh.json'),
   en: require('../lib/notification-i18n-loader.js!./en.json')
diff --git a/src/i18n/sk.json b/src/i18n/sk.json
new file mode 100644
index 00000000..cee76f5e
--- /dev/null
+++ b/src/i18n/sk.json
@@ -0,0 +1,512 @@
+{
+  "about": {
+    "mrf": {
+      "federation": "Federácia",
+      "keyword": {
+        "keyword_policies": "Pravidlá pre kľúčové slová",
+        "ftl_removal": "Odstránenie z časovej osy \"Celej známej siete\"",
+        "reject": "Odmietni",
+        "replace": "Nahraď",
+        "is_replaced_by": "→"
+      },
+      "mrf_policies": "Povoliť MRF pravidlá",
+      "mrf_policies_desc": "MRF pravidlá upravujú správanie servera v rámci federácie s inými.  Nasledovné pravidlá sú aktívne:",
+      "simple": {
+        "simple_policies": "Pravidlá špecifické pre tento server",
+        "instance": "Server",
+        "reason": "Dôvod",
+        "not_applicable": "N/A",
+        "accept": "Prijať",
+        "accept_desc": "Tento server preberá správy len z nasledovných serverov:",
+        "reject": "Odmietnuť",
+        "reject_desc": "Tento server preberá správy spravy z nasledovných serverov:",
+        "quarantine": "Karanténa",
+        "quarantine_desc": "Tento server posiela verejné oznamy len na nasledovné servre:",
+        "ftl_removal": "Odstránenie časovej osy \"Známa sieť\"",
+        "ftl_removal_desc": "Tento server odstraňuje nasledovné serverov zo svojej časovej osy \"Známa sieť\":",
+        "media_removal": "Odstránenie médií",
+        "media_removal_desc": "Tento server odstraňuje médiá zo správ nasledovných serverov:",
+        "media_nsfw": "Označenie médií ako citlivých",
+        "media_nsfw_desc": "Tento server označuje média ako citlivé v správach z nasledovných serverov:"
+      }
+    },
+    "staff": "Personál"
+  },
+  "shoutbox": {
+    "title": "Verejné fórum"
+  },
+  "domain_mute_card": {
+    "mute": "Utíš",
+    "mute_progress": "Utišujem…",
+    "unmute": "Povoľ oznamy",
+    "unmute_progress": "Povoľujem oznamy…"
+  },
+  "exporter": {
+    "export": "Export",
+    "processing": "Spracováva sa, čoskoro sa ti ponúknu na stiahnutie súbory s dátami exportu"
+  },
+  "features_panel": {
+    "shout": "Verejné fórum",
+    "pleroma_chat_messages": "Pleroma Chat",
+    "gopher": "Gopher",
+    "media_proxy": "Proxy pre médiá",
+    "scope_options": "Nastavenia rámca",
+    "text_limit": "Limit počtu znakov",
+    "title": "Vlastnosti",
+    "who_to_follow": "Koho nasledovať",
+    "upload_limit": "Limit nahrávania"
+  },
+  "finder": {
+    "error_fetching_user": "Chyba načítavania užívateľa",
+    "find_user": "Nájsť užívateľa"
+  },
+  "general": {
+    "apply": "Použiť",
+    "submit": "Odoslať",
+    "more": "Viac",
+    "loading": "Nahrávam…",
+    "generic_error": "Nastala chyba",
+    "error_retry": "Zopakuj znova, prosím",
+    "retry": "Zopakuj znova",
+    "optional": "nepovinné",
+    "show_more": "Zobraz viac",
+    "show_less": "Zobraz menej",
+    "dismiss": "Zahoď",
+    "cancel": "Zruš",
+    "disable": "Vypni",
+    "enable": "Zapni",
+    "confirm": "Potvrdiť",
+    "verify": "Overiť",
+    "close": "Zatvoriť",
+    "peek": "Vybrať",
+    "role": {
+      "admin": "Správca",
+      "moderator": "Moderátor"
+    },
+    "flash_content": "Klikni pre zobrazenie Flash obsahu prostredníctvom Ruffle (experimentálne, nemusí fungovať).",
+    "flash_security": "Flash obsah je potencionálne nebezpečný, keďže je to produkt s uzatvoreným kódom.",
+    "flash_fail": "Nepodarilo sa nahrať Flash obsah, pre detaily pozri konzolu prehliadača.",
+    "scope_in_timeline": {
+      "direct": "Priame",
+      "private": "Len pre nasledovníkov",
+      "public": "Verejné",
+      "unlisted": "Nezaradené"
+    }
+  },
+  "image_cropper": {
+    "crop_picture": "Orezať obrázok",
+    "save": "Uložiť",
+    "save_without_cropping": "Ulož bez orezania",
+    "cancel": "Zrušiť"
+  },
+  "importer": {
+    "submit": "Odoslať",
+    "success": "Úspečne naimportované.",
+    "error": "Pri importe súboru nastala chyba."
+  },
+  "login": {
+    "login": "Prihlásiť sa",
+    "description": "Prihlásiť pomocou OAuth",
+    "logout": "Odhlásiť sa",
+    "password": "Heslo",
+    "placeholder": "napr. peter",
+    "register": "Registrácia",
+    "username": "Meno užívateľa",
+    "hint": "Prihlás sa, aby si sa mohol zúčastniť konverzácie",
+    "authentication_code": "Autentifikačný kód",
+    "enter_recovery_code": "Zadaj kód obnovenia",
+    "enter_two_factor_code": "Zadaj 2-fázový validačný kód",
+    "recovery_code": "Kód obnovenia",
+    "heading": {
+      "totp": "2-fázové overenie",
+      "recovery": "2-fázové obnova"
+    }
+  },
+  "media_modal": {
+    "previous": "Predchádzajúce",
+    "next": "Nasledujúce",
+    "counter": "{current} / {total}",
+    "hide": "Zatvoriť prehliadač médií"
+  },
+  "nav": {
+    "about": "O stránke",
+    "administration": "Administrácia",
+    "back": "Späť",
+    "friend_requests": "Žiadosti o priateľstvo",
+    "mentions": "Zmienky",
+    "interactions": "Interakcie",
+    "dms": "Priame správy",
+    "public_tl": "Verejná časová os",
+    "timeline": "Časová os",
+    "home_timeline": "Domáca časová os",
+    "twkn": "Známa sieť",
+    "bookmarks": "Záložky",
+    "user_search": "Hľadanie užívateľa",
+    "search": "Hladať",
+    "who_to_follow": "Koho nasledovať",
+    "preferences": "Nastavenia",
+    "timelines": "Časové osy",
+    "chats": "Chaty"
+  },
+  "notifications": {
+    "broken_favorite": "Neznáma správa, dohľadávam ju…",
+    "error": "Chyba získavania upozornení: {0}",
+    "favorited_you": "si obľúbil tvoju správu",
+    "followed_you": "ťa nasleduje",
+    "follow_request": "ťa chce nasledovať",
+    "load_older": "Nahrať staršie upozornenia",
+    "notifications": "Upozornenia",
+    "read": "Prečítané!",
+    "repeated_you": "zopakoval tvoju správu",
+    "no_more_notifications": "Žiadne ďalšie upozornenia",
+    "migrated_to": "sa presťahoval na",
+    "reacted_with": "reagoval nasledovne {0}"
+  },
+  "polls": {
+    "add_poll": "Pridať anketu",
+    "add_option": "Pridať možnosť",
+    "option": "Možnosť",
+    "votes": "hlasy",
+    "people_voted_count": "{count} volič | {count} voličov",
+    "votes_count": "{count} hlas | {count} hlasov",
+    "vote": "Hlas",
+    "type": "Typ ankety",
+    "single_choice": "Výber jednej možnosti",
+    "multiple_choices": "Výber viacerých možností",
+    "expiry": "Vek ankety",
+    "expires_in": "Anketa končí za {0}",
+    "expired": "Anketa skončila pre {0}",
+    "not_enough_options": "Príliš málo jedinečných možností v ankete"
+  },
+  "emoji": {
+    "stickers": "Nálepka",
+    "emoji": "Emotikon",
+    "keep_open": "Ponechaj okno výberu otvorené",
+    "search_emoji": "Vyhladať emotikon",
+    "add_emoji": "Vložiť emotikon",
+    "custom": "Vlastný emotikon",
+    "unicode": "Unicode emotikon",
+    "load_all_hint": "Nahralo sa prvých {saneAmount} emotikonov, nahranie všetkých by mohlo spôsobiť zníženie výkonu.",
+    "load_all": "Nahrať všetkých {emojiAmount} emotikonov"
+  },
+  "errors": {
+    "storage_unavailable": "Pleroma nemôže používať úložisko prehliadača. Tvoje prihlasovacie meno a lokálne nastavenia nebudú uchované a môžu sa vyskytnúť neočakávané chyby. Skús povoliť cookie."
+  },
+  "interactions": {
+    "favs_repeats": "Zopakovania a obľúbené",
+    "follows": "Nový nasledovatelia",
+    "moves": "Užívateľ sa sťahuje",
+    "load_older": "Nahrať staršiu komunikáciu"
+  },
+  "post_status": {
+    "new_status": "Poslať novú správu",
+    "account_not_locked_warning": "Tvoj účen nie je {0}. Ktokoľvek ťa môže začať nasledovať a tak vidieť správy určené len pre nasledovateľov.",
+    "account_not_locked_warning_link": "uzamknuté",
+    "attachments_sensitive": "Označiť prílohy ako citlivé",
+    "media_description": "Popis média",
+    "content_type": {
+      "text/plain": "Obyčajný text",
+      "text/html": "HTML",
+      "text/markdown": "Markdown",
+      "text/bbcode": "BBCode"
+    },
+    "content_warning": "Nadpis (nepovinné)",
+    "default": "Práve som ...",
+    "direct_warning_to_all": "Túto správu bude vidieť každý užívateľ, ktorého v nej spomenieš.",
+    "direct_warning_to_first_only": "Táto správa bude viditeľná len pre užívateľov, ktorých vymenuješ na začiatku správy.",
+    "posting": "Posielanie",
+    "post": "Poslať",
+    "preview": "Náhľad",
+    "preview_empty": "Prázdne",
+    "empty_status_error": "Nie je možné odoslať prázdnu správu bez priložených súborov",
+    "media_description_error": "Nepodarilo sa aktualizovať média, skús znova",
+    "scope_notice": {
+      "public": "Túto správu bude vidieť každý",
+      "private": "Túto správu budú vidieť len tvoji nasledovníci",
+      "unlisted": "Táto správa nebude viditeľná na verejnej časovej osi a v celej známej sieti"
+    },
+    "scope": {
+      "direct": "Priama správa - zobrazí sa len užívateľom spomenutým v správe",
+      "private": "Pre nasledovníkov - zobrazí sa len tvojim nasledovníkom",
+      "public": "Verejné - zobrazí sa vo všetkých časových osiach",
+      "unlisted": "Nezaradené - nezobrazí sa v žiadnej časovej osy"
+    }
+  },
+  "registration": {
+    "bio": "Životopis",
+    "email": "Email",
+    "fullname": "Zobrazované meno",
+    "password_confirm": "Potvrdenie hesla",
+    "registration": "Registrácia",
+    "token": "Pozývací kód",
+    "captcha": "CAPTCHA",
+    "new_captcha": "Klikni na obrázok a vnikne nová captcha",
+    "username_placeholder": "napr. peter",
+    "fullname_placeholder": "napr. Peter Kukurica",
+    "bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.",
+    "reason": "Dôvod registrácie",
+    "reason_placeholder": "Tento server schvaľuje registrácie manuálne.\nZanechaj správcom dôvod, prečo máš záujem vytvoriť si tu účet.",
+    "register": "Registrácia",
+    "validations": {
+      "username_required": "nemôže byť prázdne",
+      "fullname_required": "nemôže byť prázdne",
+      "email_required": "nemôže byť prázdne",
+      "password_required": "nemôže byť prázdne",
+      "password_confirmation_required": "nemôže byť prázdne",
+      "password_confirmation_match": "musí byť rovnaké ako heslo"
+    }
+  },
+  "remote_user_resolver": {
+    "remote_user_resolver": "Vzdialené overenie užívateľa",
+    "searching_for": "Hľadám...",
+    "error": "Nenájdené."
+  },
+  "selectable_list": {
+    "select_all": "Vybrať všetko"
+  },
+  "time": {
+    "day": "{0} deň",
+    "days": "{0} dní",
+    "day_short": "{0}d",
+    "days_short": "{0}d",
+    "hour": "{0} hodina",
+    "hours": "{0} hodín",
+    "hour_short": "{0}h",
+    "hours_short": "{0}h",
+    "in_future": "za {0}",
+    "in_past": "pred {0}",
+    "minute": "{0} minúta",
+    "minutes": "{0} minút",
+    "minute_short": "{0}min",
+    "minutes_short": "{0}min",
+    "month": "{0} mesiac",
+    "months": "{0} mesiacov",
+    "month_short": "{0}mes",
+    "months_short": "{0}mes",
+    "now": "práve teraz",
+    "now_short": "teraz",
+    "second": "{0} sekunda",
+    "seconds": "{0} sekúnd",
+    "second_short": "{0}s",
+    "seconds_short": "{0}s",
+    "week": "{0} týždeň",
+    "weeks": "{0} týždňov",
+    "week_short": "{0}t",
+    "weeks_short": "{0}t",
+    "year": "{0} rok",
+    "years": "{0} rokov",
+    "year_short": "{0}r",
+    "years_short": "{0}r"
+  },
+  "timeline": {
+    "collapse": "Zbaliť",
+    "conversation": "Konverzácia",
+    "error": "Chyba pri nahrávaní časovej správy: {0}",
+    "load_older": "Nahrať staršie správy",
+    "no_retweet_hint": "Správa je označená ako len-pre-nasledovateľov alebo ako priama a nemôže byť zopakovaná na tvojej časovej osy.",
+    "repeated": "zopakované",
+    "show_new": "Zobraziť nové",
+    "reload": "Znovu nahrať",
+    "up_to_date": "Aktuálne",
+    "no_more_statuses": "Žiadne ďalšie správy",
+    "no_statuses": "Žiadne správy",
+    "socket_reconnected": "Prepojenie v reálnom čase bolo úspešne vytvorené",
+    "socket_broke": "Strata prepojenia v reálnom čase: chyba CloseEvent kód {0}"
+  },
+  "status": {
+    "favorites": "Obľúbené",
+    "repeats": "Opakovania",
+    "delete": "Zmazať správu",
+    "pin": "Pripnúť na stránku užívateľa",
+    "unpin": "Odopnúť zo stránky užívateľa",
+    "pinned": "Pripnuté",
+    "bookmark": "Vytvoriť záložku",
+    "unbookmark": "Zmazať záložku",
+    "delete_confirm": "Skutočne chceš zmazať túto správu?",
+    "reply_to": "Odpovedať komu",
+    "mentions": "Spomenutia",
+    "replies_list": "Odpovede:",
+    "replies_list_with_others": "Odpoveď (+{numReplies} iný): | Odpoveď (+{numReplies} iných):",
+    "mute_conversation": "Stíšiť konverzáciu",
+    "unmute_conversation": "Oznamovať konverzáciu",
+    "status_unavailable": "Neznámy status",
+    "copy_link": "Skopírovať odkaz do správy",
+    "external_source": "Vzdialený zdroj",
+    "thread_muted": "Konverzácia stíšená",
+    "thread_muted_and_words": ", má slová:",
+    "show_full_subject": "Zobraziť celý nadpis",
+    "hide_full_subject": "Skry celý nadpis",
+    "show_content": "Zobraziť obsah",
+    "hide_content": "Skryť obsah",
+    "status_deleted": "Táto správa bola zmazaná",
+    "nsfw": "NSFW",
+    "expand": "Rozbaliť správu",
+    "you": "(ty)",
+    "plus_more": "+{number} ďalších",
+    "many_attachments": "Správa má {number} príloh",
+    "collapse_attachments": "Zabaliť médiá",
+    "show_all_attachments": "Zobraz všetky prílohy",
+    "show_attachment_in_modal": "Zobraz médiá modálne",
+    "show_attachment_description": "Náhľad popisku (otvor prílohu pre zobrazenie celého popisku)",
+    "hide_attachment": "Skryť prílohy",
+    "remove_attachment": "Odstrániť prílohy",
+    "attachment_stop_flash": "Zastaviť prehrávač Flashu",
+    "move_up": "Presuň prílohu doľava",
+    "move_down": "Presuň prílohu doprava",
+    "open_gallery": "Otvoriť galériu",
+    "thread_hide": "Skry túto konverzáciu",
+    "thread_show": "Zobraz túto konverzáciu",
+    "thread_show_full": "Zobraz všetko pod touto konverzáciou (celkovo {numStatus} správa, max hĺbka {depth}) | Zobraz všetko pod touto konverzáciou (celkovo {numStatus} správ, max hĺbka {depth})",
+    "thread_show_full_with_icon": "{icon} {text}",
+    "thread_follow": "Zobraz zvyšnú časť tejto konverzácie (celkovo {numStatus} správa) | Zobraz zvyšnú časť tejto konverzácie (celkovo {numStatus} správ)",
+    "thread_follow_with_icon": "{icon} {text}",
+    "ancestor_follow": "Pozri {numReplies} ďalšiu odpoveď pod touto správou | Pozri {numReplies} ďalších odpovedí pod touto správou",
+    "ancestor_follow_with_icon": "{icon} {text}",
+    "show_all_conversation_with_icon": "{icon} {text}",
+    "show_all_conversation": "Zobraz celú konverzáciu ({numStatus} iná správa) | Zobraz celú konverzáciu ({numStatus} iných správ)",
+    "show_only_conversation_under_this": "Zobraz len správy súvisiace s touto správou"
+  },
+  "user_card": {
+    "approve": "Schváliť",
+    "block": "Zablokovať",
+    "blocked": "Blokované!",
+    "deactivated": "Neaktívne",
+    "deny": "Zakázané",
+    "edit_profile": "Uraviť profil",
+    "favorites": "Obľúbené",
+    "follow": "Nasledovať",
+    "follow_cancel": "Požiadavka zrušená",
+    "follow_sent": "Požiadavka zaslaná!",
+    "follow_progress": "Žiadam o povolenie…",
+    "follow_unfollow": "Prestať sledovať",
+    "followees": "Nasleduje",
+    "followers": "Nasledovatelia",
+    "following": "Nasleduješ!",
+    "follows_you": "Nasleduje teba!",
+    "hidden": "Skryté",
+    "its_you": "To si ty!",
+    "media": "Média",
+    "mention": "Spomenul",
+    "message": "Správa",
+    "mute": "Stíšiť",
+    "muted": "Stíšené",
+    "per_day": "za deň",
+    "remote_follow": "Nasledovanie z ďaleka",
+    "report": "Nahlásiť",
+    "statuses": "Vytvorených správ",
+    "subscribe": "Prihlásiť k odberu",
+    "unsubscribe": "Odhlásiť z odberu",
+    "unblock": "Odblokovať",
+    "unblock_progress": "Oblokováva sa…",
+    "block_progress": "Blokujem…",
+    "unmute": "Povoliť oznamy",
+    "unmute_progress": "Povoľujem oznamy…",
+    "mute_progress": "Stišujem…",
+    "hide_repeats": "Skry zopakovania",
+    "show_repeats": "Zobraz zopakovania",
+    "bot": "Robot",
+    "admin_menu": {
+      "moderation": "Moderovanie",
+      "grant_admin": "Povoliť spravovanie",
+      "revoke_admin": "Zakázať spravovanie",
+      "grant_moderator": "Povoliť moderovanie",
+      "revoke_moderator": "Zakázať moderovanie",
+      "activate_account": "Aktivovať účet",
+      "deactivate_account": "Deaktivovať účet",
+      "delete_account": "Zmazať účet",
+      "force_nsfw": "Označ všetky správy ako NSFW",
+      "strip_media": "Odstrániť média zo správy",
+      "force_unlisted": "Vynúť, aby správy neboli zobrazované",
+      "sandbox": "Vynúť, aby správy boli len pre nasledovateľov",
+      "disable_remote_subscription": "Odstrániť prístup k serveru nasledovnému vzdialenému užívateľovi",
+      "disable_any_subscription": "Zakázať nasledovanie užívateľov",
+      "quarantine": "Zakázať federáciu správ užívateľa",
+      "delete_user": "Zmazať užívateľa",
+      "delete_user_confirmation": "Si si úplne istý? Táto akcia sa nedá zobrať späť."
+    },
+    "highlight": {
+      "disabled": "Bez zvýraznenia",
+      "solid": "Jednoliate pozadie",
+      "striped": "Šrafované pozadie",
+      "side": "Pásik na boku"
+    }
+  },
+  "user_profile": {
+    "timeline_title": "Časová os užívateľa",
+    "profile_does_not_exist": "Prepáč, tento profil neexistuje.",
+    "profile_loading_error": "Prepáč, nastala chyba pri nahrávaní profilu."
+  },
+  "user_reporting": {
+    "title": "Nahlásení {0}",
+    "add_comment_description": "Hlásnenie bude zaslané moderátorom servera. Nižšie môžeš napísať dôvod prečo tento účet nahlasuješ:",
+    "additional_comments": "Ďalšie poznámky",
+    "forward_description": "Účet je z iného servera. Poslať kópiu tohto hlásenia aj tam?",
+    "forward_to": "Preposlať komu {0}",
+    "submit": "Odoslať",
+    "generic_error": "Nastala chyba pri vykonaní tvojej požiadavky."
+  },
+  "who_to_follow": {
+    "more": "Viac",
+    "who_to_follow": "Koho nasledovať"
+  },
+  "tool_tip": {
+    "media_upload": "Nahrať médium",
+    "repeat": "Zopakovať",
+    "reply": "Odpovedať",
+    "favorite": "Obľúbené",
+    "add_reaction": "Reagovať",
+    "user_settings": "Nastavenia užívateľa",
+    "accept_follow_request": "Prijať požiadavku nasledovníka",
+    "reject_follow_request": "Odmietnuť požiadavku nasledovníka",
+    "bookmark": "Záložka"
+  },
+  "upload": {
+    "error": {
+      "base": "Nahrávanie bolo neúspešné.",
+      "message": "Nahrávanie bolo neúspešné: {0}",
+      "file_too_big": "Súbor je príliš veľký [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+      "default": "Vyskúšaj opäť neskôr"
+    }
+  },
+  "search": {
+    "people": "Ľudia",
+    "hashtags": "Haštagy",
+    "person_talking": "{count} človek hovorí",
+    "people_talking": "{count} ľudí hovorí",
+    "no_results": "Žiadne výsledky"
+  },
+  "password_reset": {
+    "forgot_password": "Zabudol si heslo?",
+    "password_reset": "Obnovenie hesla",
+    "instruction": "Zadaj svoju emailovú adresu alebo užívateľské meno. Pošleme ti odkaz pomocou, ktorého môžeš obnoviť svoje heslo.",
+    "placeholder": "Tvoj email alebo užívateľské meno",
+    "check_email": "V novom emaile ti bol doručený odkaz na spôsob, ako obnovíš svoje heslo.",
+    "return_home": "Návrat na domácu stránku",
+    "too_many_requests": "Prekročil si limit pokusov, skús znova neskôr.",
+    "password_reset_disabled": "Obnova hesla je vypnutá. Kontaktuj, prosím, správcu tohto servera.",
+    "password_reset_required": "Musíš najskôr obnoviť heslo, ak sa chceš prihlásiť.",
+    "password_reset_required_but_mailer_is_disabled": "Musíš obnoviť svoje heslo, ale obnova hesla je na serveri vypnutá. Kontaktuj, prosím, správcu tohto servera."
+  },
+  "chats": {
+    "you": "Ty:",
+    "message_user": "Správa {nickname}",
+    "delete": "Zmazať",
+    "chats": "Rozhovor",
+    "new": "Nový rozhovor",
+    "empty_message_error": "Nie je možné odoslať prázdnu správu",
+    "more": "Viac",
+    "delete_confirm": "Skutočne chceš zmazať túto správu?",
+    "error_loading_chat": "Nastala chyba pri nahrávaní rozhovoru.",
+    "error_sending_message": "Nastala chyba pri odosielaní správ.",
+    "empty_chat_list_placeholder": "Nemáš za sebou žiadne rozhovory. Začni nový rozhovor!"
+  },
+  "file_type": {
+    "audio": "Audio",
+    "video": "Video",
+    "image": "Obrázok",
+    "file": "Súbor"
+  },
+  "display_date": {
+    "today": "Dnes"
+  }
+}
diff --git a/src/i18n/uk.json b/src/i18n/uk.json
index d9833087..4e62b4a9 100644
--- a/src/i18n/uk.json
+++ b/src/i18n/uk.json
@@ -755,7 +755,6 @@
       "deactivate_account": "Деактивувати обліковий запис",
       "delete_account": "Видалити обліковий запис",
       "moderation": "Модерація",
-      "delete_user_confirmation": "Ви абсолютно впевнені? Цю дію неможливо буде скасовувати.",
       "delete_user": "Видалити обліковий запис",
       "strip_media": "Вилучити медіа з дописів користувача",
       "force_nsfw": "Позначити всі дописи як NSFW",
diff --git a/src/i18n/vi.json b/src/i18n/vi.json
index c77ad4ca..fd7ae25c 100644
--- a/src/i18n/vi.json
+++ b/src/i18n/vi.json
@@ -772,8 +772,7 @@
       "quarantine": "Không cho phép tút liên hợp",
       "delete_user": "Xóa người dùng",
       "revoke_moderator": "Gỡ bỏ Quản trị viên",
-      "force_unlisted": "Đánh dấu tất cả tút là hạn chế",
-      "delete_user_confirmation": "Bạn chắc chắn chưa? Hành động này không thể phục hồi."
+      "force_unlisted": "Đánh dấu tất cả tút là hạn chế"
     },
     "highlight": {
       "disabled": "Không nổi bật",
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index abba4be9..dd0e6827 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -714,8 +714,7 @@
       "disable_remote_subscription": "禁止从远程实例关注用户",
       "disable_any_subscription": "完全禁止关注用户",
       "quarantine": "从联合实例中禁止用户帖子",
-      "delete_user": "删除用户",
-      "delete_user_confirmation": "你确定吗?此操作无法撤销。"
+      "delete_user": "删除用户"
     },
     "hidden": "已隐藏",
     "show_repeats": "显示转发",
diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json
index 2c4dc3fb..6f0f63b5 100644
--- a/src/i18n/zh_Hant.json
+++ b/src/i18n/zh_Hant.json
@@ -747,7 +747,6 @@
     "admin_menu": {
       "delete_account": "刪除賬號",
       "delete_user": "刪除用戶",
-      "delete_user_confirmation": "你確認嗎?此操作無法撤銷。",
       "moderation": "調停",
       "grant_admin": "賦予管理權限",
       "revoke_admin": "撤銷管理權限",
diff --git a/src/main.js b/src/main.js
index 7d2c82cb..33666308 100644
--- a/src/main.js
+++ b/src/main.js
@@ -20,6 +20,7 @@ import reportsModule from './modules/reports.js'
 import pollsModule from './modules/polls.js'
 import postStatusModule from './modules/postStatus.js'
 import chatsModule from './modules/chats.js'
+import announcementsModule from './modules/announcements.js'
 
 import { createI18n } from 'vue-i18n'
 
@@ -83,7 +84,8 @@ const persistedStateOptions = {
       reports: reportsModule,
       polls: pollsModule,
       postStatus: postStatusModule,
-      chats: chatsModule
+      chats: chatsModule,
+      announcements: announcementsModule
     },
     plugins,
     strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/announcements.js b/src/modules/announcements.js
new file mode 100644
index 00000000..0f8b6d09
--- /dev/null
+++ b/src/modules/announcements.js
@@ -0,0 +1,118 @@
+const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5
+
+export const defaultState = {
+  announcements: [],
+  fetchAnnouncementsTimer: undefined
+}
+
+export const mutations = {
+  setAnnouncements (state, announcements) {
+    state.announcements = announcements
+  },
+  setAnnouncementRead (state, { id, read }) {
+    const index = state.announcements.findIndex(a => a.id === id)
+
+    if (index < 0) {
+      return
+    }
+
+    state.announcements[index].read = read
+  },
+  setFetchAnnouncementsTimer (state, timer) {
+    state.fetchAnnouncementsTimer = timer
+  }
+}
+
+export const getters = {
+  unreadAnnouncementCount (state, _getters, rootState) {
+    if (!rootState.users.currentUser) {
+      return 0
+    }
+
+    const unread = state.announcements.filter(announcement => !(announcement.inactive || announcement.read))
+    return unread.length
+  }
+}
+
+const announcements = {
+  state: defaultState,
+  mutations,
+  getters,
+  actions: {
+    fetchAnnouncements (store) {
+      const currentUser = store.rootState.users.currentUser
+      const isAdmin = currentUser && currentUser.role === 'admin'
+
+      const getAnnouncements = async () => {
+        if (!isAdmin) {
+          return store.rootState.api.backendInteractor.fetchAnnouncements()
+        }
+
+        const all = await store.rootState.api.backendInteractor.adminFetchAnnouncements()
+        const visible = await store.rootState.api.backendInteractor.fetchAnnouncements()
+        const visibleObject = visible.reduce((a, c) => {
+          a[c.id] = c
+          return a
+        }, {})
+        const getWithinVisible = announcement => visibleObject[announcement.id]
+
+        all.forEach(announcement => {
+          const visibleAnnouncement = getWithinVisible(announcement)
+          if (!visibleAnnouncement) {
+            announcement.inactive = true
+          } else {
+            announcement.read = visibleAnnouncement.read
+          }
+        })
+
+        return all
+      }
+
+      return getAnnouncements()
+        .then(announcements => {
+          store.commit('setAnnouncements', announcements)
+        })
+    },
+    markAnnouncementAsRead (store, id) {
+      return store.rootState.api.backendInteractor.dismissAnnouncement({ id })
+        .then(() => {
+          store.commit('setAnnouncementRead', { id, read: true })
+        })
+    },
+    startFetchingAnnouncements (store) {
+      if (store.state.fetchAnnouncementsTimer) {
+        return
+      }
+
+      const interval = setInterval(() => store.dispatch('fetchAnnouncements'), FETCH_ANNOUNCEMENT_INTERVAL_MS)
+      store.commit('setFetchAnnouncementsTimer', interval)
+
+      return store.dispatch('fetchAnnouncements')
+    },
+    stopFetchingAnnouncements (store) {
+      const interval = store.state.fetchAnnouncementsTimer
+      store.commit('setFetchAnnouncementsTimer', undefined)
+      clearInterval(interval)
+    },
+    postAnnouncement (store, { content, startsAt, endsAt, allDay }) {
+      return store.rootState.api.backendInteractor.postAnnouncement({ content, startsAt, endsAt, allDay })
+        .then(() => {
+          return store.dispatch('fetchAnnouncements')
+        })
+    },
+    editAnnouncement (store, { id, content, startsAt, endsAt, allDay }) {
+      return store.rootState.api.backendInteractor.editAnnouncement({ id, content, startsAt, endsAt, allDay })
+        .then(() => {
+          return store.dispatch('fetchAnnouncements')
+        })
+    },
+    deleteAnnouncement (store, id) {
+      return store.rootState.api.backendInteractor.deleteAnnouncement({ id })
+        .then(() => {
+          return store.dispatch('fetchAnnouncements')
+        })
+    }
+  }
+}
+
+export default announcements
diff --git a/src/modules/api.js b/src/modules/api.js
index e9bf8c46..0521db5d 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -163,6 +163,7 @@ const api = {
                 dispatch('startFetchingTimeline', { timeline: 'friends' })
                 dispatch('startFetchingNotifications')
                 dispatch('startFetchingChats')
+                dispatch('startFetchingAnnouncements')
                 dispatch('pushGlobalNotice', {
                   level: 'error',
                   messageKey: 'timeline.socket_broke',
@@ -261,6 +262,18 @@ const api = {
       store.commit('removeFetcher', { fetcherName: 'lists', fetcher })
     },
 
+    // Lists
+    startFetchingAnnouncements (store) {
+      if (store.state.fetchers['announcements']) return
+      const fetcher = store.state.backendInteractor.startFetchingAnnouncements({ store })
+      store.commit('addFetcher', { fetcherName: 'announcements', fetcher })
+    },
+    stopFetchingAnnouncements (store) {
+      const fetcher = store.state.fetchers.announcements
+      if (!fetcher) return
+      store.commit('removeFetcher', { fetcherName: 'announcements', fetcher })
+    },
+
     // Pleroma websocket
     setWsToken (store, token) {
       store.commit('setWsToken', token)
diff --git a/src/services/announcements_fetcher/announcements_fetcher.service.js b/src/services/announcements_fetcher/announcements_fetcher.service.js
new file mode 100644
index 00000000..6701e25a
--- /dev/null
+++ b/src/services/announcements_fetcher/announcements_fetcher.service.js
@@ -0,0 +1,22 @@
+import apiService from '../api/api.service.js'
+import { promiseInterval } from '../promise_interval/promise_interval.js'
+
+const fetchAndUpdate = ({ store, credentials }) => {
+  return apiService.fetchAnnouncements({ credentials })
+    .then(announcements => {
+      store.commit('setAnnouncements', announcements)
+    }, () => {})
+    .catch(() => {})
+}
+
+const startFetching = ({ credentials, store }) => {
+  const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
+  boundFetchAndUpdate()
+  return promiseInterval(boundFetchAndUpdate, 60000)
+}
+
+const announcementsFetcher = {
+  startFetching
+}
+
+export default announcementsFetcher
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 8cf2a763..a98ea7f7 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -87,6 +87,8 @@ const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
 const MASTODON_LISTS_URL = '/api/v1/lists'
 const MASTODON_STREAMING = '/api/v1/streaming'
 const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
+const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements'
+const MASTODON_ANNOUNCEMENTS_DISMISS_URL = id => `/api/v1/announcements/${id}/dismiss`
 const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
 const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
 const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
@@ -95,6 +97,11 @@ const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
 const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
 const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
 const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
+const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
+const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
+const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
+const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
+const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
 
 const oldfetch = window.fetch
 
@@ -1054,6 +1061,25 @@ const fetchBlocks = ({ credentials }) => {
     .then((users) => users.map(parseUser))
 }
 
+const addBackup = ({ credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_BACKUP_URL,
+    method: 'POST',
+    credentials
+  })
+}
+
+const listBackups = ({ credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_BACKUP_URL,
+    method: 'GET',
+    credentials,
+    params: {
+      _cacheBooster: (new Date()).getTime()
+    }
+  })
+}
+
 const fetchOAuthTokens = ({ credentials }) => {
   const url = '/api/oauth_tokens.json'
 
@@ -1271,6 +1297,66 @@ const dismissNotification = ({ credentials, id }) => {
   })
 }
 
+const adminFetchAnnouncements = ({ credentials }) => {
+  return promisedRequest({ url: PLEROMA_ANNOUNCEMENTS_URL, credentials })
+}
+
+const fetchAnnouncements = ({ credentials }) => {
+  return promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials })
+}
+
+const dismissAnnouncement = ({ id, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id),
+    credentials,
+    method: 'POST'
+  })
+}
+
+const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
+  const payload = { content }
+
+  if (typeof startsAt !== 'undefined') {
+    payload['starts_at'] = startsAt ? new Date(startsAt).toISOString() : null
+  }
+
+  if (typeof endsAt !== 'undefined') {
+    payload['ends_at'] = endsAt ? new Date(endsAt).toISOString() : null
+  }
+
+  if (typeof allDay !== 'undefined') {
+    payload['all_day'] = allDay
+  }
+
+  return payload
+}
+
+const postAnnouncement = ({ credentials, content, startsAt, endsAt, allDay }) => {
+  return promisedRequest({
+    url: PLEROMA_POST_ANNOUNCEMENT_URL,
+    credentials,
+    method: 'POST',
+    payload: announcementToPayload({ content, startsAt, endsAt, allDay })
+  })
+}
+
+const editAnnouncement = ({ id, credentials, content, startsAt, endsAt, allDay }) => {
+  return promisedRequest({
+    url: PLEROMA_EDIT_ANNOUNCEMENT_URL(id),
+    credentials,
+    method: 'PATCH',
+    payload: announcementToPayload({ content, startsAt, endsAt, allDay })
+  })
+}
+
+const deleteAnnouncement = ({ id, credentials }) => {
+  return promisedRequest({
+    url: PLEROMA_DELETE_ANNOUNCEMENT_URL(id),
+    credentials,
+    method: 'DELETE'
+  })
+}
+
 export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
   return Object.entries({
     ...(credentials
@@ -1520,6 +1606,8 @@ const apiService = {
   generateMfaBackupCodes,
   mfaSetupOTP,
   mfaConfirmOTP,
+  addBackup,
+  listBackups,
   fetchFollowRequests,
   fetchLists,
   createList,
@@ -1556,7 +1644,13 @@ const apiService = {
   chatMessages,
   sendChatMessage,
   readChat,
-  deleteChatMessage
+  deleteChatMessage,
+  fetchAnnouncements,
+  dismissAnnouncement,
+  postAnnouncement,
+  editAnnouncement,
+  deleteAnnouncement,
+  adminFetchAnnouncements
 }
 
 export default apiService
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 62ee8549..41fa34fc 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -3,6 +3,7 @@ import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js'
 import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
 import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
 import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
+import announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js'
 
 const backendInteractorService = credentials => ({
   startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) {
@@ -29,6 +30,10 @@ const backendInteractorService = credentials => ({
     return listsFetcher.startFetching({ store, credentials })
   },
 
+  startFetchingAnnouncements ({ store }) {
+    return announcementsFetcher.startFetching({ store, credentials })
+  },
+
   startUserSocket ({ store }) {
     const serv = store.rootState.instance.server.replace('http', 'ws')
     const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
diff --git a/src/services/date_utils/date_utils.js b/src/services/date_utils/date_utils.js
index 32e13bca..677c184c 100644
--- a/src/services/date_utils/date_utils.js
+++ b/src/services/date_utils/date_utils.js
@@ -10,31 +10,29 @@ export const relativeTime = (date, nowThreshold = 1) => {
   if (typeof date === 'string') date = Date.parse(date)
   const round = Date.now() > date ? Math.floor : Math.ceil
   const d = Math.abs(Date.now() - date)
-  let r = { num: round(d / YEAR), key: 'time.years' }
+  let r = { num: round(d / YEAR), key: 'time.unit.years' }
   if (d < nowThreshold * SECOND) {
     r.num = 0
     r.key = 'time.now'
   } else if (d < MINUTE) {
     r.num = round(d / SECOND)
-    r.key = 'time.seconds'
+    r.key = 'time.unit.seconds'
   } else if (d < HOUR) {
     r.num = round(d / MINUTE)
-    r.key = 'time.minutes'
+    r.key = 'time.unit.minutes'
   } else if (d < DAY) {
     r.num = round(d / HOUR)
-    r.key = 'time.hours'
+    r.key = 'time.unit.hours'
   } else if (d < WEEK) {
     r.num = round(d / DAY)
-    r.key = 'time.days'
+    r.key = 'time.unit.days'
   } else if (d < MONTH) {
     r.num = round(d / WEEK)
-    r.key = 'time.weeks'
+    r.key = 'time.unit.weeks'
   } else if (d < YEAR) {
     r.num = round(d / MONTH)
-    r.key = 'time.months'
+    r.key = 'time.unit.months'
   }
-  // Remove plural form when singular
-  if (r.num === 1) r.key = r.key.slice(0, -1)
   return r
 }
 
diff --git a/test/unit/specs/services/date_utils/date_utils.spec.js b/test/unit/specs/services/date_utils/date_utils.spec.js
index 2d61dbac..bd1efe81 100644
--- a/test/unit/specs/services/date_utils/date_utils.spec.js
+++ b/test/unit/specs/services/date_utils/date_utils.spec.js
@@ -11,30 +11,30 @@ describe('DateUtils', () => {
 
     it('rounds down for past', () => {
       const time = Date.now() - 1.8 * DateUtils.HOUR
-      expect(DateUtils.relativeTime(time)).to.eql({ num: 1, key: 'time.hour' })
+      expect(DateUtils.relativeTime(time)).to.eql({ num: 1, key: 'time.unit.hours' })
     })
 
     it('rounds up for future', () => {
       const time = Date.now() + 1.8 * DateUtils.HOUR
-      expect(DateUtils.relativeTime(time)).to.eql({ num: 2, key: 'time.hours' })
+      expect(DateUtils.relativeTime(time)).to.eql({ num: 2, key: 'time.unit.hours' })
     })
 
     it('uses plural when necessary', () => {
       const time = Date.now() - 3.8 * DateUtils.WEEK
-      expect(DateUtils.relativeTime(time)).to.eql({ num: 3, key: 'time.weeks' })
+      expect(DateUtils.relativeTime(time)).to.eql({ num: 3, key: 'time.unit.weeks' })
     })
 
     it('works with date string', () => {
       const time = Date.now() - 4 * DateUtils.MONTH
       const dateString = new Date(time).toISOString()
-      expect(DateUtils.relativeTime(dateString)).to.eql({ num: 4, key: 'time.months' })
+      expect(DateUtils.relativeTime(dateString)).to.eql({ num: 4, key: 'time.unit.months' })
     })
   })
 
   describe('relativeTimeShort', () => {
     it('returns the short version of the same relative time', () => {
       const time = Date.now() + 2 * DateUtils.YEAR
-      expect(DateUtils.relativeTimeShort(time)).to.eql({ num: 2, key: 'time.years_short' })
+      expect(DateUtils.relativeTimeShort(time)).to.eql({ num: 2, key: 'time.unit.years_short' })
     })
   })
 })
diff --git a/yarn.lock b/yarn.lock
index e46558e9..ba45ceaf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1132,10 +1132,10 @@
   dependencies:
     "@fortawesome/fontawesome-common-types" "^0.2.36"
 
-"@fortawesome/vue-fontawesome@3.0.0-5":
-  version "3.0.0-5"
-  resolved "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-5.tgz"
-  integrity sha512-aNmBT4bOecrFsZTog1l6AJDQHPP3ocXV+WQ3Ogy8WZCqstB/ahfhH4CPu5i4N9Hw0MBKXqE+LX+NbUxcj8cVTw==
+"@fortawesome/vue-fontawesome@3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.1.tgz#ced35cefc52b364f7db973f2fe9f50c3dd160715"
+  integrity sha512-CdXZJoCS+aEPec26ZP7hWWU3SaJlQPZSCGdgpQ2qGl2HUmtUUNrI3zC4XWdn1JUmh3t5OuDeRG1qB4eGRNSD4A==
 
 "@humanwhocodes/config-array@^0.9.2":
   version "0.9.5"
@@ -1903,17 +1903,17 @@
   resolved "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.0.0-rc.17.tgz"
   integrity sha512-7LHZKsFRV/HqDoMVY+cJamFzgHgsrmQFalROHC5FMWrzPzd+utG5e11krj1tVsnxYufGA2ABShX4nlcHXED+zQ==
 
-"@vuelidate/core@2.0.0-alpha.41":
-  version "2.0.0-alpha.41"
-  resolved "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.0-alpha.41.tgz"
-  integrity sha512-fST7s5wiLW8ZNTexe8+7fDdBZYT7HjbuA43/XDtKTlHs1BMRDDaBoFLZbHSqmHisQvGXa7zLG9bvG8X5cHZaxg==
+"@vuelidate/core@2.0.0-alpha.42":
+  version "2.0.0-alpha.42"
+  resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.0-alpha.42.tgz#f8b4b7c112af374d3f37e38bff34f8bc2da21c5e"
+  integrity sha512-yLp5/5IkwNZP8214TqEuexlFLGoEZybEad2OZu/heOYPnERm6tgiWHZltc0USCuQ1JVZ2EJuPRHmqMl/G/N7tw==
   dependencies:
     vue-demi "^0.12.0"
 
-"@vuelidate/validators@2.0.0-alpha.27":
-  version "2.0.0-alpha.27"
-  resolved "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.0-alpha.27.tgz"
-  integrity sha512-omCUVP+gr2kKBMQwdKsOYPWYk/Fu92K93LRnl8Vk856UudNlb89wCreh0/Q8DpVEOisLljI3T7026QCo9eSq4Q==
+"@vuelidate/validators@2.0.0-alpha.30":
+  version "2.0.0-alpha.30"
+  resolved "https://registry.yarnpkg.com/@vuelidate/validators/-/validators-2.0.0-alpha.30.tgz#978e676b5b5dc160e6a83fdf8c1bf26052f46e88"
+  integrity sha512-XH0oIU1+6bTZ1Kd1RNf7AMDsAahj1hUjLhbFUIrDhKIUKMFvG4658pqYATePNqhAegENFA+RDAPhsDXV/MB2wQ==
   dependencies:
     vue-demi "^0.12.0"
 
@@ -2181,9 +2181,10 @@ ansi-escapes@^4.2.1:
   dependencies:
     type-fest "^0.21.3"
 
-ansi-html@0.0.7:
-  version "0.0.7"
-  resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz"
+ansi-html-community@0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
+  integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
 
 ansi-regex@^2.0.0:
   version "2.1.1"
@@ -3143,23 +3144,20 @@ chardet@^0.7.0:
   version "0.7.0"
   resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz"
 
-chokidar@^2.0.0:
-  version "2.1.6"
-  resolved "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz"
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
   dependencies:
-    anymatch "^2.0.0"
-    async-each "^1.0.1"
-    braces "^2.3.2"
-    glob-parent "^3.1.0"
-    inherits "^2.0.3"
-    is-binary-path "^1.0.0"
-    is-glob "^4.0.0"
-    normalize-path "^3.0.0"
-    path-is-absolute "^1.0.0"
-    readdirp "^2.2.1"
-    upath "^1.1.1"
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
   optionalDependencies:
-    fsevents "^1.2.7"
+    fsevents "~2.3.2"
 
 chokidar@^2.1.8:
   version "2.1.8"
@@ -3195,21 +3193,6 @@ chokidar@^3.4.1:
   optionalDependencies:
     fsevents "~2.3.1"
 
-chokidar@^3.5.1:
-  version "3.5.3"
-  resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
-  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
-  dependencies:
-    anymatch "~3.1.2"
-    braces "~3.0.2"
-    glob-parent "~5.1.2"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.6.0"
-  optionalDependencies:
-    fsevents "~2.3.2"
-
 chownr@^1.1.1:
   version "1.1.1"
   resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz"
@@ -5510,9 +5493,10 @@ html-comment-regex@^1.1.0:
   version "1.1.2"
   resolved "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz"
 
-html-entities@^1.2.0:
-  version "1.2.1"
-  resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz"
+html-entities@^2.1.0:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
+  integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
 
 html-escaper@^2.0.0:
   version "2.0.2"
@@ -5691,6 +5675,11 @@ immediate@~3.0.5:
   version "3.0.6"
   resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
 
+immutable@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
+  integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
+
 import-cwd@^2.0.0:
   version "2.1.0"
   resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz"
@@ -6221,10 +6210,10 @@ isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
 
-iso-639-1@2.1.13:
-  version "2.1.13"
-  resolved "https://registry.npmjs.org/iso-639-1/-/iso-639-1-2.1.13.tgz"
-  integrity sha512-stYt3u6OnVDNcK4IWARGXmTOOY5Wa5g4bUmBsttZp/55ZiEjDUibR3C59ZnorKoSS0tfJmFuGMST3ksnY1zu7Q==
+iso-639-1@2.1.15:
+  version "2.1.15"
+  resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.1.15.tgz#20cf78a4f691aeb802c16f17a6bad7d99271e85d"
+  integrity sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==
 
 isobject@^2.0.0:
   version "2.1.0"
@@ -9648,23 +9637,25 @@ samsam@1.x, samsam@^1.1.3:
   version "1.3.0"
   resolved "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz"
 
-sass-loader@7.2.0:
-  version "7.2.0"
-  resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-7.2.0.tgz"
-  integrity sha512-h8yUWaWtsbuIiOCgR9fd9c2lRXZ2uG+h8Dzg/AGNj+Hg/3TO8+BBAW9mEP+mh8ei+qBKqSJ0F1FLlYjNBc61OA==
+sass-loader@7.3.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f"
+  integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==
   dependencies:
     clone-deep "^4.0.1"
     loader-utils "^1.0.1"
     neo-async "^2.5.0"
     pify "^4.0.1"
-    semver "^5.5.0"
+    semver "^6.3.0"
 
-sass@1.20.1:
-  version "1.20.1"
-  resolved "https://registry.npmjs.org/sass/-/sass-1.20.1.tgz"
-  integrity sha512-BnCawee/L5kVG3B/5Jg6BFwASqUwFVE6fj2lnkVuSXDgQ7gMAhY9a2yPeqsKhJMCN+Wgx0r2mAW7XF/aTF5qtA==
+sass@1.53.0:
+  version "1.53.0"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.53.0.tgz#eab73a7baac045cc57ddc1d1ff501ad2659952eb"
+  integrity sha512-zb/oMirbKhUgRQ0/GFz8TSAwRq2IlR29vOUJZOx0l8sV+CkHUfHa4u5nqrG+1VceZp7Jfj59SVW9ogdhTvJDcQ==
   dependencies:
-    chokidar "^2.0.0"
+    chokidar ">=3.0.0 <4.0.0"
+    immutable "^4.0.0"
+    source-map-js ">=0.6.2 <2.0.0"
 
 sax@~1.2.1:
   version "1.2.4"
@@ -10001,7 +9992,7 @@ source-list-map@^2.0.0:
   version "2.0.1"
   resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
 
-source-map-js@^1.0.2:
+"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
   version "1.0.2"
   resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
@@ -11229,15 +11220,15 @@ webpack-dev-middleware@3.7.3, webpack-dev-middleware@^3.7.0:
     range-parser "^1.2.1"
     webpack-log "^2.0.0"
 
-webpack-hot-middleware@2.24.3:
-  version "2.24.3"
-  resolved "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.24.3.tgz"
-  integrity sha512-pPlmcdoR2Fn6UhYjAhp1g/IJy1Yc9hD+T6O9mjRcWV2pFbBjIFoJXhP0CoD0xPOhWJuWXuZXGBga9ybbOdzXpg==
+webpack-hot-middleware@2.25.1:
+  version "2.25.1"
+  resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.25.1.tgz#581f59edf0781743f4ca4c200fd32c9266c6cf7c"
+  integrity sha512-Koh0KyU/RPYwel/khxbsDz9ibDivmUbrRuKSSQvW42KSDdO4w23WI3SkHpSUKHE76LrFnnM/L7JCrpBwu8AXYw==
   dependencies:
-    ansi-html "0.0.7"
-    html-entities "^1.2.0"
+    ansi-html-community "0.0.8"
+    html-entities "^2.1.0"
     querystring "^0.2.0"
-    strip-ansi "^3.0.0"
+    strip-ansi "^6.0.0"
 
 webpack-log@^2.0.0:
   version "2.0.0"