about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen <eugen@zeonfederated.com>2017-04-13 13:26:16 +0200
committerGitHub <noreply@github.com>2017-04-13 13:26:16 +0200
commit1a12fd14d438380e24421e9c8a8894cc705aba51 (patch)
treeb276d81e18839706eaebe1db5870edc0b8628064
parenta18fd491b9ad9b2e1677d0e0355712a08967fe14 (diff)
parent282bb55c3cae07229d4c9a2fe58c1c2a136c57b9 (diff)
Merge branch 'master' into master
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx5
-rw-r--r--app/assets/javascripts/components/features/community_timeline/index.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/index.jsx2
-rw-r--r--app/assets/javascripts/components/features/getting_started/index.jsx4
-rw-r--r--app/assets/javascripts/components/features/public_timeline/index.jsx2
-rw-r--r--app/assets/javascripts/components/locales/bg.jsx68
-rw-r--r--app/assets/javascripts/components/locales/en.jsx153
-rw-r--r--app/assets/javascripts/components/locales/index.jsx3
-rw-r--r--app/assets/javascripts/components/locales/ja.jsx4
-rw-r--r--app/assets/javascripts/components/locales/pt.jsx62
-rw-r--r--app/assets/javascripts/components/locales/ru.jsx43
-rw-r--r--app/assets/stylesheets/forms.scss2
-rw-r--r--app/controllers/admin/accounts_controller.rb48
-rw-r--r--app/controllers/admin/silences_controller.rb23
-rw-r--r--app/controllers/admin/suspensions_controller.rb23
-rw-r--r--app/controllers/settings/exports/base_controller.rb2
-rw-r--r--app/controllers/settings/exports/blocked_accounts_controller.rb4
-rw-r--r--app/controllers/settings/exports/following_accounts_controller.rb4
-rw-r--r--app/controllers/settings/exports/muted_accounts_controller.rb4
-rw-r--r--app/controllers/settings/exports_controller.rb5
-rw-r--r--app/controllers/well_known/host_meta_controller.rb13
-rw-r--r--app/controllers/well_known/webfinger_controller.rb43
-rw-r--r--app/controllers/xrd_controller.rb55
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/models/account_filter.rb36
-rw-r--r--app/models/export.rb38
-rw-r--r--app/views/admin/accounts/show.html.haml8
-rw-r--r--app/views/auth/sessions/two_factor.html.haml2
-rw-r--r--app/views/settings/exports/show.html.haml8
-rw-r--r--app/views/well_known/host_meta/show.xml.ruby (renamed from app/views/xrd/host_meta.xml.ruby)0
-rw-r--r--app/views/well_known/webfinger/show.json.rabl (renamed from app/views/xrd/webfinger.json.rabl)0
-rw-r--r--app/views/well_known/webfinger/show.xml.ruby (renamed from app/views/xrd/webfinger.xml.ruby)0
-rw-r--r--config/application.rb1
-rw-r--r--config/locales/bg.yml169
-rw-r--r--config/locales/devise.bg.yml61
-rw-r--r--config/locales/doorkeeper.bg.yml113
-rw-r--r--config/locales/doorkeeper.ja.yml32
-rw-r--r--config/locales/ja.yml2
-rw-r--r--config/locales/ru.yml19
-rw-r--r--config/locales/simple_form.bg.yml46
-rw-r--r--config/locales/simple_form.ru.yml2
-rw-r--r--config/routes.rb13
-rw-r--r--spec/controllers/admin/silences_controller_spec.rb24
-rw-r--r--spec/controllers/admin/suspensions_controller_spec.rb24
-rw-r--r--spec/controllers/settings/exports_controller_spec.rb3
-rw-r--r--spec/controllers/well_known/host_meta_controller_spec.rb13
-rw-r--r--spec/controllers/well_known/webfinger_controller_spec.rb21
-rw-r--r--spec/controllers/xrd_controller_spec.rb26
-rw-r--r--spec/models/account_filter_spec.rb31
-rw-r--r--spec/requests/host_meta_request_spec.rb12
-rw-r--r--spec/routing/well_known_routes_spec.rb15
52 files changed, 1055 insertions, 241 deletions
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index d8810dc64..b9086de42 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -52,8 +52,8 @@ import no from 'react-intl/locale-data/no';
 import ru from 'react-intl/locale-data/ru';
 import uk from 'react-intl/locale-data/uk';
 import zh from 'react-intl/locale-data/zh';
+import bg from 'react-intl/locale-data/bg';
 import { localeData as zh_hk } from '../locales/zh-hk';
-
 import getMessagesForLocale from '../locales';
 import { hydrateStore } from '../actions/store';
 import createStream from '../stream';
@@ -66,7 +66,6 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
   basename: '/web'
 });
 
-
 addLocaleData([
   ...en,
   ...de,
@@ -82,9 +81,9 @@ addLocaleData([
   ...uk,
   ...zh,
   ...zh_hk,
+  ...bg,
 ]);
 
-
 const Mastodon = React.createClass({
 
   propTypes: {
diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx
index 0957338cf..acfc30b65 100644
--- a/app/assets/javascripts/components/features/community_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/community_timeline/index.jsx
@@ -14,7 +14,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 import createStream from '../../stream';
 
 const messages = defineMessages({
-  title: { id: 'column.community', defaultMessage: 'Local' }
+  title: { id: 'column.community', defaultMessage: 'Local timeline' }
 });
 
 const mapStateToProps = state => ({
diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index cb4b62f6c..d2e65359f 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -19,7 +19,7 @@ import TextIconButton from './text_icon_button';
 const messages = defineMessages({
   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
   spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' },
-  publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }
+  publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }
 });
 
 const ComposeForm = React.createClass({
diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx
index 9421de3ff..33e16472c 100644
--- a/app/assets/javascripts/components/features/compose/index.jsx
+++ b/app/assets/javascripts/components/features/compose/index.jsx
@@ -12,7 +12,7 @@ import SearchResultsContainer from './containers/search_results_container';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
-  public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
+  public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
   community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
   logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx
index 0656bf69a..05bfcc221 100644
--- a/app/assets/javascripts/components/features/getting_started/index.jsx
+++ b/app/assets/javascripts/components/features/getting_started/index.jsx
@@ -7,11 +7,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 
 const messages = defineMessages({
   heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
-  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
+  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
   community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
   follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
-  sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' },
+  sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
   favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
   blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
   info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx
index 6d766a83b..a7ac95ab4 100644
--- a/app/assets/javascripts/components/features/public_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -14,7 +14,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 import createStream from '../../stream';
 
 const messages = defineMessages({
-  title: { id: 'column.public', defaultMessage: 'Whole Known Network' }
+  title: { id: 'column.public', defaultMessage: 'Federated timeline' }
 });
 
 const mapStateToProps = state => ({
diff --git a/app/assets/javascripts/components/locales/bg.jsx b/app/assets/javascripts/components/locales/bg.jsx
new file mode 100644
index 000000000..cac984aae
--- /dev/null
+++ b/app/assets/javascripts/components/locales/bg.jsx
@@ -0,0 +1,68 @@
+const bg = {
+  "column_back_button.label": "Назад",
+  "lightbox.close": "Затвори",
+  "loading_indicator.label": "Зареждане...",
+  "status.mention": "Споменаване",
+  "status.delete": "Изтриване",
+  "status.reply": "Отговор",
+  "status.reblog": "Споделяне",
+  "status.favourite": "Предпочитани",
+  "status.reblogged_by": "{name} сподели",
+  "status.sensitive_warning": "Деликатно съдържание",
+  "status.sensitive_toggle": "Покажи",
+  "video_player.toggle_sound": "Звук",
+  "account.mention": "Споменаване",
+  "account.edit_profile": "Редактирай профила си",
+  "account.unblock": "Не блокирай",
+  "account.unfollow": "Не следвай",
+  "account.block": "Блокирай",
+  "account.follow": "Последвай",
+  "account.posts": "Публикации",
+  "account.follows": "Следвам",
+  "account.followers": "Последователи",
+  "account.follows_you": "Твой последовател",
+  "account.requested": "В очакване на одобрение",
+  "getting_started.heading": "Първи стъпки",
+  "getting_started.about_addressing": "Можеш да последваш потребител, ако знаеш потребителското му име и домейна, на който се намира, като в полето за търсене ги въведеш по този начин: име@домейн",
+  "getting_started.about_shortcuts": "Ако с търсения потребител се намирате на един и същ домейн, достатъчно е да въведеш само името. Същото важи и за споменаване на хора в публикации.",
+  "getting_started.about_developer": "Можеш да потърсиш разработчика на този проект като: Gargron@mastodon.social",
+  "getting_started.open_source_notice": "Mastodon е софтуер с отворен код. Можеш да помогнеш или да докладваш за проблеми в Github: {github}.",
+  "column.home": "Начало",
+  "column.mentions": "Споменавания",
+  "column.public": "Публичен канал",
+  "column.notifications": "Известия",
+  "tabs_bar.compose": "Съставяне",
+  "tabs_bar.home": "Начало",
+  "tabs_bar.mentions": "Споменавания",
+  "tabs_bar.public": "Публичен канал",
+  "tabs_bar.notifications": "Известия",
+  "compose_form.placeholder": "Какво си мислиш?",
+  "compose_form.publish": "Раздумай",
+  "compose_form.sensitive": "Отбележи съдържанието като деликатно",
+  "compose_form.spoiler": "Скрий текста зад предупреждение",
+  "compose_form.private": "Отбележи като поверително",
+  "compose_form.privacy_disclaimer": "Поверителни публикации ще бъдат изпратени до споменатите потребители на {domains}. Доверяваш ли се на {domainsCount, plural, one {that server} other {those servers}}, че няма да издаде твоята публикация?",
+  "compose_form.unlisted": "Не показвай в публичния канал",
+  "navigation_bar.edit_profile": "Редактирай профил",
+  "navigation_bar.preferences": "Предпочитания",
+  "navigation_bar.public_timeline": "Публичен канал",
+  "navigation_bar.logout": "Излизане",
+  "reply_indicator.cancel": "Отказ",
+  "search.placeholder": "Търсене",
+  "search.account": "Акаунт",
+  "search.hashtag": "Хаштаг",
+  "upload_button.label": "Добави медия",
+  "upload_form.undo": "Отмяна",
+  "notification.follow": "{name} те последва",
+  "notification.favourite": "{name} хареса твоята публикация",
+  "notification.reblog": "{name} сподели твоята публикация",
+  "notification.mention": "{name} те спомена",
+  "notifications.column_settings.alert": "Десктоп известия",
+  "notifications.column_settings.show": "Покажи в колона",
+  "notifications.column_settings.follow": "Нови последователи:",
+  "notifications.column_settings.favourite": "Предпочитани:",
+  "notifications.column_settings.mention": "Споменавания:",
+  "notifications.column_settings.reblog": "Споделяния:",
+};
+
+export default en;
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index f249b1967..1834567f1 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -1,72 +1,129 @@
+/**
+ * Note for Contributors:
+ * This file (en.jsx) serve as a template for other languages.
+ * To make other contributors' life easier, please REMEMBER:
+ *   1. to add your new string here; and
+ *   2. to remove old strings that are no longer needed; and
+ *   3. to sort the strings by the key.
+ * Thanks!
+ */
 const en = {
-  "column_back_button.label": "Back",
-  "lightbox.close": "Close",
-  "loading_indicator.label": "Loading...",
-  "status.mention": "Mention @{name}",
-  "status.delete": "Delete",
-  "status.reply": "Reply",
-  "status.reblog": "Boost",
-  "status.favourite": "Favourite",
-  "status.reblogged_by": "{name} boosted",
-  "status.sensitive_warning": "Sensitive content",
-  "status.sensitive_toggle": "Click to view",
-  "status.show_more": "Show more",
-  "status.show_less": "Show less",
-  "status.open": "Expand this status",
-  "status.report": "Report @{name}",
-  "video_player.toggle_sound": "Toggle sound",
-  "account.mention": "Mention @{name}",
-  "account.edit_profile": "Edit profile",
-  "account.unblock": "Unblock @{name}",
-  "account.unfollow": "Unfollow",
   "account.block": "Block @{name}",
+  "account.disclaimer": "This user is from another instance. This number may be larger.",
+  "account.edit_profile": "Edit profile",
   "account.follow": "Follow",
-  "account.posts": "Posts",
-  "account.follows": "Follows",
   "account.followers": "Followers",
   "account.follows_you": "Follows you",
+  "account.follows": "Follows",
+  "account.mention": "Mention @{name}",
+  "account.mute": "Mute @{name}",
+  "account.posts": "Posts",
+  "account.report": "Report @{name}",
   "account.requested": "Awaiting approval",
-  "getting_started.heading": "Getting started",
-  "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
-  "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
-  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
-  "column.home": "Home",
+  "account.unblock": "Unblock @{name}",
+  "account.unfollow": "Unfollow",
+  "account.unmute": "Unmute @{name}",
+  "boost_modal.combo": "You can press {combo} to skip this next time",
+  "column_back_button.label": "Back",
+  "column.blocks": "Blocked users",
   "column.community": "Local timeline",
-  "column.public": "Federated timeline",
+  "column.favourites": "Favourites",
+  "column.follow_requests": "Follow requests",
+  "column.home": "Home",
   "column.notifications": "Notifications",
-  "tabs_bar.compose": "Compose",
-  "tabs_bar.home": "Home",
-  "tabs_bar.mentions": "Mentions",
-  "tabs_bar.public": "Federated timeline",
-  "tabs_bar.notifications": "Notifications",
+  "column.public": "Federated timeline",
   "compose_form.placeholder": "What is on your mind?",
+  "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
   "compose_form.publish": "Toot",
   "compose_form.sensitive": "Mark media as sensitive",
+  "compose_form.spoiler_placeholder": "Content warning",
   "compose_form.spoiler": "Hide text behind warning",
-  "compose_form.private": "Mark as private",
-  "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
-  "compose_form.unlisted": "Do not display on public timelines",
+  "emoji_button.label": "Insert emoji",
+  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
+  "empty_column.hashtag": "There is nothing in this hashtag yet.",
+  "empty_column.home.public_timeline": "the public timeline",
+  "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
+  "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
+  "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
+  "follow_request.authorize": "Authorize",
+  "follow_request.reject": "Rejec",
+  "getting_started.apps": "Various apps are available",
+  "getting_started.heading": "Getting started",
+  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
+  "home.column_settings.advanced": "Advanced",
+  "home.column_settings.basic": "Basic",
+  "home.column_settings.filter_regex": "Filter out by regular expressions",
+  "home.column_settings.show_reblogs": "Show boosts",
+  "home.column_settings.show_replies": "Show replies",
+  "home.settings": "Column settings",
+  "lightbox.close": "Close",
+  "loading_indicator.label": "Loading...",
+  "media_gallery.toggle_visible": "Toggle visibility",
+  "missing_indicator.label": "Not found",
+  "navigation_bar.blocks": "Blocked users",
+  "navigation_bar.community_timeline": "Local timeline",
   "navigation_bar.edit_profile": "Edit profile",
+  "navigation_bar.favourites": "Favourites",
+  "navigation_bar.follow_requests": "Follow requests",
+  "navigation_bar.info": "Extended information",
+  "navigation_bar.logout": "Logout",
   "navigation_bar.preferences": "Preferences",
-  "navigation_bar.community_timeline": "Local timeline",
   "navigation_bar.public_timeline": "Federated timeline",
-  "navigation_bar.logout": "Logout",
-  "reply_indicator.cancel": "Cancel",
-  "search.placeholder": "Search",
-  "search.account": "Account",
-  "search.hashtag": "Hashtag",
-  "upload_button.label": "Add media",
-  "upload_form.undo": "Undo",
-  "notification.follow": "{name} followed you",
   "notification.favourite": "{name} favourited your status",
+  "notification.follow": "{name} followed you",
   "notification.reblog": "{name} boosted your status",
-  "notification.mention": "{name} mentioned you",
+  "notifications.clear_confirmation": "Are you sure you want to clear all your notifications?",
+  "notifications.clear": "Clear notifications",
   "notifications.column_settings.alert": "Desktop notifications",
-  "notifications.column_settings.show": "Show in column",
-  "notifications.column_settings.follow": "New followers:",
   "notifications.column_settings.favourite": "Favourites:",
+  "notifications.column_settings.follow": "New followers:",
   "notifications.column_settings.mention": "Mentions:",
   "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.show": "Show in column",
+  "notifications.column_settings.sound": "Play sound",
+  "notifications.settings": "Column settings",
+  "privacy.change": "Adjust status privacy",
+  "privacy.direct.long": "Post to mentioned users only",
+  "privacy.direct.short": "Direct",
+  "privacy.private.long": "Post to followers only",
+  "privacy.private.short": "Private",
+  "privacy.public.long": "Post to public timelines",
+  "privacy.public.short": "Public",
+  "privacy.unlisted.long": "Do not show in public timelines",
+  "privacy.unlisted.short": "Unlisted",
+  "reply_indicator.cancel": "Cancel",
+  "report.heading": "New report",
+  "report.placeholder": "Additional comments",
+  "report.submit": "Submit",
+  "report.target": "Reporting",
+  "search_results.total": "{count} {count, plural, one {result} other {results}}",
+  "search.placeholder": "Search",
+  "search.status_by": "Status by {name}",
+  "status.delete": "Delete",
+  "status.favourite": "Favourite",
+  "status.load_more": "Load more",
+  "status.media_hidden": "Media hidden",
+  "status.mention": "Mention @{name}",
+  "status.open": "Expand this status",
+  "status.reblog": "Boost",
+  "status.reblogged_by": "{name} boosted",
+  "status.reply": "Reply",
+  "status.report": "Report @{name}",
+  "status.sensitive_toggle": "Click to view",
+  "status.sensitive_warning": "Sensitive content",
+  "status.show_less": "Show less",
+  "status.show_more": "Show more",
+  "tabs_bar.compose": "Compose",
+  "tabs_bar.federated_timeline": "Federated",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notifications",
+  "upload_area.title": "Drag & drop to upload",
+  "upload_button.label": "Add media",
+  "upload_form.undo": "Undo",
+  "upload_progress.label": "Uploading...",
+  "video_player.toggle_sound": "Toggle sound",
+  "video_player.toggle_visible": "Toggle visibility",
 };
 
 export default en;
diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx
index e772c1074..f14568a3d 100644
--- a/app/assets/javascripts/components/locales/index.jsx
+++ b/app/assets/javascripts/components/locales/index.jsx
@@ -11,7 +11,7 @@ import eo from './eo';
 import ru from './ru';
 import ja from './ja';
 import zh_hk from './zh-hk';
-
+import bg from './bg';
 
 const locales = {
   en,
@@ -27,6 +27,7 @@ const locales = {
   ru,
   ja,
   'zh-HK': zh_hk,
+  bg,
 };
 
 export default function getMessagesForLocale (locale) {
diff --git a/app/assets/javascripts/components/locales/ja.jsx b/app/assets/javascripts/components/locales/ja.jsx
index 25a6f7f67..fdfc91c29 100644
--- a/app/assets/javascripts/components/locales/ja.jsx
+++ b/app/assets/javascripts/components/locales/ja.jsx
@@ -39,8 +39,8 @@ const ja = {
   "tabs_bar.compose": "投稿",
   "tabs_bar.home": "ホーム",
   "tabs_bar.mentions": "返信",
-  "tabs_bar.local_timeline": "ローカルTL",
-  "tabs_bar.federated_timeline": "連合TL",
+  "tabs_bar.local_timeline": "ローカル",
+  "tabs_bar.federated_timeline": "連合",
   "tabs_bar.notifications": "通知",
   "compose_form.placeholder": "今なにしてる?",
   "compose_form.publish": "トゥート",
diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx
index 8d1b88c75..cd345a585 100644
--- a/app/assets/javascripts/components/locales/pt.jsx
+++ b/app/assets/javascripts/components/locales/pt.jsx
@@ -14,59 +14,115 @@ const pt = {
   "status.show_less": "Mostrar menos",
   "status.open": "Expandir",
   "status.report": "Reportar @{name}",
+  "status.load_more": "Carregar mais",
+  "status.media_hidden": "Media escondida",
   "video_player.toggle_sound": "Ligar/Desligar som",
+  "video_player.toggle_visible": "Ligar/Desligar vídeo",
   "account.mention": "Mencionar @{name}",
   "account.edit_profile": "Editar perfil",
   "account.unblock": "Não bloquear @{name}",
   "account.unfollow": "Não seguir",
   "account.block": "Bloquear @{name}",
+  "account.mute": "Mute",
+  "account.unmute": "Remover Mute",
   "account.follow": "Seguir",
   "account.posts": "Posts",
   "account.follows": "Segue",
   "account.followers": "Seguidores",
   "account.follows_you": "É teu seguidor",
   "account.requested": "A aguardar aprovação",
+  "account.report": "Denunciar",
+  "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.",
   "getting_started.heading": "Primeiros passos",
   "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.",
   "getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.",
+  "getting_started.about_developer": "Pode seguir o developer deste projecto em Gargron@mastodon.social",
   "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.",
   "column.home": "Home",
   "column.community": "Local",
-  "column.public": "Público",
+  "column.public": "Global",
   "column.notifications": "Notificações",
+  "column.blocks": "Utilizadores Bloqueados",
+  "column.favourites": "Favoritos",
+  "column.follow_requests": "Seguidores Pendentes",
+  "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
+  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
+  "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
+  "empty_column.home.public_timeline": "global",
+  "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
+  "empty_column.hashtag": "Não existe qualquer conteúdo com essa hashtag",
   "tabs_bar.compose": "Criar",
   "tabs_bar.home": "Home",
   "tabs_bar.mentions": "Menções",
   "tabs_bar.public": "Público",
   "tabs_bar.notifications": "Notificações",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.federated_timeline": "Global",
   "compose_form.placeholder": "Em que estás a pensar?",
   "compose_form.publish": "Publicar",
-  "compose_form.sensitive": "Media com conteúdo sensível",
+  "compose_form.sensitive": "Marcar media como conteúdo sensível",
   "compose_form.spoiler": "Esconder texto com aviso",
+  "compose_form.spoiler_placeholder": "Aviso",
   "compose_form.private": "Tornar privado",
   "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
   "compose_form.unlisted": "Não mostrar na listagem pública",
+  "emoji_button.label": "Inserir Emoji",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.community_timeline": "Local",
-  "navigation_bar.public_timeline": "Público",
+  "navigation_bar.public_timeline": "Global",
+  "navigation_bar.blocks": "Utilizadores bloqueados",
+  "navigation_bar.favourites": "Favoritos",
+  "navigation_bar.info": "Mais informações",
   "navigation_bar.logout": "Sair",
+  "navigation_bar.follow_requests": "Seguidores pendentes",
   "reply_indicator.cancel": "Cancelar",
   "search.placeholder": "Pesquisar",
   "search.account": "Conta",
   "search.hashtag": "Hashtag",
+  "search_results.total": "{count} {count, plural, one {resultado} other {resultados}}",
+  "search.status_by": "Post de {name}",
   "upload_button.label": "Adicionar media",
   "upload_form.undo": "Anular",
+  "upload_progress.label": "A gravar…",
+  "upload_area.title": "Arraste e solte para enviar",
   "notification.follow": "{name} seguiu-te",
   "notification.favourite": "{name} adicionou o teu post aos favoritos",
   "notification.reblog": "{name} partilhou o teu post",
   "notification.mention": "{name} mencionou-te",
   "notifications.column_settings.alert": "Notificações no computador",
   "notifications.column_settings.show": "Mostrar nas colunas",
+  "notifications.column_settings.sound": "Reproduzir som",
   "notifications.column_settings.follow": "Novos seguidores:",
   "notifications.column_settings.favourite": "Favoritos:",
   "notifications.column_settings.mention": "Menções:",
   "notifications.column_settings.reblog": "Partilhas:",
+  "notifications.clear": "Limpar notificações",
+  "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
+  "notifications.settings": "Parâmetros da lista de Notificações",
+  "privacy.public.short": "Público",
+  "privacy.public.long": "Publicar em todos os feeds",
+  "privacy.unlisted.short": "Não listar",
+  "privacy.unlisted.long": "Não publicar nos feeds públicos",
+  "privacy.private.short": "Privado",
+  "privacy.private.long": "Apenas para os seguidores",
+  "privacy.direct.short": "Directo",
+  "privacy.direct.long": "Apenas para utilizadores mencionados",
+  "privacy.change": "Ajustar a privacidade da mensagem",
+  "media_gallery.toggle_visible": "Modificar a visibilidade",
+  "missing_indicator.label": "Não encontrado",
+  "follow_request.authorize": "Autorizar",
+  "follow_request.reject": "Rejeitar",
+  "home.settings": "Parâmetros da coluna Home",
+  "home.column_settings.basic": "Básico",
+  "home.column_settings.show_reblogs": "Mostrar as partilhas",
+  "home.column_settings.show_replies": "Mostrar as respostas",
+  "home.column_settings.advanced": "Avançadas",
+  "home.column_settings.filter_regex": "Filtrar com uma expressão regular",
+  "report.heading": "Nova denuncia",
+  "report.placeholder": "Comentários adicionais",
+  "report.submit": "Enviar",
+  "report.target": "Denunciar"
 };
 
 export default pt;
diff --git a/app/assets/javascripts/components/locales/ru.jsx b/app/assets/javascripts/components/locales/ru.jsx
index e109005a7..30a92df86 100644
--- a/app/assets/javascripts/components/locales/ru.jsx
+++ b/app/assets/javascripts/components/locales/ru.jsx
@@ -10,22 +10,29 @@ const ru = {
   "status.reblogged_by": "{name} продвинул(а)",
   "status.sensitive_warning": "Чувствительный контент",
   "status.sensitive_toggle": "Нажмите для просмотра",
+  "status.show_more": "Развернуть",
+  "status.show_less": "Свернуть",
+  "status.open": "Развернуть статус",
+  "status.report": "Пожаловаться",
+  "status.load_more": "Показать еще", 
   "video_player.toggle_sound": "Вкл./выкл. звук",
-  "account.mention": "Упомянуть @{name}",
+  "account.mention": "Упомянуть",
   "account.edit_profile": "Изменить профиль",
-  "account.unblock": "Разблокировать @{name}",
+  "account.unblock": "Разблокировать",
   "account.unfollow": "Отписаться",
-  "account.block": "Блокировать @{name}",
+  "account.block": "Блокировать",
+  "account.mute": "Заглушить",
   "account.follow": "Подписаться",
   "account.posts": "Посты",
   "account.follows": "Подписки",
-  "account.followers": "Подписчики",
+  "account.followers": "Подписаны",
   "account.follows_you": "Подписан(а) на Вас",
   "account.requested": "Ожидает подтверждения",
   "getting_started.heading": "Добро пожаловать",
   "getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
   "getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
   "getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.",
+  "getting_started.apps": "Доступны различные приложения.",
   "column.home": "Главная",
   "column.community": "Локальная лента",
   "column.public": "Глобальная лента",
@@ -36,7 +43,7 @@ const ru = {
   "tabs_bar.public": "Глобальная лента",
   "tabs_bar.notifications": "Уведомления",
   "compose_form.placeholder": "О чем Вы думаете?",
-  "compose_form.publish": "Протрубить",
+  "compose_form.publish": "Трубить",
   "compose_form.sensitive": "Отметить как чувствительный контент",
   "compose_form.spoiler": "Скрыть текст за предупреждением",
   "compose_form.private": "Отметить как приватное",
@@ -47,6 +54,9 @@ const ru = {
   "navigation_bar.community_timeline": "Локальная лента",
   "navigation_bar.public_timeline": "Глобальная лента",
   "navigation_bar.logout": "Выйти",
+  "navigation_bar.info": "Об узле",
+  "navigation_bar.favourites": "Понравившееся",
+  "navigation_bar.blocks": "Список блокировки",
   "reply_indicator.cancel": "Отмена",
   "search.placeholder": "Поиск",
   "search.account": "Аккаунт",
@@ -57,12 +67,35 @@ const ru = {
   "notification.favourite": "{name} понравился Ваш статус",
   "notification.reblog": "{name} продвинул(а) Ваш статус",
   "notification.mention": "{name} упомянул(а) Вас",
+  "home.settings": "Настройки колонки",
+  "home.column_settings.basic": "Основные",
+  "home.column_settings.advanced": "Дополнительные",
+  "home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
+  "home.column_settings.show_replies": "Показывать продвижения",
+  "home.column_settings.show_replies": "Показывать ответы",
+  "notifications.clear": "Очистить уведомления",
+  "notifications.settings": "Настройки колонки",
   "notifications.column_settings.alert": "Десктопные уведомления",
   "notifications.column_settings.show": "Показывать в колонке",
   "notifications.column_settings.follow": "Новые подписчики:",
   "notifications.column_settings.favourite": "Нравится:",
   "notifications.column_settings.mention": "Упоминания:",
   "notifications.column_settings.reblog": "Продвижения:",
+  "notifications.column_settings.sound": "Проигрывать звук",
+  "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.",
+  "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.",
+  "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
+  "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.",
+  "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.",
+  "empty_column.home.public_timeline": "публичные ленты",
+  "privacy.public.short": "Публичный",
+  "privacy.public.long": "Показать в публичных лентах",
+  "privacy.unlisted.short": "Скрытый",
+  "privacy.unlisted.long": "Не показывать в лентах",
+  "privacy.private.short": "Приватный",
+  "privacy.private.long": "Показать только подписчикам",
+  "privacy.direct.short": "Направленный",
+  "privacy.direct.long": "Показать только упомянутым",
 };
 
 export default ru;
diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss
index 2e3a4f147..e5e8697a0 100644
--- a/app/assets/stylesheets/forms.scss
+++ b/app/assets/stylesheets/forms.scss
@@ -88,7 +88,7 @@ code {
     }
   }
 
-  input[type=text], input[type=email], input[type=password], textarea {
+  input[type=text], input[type=number], input[type=email], input[type=password], textarea {
     background: transparent;
     box-sizing: border-box;
     border: 0;
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 71cb8edd8..0e9e52f42 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -2,49 +2,29 @@
 
 module Admin
   class AccountsController < BaseController
-    before_action :set_account, except: :index
-
     def index
-      @accounts = Account.alphabetic.page(params[:page])
-
-      @accounts = @accounts.local                             if params[:local].present?
-      @accounts = @accounts.remote                            if params[:remote].present?
-      @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present?
-      @accounts = @accounts.silenced                          if params[:silenced].present?
-      @accounts = @accounts.recent                            if params[:recent].present?
-      @accounts = @accounts.suspended                         if params[:suspended].present?
-    end
-
-    def show; end
-
-    def suspend
-      Admin::SuspensionWorker.perform_async(@account.id)
-      redirect_to admin_accounts_path
+      @accounts = filtered_accounts.page(params[:page])
     end
 
-    def unsuspend
-      @account.update(suspended: false)
-      redirect_to admin_accounts_path
-    end
-
-    def silence
-      @account.update(silenced: true)
-      redirect_to admin_accounts_path
-    end
-
-    def unsilence
-      @account.update(silenced: false)
-      redirect_to admin_accounts_path
+    def show
+      @account = Account.find(params[:id])
     end
 
     private
 
-    def set_account
-      @account = Account.find(params[:id])
+    def filtered_accounts
+      AccountFilter.new(filter_params).results
     end
 
-    def account_params
-      params.require(:account).permit(:silenced, :suspended)
+    def filter_params
+      params.permit(
+        :local,
+        :remote,
+        :by_domain,
+        :silenced,
+        :recent,
+        :suspended
+      )
     end
   end
 end
diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb
new file mode 100644
index 000000000..81a3008b9
--- /dev/null
+++ b/app/controllers/admin/silences_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Admin
+  class SilencesController < BaseController
+    before_action :set_account
+
+    def create
+      @account.update(silenced: true)
+      redirect_to admin_accounts_path
+    end
+
+    def destroy
+      @account.update(silenced: false)
+      redirect_to admin_accounts_path
+    end
+
+    private
+
+    def set_account
+      @account = Account.find(params[:account_id])
+    end
+  end
+end
diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb
new file mode 100644
index 000000000..5d9048d94
--- /dev/null
+++ b/app/controllers/admin/suspensions_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Admin
+  class SuspensionsController < BaseController
+    before_action :set_account
+
+    def create
+      Admin::SuspensionWorker.perform_async(@account.id)
+      redirect_to admin_accounts_path
+    end
+
+    def destroy
+      @account.update(suspended: false)
+      redirect_to admin_accounts_path
+    end
+
+    private
+
+    def set_account
+      @account = Account.find(params[:account_id])
+    end
+  end
+end
diff --git a/app/controllers/settings/exports/base_controller.rb b/app/controllers/settings/exports/base_controller.rb
index 0b790959f..c082ed806 100644
--- a/app/controllers/settings/exports/base_controller.rb
+++ b/app/controllers/settings/exports/base_controller.rb
@@ -6,7 +6,7 @@ module Settings
       before_action :authenticate_user!
 
       def index
-        export_data = Export.new(export_accounts).to_csv
+        @export = Export.new(current_account)
 
         respond_to do |format|
           format.csv { send_data export_data, filename: export_filename }
diff --git a/app/controllers/settings/exports/blocked_accounts_controller.rb b/app/controllers/settings/exports/blocked_accounts_controller.rb
index 9c4bcaa53..f1115b21e 100644
--- a/app/controllers/settings/exports/blocked_accounts_controller.rb
+++ b/app/controllers/settings/exports/blocked_accounts_controller.rb
@@ -5,8 +5,8 @@ module Settings
     class BlockedAccountsController < BaseController
       private
 
-      def export_accounts
-        current_account.blocking
+      def export_data
+        @export.to_blocked_accounts_csv
       end
     end
   end
diff --git a/app/controllers/settings/exports/following_accounts_controller.rb b/app/controllers/settings/exports/following_accounts_controller.rb
index 8d06bcc95..0011d2463 100644
--- a/app/controllers/settings/exports/following_accounts_controller.rb
+++ b/app/controllers/settings/exports/following_accounts_controller.rb
@@ -5,8 +5,8 @@ module Settings
     class FollowingAccountsController < BaseController
       private
 
-      def export_accounts
-        current_account.following
+      def export_data
+        @export.to_following_accounts_csv
       end
     end
   end
diff --git a/app/controllers/settings/exports/muted_accounts_controller.rb b/app/controllers/settings/exports/muted_accounts_controller.rb
index a77a9af6d..dfe72cfcb 100644
--- a/app/controllers/settings/exports/muted_accounts_controller.rb
+++ b/app/controllers/settings/exports/muted_accounts_controller.rb
@@ -5,8 +5,8 @@ module Settings
     class MutedAccountsController < BaseController
       private
 
-      def export_accounts
-        current_account.muting
+      def export_data
+        @export.to_muted_accounts_csv
       end
     end
   end
diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb
index 77dea3231..ae62f00c1 100644
--- a/app/controllers/settings/exports_controller.rb
+++ b/app/controllers/settings/exports_controller.rb
@@ -6,9 +6,6 @@ class Settings::ExportsController < ApplicationController
   before_action :authenticate_user!
 
   def show
-    @total_storage = current_account.media_attachments.sum(:file_file_size)
-    @total_follows = current_account.following.count
-    @total_blocks  = current_account.blocking.count
-    @total_mutes = current_account.muting.count
+    @export = Export.new(current_account)
   end
 end
diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb
new file mode 100644
index 000000000..2f0960acd
--- /dev/null
+++ b/app/controllers/well_known/host_meta_controller.rb
@@ -0,0 +1,13 @@
+  # frozen_string_literal: true
+
+module WellKnown
+  class HostMetaController < ApplicationController
+    def show
+      @webfinger_template = "#{webfinger_url}?resource={uri}"
+
+      respond_to do |format|
+        format.xml { render content_type: 'application/xrd+xml' }
+      end
+    end
+  end
+end
diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb
new file mode 100644
index 000000000..1a8ef5f90
--- /dev/null
+++ b/app/controllers/well_known/webfinger_controller.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module WellKnown
+  class WebfingerController < ApplicationController
+    def show
+      @account = Account.find_local!(username_from_resource)
+      @canonical_account_uri = @account.to_webfinger_s
+      @magic_key = pem_to_magic_key(@account.keypair.public_key)
+
+      respond_to do |format|
+        format.xml  { render content_type: 'application/xrd+xml' }
+        format.json { render content_type: 'application/jrd+json' }
+      end
+    rescue ActiveRecord::RecordNotFound
+      head 404
+    end
+
+    private
+
+    def username_from_resource
+      WebfingerResource.new(resource_param).username
+    end
+
+    def pem_to_magic_key(public_key)
+      modulus, exponent = [public_key.n, public_key.e].map do |component|
+        result = []
+
+        until component.zero?
+          result << [component % 256].pack('C')
+          component >>= 8
+        end
+
+        result.reverse.join
+      end
+
+      (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
+    end
+
+    def resource_param
+      params.require(:resource)
+    end
+  end
+end
diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb
deleted file mode 100644
index 2886315ac..000000000
--- a/app/controllers/xrd_controller.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-class XrdController < ApplicationController
-  before_action :set_default_format_xml, only: :host_meta
-
-  def host_meta
-    @webfinger_template = "#{webfinger_url}?resource={uri}"
-
-    respond_to do |format|
-      format.xml { render content_type: 'application/xrd+xml' }
-    end
-  end
-
-  def webfinger
-    @account = Account.find_local!(username_from_resource)
-    @canonical_account_uri = @account.to_webfinger_s
-    @magic_key = pem_to_magic_key(@account.keypair.public_key)
-
-    respond_to do |format|
-      format.xml  { render content_type: 'application/xrd+xml' }
-      format.json { render content_type: 'application/jrd+json' }
-    end
-  rescue ActiveRecord::RecordNotFound
-    head 404
-  end
-
-  private
-
-  def set_default_format_xml
-    request.format = 'xml' if request.headers['HTTP_ACCEPT'].nil? && params[:format].nil?
-  end
-
-  def username_from_resource
-    WebfingerResource.new(resource_param).username
-  end
-
-  def pem_to_magic_key(public_key)
-    modulus, exponent = [public_key.n, public_key.e].map do |component|
-      result = []
-
-      until component.zero?
-        result << [component % 256].pack('C')
-        component >>= 8
-      end
-
-      result.reverse.join
-    end
-
-    (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
-  end
-
-  def resource_param
-    params.require(:resource)
-  end
-end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 211b57042..212f88c39 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -16,6 +16,7 @@ module SettingsHelper
     ja: '日本語',
     'zh-CN': '简体中文',
     'zh-HK': '繁體中文(香港)',
+    bg: 'Български',
   }.freeze
 
   def human_locale(locale)
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
new file mode 100644
index 000000000..a8d8c8837
--- /dev/null
+++ b/app/models/account_filter.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class AccountFilter
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def results
+    scope = Account.alphabetic
+    params.each do |key, value|
+      scope = scope.merge scope_for(key, value)
+    end
+    scope
+  end
+
+  def scope_for(key, value)
+    case key
+    when /local/
+      Account.local
+    when /remote/
+      Account.remote
+    when /by_domain/
+      Account.where(domain: value)
+    when /silenced/
+      Account.silenced
+    when /recent/
+      Account.recent
+    when /suspended/
+      Account.suspended
+    else
+      raise "Unknown filter: #{key}"
+    end
+  end
+end
diff --git a/app/models/export.rb b/app/models/export.rb
index cd1a58eb6..f0d5dd255 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -2,13 +2,43 @@
 require 'csv'
 
 class Export
-  attr_reader :accounts
+  attr_reader :account
 
-  def initialize(accounts)
-    @accounts = accounts
+  def initialize(account)
+    @account = account
   end
 
-  def to_csv
+  def to_blocked_accounts_csv
+    to_csv account.blocking
+  end
+
+  def to_muted_accounts_csv
+    to_csv account.muting
+  end
+
+  def to_following_accounts_csv
+    to_csv account.following
+  end
+
+  def total_storage
+    account.media_attachments.sum(:file_file_size)
+  end
+
+  def total_follows
+    account.following.count
+  end
+
+  def total_blocks
+    account.blocking.count
+  end
+
+  def total_mutes
+    account.muting.count
+  end
+
+  private
+
+  def to_csv(accounts)
     CSV.generate do |csv|
       accounts.each do |account|
         csv << [(account.local? ? account.local_username_and_domain : account.acct)]
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index ba1c3bae7..22901aed1 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -62,11 +62,11 @@
           = number_to_human_size @account.media_attachments.sum('file_file_size')
 
 - if @account.silenced?
-  = link_to 'Undo silence', unsilence_admin_account_path(@account.id), method: :post, class: 'button'
+  = link_to 'Undo silence', admin_account_silence_path(@account.id), method: :delete, class: 'button'
 - else
-  = link_to 'Silence', silence_admin_account_path(@account.id), method: :post, class: 'button'
+  = link_to 'Silence', admin_account_silence_path(@account.id), method: :post, class: 'button'
 
 - if @account.suspended?
-  = link_to 'Undo suspension', unsuspend_admin_account_path(@account.id), method: :post, class: 'button'
+  = link_to 'Undo suspension', admin_account_suspension_path(@account.id), method: :delete, class: 'button'
 - else
-  = link_to 'Perform full suspension', suspend_admin_account_path(@account.id), method: :post, data: { confirm: 'Are you sure?' }, class: 'button'
+  = link_to 'Perform full suspension', admin_account_suspension_path(@account.id), method: :post, data: { confirm: 'Are you sure?' }, class: 'button'
diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml
index 8bf998554..1deff82b2 100644
--- a/app/views/auth/sessions/two_factor.html.haml
+++ b/app/views/auth/sessions/two_factor.html.haml
@@ -2,7 +2,7 @@
   = t('auth.login')
 
 = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
-  = f.input :otp_attempt, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt') }, required: true, autofocus: true, autocomplete: 'off'
+  = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt') }, required: true, autofocus: true, autocomplete: 'off'
 
   .actions
     = f.button :button, t('auth.login'), type: :submit
diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml
index 51be40fb6..f2f6f9556 100644
--- a/app/views/settings/exports/show.html.haml
+++ b/app/views/settings/exports/show.html.haml
@@ -5,17 +5,17 @@
   %tbody
     %tr
       %th= t('exports.storage')
-      %td= number_to_human_size @total_storage
+      %td= number_to_human_size @export.total_storage
       %td
     %tr
       %th= t('exports.follows')
-      %td= @total_follows
+      %td= @export.total_follows
       %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
     %tr
       %th= t('exports.blocks')
-      %td= @total_blocks
+      %td= @export.total_blocks
       %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv)
     %tr
       %th= t('exports.mutes')
-      %td= @total_mutes
+      %td= @export.total_mutes
       %td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)
diff --git a/app/views/xrd/host_meta.xml.ruby b/app/views/well_known/host_meta/show.xml.ruby
index 07d026471..07d026471 100644
--- a/app/views/xrd/host_meta.xml.ruby
+++ b/app/views/well_known/host_meta/show.xml.ruby
diff --git a/app/views/xrd/webfinger.json.rabl b/app/views/well_known/webfinger/show.json.rabl
index e637ed9d3..e637ed9d3 100644
--- a/app/views/xrd/webfinger.json.rabl
+++ b/app/views/well_known/webfinger/show.json.rabl
diff --git a/app/views/xrd/webfinger.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby
index 80ac71d27..80ac71d27 100644
--- a/app/views/xrd/webfinger.xml.ruby
+++ b/app/views/well_known/webfinger/show.xml.ruby
diff --git a/config/application.rb b/config/application.rb
index 2c720474a..1383d45a5 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -27,6 +27,7 @@ module Mastodon
 
     config.i18n.available_locales = [
       :en,
+      :bg,
       :de,
       :eo,
       :es,
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
new file mode 100644
index 000000000..a8687f3ca
--- /dev/null
+++ b/config/locales/bg.yml
@@ -0,0 +1,169 @@
+---
+bg:
+  about:
+    about_mastodon: Mastodon е <em>безплатен</em> сървър с <em>отворен код</em> за социални мрежи. Като <em>децентрализирана</em> алтернатива на комерсиалните платформи, той позволява избягването на риска от монополизация на твоята комуникация от единични компании. Изберете си сървър, на който се доверявате, и ще можете да контактувате с всички останали. Всеки може да пусне Mastodon и лесно да вземе участие в <em>социалната мрежа</em>.
+    about_this: За тази инстанция
+    apps: Приложения
+    business_email: 'Служебен e-mail:'
+    closed_registrations: В момента регистрациите за тази инстанция са затворени.
+    contact: За контакти
+    description_headline: Какво е %{domain}?
+    domain_count_after: други инстанции
+    domain_count_before: Свързани към
+    features:
+      api: Отворено API за приложения и услуги
+      blocks: Богат на инструменти за блокиране и заглушаване
+      characters: Публикации от 500 символа
+      chronology: Публикациите се показват хронологично
+      ethics: 'Етичен дизайн: без реклами и проследяване'
+      gifv: GIFV комплекти и кратки видео клипове
+      privacy: Настройване на поверителността за всяка публикация
+      public: Публични канали
+    features_headline: Какво откроява Mastodon
+    get_started: Първи стъпки
+    links: Връзки
+    other_instances: Други инстанции
+    source_code: Програмен код
+    status_count_after: публикации
+    status_count_before: Написали
+    terms: Условия
+    user_count_after: потребители
+    user_count_before: Дом на
+  accounts:
+    follow: Последвай
+    followers: Последователи
+    following: Следва
+    nothing_here: Тук няма никого!
+    people_followed_by: Хора, които %{name} следва
+    people_who_follow: Хора, които следват %{name}
+    posts: Публикации
+    remote_follow: Последвай
+    unfollow: Не следвай
+  application_mailer:
+    settings: 'Промяна на предпочитанията за e-mail: %{link}'
+    signature: Mastodon известия от %{instance}
+    view: 'Преглед:'
+  applications:
+    invalid_url: Предоставеният URL е невалиден
+  auth:
+    change_password: Идентификационни данни
+    didnt_get_confirmation: Не получих инструкции за потвърждение
+    forgot_password: Забравих си паролата
+    login: Влизане
+    logout: Излизане
+    register: Регистрация
+    resend_confirmation: Изпрати отново инструкции за потвърждение
+    reset_password: Подновяване на паролата
+    set_new_password: Задай нова парола
+  authorize_follow:
+    error: Възникна грешка в откриването на потребителя
+    follow: Последвай
+    prompt_html: '(<strong>%{self}</strong>), молбата ти беше изпратена до:'
+    title: Последвай %{acct}
+  datetime:
+    distance_in_words:
+      about_x_hours: "%{count} ч."
+      about_x_months: "%{count} м."
+      about_x_years: "%{count} г."
+      almost_x_years: "%{count} г."
+      half_a_minute: Току-що
+      less_than_x_minutes: "%{count} мин."
+      less_than_x_seconds: Току-що
+      over_x_years: "%{count} г."
+      x_days: "%{count} дни"
+      x_minutes: "%{count} мин."
+      x_months: "%{count} м."
+      x_seconds: "%{count} сек."
+  exports:
+    blocks: Вашите блокирания
+    csv: CSV
+    follows: Вашите следвания
+    storage: Съхранение на мултимедия
+  generic:
+    changes_saved_msg: Успешно запазване на промените!
+    powered_by: поддържано от %{link}
+    save_changes: Запази промените
+    validation_errors:
+      one: Нещо все още не е наред! Моля, прегледай грешката по-долу
+      other: Нещо все още не е наред! Моля, прегледай грешките по-долу
+  imports:
+    preface: Можеш да импортираш някои данни, като например всички хора, които следваш или блокираш в акаунта си на тази инстанция, от файлове, създадени чрез експорт в друга инстанция.
+    success: Твоите данни бяха успешно качени и ще бъдат обработени впоследствие.
+    types:
+      blocking: Списък на блокираните
+      following: Списък на последователите
+    upload: Качване
+  landing_strip_html: <strong>%{name}</strong> е потребител от <strong>%{domain}</strong>. Можеш да ги следваш, или да контактуваш с тях, ако имаш акаунт където и да е из федерираната вселена на Mastodon. Ако нямаш акаунт, можеш да си <a href="%{sign_up_path}">създадеш ето тук</a>.
+  notification_mailer:
+    digest:
+      body: 'Ето кратко резюме на нещата, които се случиха от последното ти посещение в %{instance} на %{since}:'
+      mention: "%{name} те спомена в:"
+      new_followers_summary:
+        one: Имаш един нов последовател! Ура!
+        other: Имаш %{count} нови последователи! Изумително!
+      subject:
+        one: "1 ново известие от последното ти посещение \U0001F418"
+        other: "%{count} нови известия от последното ти посещение \U0001F418"
+    favourite:
+      body: 'Публикацията ти беше харесана от %{name}:'
+      subject: "%{name} хареса твоята публикация"
+    follow:
+      body: "%{name} те последва!"
+      subject: "%{name} те последва"
+    follow_request:
+      body: "%{name} помоли за разрешение да те последва"
+      subject: 'Чакащ последовател: %{name}'
+    mention:
+      body: '%{name} те спомена в:'
+      subject: '%{name} те спомена'
+    reblog:
+      body: 'Твоята публикация беше споделена от %{name}:'
+      subject: "%{name} сподели публикацията ти"
+  pagination:
+    next: Напред
+    prev: Назад
+  remote_follow:
+    acct: Въведи потребителско_име@домейн, от които искаш да следваш
+    missing_resource: Неуспешно търсене на нужния URL за пренасочване за твоя акаунт
+    proceed: Започни следване
+    prompt: 'Ще последваш:'
+  settings:
+    authorized_apps: Упълномощени приложения
+    back: Обратно към Mastodon
+    edit_profile: Редактирай профила си
+    export: Експортиране на данни
+    import: Импортиране
+    preferences: Предпочитания
+    settings: Настройки
+    two_factor_auth: Двустепенно удостоверяване
+  statuses:
+    open_in_web: Отвори в уеб
+    over_character_limit: прехвърлен лимит от %{max} символа
+    show_more: Покажи повече
+    visibilities:
+      private: Покажи само на последователите си
+      public: Публично
+      unlisted: Публично, но не показвай в публичния канал
+  stream_entries:
+    click_to_show: Покажи
+    reblogged: споделено
+    sensitive_content: Деликатно съдържание
+  time:
+    formats:
+      default: "%d %b, %Y, %H:%M"
+  two_factor_auth:
+    description_html: При активация на <strong>двустепенно удостоверяване</strong>, за да влезеш в приложението, ще трябва да използваш телефона си. През него ще се генерира код, който да въвеждаш при влизане.
+    disable: Деактивирай
+    enable: Активирай
+    instructions_html: "<strong>Сканирай този QR код с Google Authenticator или подобно приложение от своя телефон</strong>. Oтсега нататък, това приложение ще генерира код, който ще трябва да въвеждаш при всяко влизане."
+    plaintext_secret_html: "Тайна в обикновен текст: <samp>%{secret}</samp>"
+    warning: Ако не можеш да настроиш приложението за удостверяване сега, избери "Деактивирай". В противен случай, няма да можеш да влезеш в акаунта си.
+  users:
+    invalid_email: E-mail адресът е невалиден
+    invalid_otp_token: Невалиден код
+  will_paginate:
+    page_gap: "&hellip;"
+  media_attachments:
+    validations:
+      too_many: Не мога да прикача повече от 4 файла
+      images_and_video: Не мога да прикача видеоклип към публикация, която вече съдържа изображения
diff --git a/config/locales/devise.bg.yml b/config/locales/devise.bg.yml
new file mode 100644
index 000000000..7485b8236
--- /dev/null
+++ b/config/locales/devise.bg.yml
@@ -0,0 +1,61 @@
+---
+bg:
+  devise:
+    confirmations:
+      confirmed: Твоят профил беше успешно потвърден. Влизането в профила е успешно.
+      send_instructions: Ще получиш писмо с инструкции как да потвърдиш своя профил до няколко минути.
+      send_paranoid_instructions: Ако твоят имейл адрес съществува в базата ни, ще получиш там инструкции как да потвърдиш своя профил.
+    failure:
+      already_authenticated: Вече си вътре в профила си.
+      inactive: Профилът ти все още не е активиран.
+      invalid: Невалиден имейл адрес или парола.
+      last_attempt: Разполагаш с още един опит преди профилът ти да бъде заключен.
+      locked: Профилът ти е заключен.
+      not_found_in_database: "Невалидни стойности за %{authentication_keys} или парола."
+      timeout: Сесията ти изтече, моля влез отново, за да продължиш.
+      unauthenticated: Преди да продължиш, трябва да влезеш в профила си или да се регистрираш.
+      unconfirmed: Преди да продължиш, трябва да потвърдиш регистрацията си.
+    mailer:
+      confirmation_instructions:
+        subject: 'Mastodon: Инструкции за потвърждаване'
+      password_change:
+        subject: 'Mastodon: Паролата е променена'
+      reset_password_instructions:
+        subject: 'Инструкции за смяна на паролата'
+      unlock_instructions:
+        subject: 'Инструкции за отключване'
+    omniauth_callbacks:
+      failure: "Не успяхме да те упълномощим чрез %{kind}, защото \"%{reason}\"."
+      success: "Успешно упълномощаване чрез %{kind} профил."
+    passwords:
+      no_token: Може да достъпваш тази страница само от имейл за промяна на паролата. Ако тази страница е отворена от такъв имейл, увери се, че използваш целия URL-адрес, който сме ти изпратили.
+      send_instructions: Ще получиш писмо с инструкции как да промениш паролата си до няколко минути.
+      send_paranoid_instructions: Ако твоят имейл адрес съществува в базата ни, ще получиш там инструкции за промяна на своята парола.
+      updated: Паролата ти беше променена успешно. Влизането в профила е успешно.
+      updated_not_active: Паролата ти беше променена успешно.
+    registrations:
+      destroyed: Довиждане! Твоят профил беше успешно изтрит. Надяваме се скоро да те видим отново.
+      signed_up: Привет! Регистрирацията ти е успешна.
+      signed_up_but_inactive: Регистрирацията ти е успешна. Въпреки това, не можеш да влезеш в профила си, защото той все още не е потвърден.
+      signed_up_but_locked: Регистрирацията ти е успешна. Въпреки това, не можеш да влезеш в профила си, защото той е заключен.
+      signed_up_but_unconfirmed: Писмо с връзка за потвърждаване на профила ти беше изпратено на твоя имейл адрес. Моля, отвори връзката, за да активираш своя профил.
+      update_needs_confirmation: Профилът ти е успешно променен, но ние трябва да проверим твоя нов имейл адрес. Моля, провери пощата си и отвори връзката за потвърждаване на новия адрес.
+      updated: Профилът ти е успешно променен.
+    sessions:
+      already_signed_out: Успешно излизане от профила.
+      signed_in: Успешно влизане.
+      signed_out: Успешно излизане.
+    unlocks:
+      send_instructions: Ще получиш писмо с инструкции как да отключиш профила си до няколко минути.
+      send_paranoid_instructions: Ако твоят профил съществува в базата ни, на своя имейл адрес ще получиш инструкции за отключването му до няколко минути.
+      unlocked: Твоят профил беше отключен успешно. За да продължиш, влез в него.
+  errors:
+    messages:
+      already_confirmed: е вече потвърден, моля опитай да влезеш в профила си с него
+      confirmation_period_expired: "трябва да се потвърди в рамките на %{period}, моля направи нова заявка за потвърждение"
+      expired: е изтекъл, моля заяви нов
+      not_found: не е намерен
+      not_locked: не бе заключен
+      not_saved:
+        one: "Една грешка попречи този %{resource} да бъде записан:"
+        other: "%{count} грешки попречиха този %{resource} да бъде записан:"
diff --git a/config/locales/doorkeeper.bg.yml b/config/locales/doorkeeper.bg.yml
new file mode 100644
index 000000000..6fafdfc55
--- /dev/null
+++ b/config/locales/doorkeeper.bg.yml
@@ -0,0 +1,113 @@
+---
+bg:
+  activerecord:
+    attributes:
+      doorkeeper/application:
+        name: Име
+        redirect_uri: URI за пренасочване
+    errors:
+      models:
+        doorkeeper/application:
+          attributes:
+            redirect_uri:
+              fragment_present: не може да съдържа фрагмент.
+              invalid_uri: трябва да е валидно URI.
+              relative_uri: трябва да е абсолютно URI.
+              secured_uri: трябва да е HTTPS/SSL URI.
+  doorkeeper:
+    applications:
+      buttons:
+        authorize: Упълномощаване
+        cancel: Отказ
+        destroy: Унищожаване
+        edit: Редакция
+        submit: Изпращане
+      confirmations:
+        destroy: Потвърждаваш ли изтриването?
+      edit:
+        title: Редактиране на приложението
+      form:
+        error: О, не! Провери формата за възможни грешки
+      help:
+        native_redirect_uri: Изполвай %{native_redirect_uri} за локални тестове
+        redirect_uri: Използвай един ред за всяко URI
+        scopes: Разделяй диапазоните с интервал. Остави празно, за да използваш диапазона по подразбиране.
+      index:
+        callback_url: URL за обратно повикване
+        name: Име
+        new: Ново приложение
+        title: Твоите приложения
+      new:
+        title: Ново приложение
+      show:
+        actions: Действия
+        application_id: Идентификатор на приложението
+        callback_urls: URL-и за обратно повикване
+        scopes: Диапазони
+        secret: Тайна
+        title: 'Приложение: %{name}'
+    authorizations:
+      buttons:
+        authorize: Упълномощаване
+        deny: Отказ
+      error:
+        title: Възникна грешка
+      new:
+        able_to: Ще е възможно
+        prompt: Приложението %{client_name} заявява достъп до твоя акаунт
+        title: Изисква се упълномощаване
+      show:
+        title: Код за упълномощаване
+    authorized_applications:
+      buttons:
+        revoke: Отмяна
+      confirmations:
+        revoke: Потвърждаваш ли отмяната?
+      index:
+        application: Приложение
+        created_at: Създадено на
+        date_format: "%Y-%m-%d %H:%M:%S"
+        scopes: Диапазони
+        title: Твоите упълномощени приложения
+    errors:
+      messages:
+        access_denied: Заявката беше отказана от собственика на ресурса или от сървъра за упълномощаване.
+        credential_flow_not_configured: Resource Owner Password Credentials предизвика грешка, заради това, че настройките за Doorkeeper.configure.resource_owner_from_credentials липсват.
+        invalid_client: Удостоверяването на клиента предизвика грешка, поради непознат клиент, липсващо клиентско удостоверяване, или заради това, че методът на удостоверяване не се поддържа.
+        invalid_grant: Предоставеното удостоверение за достъп е невалидно, изтекло, отхвърлено, не съвпада с пренасочващото URI, използвано в заявката за удостоверение, или е бил издадено от друг клиент.
+        invalid_redirect_uri: Наличното пренасочващо URI е невалидно.
+        invalid_request: Заявката е с липсващ задължителен параметър, включва стойност на параметъра, която не се поддържа, или е изкривена по друг начин.
+        invalid_resource_owner: Предоставените идентификационни данни на притежателя на ресурса са невалидни, или притежателят не може да бъде намерен.
+        invalid_scope: Заявеният диапазон е невалиден, неизвестен или изкривен.
+        invalid_token:
+          expired: Маркерът за достъп изтече
+          revoked: Маркерът за достъп беше отхвърлен
+          unknown: Маркерът за достъп е невалиден
+        resource_owner_authenticator_not_configured: Намирането на Resource Owner се провали поради липса на конфигурация на Doorkeeper.configure.resource_owner_authenticator.
+        server_error: Сървърът за удостоверяване попадна на неочаквано условие, което предотврати изпълнението на заявката.
+        temporarily_unavailable: Сървърът за удостоверяване не може да се справи със заявката в момента поради временно претоварване или профилактика на сървъра.
+        unauthorized_client: Клиентът не е удостоверен да изпълни заявката по този начин.
+        unsupported_grant_type: Типът на удостоврението за достъп не се поддържа от сървъра за удостоверяване.
+        unsupported_response_type: Удостоверяващият сървър не поддържа този тип отговор.
+    flash:
+      applications:
+        create:
+          notice: Приложението е създадено.
+        destroy:
+          notice: Приложението е изтрито.
+        update:
+          notice: Приложението е обновено.
+      authorized_applications:
+        destroy:
+          notice: Приложението е отказано.
+    layouts:
+      admin:
+        nav:
+          applications: Приложения
+          oauth2_provider: OAuth2 доставчик
+      application:
+        title: Нужно е упълномощаване по OAuth
+    scopes:
+      follow: следването, блокирането, деблокирането и отмяната на следването на акаунтите
+      read: четенето на данните от твоя акаунт
+      write: публикуването от твое име
diff --git a/config/locales/doorkeeper.ja.yml b/config/locales/doorkeeper.ja.yml
index 35592bc49..d3ea93789 100644
--- a/config/locales/doorkeeper.ja.yml
+++ b/config/locales/doorkeeper.ja.yml
@@ -25,7 +25,7 @@ ja:
       confirmations:
         destroy: 本当に削除しますか?
       edit:
-        title: アプリケーションの編集
+        title: アプリの編集
       form:
         error: フォームにエラーが無いか確認してください。
       help:
@@ -35,17 +35,17 @@ ja:
       index:
         callback_url: コールバックURL
         name: 名前
-        new: 新規アプリケーション
-        title: あなたのアプリケーション
+        new: 新規アプリ
+        title: アプリ
       new:
-        title: 新規アプリケーション
+        title: 新規アプリ
       show:
         actions: アクション
         application_id: アクションId
         callback_urls: コールバックurl
         scopes: アクセス権
         secret: 非公開
-        title: 'アプリケーション: %{name}'
+        title: 'アプリ: %{name}'
     authorizations:
       buttons:
         authorize: 承認
@@ -53,8 +53,8 @@ ja:
       error:
         title: エラーが発生しました。
       new:
-        able_to: このアプリケーションは以下のことができます
-        prompt: アプリケーション %{client_name} があなたのアカウントへのアクセスを要求しています。
+        able_to: このアプリは以下のことができます
+        prompt: アプリ %{client_name} があなたのアカウントへのアクセスを要求しています。
         title: 認証が必要です。
       show:
         title: 認証コード
@@ -68,7 +68,7 @@ ja:
         created_at: 許可した日時
         date_format: "%Y年%m月%d日 %H時%M分%S秒"
         scopes: アクセス権
-        title: 認証済みアプリケーション
+        title: 認証済みアプリ
     errors:
       messages:
         access_denied: リソースの所有者または認証サーバーが要求を拒否しました。
@@ -92,22 +92,22 @@ ja:
     flash:
       applications:
         create:
-          notice: アプリケーションが作成されました。
+          notice: アプリが作成されました。
         destroy:
-          notice: アプリケーションが削除されました。
+          notice: アプリが削除されました。
         update:
-          notice: アプリケーションが更新されました。
+          notice: アプリが更新されました。
       authorized_applications:
         destroy:
-          notice: アプリケーションが取り消されました。
+          notice: アプリが取り消されました。
     layouts:
       admin:
         nav:
-          applications: アプリケーション
+          applications: アプリ
           oauth2_provider: OAuth2プロバイダー
       application:
-        title: OAuth認証が必要です。
+        title: OAuth認証
     scopes:
       follow: アカウントのフォロー, ブロック, ブロック解除, フォロー解除
-      read: アカウントへのデータの読み取り
-      write: アカウントからの投稿の書き込み
+      read: アカウントからのデータの読み取り
+      write: アカウントへのデータの書き込み
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 9407c7669..cd6b6543d 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -46,7 +46,7 @@ ja:
   applications:
     invalid_url: URLが無効です
   auth:
-    change_password: 資格情報
+    change_password: ログイン情報
     didnt_get_confirmation: 確認メールを受信できませんか?
     forgot_password: パスワードをお忘れですか?
     login: ログイン
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index fab178629..0c2725855 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -9,7 +9,7 @@ ru:
     contact: Связаться
     description_headline: Что такое %{domain}?
     domain_count_after: другими узлами
-    domain_count_before: Связывается с
+    domain_count_before: Связан с
     features:
       api: Открытый API для приложений и сервисов
       blocks: Продвинутые инструменты блокирования и глушения
@@ -25,7 +25,7 @@ ru:
     other_instances: Другие узлы
     source_code: Исходный код
     status_count_after: статусов
-    status_count_before: Автор
+    status_count_before: Опубликовано
     terms: Условия
     user_count_after: пользователей
     user_count_before: Здесь живет
@@ -42,7 +42,7 @@ ru:
   application_mailer:
     settings: 'Изменить настройки e-mail: %{link}'
     signature: Уведомления Mastodon от %{instance}
-    view: 'View:'
+    view: 'Просмотр:'
   applications:
     invalid_url: Введенный URL неверен
   auth:
@@ -126,7 +126,7 @@ ru:
     acct: Введите username@domain, откуда Вы хотите подписаться
     missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей
     proceed: Продолжить подписку
-    prompt: 'Вы ходите подписаться на:'
+    prompt: 'Вы хотите подписаться на:'
   settings:
     authorized_apps: Авторизованные приложения
     back: Назад в Mastodon
@@ -142,8 +142,8 @@ ru:
     show_more: Подробнее
     visibilities:
       private: Показывать только подписчикам
-      public: Публичный
-      unlisted: Публичный, но без отображения в публичных лентах
+      public: Показывать всем
+      unlisted: Показывать всем, но не отображать в публичных лентах
   stream_entries:
     click_to_show: Показать
     reblogged: продвинул(а)
@@ -156,8 +156,13 @@ ru:
     disable: Отключить
     enable: Включить
     instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа."
+    manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
     plaintext_secret_html: 'Секрет открытым текстом: <samp>%{secret}</samp>'
-    warning: Если сейчас у Вас не получается настроить аутентификатор, нажмите "отключить", иначе Вы не сможете войти!
+    code_hint: 'Для подтверждения введите код, сгенерированный приложением аутентификатора'
+    setup: Настроить
+    warning: 'Если сейчас у Вас не получается настроить аутентификатор, нажмите "отключить", иначе Вы не сможете войти!'
   users:
     invalid_email: Введенный e-mail неверен
     invalid_otp_token: Введен неверный код
+  will_paginate:
+    page_gap: "&hellip;"
diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml
new file mode 100644
index 000000000..55b80277d
--- /dev/null
+++ b/config/locales/simple_form.bg.yml
@@ -0,0 +1,46 @@
+---
+bg:
+  simple_form:
+    hints:
+      defaults:
+        avatar: PNG, GIF или JPG. До 2MB. Ще бъде смалена до 120x120 пиксела
+        display_name: До 30 символа
+        header: PNG, GIF или JPG. До 2MB. Ще бъде смалена до 700x335 пиксела
+        locked: Изисква ръчно одобрение на последователите. По подразбиране, публикациите са достъпни само до последователи.
+        note: До 160 символа
+      imports:
+        data: CSV файл, експортиран от друга инстанция на Mastodon
+    labels:
+      defaults:
+        avatar: Аватар
+        confirm_new_password: Потвърди новата парола
+        confirm_password: Потвърди паролата
+        current_password: Текуща парола
+        data: Данни
+        display_name: Показвано име
+        email: E-mail адрес
+        header: Заглавен ред
+        locale: Език
+        locked: Направи акаунта поверителен
+        new_password: Нова парола
+        note: Био
+        otp_attempt: Двустепенен код
+        password: Парола
+        setting_default_privacy: Поверителност на публикациите
+        type: Тип на импортиране
+        username: Потребителско име
+      interactions:
+        must_be_follower: Блокирай известия от не-последователи
+        must_be_following: Блокирай известия от хора, които не следваш
+      notification_emails:
+        digest: Изпращай извлечения на съобщенията
+        favourite: Изпращай e-mail, когато някой хареса твоя публикация
+        follow: Изпращай e-mail, когато някой те последва
+        follow_request: Изпращай e-mail, когато някой пожелае да те последва
+        mention: Изпращай e-mail, когато някой те спомене
+        reblog: Изпращай e-mail, когато някой сподели твоя публикация
+    'no': 'Не'
+    required:
+      mark: "*"
+      text: задължително
+    'yes': 'Да'
diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml
index 6f4873bfd..b7d8e4e05 100644
--- a/config/locales/simple_form.ru.yml
+++ b/config/locales/simple_form.ru.yml
@@ -26,7 +26,7 @@ ru:
         note: О Вас
         otp_attempt: Двухфакторный код
         password: Пароль
-        setting_default_privacy: Приватность постов
+        setting_default_privacy: Видимость постов
         type: Тип импорта
         username: Имя пользователя
       interactions:
diff --git a/config/routes.rb b/config/routes.rb
index 78bf7870c..6e48d3b92 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,3 +1,4 @@
+
 # frozen_string_literal: true
 
 require 'sidekiq/web'
@@ -14,8 +15,8 @@ Rails.application.routes.draw do
     controllers authorizations: 'oauth/authorizations', authorized_applications: 'oauth/authorized_applications'
   end
 
-  get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta
-  get '.well-known/webfinger', to: 'xrd#webfinger', as: :webfinger, defaults: { format: 'json' }
+  get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' }
+  get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger, defaults: { format: 'json' }
 
   devise_for :users, path: 'auth', controllers: {
     sessions:           'auth/sessions',
@@ -89,12 +90,8 @@ Rails.application.routes.draw do
     end
 
     resources :accounts, only: [:index, :show] do
-      member do
-        post :silence
-        post :unsilence
-        post :suspend
-        post :unsuspend
-      end
+      resource :silence, only: [:create, :destroy]
+      resource :suspension, only: [:create, :destroy]
     end
   end
 
diff --git a/spec/controllers/admin/silences_controller_spec.rb b/spec/controllers/admin/silences_controller_spec.rb
new file mode 100644
index 000000000..7c541d797
--- /dev/null
+++ b/spec/controllers/admin/silences_controller_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+describe Admin::SilencesController do
+  let(:account) { Fabricate(:account) }
+  before do
+    sign_in Fabricate(:user, admin: true), scope: :user
+  end
+
+  describe 'POST #create' do
+    it 'redirects to admin accounts page' do
+      post :create, params: { account_id: account.id }
+
+      expect(response).to redirect_to(admin_accounts_path)
+    end
+  end
+
+  describe 'DELETE #destroy' do
+    it 'redirects to admin accounts page' do
+      delete :destroy, params: { account_id: account.id }
+
+      expect(response).to redirect_to(admin_accounts_path)
+    end
+  end
+end
diff --git a/spec/controllers/admin/suspensions_controller_spec.rb b/spec/controllers/admin/suspensions_controller_spec.rb
new file mode 100644
index 000000000..9096f067e
--- /dev/null
+++ b/spec/controllers/admin/suspensions_controller_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+describe Admin::SuspensionsController do
+  let(:account) { Fabricate(:account) }
+  before do
+    sign_in Fabricate(:user, admin: true), scope: :user
+  end
+
+  describe 'POST #create' do
+    it 'redirects to admin accounts page' do
+      post :create, params: { account_id: account.id }
+
+      expect(response).to redirect_to(admin_accounts_path)
+    end
+  end
+
+  describe 'DELETE #destroy' do
+    it 'redirects to admin accounts page' do
+      delete :destroy, params: { account_id: account.id }
+
+      expect(response).to redirect_to(admin_accounts_path)
+    end
+  end
+end
diff --git a/spec/controllers/settings/exports_controller_spec.rb b/spec/controllers/settings/exports_controller_spec.rb
index ff98f3ad9..2be6e4744 100644
--- a/spec/controllers/settings/exports_controller_spec.rb
+++ b/spec/controllers/settings/exports_controller_spec.rb
@@ -1,6 +1,8 @@
 require 'rails_helper'
 
 describe Settings::ExportsController do
+  render_views
+
   before do
     sign_in Fabricate(:user), scope: :user
   end
@@ -8,6 +10,7 @@ describe Settings::ExportsController do
   describe 'GET #show' do
     it 'returns http success' do
       get :show
+
       expect(response).to have_http_status(:success)
     end
   end
diff --git a/spec/controllers/well_known/host_meta_controller_spec.rb b/spec/controllers/well_known/host_meta_controller_spec.rb
new file mode 100644
index 000000000..8a040021a
--- /dev/null
+++ b/spec/controllers/well_known/host_meta_controller_spec.rb
@@ -0,0 +1,13 @@
+require 'rails_helper'
+
+describe WellKnown::HostMetaController, type: :controller do
+  render_views
+
+  describe 'GET #show' do
+    it 'returns http success' do
+      get :show, format: :xml
+
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb
new file mode 100644
index 000000000..6e493b037
--- /dev/null
+++ b/spec/controllers/well_known/webfinger_controller_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+describe WellKnown::WebfingerController, type: :controller do
+  render_views
+
+  describe 'GET #show' do
+    let(:alice) { Fabricate(:account, username: 'alice') }
+
+    it 'returns http success when account can be found' do
+      get :show, params: { resource: alice.to_webfinger_s }, format: :json
+
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'returns http not found when account cannot be found' do
+      get :show, params: { resource: 'acct:not@existing.com' }, format: :json
+
+      expect(response).to have_http_status(:not_found)
+    end
+  end
+end
diff --git a/spec/controllers/xrd_controller_spec.rb b/spec/controllers/xrd_controller_spec.rb
deleted file mode 100644
index 33b17f152..000000000
--- a/spec/controllers/xrd_controller_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe XrdController, type: :controller do
-  render_views
-
-  describe 'GET #host_meta' do
-    it 'returns http success' do
-      get :host_meta
-      expect(response).to have_http_status(:success)
-    end
-  end
-
-  describe 'GET #webfinger' do
-    let(:alice) { Fabricate(:account, username: 'alice') }
-
-    it 'returns http success when account can be found' do
-      get :webfinger, params: { resource: alice.to_webfinger_s }, format: :json
-      expect(response).to have_http_status(:success)
-    end
-
-    it 'returns http not found when account cannot be found' do
-      get :webfinger, params: { resource: 'acct:not@existing.com' }, format: :json
-      expect(response).to have_http_status(:not_found)
-    end
-  end
-end
diff --git a/spec/models/account_filter_spec.rb b/spec/models/account_filter_spec.rb
new file mode 100644
index 000000000..1599c5ae8
--- /dev/null
+++ b/spec/models/account_filter_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe AccountFilter do
+  describe 'with empty params' do
+    it 'defaults to alphabetic account list' do
+      filter = AccountFilter.new({})
+
+      expect(filter.results).to eq Account.alphabetic
+    end
+  end
+
+  describe 'with invalid params' do
+    it 'raises with key error' do
+      filter = AccountFilter.new(wrong: true)
+
+      expect { filter.results }.to raise_error(/wrong/)
+    end
+  end
+
+  describe 'with valid params' do
+    it 'combines filters on Account' do
+      filter = AccountFilter.new(by_domain: 'test.com', silenced: true)
+
+      allow(Account).to receive(:where).and_return(Account.none)
+      allow(Account).to receive(:silenced).and_return(Account.none)
+      filter.results
+      expect(Account).to have_received(:where).with(domain: 'test.com')
+      expect(Account).to have_received(:silenced)
+    end
+  end
+end
diff --git a/spec/requests/host_meta_request_spec.rb b/spec/requests/host_meta_request_spec.rb
new file mode 100644
index 000000000..0c51b5f48
--- /dev/null
+++ b/spec/requests/host_meta_request_spec.rb
@@ -0,0 +1,12 @@
+require "rails_helper"
+
+describe "The host_meta route" do
+  describe "requested without accepts headers" do
+    it "returns an xml response" do
+      get host_meta_url
+
+      expect(response).to have_http_status(:success)
+      expect(response.content_type).to eq "application/xrd+xml"
+    end
+  end
+end
diff --git a/spec/routing/well_known_routes_spec.rb b/spec/routing/well_known_routes_spec.rb
new file mode 100644
index 000000000..9540c3de3
--- /dev/null
+++ b/spec/routing/well_known_routes_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+describe 'the host-meta route' do
+  it 'routes to correct place with xml format' do
+    expect(get('/.well-known/host-meta')).
+      to route_to('well_known/host_meta#show', format: 'xml')
+  end
+end
+
+describe 'the webfinger route' do
+  it 'routes to correct place with json format' do
+    expect(get('/.well-known/webfinger')).
+      to route_to('well_known/webfinger#show', format: 'json')
+  end
+end