about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.github/CODEOWNERS (renamed from CODEOWNERS)0
-rw-r--r--.github/ISSUE_TEMPLATE.md (renamed from ISSUE_TEMPLATE.md)0
-rw-r--r--app/controllers/admin/accounts_controller.rb3
-rw-r--r--app/controllers/admin/custom_emojis_controller.rb4
-rw-r--r--app/helpers/admin/filter_helper.rb9
-rw-r--r--app/javascript/mastodon/components/avatar.js5
-rw-r--r--app/javascript/mastodon/components/avatar_overlay.js13
-rw-r--r--app/javascript/mastodon/features/compose/components/compose_form.js12
-rw-r--r--app/javascript/mastodon/features/compose/components/reply_indicator.js8
-rw-r--r--app/javascript/mastodon/features/compose/components/upload.js2
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js2
-rw-r--r--app/javascript/mastodon/features/standalone/hashtag_timeline/index.js12
-rw-r--r--app/javascript/mastodon/features/standalone/public_timeline/index.js12
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js5
-rw-r--r--app/javascript/mastodon/locales/ar.json36
-rw-r--r--app/javascript/mastodon/locales/ca.json20
-rw-r--r--app/javascript/mastodon/locales/en.json2
-rw-r--r--app/javascript/mastodon/locales/fr.json2
-rw-r--r--app/javascript/mastodon/locales/gl.json20
-rw-r--r--app/javascript/mastodon/locales/ja.json16
-rw-r--r--app/javascript/mastodon/locales/nl.json20
-rw-r--r--app/javascript/mastodon/locales/oc.json2
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json24
-rw-r--r--app/javascript/mastodon/locales/pt.json34
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json2
-rw-r--r--app/javascript/mastodon/stream.js8
-rw-r--r--app/javascript/styles/mastodon/components.scss491
-rw-r--r--app/javascript/styles/mastodon/rtl.scss26
-rw-r--r--app/lib/provider_discovery.rb19
-rw-r--r--app/models/account_filter.rb2
-rw-r--r--app/models/custom_emoji_filter.rb2
-rw-r--r--app/services/fetch_link_card_service.rb56
-rw-r--r--app/services/fetch_remote_status_service.rb2
-rw-r--r--app/services/follow_service.rb2
-rw-r--r--app/views/admin/accounts/_account.html.haml17
-rw-r--r--app/views/admin/accounts/index.html.haml9
-rw-r--r--app/views/admin/accounts/show.html.haml3
-rw-r--r--app/views/admin/custom_emojis/_custom_emoji.html.haml2
-rw-r--r--app/views/admin/custom_emojis/index.html.haml14
-rw-r--r--config/initializers/ostatus.rb9
-rw-r--r--config/locales/ar.yml73
-rw-r--r--config/locales/ca.yml4
-rw-r--r--config/locales/devise.ar.yml3
-rw-r--r--config/locales/devise.ja.yml4
-rw-r--r--config/locales/devise.pt.yml2
-rw-r--r--config/locales/doorkeeper.gl.yml33
-rw-r--r--config/locales/en.yml4
-rw-r--r--config/locales/fr.yml21
-rw-r--r--config/locales/ja.yml55
-rw-r--r--config/locales/nl.yml5
-rw-r--r--config/locales/oc.yml9
-rw-r--r--config/locales/pl.yml5
-rw-r--r--config/locales/pt-BR.yml8
-rw-r--r--config/locales/simple_form.gl.yml75
-rw-r--r--config/locales/zh-CN.yml29
-rw-r--r--db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb6
-rw-r--r--db/schema.rb4
-rw-r--r--lib/mastodon/version.rb2
-rw-r--r--streaming/index.js102
59 files changed, 829 insertions, 542 deletions
diff --git a/CODEOWNERS b/.github/CODEOWNERS
index 32919bd50..32919bd50 100644
--- a/CODEOWNERS
+++ b/.github/CODEOWNERS
diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index c78bcb492..c78bcb492 100644
--- a/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index e9a512e70..7428c3f22 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -89,7 +89,8 @@ module Admin
         :username,
         :display_name,
         :email,
-        :ip
+        :ip,
+        :staff
       )
     end
   end
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index 3fa2a0b72..ccab03de4 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -92,7 +92,9 @@ module Admin
     def filter_params
       params.permit(
         :local,
-        :remote
+        :remote,
+        :by_domain,
+        :shortcode
       )
     end
   end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 9443934b3..359c43d0e 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -1,11 +1,12 @@
 # frozen_string_literal: true
 
 module Admin::FilterHelper
-  ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
-  REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
-  INVITE_FILTER = %i(available expired).freeze
+  ACCOUNT_FILTERS      = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze
+  REPORT_FILTERS       = %i(resolved account_id target_account_id).freeze
+  INVITE_FILTER        = %i(available expired).freeze
+  CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
 
-  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER
+  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
     new_url = filtered_url_for(link_to_params)
diff --git a/app/javascript/mastodon/components/avatar.js b/app/javascript/mastodon/components/avatar.js
index f7c484ee3..570505833 100644
--- a/app/javascript/mastodon/components/avatar.js
+++ b/app/javascript/mastodon/components/avatar.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from '../initial_state';
 
 export default class Avatar extends React.PureComponent {
 
@@ -8,12 +9,12 @@ export default class Avatar extends React.PureComponent {
     account: ImmutablePropTypes.map.isRequired,
     size: PropTypes.number.isRequired,
     style: PropTypes.object,
-    animate: PropTypes.bool,
     inline: PropTypes.bool,
+    animate: PropTypes.bool,
   };
 
   static defaultProps = {
-    animate: false,
+    animate: autoPlayGif,
     size: 20,
     inline: false,
   };
diff --git a/app/javascript/mastodon/components/avatar_overlay.js b/app/javascript/mastodon/components/avatar_overlay.js
index f5d67b34e..3ec1d7730 100644
--- a/app/javascript/mastodon/components/avatar_overlay.js
+++ b/app/javascript/mastodon/components/avatar_overlay.js
@@ -1,22 +1,29 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from '../initial_state';
 
 export default class AvatarOverlay extends React.PureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
     friend: ImmutablePropTypes.map.isRequired,
+    animate: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    animate: autoPlayGif,
   };
 
   render() {
-    const { account, friend } = this.props;
+    const { account, friend, animate } = this.props;
 
     const baseStyle = {
-      backgroundImage: `url(${account.get('avatar_static')})`,
+      backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
     };
 
     const overlayStyle = {
-      backgroundImage: `url(${friend.get('avatar_static')})`,
+      backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
     };
 
     return (
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 7890755f3..a876c5197 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -156,6 +156,8 @@ export default class ComposeForm extends ImmutablePureComponent {
 
     return (
       <div className='compose-form'>
+        <WarningContainer />
+
         <Collapsable isVisible={this.props.spoiler} fullHeight={50}>
           <div className='spoiler-input'>
             <label>
@@ -165,8 +167,6 @@ export default class ComposeForm extends ImmutablePureComponent {
           </div>
         </Collapsable>
 
-        <WarningContainer />
-
         <ReplyIndicatorContainer />
 
         <div className='compose-form__autosuggest-wrapper'>
@@ -199,11 +199,11 @@ export default class ComposeForm extends ImmutablePureComponent {
             <SensitiveButtonContainer />
             <SpoilerButtonContainer />
           </div>
+          <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
+        </div>
 
-          <div className='compose-form__publish'>
-            <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
-            <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
-          </div>
+        <div className='compose-form__publish'>
+          <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js
index 7672440b4..d8cda96f3 100644
--- a/app/javascript/mastodon/features/compose/components/reply_indicator.js
+++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js
@@ -6,6 +6,7 @@ import IconButton from '../../../components/icon_button';
 import DisplayName from '../../../components/display_name';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { isRtl } from '../../../rtl';
 
 const messages = defineMessages({
   cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@@ -42,7 +43,10 @@ export default class ReplyIndicator extends ImmutablePureComponent {
       return null;
     }
 
-    const content  = { __html: status.get('contentHtml') };
+    const content = { __html: status.get('contentHtml') };
+    const style   = {
+      direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
+    };
 
     return (
       <div className='reply-indicator'>
@@ -55,7 +59,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
           </a>
         </div>
 
-        <div className='reply-indicator__content' dangerouslySetInnerHTML={content} />
+        <div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index 6ab76492a..3a3d17710 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -62,7 +62,7 @@ export default class Upload extends ImmutablePureComponent {
   render () {
     const { intl, media } = this.props;
     const active          = this.state.hovered || this.state.focused;
-    const description     = this.state.dirtyDescription || media.get('description') || '';
+    const description     = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
 
     return (
       <div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index 1dcd4de14..ae136e48f 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -161,7 +161,7 @@ export default class ListTimeline extends React.PureComponent {
           scrollKey={`list_timeline-${columnId}`}
           timelineId={`list:${id}`}
           loadMore={this.handleLoadMore}
-          emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
+          emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
         />
       </Column>
     );
diff --git a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
index f15fbb2f4..f14be2aaf 100644
--- a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
@@ -8,6 +8,7 @@ import {
 } from '../../../actions/timelines';
 import Column from '../../../components/column';
 import ColumnHeader from '../../../components/column_header';
+import { connectHashtagStream } from '../../../actions/streaming';
 
 @connect()
 export default class HashtagTimeline extends React.PureComponent {
@@ -29,16 +30,13 @@ export default class HashtagTimeline extends React.PureComponent {
     const { dispatch, hashtag } = this.props;
 
     dispatch(refreshHashtagTimeline(hashtag));
-
-    this.polling = setInterval(() => {
-      dispatch(refreshHashtagTimeline(hashtag));
-    }, 10000);
+    this.disconnect = dispatch(connectHashtagStream(hashtag));
   }
 
   componentWillUnmount () {
-    if (typeof this.polling !== 'undefined') {
-      clearInterval(this.polling);
-      this.polling = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js
index de4b5320a..5805d1a10 100644
--- a/app/javascript/mastodon/features/standalone/public_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js
@@ -9,6 +9,7 @@ import {
 import Column from '../../../components/column';
 import ColumnHeader from '../../../components/column_header';
 import { defineMessages, injectIntl } from 'react-intl';
+import { connectPublicStream } from '../../../actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
@@ -35,16 +36,13 @@ export default class PublicTimeline extends React.PureComponent {
     const { dispatch } = this.props;
 
     dispatch(refreshPublicTimeline());
-
-    this.polling = setInterval(() => {
-      dispatch(refreshPublicTimeline());
-    }, 3000);
+    this.disconnect = dispatch(connectPublicStream());
   }
 
   componentWillUnmount () {
-    if (typeof this.polling !== 'undefined') {
-      clearInterval(this.polling);
-      this.polling = null;
+    if (this.disconnect) {
+      this.disconnect();
+      this.disconnect = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 93ed9e605..c5b3c20d4 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -27,6 +27,8 @@ const componentMap = {
   'LIST': ListTimeline,
 };
 
+const isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
+
 @component => injectIntl(component, { withRef: true })
 export default class ColumnsArea extends ImmutablePureComponent {
 
@@ -79,7 +81,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
 
   handleChildrenContentChange() {
     if (!this.props.singleColumn) {
-      this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
+      const modifier = isRtlLayout ? -1 : 1;
+      this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
     }
   }
 
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index ec66a0027..d699a69df 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -36,9 +36,9 @@
   "column.favourites": "المفضلة",
   "column.follow_requests": "طلبات المتابعة",
   "column.home": "الرئيسية",
-  "column.lists": "Lists",
+  "column.lists": "القوائم",
   "column.mutes": "الحسابات المكتومة",
-  "column.notifications": "الإشعارات",
+  "column.notifications": "الإخطارات",
   "column.pins": "التبويقات المثبتة",
   "column.public": "الخيط العام الموحد",
   "column_back_button.label": "العودة",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "حذف",
   "confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟",
   "confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "أكتم",
@@ -109,32 +109,32 @@
   "home.settings": "إعدادات العمود",
   "keyboard_shortcuts.back": "للعودة",
   "keyboard_shortcuts.boost": "للترقية",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة",
+  "keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص",
   "keyboard_shortcuts.description": "Description",
   "keyboard_shortcuts.down": "للإنتقال إلى أسفل القائمة",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "للإضافة إلى المفضلة",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
+  "keyboard_shortcuts.hotkey": "مفتاح الإختصار",
+  "keyboard_shortcuts.legend": "لعرض هذا المفتاح",
   "keyboard_shortcuts.mention": "لذِكر الناشر",
   "keyboard_shortcuts.reply": "للردّ",
-  "keyboard_shortcuts.search": "to focus search",
+  "keyboard_shortcuts.search": "للتركيز على البحث",
   "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
   "lightbox.close": "إغلاق",
   "lightbox.next": "التالي",
   "lightbox.previous": "العودة",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "أضف إلى القائمة",
+  "lists.account.remove": "إحذف من القائمة",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "تعديل القائمة",
+  "lists.new.create": "إنشاء قائمة",
+  "lists.new.title_placeholder": "عنوان القائمة الجديدة",
+  "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
+  "lists.subheading": "قوائمك",
   "loading_indicator.label": "تحميل ...",
   "media_gallery.toggle_visible": "عرض / إخفاء",
   "missing_indicator.label": "تعذر العثور عليه",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "طلبات المتابعة",
   "navigation_bar.info": "معلومات إضافية",
   "navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "القوائم",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "الحسابات المكتومة",
   "navigation_bar.pins": "التبويقات المثبتة",
@@ -209,7 +209,7 @@
   "search_popout.search_format": "نمط البحث المتقدم",
   "search_popout.tips.hashtag": "وسم",
   "search_popout.tips.status": "حالة",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.text": "جملة قصيرة تُمكّنُك من عرض أسماء و حسابات و كلمات رمزية",
   "search_popout.tips.user": "مستخدِم",
   "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "standalone.public_title": "نظرة على ...",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index f705937fd..62d85a5e1 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Peticions per seguir-te",
   "column.home": "Inici",
-  "column.lists": "Lists",
+  "column.lists": "Llistes",
   "column.mutes": "Usuaris silenciats",
   "column.notifications": "Notificacions",
   "column.pins": "Toot fixat",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Esborrar",
   "confirmations.delete.message": "Estàs segur que vols esborrar aquest estat?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Estàs segur que vols esborrar permanenment aquesta llista?",
   "confirmations.domain_block.confirm": "Amagar tot el domini",
   "confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
   "confirmations.mute.confirm": "Silenciar",
@@ -127,14 +127,14 @@
   "lightbox.close": "Tancar",
   "lightbox.next": "Següent",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Afegir a la llista",
+  "lists.account.remove": "Treure de la llista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar llista",
+  "lists.new.create": "Afegir llista",
+  "lists.new.title_placeholder": "Nou títol de llista",
+  "lists.search": "Cercar entre les persones que segueixes",
+  "lists.subheading": "Les teves llistes",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
   "missing_indicator.label": "No trobat",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Sol·licituds de seguiment",
   "navigation_bar.info": "Informació addicional",
   "navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Llistes",
   "navigation_bar.logout": "Tancar sessió",
   "navigation_bar.mutes": "Usuaris silenciats",
   "navigation_bar.pins": "Toots fixats",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 0f766af6a..3fc4a8c96 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -93,7 +93,7 @@
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
   "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.public_timeline": "the public timeline",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
   "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",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index a7a8876d0..01cbd2657 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -91,7 +91,7 @@
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
   "empty_column.home.public_timeline": "le fil public",
-  "empty_column.list": "Il n'y a rien dans cette liste pour l'instant.",
+  "empty_column.list": "Il n'y a rien dans cette liste pour l'instant. Dès que des personnes de cette listes publierons de nouveaux statuts ils apparaîtront ici.",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
   "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
   "follow_request.authorize": "Accepter",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index bb0b1a9fd..6398daa11 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favoritas",
   "column.follow_requests": "Peticións de seguimento",
   "column.home": "Inicio",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Usuarias acaladas",
   "column.notifications": "Notificacións",
   "column.pins": "Mensaxes fixadas",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Borrar",
   "confirmations.delete.message": "Está segura de que quere eliminar este estado?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Estás seguro de que queres eliminar permanentemente esta lista?",
   "confirmations.domain_block.confirm": "Agochar un dominio completo",
   "confirmations.domain_block.message": "Realmente está segura de que quere bloquear por completo o dominio {domain}? Normalmente é suficiente, e preferible, bloquear de xeito selectivo varios elementos.",
   "confirmations.mute.confirm": "Acalar",
@@ -127,14 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Seguinte",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Engadir á lista",
+  "lists.account.remove": "Eliminar da lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Engadir lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Procurar entre a xente que segues",
+  "lists.subheading": "As túas listas",
   "loading_indicator.label": "Cargando...",
   "media_gallery.toggle_visible": "Dar visibilidade",
   "missing_indicator.label": "Non atopado",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Peticións de seguimento",
   "navigation_bar.info": "Sobre esta instancia",
   "navigation_bar.keyboard_shortcuts": "Atallos do teclado",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuarias acaladas",
   "navigation_bar.pins": "Mensaxes fixadas",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 40ad66a7f..68abd906f 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -95,7 +95,7 @@
   "empty_column.home.public_timeline": "連合タイムライン",
   "empty_column.list": "このリストにはまだなにもありません。",
   "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
-  "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
+  "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう",
   "follow_request.authorize": "許可",
   "follow_request.reject": "拒否",
   "getting_started.appsshort": "アプリ",
@@ -162,12 +162,12 @@
   "notifications.clear": "通知を消去",
   "notifications.clear_confirmation": "本当に通知を消去しますか?",
   "notifications.column_settings.alert": "デスクトップ通知",
-  "notifications.column_settings.favourite": "お気に入り",
-  "notifications.column_settings.follow": "新しいフォロワー",
-  "notifications.column_settings.mention": "返信",
+  "notifications.column_settings.favourite": "お気に入り:",
+  "notifications.column_settings.follow": "新しいフォロワー:",
+  "notifications.column_settings.mention": "返信:",
   "notifications.column_settings.push": "プッシュ通知",
   "notifications.column_settings.push_meta": "このデバイス",
-  "notifications.column_settings.reblog": "ブースト",
+  "notifications.column_settings.reblog": "ブースト:",
   "notifications.column_settings.show": "カラムに表示",
   "notifications.column_settings.sound": "通知音を再生",
   "onboarding.done": "完了",
@@ -176,7 +176,7 @@
   "onboarding.page_four.home": "「ホーム」タイムラインではあなたがフォローしている人の投稿を表示します。",
   "onboarding.page_four.notifications": "「通知」ではあなたへの他の人からの関わりを表示します。",
   "onboarding.page_one.federation": "Mastodonは誰でも参加できるSNSです。",
-  "onboarding.page_one.handle": "あなたは今数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です。",
+  "onboarding.page_one.handle": "今あなたは数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です",
   "onboarding.page_one.welcome": "Mastodonへようこそ!",
   "onboarding.page_six.admin": "あなたのインスタンスの管理者は{admin}です。",
   "onboarding.page_six.almost_done": "以上です。",
@@ -184,7 +184,7 @@
   "onboarding.page_six.apps_available": "iOS、Androidあるいは他のプラットフォームで使える{apps}があります。",
   "onboarding.page_six.github": "MastodonはOSSです。バグ報告や機能要望あるいは貢献を{github}から行なえます。",
   "onboarding.page_six.guidelines": "コミュニティガイドライン",
-  "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください。",
+  "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください!",
   "onboarding.page_six.various_app": "様々なモバイルアプリ",
   "onboarding.page_three.profile": "「プロフィールを編集」から、あなたの自己紹介や表示名を変更できます。またそこでは他の設定ができます。",
   "onboarding.page_three.search": "検索バーで、{illustration}や{introductions}のように特定のハッシュタグの投稿を見たり、ユーザーを探したりできます。",
@@ -215,7 +215,7 @@
   "search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
   "search_popout.tips.user": "ユーザー",
   "search_results.total": "{count, number}件の結果",
-  "standalone.public_title": "今こんな話をしています",
+  "standalone.public_title": "今こんな話をしています...",
   "status.cannot_reblog": "この投稿はブーストできません",
   "status.delete": "削除",
   "status.embed": "埋め込み",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index c290ed767..9044c2011 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
   "column.home": "Start",
-  "column.lists": "Lists",
+  "column.lists": "Lijsten",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
   "column.pins": "Vastgezette toots",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Verwijderen",
   "confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Weet je zeker dat je deze lijst permanent wilt verwijderen?",
   "confirmations.domain_block.confirm": "Negeer alles van deze server",
   "confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
   "confirmations.mute.confirm": "Negeren",
@@ -127,14 +127,14 @@
   "lightbox.close": "Sluiten",
   "lightbox.next": "Volgende",
   "lightbox.previous": "Vorige",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Aan lijst toevoegen",
+  "lists.account.remove": "Uit lijst verwijderen",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Lijst bewerken",
+  "lists.new.create": "Lijst toevoegen",
+  "lists.new.title_placeholder": "Naam nieuwe lijst",
+  "lists.search": "Zoek naar mensen die je volgt",
+  "lists.subheading": "Jouw lijsten",
   "loading_indicator.label": "Laden…",
   "media_gallery.toggle_visible": "Media wel/niet tonen",
   "missing_indicator.label": "Niet gevonden",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Volgverzoeken",
   "navigation_bar.info": "Uitgebreide informatie",
   "navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Lijsten",
   "navigation_bar.logout": "Afmelden",
   "navigation_bar.mutes": "Genegeerde gebruikers",
   "navigation_bar.pins": "Vastgezette toots",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index f8b4751d6..0d1f7c971 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -91,7 +91,7 @@
   "empty_column.hashtag": "I a pas encara de contengut ligat a aquesta etiqueta.",
   "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.public_timeline": "lo flux public",
-  "empty_column.list": "I a pas res dins la lista pel moment.",
+  "empty_column.list": "I a pas res dins la lista pel moment. Quand de membres d’aquesta lista publiquen de novèls estatuts los veiretz aquí.",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
   "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public",
   "follow_request.authorize": "Autorizar",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 6bac65865..70632846c 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -36,7 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores pendentes",
   "column.home": "Página inicial",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Usuários silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Postagens fixadas",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Excluir",
   "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?",
   "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
   "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
@@ -110,7 +110,7 @@
   "keyboard_shortcuts.back": "para navegar de volta",
   "keyboard_shortcuts.boost": "para compartilhar",
   "keyboard_shortcuts.column": "Focar um status em uma das colunas",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.compose": "para focar a área de redação",
   "keyboard_shortcuts.description": "Description",
   "keyboard_shortcuts.down": "para mover para baixo na lista",
   "keyboard_shortcuts.enter": "to open status",
@@ -127,14 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Adicionar a listas",
+  "lists.account.remove": "Remover da lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Adicionar lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Procurar entre as pessoas que você segue",
+  "lists.subheading": "Suas listas",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuários silenciados",
   "navigation_bar.pins": "Postagens fixadas",
@@ -177,7 +177,7 @@
   "onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
   "onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
   "onboarding.page_six.almost_done": "Quase acabando...",
-  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.appetoot": "Bom Apetoot!",
   "onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.",
   "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.",
   "onboarding.page_six.guidelines": "diretrizes da comunidade",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 728fb3a10..15d5deb93 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Bloquear @{name}",
   "account.block_domain": "Esconder tudo do domínio {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
@@ -19,7 +19,7 @@
   "account.share": "Partilhar o perfil @{name}",
   "account.show_reblogs": "Mostrar partilhas de @{name}",
   "account.unblock": "Não bloquear @{name}",
-  "account.unblock_domain": "Unhide {domain}",
+  "account.unblock_domain": "Mostrar {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "account.unmute_notifications": "Deixar de silenciar @{name}",
@@ -36,7 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores Pendentes",
   "column.home": "Home",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Utilizadores silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Pinned toot",
@@ -64,7 +64,7 @@
   "confirmations.delete.confirm": "Eliminar",
   "confirmations.delete.message": "De certeza que queres eliminar esta publicação?",
   "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.message": "Tens a certeza de que desejas apagar permanentemente esta lista?",
   "confirmations.domain_block.confirm": "Esconder tudo deste domínio",
   "confirmations.domain_block.message": "De certeza que queres bloquear por completo o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
@@ -88,12 +88,12 @@
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
   "empty_column.community": "Ainda não existe conteúdo local para mostrar!",
-  "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag",
+  "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.",
   "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.list": "Ainda não existem publicações nesta lista.",
   "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.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
   "getting_started.appsshort": "Aplicações",
@@ -116,7 +116,7 @@
   "keyboard_shortcuts.enter": "para expandir uma publicação",
   "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
   "keyboard_shortcuts.heading": "Atalhos do teclado",
-  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.hotkey": "Atalho",
   "keyboard_shortcuts.legend": "para mostrar esta legenda",
   "keyboard_shortcuts.mention": "para mencionar o autor",
   "keyboard_shortcuts.reply": "para responder",
@@ -127,14 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
+  "lists.account.add": "Adicionar à lista",
+  "lists.account.remove": "Remover da lista",
   "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among people you follow",
-  "lists.subheading": "Your lists",
+  "lists.edit": "Editar lista",
+  "lists.new.create": "Adicionar lista",
+  "lists.new.title_placeholder": "Novo título da lista",
+  "lists.search": "Pesquisa entre as pessoas que segues",
+  "lists.subheading": "As tuas listas",
   "loading_indicator.label": "A carregar...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
@@ -146,7 +146,7 @@
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Utilizadores silenciados",
   "navigation_bar.pins": "Posts fixos",
@@ -209,13 +209,13 @@
   "search_popout.search_format": "Formato avançado de pesquisa",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+  "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags",
   "search_popout.tips.user": "utilizador",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Espreitar lá dentro...",
   "status.cannot_reblog": "Este post não pode ser partilhado",
   "status.delete": "Eliminar",
-  "status.embed": "Embed",
+  "status.embed": "Incorporar",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Media escondida",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index cb5607cc5..3d6bd5334 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -91,7 +91,7 @@
   "empty_column.hashtag": "这个话题标签下暂时没有内容。",
   "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
   "empty_column.home.public_timeline": "公共时间轴",
-  "empty_column.list": "这个列表中暂时没有内容。",
+  "empty_column.list": "这个列表中暂时没有内容。列表中用户所发送的的新嘟文将会在这里显示。",
   "empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。",
   "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!",
   "follow_request.authorize": "同意",
diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js
index 36c68ffc5..9a6f4f26d 100644
--- a/app/javascript/mastodon/stream.js
+++ b/app/javascript/mastodon/stream.js
@@ -62,7 +62,13 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
 
 
 export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
-  const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
+  const params = [ `stream=${stream}` ];
+
+  if (accessToken !== null) {
+    params.push(`access_token=${accessToken}`);
+  }
+
+  const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`);
 
   ws.onopen      = connected;
   ws.onmessage   = e => received(JSON.parse(e.data));
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index da789ba06..be28473e5 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -265,198 +265,286 @@
 
 .compose-form {
   padding: 10px;
-}
-
-.compose-form__warning {
-  color: darken($ui-secondary-color, 65%);
-  margin-bottom: 15px;
-  background: $ui-primary-color;
-  box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
-  padding: 8px 10px;
-  border-radius: 4px;
-  font-size: 13px;
-  font-weight: 400;
 
-  strong {
+  .compose-form__warning {
     color: darken($ui-secondary-color, 65%);
-    font-weight: 500;
+    margin-bottom: 15px;
+    background: $ui-primary-color;
+    box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
+    padding: 8px 10px;
+    border-radius: 4px;
+    font-size: 13px;
+    font-weight: 400;
 
-    @each $lang in $cjk-langs {
-      &:lang(#{$lang}) {
-        font-weight: 700;
+    strong {
+      color: darken($ui-secondary-color, 65%);
+      font-weight: 500;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
+    }
+
+    a {
+      color: darken($ui-primary-color, 33%);
+      font-weight: 500;
+      text-decoration: underline;
+
+      &:hover,
+      &:active,
+      &:focus {
+        text-decoration: none;
       }
     }
   }
 
-  a {
-    color: darken($ui-primary-color, 33%);
-    font-weight: 500;
-    text-decoration: underline;
+  .compose-form__autosuggest-wrapper {
+    position: relative;
+
+    .emoji-picker-dropdown {
+      position: absolute;
+      right: 5px;
+      top: 5px;
+    }
+  }
+
+  .autosuggest-textarea,
+  .spoiler-input {
+    position: relative;
+  }
+
+  .autosuggest-textarea__textarea,
+  .spoiler-input__input {
+    display: block;
+    box-sizing: border-box;
+    width: 100%;
+    margin: 0;
+    color: $ui-base-color;
+    background: $simple-background-color;
+    padding: 10px;
+    font-family: inherit;
+    font-size: 14px;
+    resize: vertical;
+    border: 0;
+    outline: 0;
 
-    &:hover,
-    &:active,
     &:focus {
-      text-decoration: none;
+      outline: 0;
+    }
+
+    @media screen and (max-width: 600px) {
+      font-size: 16px;
     }
   }
-}
 
-.compose-form__modifiers {
-  color: $ui-base-color;
-  font-family: inherit;
-  font-size: 14px;
-  background: $simple-background-color;
-  border-radius: 0 0 4px;
-}
+  .spoiler-input__input {
+    border-radius: 4px;
+  }
 
-.compose-form__buttons-wrapper {
-  display: flex;
-  justify-content: space-between;
-}
+  .autosuggest-textarea__textarea {
+    min-height: 100px;
+    border-radius: 4px 4px 0 0;
+    padding-bottom: 0;
+    padding-right: 10px + 22px;
+    resize: none;
 
-.compose-form__buttons {
-  padding: 10px;
-  background: darken($simple-background-color, 8%);
-  box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05);
-  border-radius: 0 0 4px 4px;
-  display: flex;
+    @media screen and (max-width: 600px) {
+      height: 100px !important; // prevent auto-resize textarea
+      resize: vertical;
+    }
+  }
 
-  .icon-button {
-    box-sizing: content-box;
-    padding: 0 3px;
+  .autosuggest-textarea__suggestions {
+    box-sizing: border-box;
+    display: none;
+    position: absolute;
+    top: 100%;
+    width: 100%;
+    z-index: 99;
+    box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
+    background: $ui-secondary-color;
+    border-radius: 0 0 4px 4px;
+    color: $ui-base-color;
+    font-size: 14px;
+    padding: 6px;
+
+    &.autosuggest-textarea__suggestions--visible {
+      display: block;
+    }
   }
-}
 
-.compose-form__upload-button-icon {
-  line-height: 27px;
-}
+  .autosuggest-textarea__suggestions__item {
+    padding: 10px;
+    cursor: pointer;
+    border-radius: 4px;
 
-.compose-form__sensitive-button {
-  display: none;
+    &:hover,
+    &:focus,
+    &:active,
+    &.selected {
+      background: darken($ui-secondary-color, 10%);
+    }
+  }
+
+  .autosuggest-account,
+  .autosuggest-emoji {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: flex-start;
+    line-height: 18px;
+    font-size: 14px;
+  }
 
-  &.compose-form__sensitive-button--visible {
+  .autosuggest-account-icon,
+  .autosuggest-emoji img {
     display: block;
+    margin-right: 8px;
+    width: 16px;
+    height: 16px;
   }
 
-  .compose-form__sensitive-button__icon {
-    line-height: 27px;
+  .autosuggest-account .display-name__account {
+    color: lighten($ui-base-color, 36%);
   }
-}
 
-.compose-form__upload-wrapper {
-  overflow: hidden;
-}
+  .compose-form__modifiers {
+    color: $ui-base-color;
+    font-family: inherit;
+    font-size: 14px;
+    background: $simple-background-color;
 
-.compose-form__uploads-wrapper {
-  display: flex;
-  flex-direction: row;
-  padding: 5px;
-  flex-wrap: wrap;
-}
+    .compose-form__upload-wrapper {
+      overflow: hidden;
+    }
 
-.compose-form__upload {
-  flex: 1 1 0;
-  min-width: 40%;
-  margin: 5px;
+    .compose-form__uploads-wrapper {
+      display: flex;
+      flex-direction: row;
+      padding: 5px;
+      flex-wrap: wrap;
+    }
 
-  &-description {
-    position: absolute;
-    z-index: 2;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    box-sizing: border-box;
-    background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
-    padding: 10px;
-    opacity: 0;
-    transition: opacity .1s ease;
+    .compose-form__upload {
+      flex: 1 1 0;
+      min-width: 40%;
+      margin: 5px;
 
-    input {
-      background: transparent;
-      color: $ui-secondary-color;
-      border: 0;
-      padding: 0;
-      margin: 0;
-      width: 100%;
-      font-family: inherit;
-      font-size: 14px;
-      font-weight: 500;
+      &-description {
+        position: absolute;
+        z-index: 2;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        box-sizing: border-box;
+        background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
+        padding: 10px;
+        opacity: 0;
+        transition: opacity .1s ease;
+
+        input {
+          background: transparent;
+          color: $ui-secondary-color;
+          border: 0;
+          padding: 0;
+          margin: 0;
+          width: 100%;
+          font-family: inherit;
+          font-size: 14px;
+          font-weight: 500;
+
+          &:focus {
+            color: $white;
+          }
 
-      &:focus {
-        color: $white;
+          &::placeholder {
+            opacity: 0.54;
+            color: $ui-secondary-color;
+          }
+        }
+
+        &.active {
+          opacity: 1;
+        }
       }
 
-      &::placeholder {
-        opacity: 0.54;
-        color: $ui-secondary-color;
+      .icon-button {
+        mix-blend-mode: difference;
       }
     }
 
-    &.active {
-      opacity: 1;
+    .compose-form__upload-thumbnail {
+      border-radius: 4px;
+      background-position: center;
+      background-size: cover;
+      background-repeat: no-repeat;
+      height: 100px;
+      width: 100%;
     }
   }
 
-  .icon-button {
-    mix-blend-mode: difference;
-  }
-}
+  .compose-form__buttons-wrapper {
+    padding: 10px;
+    background: darken($simple-background-color, 8%);
+    border-radius: 0 0 4px 4px;
+    display: flex;
+    justify-content: space-between;
 
-.compose-form__upload-thumbnail {
-  border-radius: 4px;
-  background-position: center;
-  background-size: cover;
-  background-repeat: no-repeat;
-  height: 100px;
-  width: 100%;
-}
+    .compose-form__buttons {
+      display: flex;
 
-.compose-form__label {
-  display: block;
-  line-height: 24px;
-  vertical-align: middle;
+      .compose-form__upload-button-icon {
+        line-height: 27px;
+      }
 
-  &.with-border {
-    border-top: 1px solid $ui-base-color;
-    padding-top: 10px;
-  }
+      .compose-form__sensitive-button {
+        display: none;
 
-  .compose-form__label__text {
-    display: inline-block;
-    vertical-align: middle;
-    margin-bottom: 14px;
-    margin-left: 8px;
-    color: $ui-primary-color;
-  }
-}
+        &.compose-form__sensitive-button--visible {
+          display: block;
+        }
 
-.compose-form__textarea,
-.follow-form__input {
-  background: $simple-background-color;
+        .compose-form__sensitive-button__icon {
+          line-height: 27px;
+        }
+      }
+    }
 
-  &:disabled {
-    background: $ui-secondary-color;
-  }
-}
+    .icon-button {
+      box-sizing: content-box;
+      padding: 0 3px;
+    }
 
-.compose-form__autosuggest-wrapper {
-  position: relative;
+    .character-counter__wrapper {
+      align-self: center;
+      margin-right: 4px;
 
-  .emoji-picker-dropdown {
-    position: absolute;
-    right: 5px;
-    top: 5px;
+      .character-counter {
+        cursor: default;
+        font-family: 'mastodon-font-sans-serif', sans-serif;
+        font-size: 14px;
+        font-weight: 600;
+        color: lighten($ui-base-color, 12%);
+
+        &.character-counter--over {
+          color: $warning-red;
+        }
+      }
+    }
   }
-}
 
-.compose-form__publish {
-  display: flex;
-  min-width: 0;
-}
+  .compose-form__publish {
+    display: flex;
+    justify-content: flex-end;
+    min-width: 0;
 
-.compose-form__publish-button-wrapper {
-  overflow: hidden;
-  padding-top: 10px;
+    .compose-form__publish-button-wrapper {
+      overflow: hidden;
+      padding-top: 10px;
+    }
+  }
 }
 
 .emojione {
@@ -1973,121 +2061,6 @@
   cursor: default;
 }
 
-.autosuggest-textarea,
-.spoiler-input {
-  position: relative;
-}
-
-.autosuggest-textarea__textarea,
-.spoiler-input__input {
-  display: block;
-  box-sizing: border-box;
-  width: 100%;
-  margin: 0;
-  color: $ui-base-color;
-  background: $simple-background-color;
-  padding: 10px;
-  font-family: inherit;
-  font-size: 14px;
-  resize: vertical;
-  border: 0;
-  outline: 0;
-
-  &:focus {
-    outline: 0;
-  }
-
-  @media screen and (max-width: 600px) {
-    font-size: 16px;
-  }
-}
-
-.spoiler-input__input {
-  border-radius: 4px;
-}
-
-.autosuggest-textarea__textarea {
-  min-height: 100px;
-  border-radius: 4px 4px 0 0;
-  padding-bottom: 0;
-  padding-right: 10px + 22px;
-  resize: none;
-
-  @media screen and (max-width: 600px) {
-    height: 100px !important; // prevent auto-resize textarea
-    resize: vertical;
-  }
-}
-
-.autosuggest-textarea__suggestions {
-  box-sizing: border-box;
-  display: none;
-  position: absolute;
-  top: 100%;
-  width: 100%;
-  z-index: 99;
-  box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
-  background: $ui-secondary-color;
-  border-radius: 0 0 4px 4px;
-  color: $ui-base-color;
-  font-size: 14px;
-  padding: 6px;
-
-  &.autosuggest-textarea__suggestions--visible {
-    display: block;
-  }
-}
-
-.autosuggest-textarea__suggestions__item {
-  padding: 10px;
-  cursor: pointer;
-  border-radius: 4px;
-
-  &:hover,
-  &:focus,
-  &:active,
-  &.selected {
-    background: darken($ui-secondary-color, 10%);
-  }
-}
-
-.autosuggest-account,
-.autosuggest-emoji {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: flex-start;
-  line-height: 18px;
-  font-size: 14px;
-}
-
-.autosuggest-account-icon,
-.autosuggest-emoji img {
-  display: block;
-  margin-right: 8px;
-  width: 16px;
-  height: 16px;
-}
-
-.autosuggest-account .display-name__account {
-  color: lighten($ui-base-color, 36%);
-}
-
-.character-counter__wrapper {
-  line-height: 36px;
-  margin: 0 16px 0 8px;
-  padding-top: 10px;
-}
-
-.character-counter {
-  cursor: default;
-  font-size: 16px;
-}
-
-.character-counter--over {
-  color: $warning-red;
-}
-
 .getting-started__wrapper {
   position: relative;
   overflow-y: auto;
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 67bfa8a38..77420c84b 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -7,9 +7,9 @@ body.rtl {
     margin-left: 5px;
   }
 
-  .character-counter__wrapper {
-    margin-right: 8px;
-    margin-left: 16px;
+  .compose-form .compose-form__buttons-wrapper .character-counter__wrapper {
+    margin-right: 0;
+    margin-left: 4px;
   }
 
   .navigation-bar__profile {
@@ -30,6 +30,22 @@ body.rtl {
   .column-header__buttons {
     left: 0;
     right: auto;
+    margin-left: -15px;
+    margin-right: 0;
+  }
+
+  .column-inline-form .icon-button {
+    margin-left: 0;
+    margin-right: 5px;
+  }
+
+  .column-header__links .text-btn {
+    margin-left: 10px;
+    margin-right: 0;
+  }
+
+  .account__avatar-wrapper {
+    float: right;
   }
 
   .column-header__back-button {
@@ -41,10 +57,6 @@ body.rtl {
     float: left;
   }
 
-  .compose-form__modifiers {
-    border-radius: 0 0 0 4px;
-  }
-
   .setting-toggle {
     margin-left: 0;
     margin-right: 8px;
diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb
index bcc4ed500..04ba38101 100644
--- a/app/lib/provider_discovery.rb
+++ b/app/lib/provider_discovery.rb
@@ -2,13 +2,26 @@
 
 class ProviderDiscovery < OEmbed::ProviderDiscovery
   class << self
+    def get(url, **options)
+      provider = discover_provider(url, options)
+
+      options.delete(:html)
+
+      provider.get(url, options)
+    end
+
     def discover_provider(url, **options)
-      res    = Request.new(:get, url).perform
       format = options[:format]
 
-      raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
+      if options[:html]
+        html = Nokogiri::HTML(options[:html])
+      else
+        res = Request.new(:get, url).perform
+
+        raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
 
-      html = Nokogiri::HTML(res.to_s)
+        html = Nokogiri::HTML(res.to_s)
+      end
 
       if format.nil? || format == :json
         provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index 189872368..dc7a03039 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -45,6 +45,8 @@ class AccountFilter
       else
         Account.default_scoped
       end
+    when 'staff'
+      accounts_with_users.merge User.staff
     else
       raise "Unknown filter: #{key}"
     end
diff --git a/app/models/custom_emoji_filter.rb b/app/models/custom_emoji_filter.rb
index 2d1394a59..2c09ed65c 100644
--- a/app/models/custom_emoji_filter.rb
+++ b/app/models/custom_emoji_filter.rb
@@ -27,6 +27,8 @@ class CustomEmojiFilter
       CustomEmoji.remote
     when 'by_domain'
       CustomEmoji.where(domain: value)
+    when 'shortcode'
+      CustomEmoji.where(shortcode: value)
     else
       raise "Unknown filter: #{key}"
     end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 7f4518ea7..d0472a1d7 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -40,6 +40,12 @@ class FetchLinkCardService < BaseService
 
     return if res.code != 405 && (res.code != 200 || res.mime_type != 'text/html')
 
+    @response = Request.new(:get, @url).perform
+
+    return if @response.code != 200 || @response.mime_type != 'text/html'
+
+    @html = @response.to_s
+
     attempt_oembed || attempt_opengraph
   end
 
@@ -70,30 +76,32 @@ class FetchLinkCardService < BaseService
   end
 
   def attempt_oembed
-    response = OEmbed::Providers.get(@url)
+    embed = OEmbed::Providers.get(@url, html: @html)
 
-    return false unless response.respond_to?(:type)
+    return false unless embed.respond_to?(:type)
 
-    @card.type          = response.type
-    @card.title         = response.respond_to?(:title)         ? response.title         : ''
-    @card.author_name   = response.respond_to?(:author_name)   ? response.author_name   : ''
-    @card.author_url    = response.respond_to?(:author_url)    ? response.author_url    : ''
-    @card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : ''
-    @card.provider_url  = response.respond_to?(:provider_url)  ? response.provider_url  : ''
+    @card.type          = embed.type
+    @card.title         = embed.respond_to?(:title)         ? embed.title         : ''
+    @card.author_name   = embed.respond_to?(:author_name)   ? embed.author_name   : ''
+    @card.author_url    = embed.respond_to?(:author_url)    ? embed.author_url    : ''
+    @card.provider_name = embed.respond_to?(:provider_name) ? embed.provider_name : ''
+    @card.provider_url  = embed.respond_to?(:provider_url)  ? embed.provider_url  : ''
     @card.width         = 0
     @card.height        = 0
 
     case @card.type
     when 'link'
-      @card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
+      @card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url)
     when 'photo'
-      @card.embed_url = response.url
-      @card.width     = response.width.presence  || 0
-      @card.height    = response.height.presence || 0
+      return false unless embed.respond_to?(:url)
+      @card.embed_url = embed.url
+      @card.image     = URI.parse(embed.url)
+      @card.width     = embed.width.presence  || 0
+      @card.height    = embed.height.presence || 0
     when 'video'
-      @card.width  = response.width.presence  || 0
-      @card.height = response.height.presence || 0
-      @card.html   = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED)
+      @card.width  = embed.width.presence  || 0
+      @card.height = embed.height.presence || 0
+      @card.html   = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED)
     when 'rich'
       # Most providers rely on <script> tags, which is a no-no
       return false
@@ -105,17 +113,11 @@ class FetchLinkCardService < BaseService
   end
 
   def attempt_opengraph
-    response = Request.new(:get, @url).perform
-
-    return if response.code != 200 || response.mime_type != 'text/html'
-
-    html = response.to_s
-
     detector = CharlockHolmes::EncodingDetector.new
     detector.strip_tags = true
 
-    guess = detector.detect(html, response.charset)
-    page  = Nokogiri::HTML(html, nil, guess&.fetch(:encoding, nil))
+    guess = detector.detect(@html, @response.charset)
+    page  = Nokogiri::HTML(@html, nil, guess&.fetch(:encoding, nil))
 
     if meta_property(page, 'twitter:player')
       @card.type   = :video
@@ -132,16 +134,16 @@ class FetchLinkCardService < BaseService
       @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image')
     end
 
-    @card.title            = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
-    @card.description      = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
+    @card.title       = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
+    @card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
 
     return if @card.title.blank? && @card.html.blank?
 
     @card.save_with_optional_image!
   end
 
-  def meta_property(html, property)
-    html.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || html.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
+  def meta_property(page, property)
+    page.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || page.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
   end
 
   def lock_options
diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb
index 9c009335b..9c3008035 100644
--- a/app/services/fetch_remote_status_service.rb
+++ b/app/services/fetch_remote_status_service.rb
@@ -40,6 +40,6 @@ class FetchRemoteStatusService < BaseService
   end
 
   def confirmed_domain?(domain, account)
-    account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url || account.uri).normalized_host).zero?
+    account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero?
   end
 end
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 20579ca63..ac0207a0a 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -22,7 +22,7 @@ class FollowService < BaseService
     elsif source_account.requested?(target_account)
       # This isn't managed by a method in AccountInteractions, so we modify it
       # ourselves if necessary.
-      req = follow_requests.find_by(target_account: other_account)
+      req = source_account.follow_requests.find_by(target_account: target_account)
       req.update!(show_reblogs: reblogs)
       return
     end
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index 5265d77f6..598f6cddd 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -4,22 +4,11 @@
   %td.domain
     - unless account.local?
       = link_to account.domain, admin_accounts_path(by_domain: account.domain)
-  %td.protocol
-    - unless account.local?
-      = account.protocol.humanize
-  %td.confirmed
-    - if account.local?
-      - if account.user_confirmed?
-        %i.fa.fa-check
-      - else
-        %i.fa.fa-times
-  %td.subscribed
+  %td
     - if account.local?
-      = t('admin.accounts.location.local')
-    - elsif account.subscribed?
-      %i.fa.fa-check
+      = t("admin.accounts.roles.#{account.user&.role}")
     - else
-      %i.fa.fa-times
+      = account.protocol.humanize
   %td
     = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
     = table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account)
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 27a0682d8..6aa39a80a 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -31,6 +31,11 @@
         - else
           = filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1'
   .filter-subset
+    %strong= t('admin.accounts.role')
+    %ul
+      %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil
+      %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1'
+  .filter-subset
     %strong= t('admin.accounts.order.title')
     %ul
       %li= filter_link_to t('admin.accounts.order.alphabetic'), recent: nil
@@ -56,9 +61,7 @@
       %tr
         %th= t('admin.accounts.username')
         %th= t('admin.accounts.domain')
-        %th= t('admin.accounts.protocol')
-        %th= t('admin.accounts.confirmed')
-        %th= fa_icon 'paper-plane-o'
+        %th
         %th
     %tbody
       = render @accounts
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index ddb1cf15d..5f5d0995c 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -104,7 +104,7 @@
     - else
       = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account)
 
-- unless @account.local?
+- if !@account.local? && @account.hub_url.present?
   %hr
   %h3 OStatus
 
@@ -132,6 +132,7 @@
       - if @account.subscribed?
         = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account)
 
+- if !@account.local? && @account.inbox_url.present?
   %hr
   %h3 ActivityPub
 
diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml
index bab34bc8d..f7fd2538c 100644
--- a/app/views/admin/custom_emojis/_custom_emoji.html.haml
+++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml
@@ -7,7 +7,7 @@
     - if custom_emoji.local?
       = t('admin.accounts.location.local')
     - else
-      = custom_emoji.domain
+      = link_to custom_emoji.domain, admin_custom_emojis_path(by_domain: custom_emoji.domain)
   %td
     - if custom_emoji.local?
       - if custom_emoji.visible_in_picker
diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml
index 20ffb8529..89ea3a6fe 100644
--- a/app/views/admin/custom_emojis/index.html.haml
+++ b/app/views/admin/custom_emojis/index.html.haml
@@ -17,6 +17,20 @@
         - else
           = filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil
 
+= form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do
+  .fields-group
+    - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
+      - if params[key].present?
+        = hidden_field_tag key, params[key]
+
+    - %i(shortcode by_domain).each do |key|
+      .input.string.optional
+        = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.custom_emojis.#{key}")
+
+    .actions
+      %button= t('admin.accounts.search')
+      = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
+
 .table-wrapper
   %table.table
     %thead
diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb
index ba96fda22..bb8591f74 100644
--- a/config/initializers/ostatus.rb
+++ b/config/initializers/ostatus.rb
@@ -17,9 +17,12 @@ Rails.application.configure do
   config.x.alternate_domains = alternate_domains.split(/\s*,\s*/)
 
   config.action_mailer.default_url_options = { host: web_host, protocol: https ? 'https://' : 'http://', trailing_slash: false }
-  config.x.streaming_api_base_url          = 'ws://localhost:4000'
 
-  if Rails.env.production?
-    config.x.streaming_api_base_url = ENV.fetch('STREAMING_API_BASE_URL') { "ws#{https ? 's' : ''}://#{web_host}" }
+  config.x.streaming_api_base_url = ENV.fetch('STREAMING_API_BASE_URL') do
+    if Rails.env.production?
+      "ws#{https ? 's' : ''}://#{web_host}"
+    else
+      "ws://#{ENV['REMOTE_DEV'] == 'true' ? host.split(':').first : 'localhost'}:4000"
+    end
   end
 end
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index cc9594179..6dc8bc1bb 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -57,20 +57,65 @@ ar:
       order:
         title: الترتيب
       profile_url: رابط الملف الشخصي
+      role: التصريحات
+      roles:
+        admin: مدير
+        user: مستخدِم
+      search: البحث
+      statuses: المنشورات
+      title: الحسابات
+      username: إسم المستخدم
+      web: الويب
     custom_emojis:
+      copy: نسخ
       delete: حذف
+      emoji: إيموجي
+      enable: تفعيل
+      upload: رفع
+    domain_blocks:
+      domain: النطاق
+      show:
+        undo: إلغاء
+      undo: إلغاء
     email_domain_blocks:
       delete: حذف
+      domain: النطاق
+      new:
+        create: إضافة نطاق
+    instances:
+      domain_name: النطاق
+      search: البحث
     reports:
+      are_you_sure: هل أنت متأكد ؟
+      comment:
+        label: تعليق
       delete: حذف
+      report_contents: المحتويات
+      reported_by: أبلغ عنه من طرف
+      status: الحالة
+      title: التقارير
+      view: عرض
     settings:
+      contact_information:
+        email: البريد الإلكتروني المهني
       registrations:
         deletion:
           desc_html: السماح لأي مستخدم إغلاق حسابه
+        open:
+          title: فتح التسجيل
+      site_terms:
+        title: شروط الخدمة المخصصة
+      site_title: إسم مثيل الخادم
+      title: إعدادات الموقع
     statuses:
+      back_to_account: العودة إلى صفحة الحساب
       batch:
         delete: حذف
+      media:
+        title: الوسائط
+    title: الإدارة
   application_mailer:
+    salutation: "%{name},"
     settings: 'تغيير تفضيلات البريد الإلكتروني : %{link}'
     signature: إشعارات ماستدون من %{instance}
     view: 'View:'
@@ -83,6 +128,7 @@ ar:
     forgot_password: نسيت كلمة المرور ؟
     login: تسجيل الدخول
     logout: خروج
+    migrate_account: الإنتقال إلى حساب آخر
     register: إنشاء حساب
     resend_confirmation: إعادة إرسال تعليمات التأكيد
     reset_password: إعادة تعيين كلمة المرور
@@ -106,15 +152,20 @@ ar:
       x_months: "%{count} شه"
       x_seconds: "%{count}ث"
   deletes:
+    bad_password_msg: محاولة جيدة يا هاكرز ! كلمة السر خاطئة
     proceed: حذف حساب
+    success_msg: تم حذف حسابك بنجاح
   exports:
     blocks: قمت بحظر
     csv: CSV
     follows: أنت تتبع
     storage: ذاكرة التخزين
+  followers:
+    domain: النطاق
+    followers_count: عدد المتابِعين
   generic:
     changes_saved_msg: تم حفظ التعديلات بنجاح !
-    powered_by: powered by %{link}
+    powered_by: مدعوم بـ %{link}
     save_changes: حفظ التغييرات
     validation_errors:
       one: Something isn't quite right yet! Please review the error below
@@ -128,14 +179,19 @@ ar:
     upload: تحميل
   landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse.."
   landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>.
+  lists:
+    errors:
+      limit: لقد بلغت الحد الأقصى للقوائم
   media_attachments:
     validations:
       images_and_video: ليس بالإمكان إرفاق فيديو في منشور يحتوي مسبقا على صور
       too_many: لا يمكن إرفاق أكثر من 4 ملفات
+  migrations:
+    acct: username@domain للحساب الجديد
   notification_mailer:
     digest:
       body: 'Here is a brief summary of what you missed on %{instance} since your last visit on %{since}:'
-      mention: "%{name} mentioned you in:"
+      mention: "%{name} أشار إليك في :"
       new_followers_summary:
         one: لقد حصلت على متابع جديد !
         other: لقد تحصلت على %{count} متتبعين جدد ! رائع !
@@ -143,11 +199,11 @@ ar:
         one: "إشعار واحد منذ زيارتك الأخيرة \U0001F418"
         other: "%{count} إشعارات جديدة منذ زيارتك الأخيرة \U0001F418"
     favourite:
-      body: أُعجب %{name} بمنشورك
+      body: 'أُعجب %{name} بمنشورك :'
       subject: "%{name} favourited your status"
     follow:
       body: "%{name} من متتبعيك الآن !"
-      subject: "%{name} من متتبعيك الآن !"
+      subject: "%{name} من متتبعيك الآن"
     follow_request:
       body: "%{name} has requested to follow you"
       subject: 'Pending follower: %{name}'
@@ -171,16 +227,21 @@ ar:
   pagination:
     next: التالي
     prev: السابق
+  preferences:
+    languages: اللغات
+    other: إعدادات أخرى
+    publishing: النشر
   remote_follow:
-    acct: Enter your username@domain you want to follow from
+    acct: قم بإدخال عنوان حسابك username@domain الذي من خلاله تود المتابعة
     missing_resource: Could not find the required redirect URL for your account
-    proceed: Proceed to follow
+    proceed: أكمل المتابعة
     prompt: 'إنك  بصدد متابعة :'
   settings:
     authorized_apps: التطبيقات المرخص لها
     back: عودة إلى ماستدون
     edit_profile: تعديل الملف الشخصي
     export: تصدير البيانات
+    followers: المتابِعون المُرَخّصون
     import: إستيراد
     preferences: التفضيلات
     settings: الإعدادات
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 05c08c01a..11bc485da 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -286,7 +286,7 @@ ca:
         desc_html: Mostra una insígnia de personal en una pàgina d'usuari
         title: Mostra insígnia de personal
       site_description:
-        desc_html: Paràgraf introductori a la pàgina principal i en etiquetes meta.<br>Pots utilitzar etiquetes HTML, en particular <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
+        desc_html: Paràgraf introductori a la pàgina principal i en etiquetes meta. Pots utilitzar etiquetes HTML, en particular <code>&lt;a&gt;</code> i <code>&lt;em&gt;</code>.
         title: Descripció del lloc
       site_description_extended:
         desc_html: Un bon lloc per al codi de conducta, regles, directrius i altres coses que distingeixen la vostra instància. Pots utilitzar etiquetes HTML
@@ -410,7 +410,7 @@ ca:
     storage: Emmagatzematge
   followers:
     domain: Domini
-    explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. <strong> Les publicacions privades es lliuren a totes les instàncies on tens seguidors </ strong>. És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquestes instàncies.
+    explanation_html: Si desitges garantir la privacitat de les teves publicacions, has de ser conscient de qui t'està seguint. <strong> Les publicacions privades es lliuren a totes les instàncies on tens seguidors </strong>. És possible que vulguis revisar-los i eliminar seguidors si no confies en que la teva privacitat sigui respectada pel personal o el programari d'aquestes instàncies.
     followers_count: Nombre de seguidors
     lock_link: Bloca el teu compte
     purge: Elimina dels seguidors
diff --git a/config/locales/devise.ar.yml b/config/locales/devise.ar.yml
index 4fd19244d..bb91cb372 100644
--- a/config/locales/devise.ar.yml
+++ b/config/locales/devise.ar.yml
@@ -8,8 +8,11 @@ ar:
       inactive: لم يتم تنشيط حسابك بعد.
       last_attempt: بإمكانك إعادة المحاولة مرة واحدة قبل أن يتم قفل حسابك.
       locked: إن حسابك مقفل.
+      unauthenticated: يجب عليك تسجيل الدخول أو إنشاء حساب قبل المواصلة.
       unconfirmed: يجب عليك تأكيد عنوان بريدك الإلكتروني قبل المواصلة.
     mailer:
+      confirmation_instructions:
+        subject: 'ماستدون : تعليمات التأكيد لمثيل الخادوم  %{instance}'
       password_change:
         subject: 'ماستدون : تم تغيير كلمة المرور'
       reset_password_instructions:
diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml
index 2cd20732f..1a46b80b5 100644
--- a/config/locales/devise.ja.yml
+++ b/config/locales/devise.ja.yml
@@ -35,7 +35,7 @@ ja:
       updated_not_active: パスワードは正常に更新されました。
     registrations:
       destroyed: アカウントの作成はキャンセルされました。またのご利用をお待ちしています。
-      signed_up: アカウントの作成が完了しました。Mastodonへようこそ!
+      signed_up: アカウントの作成が完了しました。Mastodonへようこそ。
       signed_up_but_inactive: アカウントの作成が完了しました。しかし、アカウントが有効化されていないためログインできませんでした。
       signed_up_but_locked: アカウントの作成が完了しました。しかし、アカウントがロックされているためログインできませんでした。
       signed_up_but_unconfirmed: メールアドレスの確認用のリンクが入力したメールアドレスに送信されました。メール内のリンクをクリックしてアカウントを有効化してください。
@@ -58,4 +58,4 @@ ja:
       not_locked: ロックされていません
       not_saved:
         one: エラーが発生したため、%{resource}の保存に失敗しました。
-        other: "%{count}個のエラーが発生したため、保存に失敗しました。 %{resource}"
+        other: "%{count}個のエラーが発生したため、%{resource}の保存に失敗しました:"
diff --git a/config/locales/devise.pt.yml b/config/locales/devise.pt.yml
index dc87cefdd..a09443a9a 100644
--- a/config/locales/devise.pt.yml
+++ b/config/locales/devise.pt.yml
@@ -10,7 +10,7 @@ pt:
       inactive: A tua conta ainda não está ativada.
       invalid: "%{authentication_keys} ou palavra-passe não válida."
       last_attempt: Tens mais uma tentativa antes de a tua conta ficar bloqueada.
-      locked: A tua conta está bloqueada
+      locked: A tua conta está bloqueada.
       not_found_in_database: "%{authentication_keys} ou palavra-passe não válida."
       timeout: A tua sessão expirou. Por favor, entra de novo para continuares.
       unauthenticated: Precisas de entrar na tua conta ou registares-te antes de continuar.
diff --git a/config/locales/doorkeeper.gl.yml b/config/locales/doorkeeper.gl.yml
new file mode 100644
index 000000000..863438454
--- /dev/null
+++ b/config/locales/doorkeeper.gl.yml
@@ -0,0 +1,33 @@
+gl:
+  activerecord:
+    attributes:
+      doorkeeper/application:
+        name: Nome do aplicativo
+        redirect_uri: URI a redireccionar
+        website: Sitio web do aplicativo
+    errors:
+      models:
+        doorkeeper/application:
+          attributes:
+            redirect_uri:
+              fragment_present: non pode conter un fragmento.
+              invalid_uri: debe ser un URI válido.
+              relative_uri: debe ser un URI absoluto.
+              secured_uri: debe ser un URI HTTPS/SSL.
+  doorkeeper:
+    applications:
+      buttons:
+        authorize: Autorizar
+        cancel: Cancelar
+        destroy: Destruír
+        edit: Editar
+        submit: Enviar
+      confirmations:
+        destroy: Está segura?
+      edit:
+        title: Editar aplicativo
+      form:
+        error: Eeeeepa! Comprobe os posibles erros no formulario
+      help:
+        native_redirect_uri: Utilice %{native_redirect_uri} para probas locais
+        redirect_uri: Utilice unha liña por URI
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 12284403c..b283f94f0 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -116,6 +116,7 @@ en:
       roles:
         admin: Administrator
         moderator: Moderator
+        staff: Staff
         user: User
       salmon_url: Salmon URL
       search: Search
@@ -160,6 +161,7 @@ en:
         update_status: "%{name} updated status by %{target}"
       title: Audit log
     custom_emojis:
+      by_domain: Domain
       copied_msg: Successfully created local copy of the emoji
       copy: Copy
       copy_failed_msg: Could not make a local copy of that emoji
@@ -599,7 +601,7 @@ en:
     notifications: Notifications
     preferences: Preferences
     settings: Settings
-    two_factor_authentication: Two-factor Authentication
+    two_factor_authentication: Two-factor Auth
     your_apps: Your applications
   statuses:
     open_in_web: Open in web
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 5a22fdccc..adcb11f18 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -116,6 +116,7 @@ fr:
       roles:
         admin: Administrateur
         moderator: Modérateur
+        staff: Personnel
         user: Utilisateur
       salmon_url: URL Salmon
       search: Rechercher
@@ -135,12 +136,13 @@ fr:
       web: Web
     action_logs:
       actions:
-        confirm_user: "%{name} adresse e-mail confirmée de l'utilisateur %{target}"
+        confirm_user: "%{name} adresse courriel confirmée de l'utilisateur %{target}"
         create_custom_emoji: "%{name} a importé de nouveaux emoji %{target}"
         create_domain_block: "%{name} a bloqué le domaine %{target}"
-        create_email_domain_block: "%{name} a blacklisté le domaine de l'e-mail %{target}"
+        create_email_domain_block: "%{name} a mis le domaine du courriel %{target} sur liste noire"
+        demote_user: "%{name} a rétrogradé l'utilisateur %{target}"
         destroy_domain_block: "%{name} a débloqué le domaine %{target}"
-        destroy_email_domain_block: "%{name} a mis le domaine de l'e-mail %{target} sur liste blanche"
+        destroy_email_domain_block: "%{name} a mis le domaine du courriel %{target} sur liste blanche"
         destroy_status: "%{name} a enlevé le statut de %{target}"
         disable_2fa_user: "%{name} a désactivé l'authentification à deux facteurs pour l'utilisateur %{target}"
         disable_custom_emoji: "%{name} a désactivé l'emoji %{target}"
@@ -159,6 +161,7 @@ fr:
         update_status: "%{name} a mis à jour le statut de %{target}"
       title: Journal d'audit
     custom_emojis:
+      by_domain: Domaine
       copied_msg: Copie locale de l’émoji créée avec succès !
       copy: Copier
       copy_failed_msg: Impossible de faire une copie locale de cet émoji
@@ -191,7 +194,7 @@ fr:
         create: Créer le blocage
         hint: Le blocage de domaine n’empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes.
         severity:
-          desc_html: "<strong>Silence</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspendre</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil."
+          desc_html: "<strong>Silence</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspendre</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil. Utilisez <strong>Aucun</strong> si vous voulez simplement rejeter les fichiers multimédia."
           noop: Aucune
           silence: Masqué
           suspend: Suspendre
@@ -285,7 +288,7 @@ fr:
         desc_html: Montrer un badge de responsable sur une page utilisateur
         title: Montrer un badge de responsable
       site_description:
-        desc_html: Affichée sous la forme d’un paragraphe sur la page d’accueil et utilisée comme balise meta.<br/>Vous pouvez utiliser des balises HTML, en particulier <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
+        desc_html: Paragraphe introductif sur la page d'accueil et dans les balises meta. Vous pouvez utiliser des balises HTML, en particulier <code>&lt;a&gt;</code> et <code>&lt;em&gt;</code>.
         title: Description du site
       site_description_extended:
         desc_html: Affichée sur la page d’informations complémentaires du site<br>Vous pouvez utiliser des balises HTML
@@ -457,6 +460,9 @@ fr:
     title: Inviter des gens
   landing_strip_html: <strong>%{name}</strong> utilise %{link_to_root_path}. Vous pouvez læ suivre et interagir si vous possédez un compte quelque part dans le "fediverse".
   landing_strip_signup_html: Si ce n’est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>.
+  lists:
+    errors:
+      limit: Vous avez atteint le nombre maximum de listes
   media_attachments:
     validations:
       images_and_video: Impossible de joindre une vidéo à un statut contenant déjà des images
@@ -590,11 +596,12 @@ fr:
     open_in_web: Ouvrir sur le web
     over_character_limit: limite de caractères dépassée de %{max} caractères
     pin_errors:
-      limit: Trop de pouets épinglés
+      limit: Vous avez déjà épinglé le nombre maximum de pouets
       ownership: Vous ne pouvez pas épingler un statut ne vous appartenant pas
       private: Les statuts non-publics ne peuvent pas être épinglés
       reblog: Un partage ne peut pas être épinglé
     show_more: Afficher plus
+    title: '%{name} : "%{quote}"'
     visibilities:
       private: Abonné⋅e⋅s uniquement
       private_long: Seul⋅e⋅s vos abonné⋅e⋅s verront vos statuts
@@ -693,7 +700,7 @@ fr:
     manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l’entrer manuellement, voici le secret en clair :'
     recovery_codes: Codes de récupération
     recovery_codes_regenerated: Codes de récupération régénérés avec succès
-    recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l’accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants.
+    recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour retrouver l’accès à votre compte. <strong>Conservez les codes de récupération en sécurité</strong>. Par exemple, en les imprimant et en les stockant avec vos autres documents importants.
     setup: Installer
     wrong_code: Les codes entrés sont incorrects ! L’heure du serveur et celle de votre appareil sont-elles correctes ?
   users:
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 48f8b121d..8974a8f82 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -151,7 +151,7 @@ ja:
         memorialize_account: "%{name} さんが %{target} さんを追悼アカウントページに登録しました"
         promote_user: "%{name} さんが %{target} さんを昇格しました"
         reset_password_user: "%{name} さんが %{target} さんのパスワードをリセットしました"
-        resolve_report: "%{name} さんがレポート %{target} を棄却しました"
+        resolve_report: "%{name} さんがレポート %{target} を解決済みにしました"
         silence_account: "%{name} さんが %{target} さんをサイレンスにしました"
         suspend_account: "%{name} さんが %{target} さんを停止しました"
         unsilence_account: "%{name} さんが %{target} さんのサイレンスを解除しました"
@@ -192,13 +192,13 @@ ja:
         create: ブロックを作成
         hint: ドメインブロックはデータベース中のアカウント項目の作成を妨げませんが、遡って自動的に指定されたモデレーションをそれらのアカウントに適用します。
         severity:
-          desc_html: "<strong>サイレンス</strong>はアカウントのトゥートをフォローしていない人から隠します。<strong>停止</strong>はそのアカウントのコンテンツ、メディア、プロフィールデータをすべて削除します。"
+          desc_html: "<strong>サイレンス</strong>はアカウントのトゥートをフォローしていない人から隠します。<strong>停止</strong>はそのアカウントのコンテンツ、メディア、プロフィールデータをすべて削除します。メディアファイルの拒否は<strong>なし</strong>を使います。"
           noop: なし
           silence: サイレンス
           suspend: 停止
         title: 新規ドメインブロック
       reject_media: メディアファイルを拒否
-      reject_media_hint: ローカルに保存されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。
+      reject_media_hint: ローカルに保存されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です
       severities:
         noop: なし
         silence: サイレンス
@@ -271,7 +271,7 @@ ja:
         username: 連絡先のユーザー名
       registrations:
         closed_message:
-          desc_html: 新規登録を停止しているときにフロントページに表示されます。HTMLタグが使えます。
+          desc_html: 新規登録を停止しているときにフロントページに表示されます。HTMLタグが使えます
           title: 新規登録停止時のメッセージ
         deletion:
           desc_html: 誰でも自分のアカウントを削除できるようにします
@@ -289,14 +289,14 @@ ja:
         desc_html: フロントページへの表示と meta タグに使用される紹介文です。HTMLタグ、特に<code>&lt;a&gt;</code> と <code>&lt;em&gt;</code>が使えます。
         title: インスタンスの説明
       site_description_extended:
-        desc_html: あなたのインスタンスにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます。
+        desc_html: あなたのインスタンスにおける行動規範やルール、ガイドライン、そのほかの記述をする際に最適な場所です。HTMLタグが使えます
         title: カスタム詳細説明
       site_terms:
-        desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます。
+        desc_html: あなたは独自のプライバシーポリシーや利用規約、そのほかの法的根拠を書くことができます。HTMLタグが使えます
         title: カスタム利用規約
       site_title: インスタンスの名前
       thumbnail:
-        desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です。
+        desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です
         title: インスタンスのサムネイル
       timeline_preview:
         desc_html: ランディングページに公開タイムラインを表示します
@@ -333,7 +333,7 @@ ja:
     salutation: "%{name} さん"
     settings: 'メール設定の変更: %{link}'
     signature: Mastodon %{instance} インスタンスからの通知
-    view: リンク
+    view: 'リンク:'
   applications:
     created: アプリが作成されました
     destroyed: アプリが削除されました
@@ -359,12 +359,12 @@ ja:
     reset_password: パスワードを再発行
     set_new_password: 新しいパスワード
   authorize_follow:
-    error: 残念ながら、リモートアカウント情報の取得中にエラーが発生しました。
+    error: 残念ながら、リモートアカウント情報の取得中にエラーが発生しました
     follow: フォロー
     follow_request: 'あなたは以下のアカウントにフォローリクエストを送信しました:'
     following: '成功! あなたは現在以下のアカウントをフォローしています:'
     post_follow:
-      close: またはこのウィンドウを閉じます
+      close: またはこのウィンドウを閉じます。
       return: ユーザーのプロフィールに戻る
       web: Web を開く
     title: "%{acct} をフォロー"
@@ -384,7 +384,7 @@ ja:
       x_seconds: "%{count}秒"
   deletes:
     bad_password_msg: パスワードが違います
-    confirm_password: 本人確認のため、現在のパスワードを入力してください。
+    confirm_password: 本人確認のため、現在のパスワードを入力してください
     description_html: あなたのアカウントに含まれるコンテンツは全て削除され、アカウントは無効化されます。これは恒久的なもので、<strong>取り消すことはできません</strong>。なりすましを防ぐために、同じユーザー名で再度登録することはできなくなります。
     proceed: アカウントを削除する
     success_msg: アカウントは正常に削除されました
@@ -397,7 +397,7 @@ ja:
     '422':
       content: セキュリティ認証に失敗しました。Cookieをブロックしていませんか?
       title: セキュリティ認証に失敗
-    '429': リクエストの制限に達しました。
+    '429': リクエストの制限に達しました
     '500':
       content: もうしわけありませんが、なにかが間違っています。
       title: このページは正しくありません
@@ -419,24 +419,24 @@ ja:
       other: "%{count} 個のドメインからソフトブロックするフォロワーを処理中..."
     true_privacy_html: "<strong>プライバシーの保護はエンドツーエンドの暗号化でのみ実現可能</strong>であることに留意ください。"
     unlocked_warning_html: 誰でもあなたをフォローすることができ、あなたのプライベート投稿をすぐに見ることができます。フォローする人を限定したい場合は%{lock_link}に設定してください。
-    unlocked_warning_title: このアカウントは非公開アカウントに設定されていません。
+    unlocked_warning_title: このアカウントは非公開アカウントに設定されていません
   generic:
-    changes_saved_msg: 正常に変更されました
+    changes_saved_msg: 正常に変更されました!
     powered_by: powered by %{link}
     save_changes: 変更を保存
     use_this: これを使う
     validation_errors:
-      one: エラーが発生しました。以下のエラーを確認してください。
-      other: エラーが発生しました。以下の%{count}個のエラーを確認してください。
+      one: エラーが発生しました! 以下のエラーを確認してください
+      other: エラーが発生しました! 以下の%{count}個のエラーを確認してください
   imports:
     preface: 他のインスタンスでエクスポートされたファイルから、フォロー/ブロックした情報をこのインスタンス上のアカウントにインポートできます。
-    success: ファイルは正常にアップロードされ、現在処理中です。しばらくしてから確認してください。
+    success: ファイルは正常にアップロードされ、現在処理中です。しばらくしてから確認してください
     types:
       blocking: ブロックしたアカウントリスト
       following: フォロー中のアカウントリスト
       muting: ミュートしたアカウントリスト
     upload: アップロード
-  in_memoriam_html: 故人を偲んで
+  in_memoriam_html: 故人を偲んで。
   invites:
     delete: 無効化
     expired: 期限切れ
@@ -452,7 +452,7 @@ ja:
       one: '1'
       other: "%{count}"
     max_uses_prompt: 無制限
-    prompt: リンクを生成・共有してこのインスタンスへの新規登録を受け付けることができます。
+    prompt: リンクを生成・共有してこのインスタンスへの新規登録を受け付けることができます
     table:
       expires_at: 有効期限
       uses: 使用
@@ -467,15 +467,18 @@ ja:
     remove_all: すべて削除
   landing_strip_html: "<strong>%{name}</strong> さんはインスタンス %{link_to_root_path} のユーザーです。アカウントさえ持っていればフォローしたり会話したりできます。"
   landing_strip_signup_html: もしお持ちでないなら <a href="%{sign_up_path}">こちら</a> からサインアップできます。
+  lists:
+    errors:
+      limit: リストの上限に達しました
   media_attachments:
     validations:
-      images_and_video: 既に画像が追加されているため、動画を追加することはできません。
-      too_many: 追加できるファイルは4つまでです。
+      images_and_video: 既に画像が追加されているため、動画を追加することはできません
+      too_many: 追加できるファイルは4つまでです
   migrations:
     acct: 引っ越し先の ユーザー名@ドメイン
     currently_redirecting: 'あなたのプロフィールは引っ越し先が設定されています:'
     proceed: 保存
-    updated_msg: アカウントの引っ越し設定を更新しました
+    updated_msg: アカウントの引っ越し設定を更新しました!
   moderation:
     title: モデレーション
   notification_mailer:
@@ -492,7 +495,7 @@ ja:
       body: "%{name} さんにお気に入り登録された、あなたのトゥートがあります:"
       subject: "%{name} さんにお気に入りに登録されました"
     follow:
-      body: "%{name} さんにフォローされています"
+      body: "%{name} さんにフォローされています!"
       subject: "%{name} さんにフォローされています"
     follow_request:
       body: "%{name} さんがあなたにフォローをリクエストしました"
@@ -601,7 +604,7 @@ ja:
     open_in_web: Webで開く
     over_character_limit: 上限は %{max}文字までです
     pin_errors:
-      limit: 固定されているトゥートが多すぎます
+      limit: 固定されているトゥートの上限に達しました
       ownership: 他人のトゥートを固定することはできません
       private: 非公開のトゥートを固定することはできません
       reblog: ブーストされたトゥートを固定することはできません
@@ -692,7 +695,7 @@ ja:
     formats:
       default: "%Y年%m月%d日 %H:%M"
   two_factor_authentication:
-    code_hint: 確認するには認証アプリで表示されたコードを入力してください。
+    code_hint: 確認するには認証アプリで表示されたコードを入力してください
     description_html: "<strong>二段階認証</strong>を有効にするとログイン時、電話でコードを受け取る必要があります。"
     disable: 無効
     enable: 有効
@@ -703,7 +706,7 @@ ja:
     lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。
     manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
     recovery_codes: リカバリーコード
-    recovery_codes_regenerated: リカバリーコードが再生成されました。
+    recovery_codes_regenerated: リカバリーコードが再生成されました
     recovery_instructions_html: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。<strong>リカバリーコードは大切に保全してください。</strong>たとえば印刷してほかの重要な書類と一緒に保管することができます。
     setup: 初期設定
     wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 91c788ae6..92425672e 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -458,6 +458,9 @@ nl:
     title: Mensen uitnodigen
   landing_strip_html: "<strong>%{name}</strong> is een gebruiker op %{link_to_root_path}. Je kunt deze volgen en ermee communiceren als je op Mastodon (of ergens anders in de fediverse) een account hebt."
   landing_strip_signup_html: Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier registreren</a>.
+  lists:
+    errors:
+      limit: Je hebt het maximaal aantal lijsten bereikt
   media_attachments:
     validations:
       images_and_video: Een video kan niet aan een toot met afbeeldingen worden gekoppeld
@@ -591,7 +594,7 @@ nl:
     open_in_web: In de webapp openen
     over_character_limit: Limiet van %{max} tekens overschreden
     pin_errors:
-      limit: Te veel toots vastgezet
+      limit: Je hebt het maximaal aantal toots al vastgezet
       ownership: Een toot van iemand anders kan niet worden vastgezet
       private: Alleen openbare toots kunnen worden vastgezet
       reblog: Een boost kan niet worden vastgezet
diff --git a/config/locales/oc.yml b/config/locales/oc.yml
index 0167e9271..97d20bdf8 100644
--- a/config/locales/oc.yml
+++ b/config/locales/oc.yml
@@ -114,8 +114,9 @@ oc:
       resubscribe: Se tornar abonar
       role: Permissions
       roles:
-        admin: Admin
-        moderator: Mod
+        admin: Administrator
+        moderator: Moderator
+        staff: Personnal
         user: Uitlizaire
       salmon_url: URL Salmon
       search: Cercar
@@ -160,6 +161,7 @@ oc:
         update_status: "%{name} metèt a jorn l’estatut a %{target}"
       title: Audit log
     custom_emojis:
+      by_domain: Domeni
       copied_msg: Còpia locala de l’emoji ben creada
       copy: Copiar
       copy_failed_msg: Fracàs de la còpia locala de l’emoji
@@ -343,7 +345,7 @@ oc:
     warning: Mèfi ! Agachatz de partejar aquela donada amb degun !
     your_token: Vòstre geton d’accès
   auth:
-    agreement_html: En vos marcar acceptatz <a href="%{rules_path}">nòstres tèrmes de servici</a> e <a href="%{terms_path}">politica de confidencialitat</a>.
+    agreement_html: En vos marcar acceptatz <a href="%{rules_path}">las règlas de l’instància</a> e <a href="%{terms_path}">politica de confidencialitat</a>.
     change_password: Seguretat
     delete_account: Suprimir lo compte
     delete_account_html: Se volètz suprimir vòstre compte, podètz <a href="%{path}">o far aquí</a>. Vos demandarem que confirmetz.
@@ -677,6 +679,7 @@ oc:
       private: Se pòt pas penjar los tuts pas publics
       reblog: Se pòt pas penjar un tut partejat
     show_more: Ne veire mai
+    title: '%{name} : "%{quote}"'
     visibilities:
       private: Seguidors solament
       private_long: Mostrar pas qu’als seguidors
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 582a2cd03..905557a25 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -469,6 +469,9 @@ pl:
     remove_all: Usuń wszystkie
   landing_strip_html: "<strong>%{name}</strong> ma konto na %{link_to_root_path}. Możesz je śledzić i wejść z nim w interakcję jeśli masz konto gdziekolwiek w Fediwersum."
   landing_strip_signup_html: Jeśli jeszcze go nie masz, możesz <a href="%{sign_up_path}">stworzyć konto</a>.
+  lists:
+    errors:
+      limit: Przekroczyłeś maksymalną liczbę utworzonych list
   media_attachments:
     validations:
       images_and_video: Nie możesz załączyć pliku wideo do wpisu, który zawiera już zdjęcia
@@ -606,7 +609,7 @@ pl:
     open_in_web: Otwórz w przeglądarce
     over_character_limit: limit %{max} znaków przekroczony
     pin_errors:
-      limit: Nie możesz przypiąć więcej wpisów
+      limit: Przekroczyłeś maksymalną liczbę przypiętych wpisów
       ownership: Nie możesz przypiąć cudzego wpisu
       private: Nie możesz przypiąć niepublicznego wpisu
       reblog: Nie możesz przypiąć podbicia wpisu
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 597de9e4a..32896ab91 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -282,6 +282,9 @@ pt-BR:
         open:
           desc_html: Permitir que qualquer um crie uma conta
           title: Cadastro aberto
+      show_staff_badge:
+        desc_html: Mostrar uma insígnia de equipe na página de usuário
+        title: Mostrar insígnia de equipe
       site_description:
         desc_html: Parágrafo introdutório na página inicial e em meta tags. Você pode usar tags HTML, em especial <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
         title: Descrição da instância
@@ -350,7 +353,7 @@ pt-BR:
     login: Entrar
     logout: Sair
     migrate_account: Mudar para uma conta diferente
-    migrate_account_html: Se você quer redirecionar essa conta para uma outra você pode <a href="%{path}">configura isso aqui</a>.
+    migrate_account_html: Se você quer redirecionar essa conta para uma outra você pode <a href="%{path}">configurar isso aqui</a>.
     register: Cadastrar-se
     resend_confirmation: Reenviar instruções de confirmação
     reset_password: Redefinir senha
@@ -455,6 +458,9 @@ pt-BR:
     title: Convidar pessoas
   landing_strip_html: "<strong>%{name}</strong> é um usuário no %{link_to_root_path}. Você pode segui-lo ou interagir com ele se você tiver uma conta em qualquer lugar no fediverso."
   landing_strip_signup_html: Se não, você pode <a href="%{sign_up_path}">se cadastrar aqui</a>.
+  lists:
+    errors:
+      limit: Você alcançou o número máximo de listas
   media_attachments:
     validations:
       images_and_video: Não é possível anexar um vídeo a uma postagem que já contém imagens
diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml
new file mode 100644
index 000000000..7fa96992f
--- /dev/null
+++ b/config/locales/simple_form.gl.yml
@@ -0,0 +1,75 @@
+gl:
+  simple_form:
+    hints:
+      defaults:
+        avatar: PNG, GIF ou JPG. Como moito 2MB. Será reducida ate 120x120px
+        digest: Enviar despois de un período longo de inactividade con un resumo das
+          mencións que recibeu na súa ausencia
+        display_name:
+          one: <span class="name-counter">1</span> caracter restante
+          other: <span class="name-counter">%{count}</span> caracteres restantes
+        header: PNG, GIF ou JPG. Como moito 2MB. Será reducida a 700x335px
+        locked: Require que vostede aprove as seguidoras de xeito manual
+        note:
+          one: <span class="note-counter">1</span> caracter restante
+          other: <span class="note-counter">%{count}</span> caracteres restantes
+        setting_noindex: Afecta ao seu perfil público e páxinas de estado
+        setting_theme: Afecta ao aspecto de Mastodon en calquer dispositivo cando
+          está conectada.
+      imports:
+        data: Ficheiro CSV exportado desde outra instancia Mastodon
+      sessions:
+        otp: Introduza o código de Doble-Factor desde o seu teléfono ou utilice un
+          dos seus códigos de recuperación.
+      user:
+        filtered_languages: Os idiomas marcados filtraranse das liñas temporais públicas
+          para vostede
+    labels:
+      defaults:
+        avatar: Avatar
+        confirm_new_password: Confirme o novo contrasinal
+        confirm_password: Confirme o contrasinal
+        current_password: Contrasinal actual
+        data: Data
+        display_name: Nome mostrado
+        email: enderezo correo electrónico
+        expires_in: Caducidade despois de
+        filtered_languages: Idiomas filtrados
+        header: Cabezallo
+        locale: Idioma
+        locked: Protexer conta
+        max_uses: Número máximo de usos
+        new_password: Novo contrasinal
+        note: Sobre vostede
+        otp_attempt: Código de Doble-Factor
+        password: Contrasinal
+        setting_auto_play_gif: Reprodución automática de GIFs animados
+        setting_boost_modal: Pedir confirmación antes de promocionar
+        setting_default_privacy: Intimidade da publicación
+        setting_default_sensitive: Marcar sempre multimedia como sensible
+        setting_delete_modal: Solicitar confirmación antes de eliminar unha mensaxe
+        setting_noindex: Pedir non aparecer nas buscas dos motores de busca
+        setting_reduce_motion: Reducir o movemento nas animacións
+        setting_system_font_ui: Utilizar a tipografía por defecto do sistema
+        setting_theme: Decorado da instancia
+        setting_unfollow_modal: Solicitar confirmación antes de deixar de seguir alguén
+        severity: Severidade
+        type: Tipo de importación
+        username: Nome de usuaria
+      interactions:
+        must_be_follower: Bloquear as notificacións de non-seguidoras
+        must_be_following: Bloquea as notificacións de personas que non segue
+        must_be_following_dm: Bloquea as mensaxes directas de personas que non segue
+      notification_emails:
+        digest: Enviar correos con resumos
+        favourite: Enviar un correo cando alguén marca como favorita unha das súas
+          publicacións
+        follow: Enviar un correo cando alguén a segue
+        follow_request: Enviar un correo cando alguén solicita seguila
+        mention: Enviar un correo cando alguén a menciona
+        reblog: Enviar un correo cando alguén promociona a súa mensaxe
+    'no': Non
+    required:
+      mark: '*'
+      text: requerido
+    'yes': Si
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 3ede5c4d5..a15a1de9f 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -116,6 +116,7 @@ zh-CN:
       roles:
         admin: 管理员
         moderator: 协管
+        staff: 工作人员
         user: 普通用户
       salmon_url: Salmon URL
       search: 搜索
@@ -160,6 +161,7 @@ zh-CN:
         update_status: "%{name} 刷新了 %{target} 的嘟文"
       title: 运营日志
     custom_emojis:
+      by_domain: 域名
       copied_msg: 成功将表情复制到本地
       copy: 复制
       copy_failed_msg: 无法将表情复制到本地
@@ -281,8 +283,8 @@ zh-CN:
           desc_html: 允许任何人建立一个帐户
           title: 开放注册
       show_staff_badge:
-        desc_html: 在个人资料页上显示管理员标志
-        title: 显示管理员标志
+        desc_html: 在个人资料页上显示工作人员标志
+        title: 显示工作人员标志
       site_description:
         desc_html: 展示在首页以及 meta 标签中的网站简介。可以使用 HTML 标签,包括 <code>&lt;a&gt;</code> 和 <code>&lt;em&gt;</code>。
         title: 本站简介
@@ -341,7 +343,7 @@ zh-CN:
     warning: 一定小心,千万不要把它分享给任何人!
     your_token: 你的访问令牌
   auth:
-    agreement_html: 注册即表示你同意<a href="%{rules_path}">我们的使用条款</a>和<a href="%{terms_path}">隐私权政策</a>。
+    agreement_html: 注册即表示你同意遵守<a href="%{rules_path}">本实例的相关规定</a>和<a href="%{terms_path}">我们的使用条款</a>。
     change_password: 帐户安全
     delete_account: 删除帐户
     delete_account_html: 如果你想删除你的帐户,请<a href="%{path}">点击这里继续</a>。你需要确认你的操作。
@@ -368,18 +370,18 @@ zh-CN:
     title: 关注 %{acct}
   datetime:
     distance_in_words:
-      about_x_hours: "%{count} 时"
-      about_x_months: "%{count} 个月"
-      about_x_years: "%{count} 年"
-      almost_x_years: "%{count} 年"
+      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_minutes: "%{count}分"
       less_than_x_seconds: 刚刚
-      over_x_years: "%{count} 年"
-      x_days: "%{count} 天"
-      x_minutes: "%{count} 分"
-      x_months: "%{count} 个月"
-      x_seconds: "%{count} 秒"
+      over_x_years: "%{count}年"
+      x_days: "%{count}天"
+      x_minutes: "%{count}分"
+      x_months: "%{count}个月"
+      x_seconds: "%{count}秒"
   deletes:
     bad_password_msg: 想得美,黑客!密码输入错误
     confirm_password: 输入你当前的密码来验证身份
@@ -591,6 +593,7 @@ zh-CN:
       private: 不能置顶非公开的嘟文
       reblog: 不能置顶转嘟
     show_more: 显示更多
+    title: "%{name}:“%{quote}”"
     visibilities:
       private: 仅关注者
       private_long: 只有关注你的用户能看到
diff --git a/db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb b/db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb
new file mode 100644
index 000000000..03f2591a8
--- /dev/null
+++ b/db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb
@@ -0,0 +1,6 @@
+class RemoveDuplicateIndexesInLists < ActiveRecord::Migration[5.1]
+  def change
+    remove_index :list_accounts, name: "index_list_accounts_on_account_id"
+    remove_index :list_accounts, name: "index_list_accounts_on_list_id"
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 16882c743..cdb76aa26 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20171201000000) do
+ActiveRecord::Schema.define(version: 20171212195226) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -211,10 +211,8 @@ ActiveRecord::Schema.define(version: 20171201000000) do
     t.bigint "account_id", null: false
     t.bigint "follow_id", null: false
     t.index ["account_id", "list_id"], name: "index_list_accounts_on_account_id_and_list_id", unique: true
-    t.index ["account_id"], name: "index_list_accounts_on_account_id"
     t.index ["follow_id"], name: "index_list_accounts_on_follow_id"
     t.index ["list_id", "account_id"], name: "index_list_accounts_on_list_id_and_account_id"
-    t.index ["list_id"], name: "index_list_accounts_on_list_id"
   end
 
   create_table "lists", force: :cascade do |t|
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index f59ec97cd..a430ac225 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -17,7 +17,7 @@ module Mastodon
     end
 
     def pre
-      'rc3'
+      'rc4'
     end
 
     def flags
diff --git a/streaming/index.js b/streaming/index.js
index 3048802e3..d08b9cd87 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -97,6 +97,8 @@ const startWorker = (workerId) => {
   };
 
   const app    = express();
+  app.set('trusted proxy', process.env.TRUSTED_PROXY_IP || 'loopback,uniquelocal');
+
   const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL)));
   const server = http.createServer(app);
   const redisNamespace = process.env.REDIS_NAMESPACE || null;
@@ -177,6 +179,12 @@ const startWorker = (workerId) => {
     next();
   };
 
+  const setRemoteAddress = (req, res, next) => {
+    req.remoteAddress = req.connection.remoteAddress;
+
+    next();
+  };
+
   const accountFromToken = (token, req, next) => {
     pgPool.connect((err, client, done) => {
       if (err) {
@@ -208,17 +216,22 @@ const startWorker = (workerId) => {
     });
   };
 
-  const accountFromRequest = (req, next) => {
+  const accountFromRequest = (req, next, required = true) => {
     const authorization = req.headers.authorization;
     const location = url.parse(req.url, true);
     const accessToken = location.query.access_token;
 
     if (!authorization && !accessToken) {
-      const err = new Error('Missing access token');
-      err.statusCode = 401;
+      if (required) {
+        const err = new Error('Missing access token');
+        err.statusCode = 401;
 
-      next(err);
-      return;
+        next(err);
+        return;
+      } else {
+        next();
+        return;
+      }
     }
 
     const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken;
@@ -226,7 +239,17 @@ const startWorker = (workerId) => {
     accountFromToken(token, req, next);
   };
 
+  const PUBLIC_STREAMS = [
+    'public',
+    'public:local',
+    'hashtag',
+    'hashtag:local',
+  ];
+
   const wsVerifyClient = (info, cb) => {
+    const location = url.parse(info.req.url, true);
+    const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream);
+
     accountFromRequest(info.req, err => {
       if (!err) {
         cb(true, undefined, undefined);
@@ -234,16 +257,24 @@ const startWorker = (workerId) => {
         log.error(info.req.requestId, err.toString());
         cb(false, 401, 'Unauthorized');
       }
-    });
+    }, authRequired);
   };
 
+  const PUBLIC_ENDPOINTS = [
+    '/api/v1/streaming/public',
+    '/api/v1/streaming/public/local',
+    '/api/v1/streaming/hashtag',
+    '/api/v1/streaming/hashtag/local',
+  ];
+
   const authenticationMiddleware = (req, res, next) => {
     if (req.method === 'OPTIONS') {
       next();
       return;
     }
 
-    accountFromRequest(req, next);
+    const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path);
+    accountFromRequest(req, next, authRequired);
   };
 
   const errorMiddleware = (err, req, res, {}) => {
@@ -275,8 +306,10 @@ const startWorker = (workerId) => {
   };
 
   const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false) => {
+    const accountId = req.accountId || req.remoteAddress;
+
     const streamType = notificationOnly ? ' (notification)' : '';
-    log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}${streamType}`);
+    log.verbose(req.requestId, `Starting stream from ${id} for ${accountId}${streamType}`);
 
     const listener = message => {
       const { event, payload, queued_at } = JSON.parse(message);
@@ -286,7 +319,7 @@ const startWorker = (workerId) => {
         const delta          = now - queued_at;
         const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
 
-        log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`);
+        log.silly(req.requestId, `Transmitting for ${accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`);
         output(event, encodedPayload);
       };
 
@@ -313,26 +346,31 @@ const startWorker = (workerId) => {
             return;
           }
 
-          const queries = [
-            client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)),
-          ];
+          if (req.accountId) {
+            const queries = [
+              client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)),
+            ];
 
-          if (accountDomain) {
-            queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain]));
-          }
+            if (accountDomain) {
+              queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain]));
+            }
 
-          Promise.all(queries).then(values => {
-            done();
+            Promise.all(queries).then(values => {
+              done();
 
-            if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) {
-              return;
-            }
+              if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) {
+                return;
+              }
 
-            transmit();
-          }).catch(err => {
+              transmit();
+            }).catch(err => {
+              done();
+              log.error(err);
+            });
+          } else {
             done();
-            log.error(err);
-          });
+            transmit();
+          }
         });
       } else {
         transmit();
@@ -345,13 +383,15 @@ const startWorker = (workerId) => {
 
   // Setup stream output to HTTP
   const streamToHttp = (req, res) => {
+    const accountId = req.accountId || req.remoteAddress;
+
     res.setHeader('Content-Type', 'text/event-stream');
     res.setHeader('Transfer-Encoding', 'chunked');
 
     const heartbeat = setInterval(() => res.write(':thump\n'), 15000);
 
     req.on('close', () => {
-      log.verbose(req.requestId, `Ending stream for ${req.accountId}`);
+      log.verbose(req.requestId, `Ending stream for ${accountId}`);
       clearInterval(heartbeat);
     });
 
@@ -383,8 +423,10 @@ const startWorker = (workerId) => {
 
   // Setup stream end for WebSockets
   const streamWsEnd = (req, ws, closeHandler = false) => (id, listener) => {
+    const accountId = req.accountId || req.remoteAddress;
+
     ws.on('close', () => {
-      log.verbose(req.requestId, `Ending stream for ${req.accountId}`);
+      log.verbose(req.requestId, `Ending stream for ${accountId}`);
       unsubscribe(id, listener);
       if (closeHandler) {
         closeHandler();
@@ -392,7 +434,7 @@ const startWorker = (workerId) => {
     });
 
     ws.on('error', () => {
-      log.verbose(req.requestId, `Ending stream for ${req.accountId}`);
+      log.verbose(req.requestId, `Ending stream for ${accountId}`);
       unsubscribe(id, listener);
       if (closeHandler) {
         closeHandler();
@@ -401,6 +443,7 @@ const startWorker = (workerId) => {
   };
 
   app.use(setRequestId);
+  app.use(setRemoteAddress);
   app.use(allowCrossDomain);
   app.use(authenticationMiddleware);
   app.use(errorMiddleware);
@@ -455,6 +498,7 @@ const startWorker = (workerId) => {
     const req      = ws.upgradeReq;
     const location = url.parse(req.url, true);
     req.requestId  = uuid.v4();
+    req.remoteAddress = ws._socket.remoteAddress;
 
     ws.isAlive = true;
 
@@ -527,12 +571,14 @@ const startWorker = (workerId) => {
 
   const onError = (err) => {
     log.error(err);
+    server.close();
+    process.exit(0);
   };
 
   process.on('SIGINT', onExit);
   process.on('SIGTERM', onExit);
   process.on('exit', onExit);
-  process.on('error', onError);
+  process.on('uncaughtException', onError);
 };
 
 throng({