about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/ui/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/mastodon/features/ui/components')
-rw-r--r--app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx (renamed from app/javascript/mastodon/features/ui/components/__tests__/column-test.js)0
-rw-r--r--app/javascript/mastodon/features/ui/components/actions_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/actions_modal.js)4
-rw-r--r--app/javascript/mastodon/features/ui/components/audio_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/audio_modal.js)8
-rw-r--r--app/javascript/mastodon/features/ui/components/block_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/block_modal.js)12
-rw-r--r--app/javascript/mastodon/features/ui/components/boost_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/boost_modal.js)12
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle.jsx (renamed from app/javascript/mastodon/features/ui/components/bundle.js)10
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle_column_error.jsx (renamed from app/javascript/mastodon/features/ui/components/bundle_column_error.js)11
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle_modal_error.jsx (renamed from app/javascript/mastodon/features/ui/components/bundle_modal_error.js)4
-rw-r--r--app/javascript/mastodon/features/ui/components/column.jsx (renamed from app/javascript/mastodon/features/ui/components/column.js)6
-rw-r--r--app/javascript/mastodon/features/ui/components/column_header.jsx (renamed from app/javascript/mastodon/features/ui/components/column_header.js)2
-rw-r--r--app/javascript/mastodon/features/ui/components/column_link.jsx (renamed from app/javascript/mastodon/features/ui/components/column_link.js)0
-rw-r--r--app/javascript/mastodon/features/ui/components/column_loading.jsx (renamed from app/javascript/mastodon/features/ui/components/column_loading.js)0
-rw-r--r--app/javascript/mastodon/features/ui/components/column_subheading.jsx (renamed from app/javascript/mastodon/features/ui/components/column_subheading.js)0
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.jsx (renamed from app/javascript/mastodon/features/ui/components/columns_area.js)12
-rw-r--r--app/javascript/mastodon/features/ui/components/compare_history_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/compare_history_modal.js)14
-rw-r--r--app/javascript/mastodon/features/ui/components/compose_panel.jsx (renamed from app/javascript/mastodon/features/ui/components/compose_panel.js)7
-rw-r--r--app/javascript/mastodon/features/ui/components/confirmation_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/confirmation_modal.js)11
-rw-r--r--app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx (renamed from app/javascript/mastodon/features/ui/components/disabled_account_banner.js)6
-rw-r--r--app/javascript/mastodon/features/ui/components/drawer_loading.jsx (renamed from app/javascript/mastodon/features/ui/components/drawer_loading.js)0
-rw-r--r--app/javascript/mastodon/features/ui/components/embed_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/embed_modal.js)9
-rw-r--r--app/javascript/mastodon/features/ui/components/filter_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/filter_modal.js)4
-rw-r--r--app/javascript/mastodon/features/ui/components/focal_point_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/focal_point_modal.js)44
-rw-r--r--app/javascript/mastodon/features/ui/components/follow_requests_column_link.jsx (renamed from app/javascript/mastodon/features/ui/components/follow_requests_column_link.js)4
-rw-r--r--app/javascript/mastodon/features/ui/components/header.jsx (renamed from app/javascript/mastodon/features/ui/components/header.js)4
-rw-r--r--app/javascript/mastodon/features/ui/components/image_loader.jsx (renamed from app/javascript/mastodon/features/ui/components/image_loader.js)13
-rw-r--r--app/javascript/mastodon/features/ui/components/image_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/image_modal.js)3
-rw-r--r--app/javascript/mastodon/features/ui/components/link_footer.jsx (renamed from app/javascript/mastodon/features/ui/components/link_footer.js)22
-rw-r--r--app/javascript/mastodon/features/ui/components/list_panel.jsx (renamed from app/javascript/mastodon/features/ui/components/list_panel.js)4
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/media_modal.js)29
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_loading.jsx (renamed from app/javascript/mastodon/features/ui/components/modal_loading.js)0
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.jsx (renamed from app/javascript/mastodon/features/ui/components/modal_root.js)12
-rw-r--r--app/javascript/mastodon/features/ui/components/mute_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/mute_modal.js)14
-rw-r--r--app/javascript/mastodon/features/ui/components/navigation_panel.jsx (renamed from app/javascript/mastodon/features/ui/components/navigation_panel.js)7
-rw-r--r--app/javascript/mastodon/features/ui/components/report_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/report_modal.js)6
-rw-r--r--app/javascript/mastodon/features/ui/components/sign_in_banner.jsx (renamed from app/javascript/mastodon/features/ui/components/sign_in_banner.js)2
-rw-r--r--app/javascript/mastodon/features/ui/components/upload_area.jsx (renamed from app/javascript/mastodon/features/ui/components/upload_area.js)2
-rw-r--r--app/javascript/mastodon/features/ui/components/video_modal.jsx (renamed from app/javascript/mastodon/features/ui/components/video_modal.js)13
-rw-r--r--app/javascript/mastodon/features/ui/components/zoomable_image.jsx (renamed from app/javascript/mastodon/features/ui/components/zoomable_image.js)36
38 files changed, 198 insertions, 149 deletions
diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx
index a56859be0..a56859be0 100644
--- a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js
+++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx
diff --git a/app/javascript/mastodon/features/ui/components/actions_modal.js b/app/javascript/mastodon/features/ui/components/actions_modal.jsx
index 67be69d43..35090e242 100644
--- a/app/javascript/mastodon/features/ui/components/actions_modal.js
+++ b/app/javascript/mastodon/features/ui/components/actions_modal.jsx
@@ -23,7 +23,7 @@ export default class ActionsModal extends ImmutablePureComponent {
     return (
       <li key={`${text}-${i}`}>
         <a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
-          {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
+          {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex={-1} inverted />}
           <div>
             <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
             <div>{meta}</div>
@@ -31,7 +31,7 @@ export default class ActionsModal extends ImmutablePureComponent {
         </a>
       </li>
     );
-  }
+  };
 
   render () {
     return (
diff --git a/app/javascript/mastodon/features/ui/components/audio_modal.js b/app/javascript/mastodon/features/ui/components/audio_modal.jsx
index c46fefce8..c0ac12ba7 100644
--- a/app/javascript/mastodon/features/ui/components/audio_modal.js
+++ b/app/javascript/mastodon/features/ui/components/audio_modal.jsx
@@ -7,15 +7,16 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import Footer from 'mastodon/features/picture_in_picture/components/footer';
 
 const mapStateToProps = (state, { statusId }) => ({
+  language: state.getIn(['statuses', statusId, 'language']),
   accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
 });
 
-export default @connect(mapStateToProps)
 class AudioModal extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
     statusId: PropTypes.string.isRequired,
+    language: PropTypes.string,
     accountStaticAvatar: PropTypes.string.isRequired,
     options: PropTypes.shape({
       autoPlay: PropTypes.bool,
@@ -25,7 +26,7 @@ class AudioModal extends ImmutablePureComponent {
   };
 
   render () {
-    const { media, accountStaticAvatar, statusId, onClose } = this.props;
+    const { media, language, accountStaticAvatar, statusId, onClose } = this.props;
     const options = this.props.options || {};
 
     return (
@@ -34,6 +35,7 @@ class AudioModal extends ImmutablePureComponent {
           <Audio
             src={media.get('url')}
             alt={media.get('description')}
+            lang={language}
             duration={media.getIn(['meta', 'original', 'duration'], 0)}
             height={150}
             poster={media.get('preview_url') || accountStaticAvatar}
@@ -52,3 +54,5 @@ class AudioModal extends ImmutablePureComponent {
   }
 
 }
+
+export default connect(mapStateToProps, null, null, { forwardRef: true })(AudioModal);
diff --git a/app/javascript/mastodon/features/ui/components/block_modal.js b/app/javascript/mastodon/features/ui/components/block_modal.jsx
index a07baeaa6..a9506aa69 100644
--- a/app/javascript/mastodon/features/ui/components/block_modal.js
+++ b/app/javascript/mastodon/features/ui/components/block_modal.jsx
@@ -36,8 +36,6 @@ const mapDispatchToProps = dispatch => {
   };
 };
 
-export default @connect(makeMapStateToProps, mapDispatchToProps)
-@injectIntl
 class BlockModal extends React.PureComponent {
 
   static propTypes = {
@@ -55,20 +53,20 @@ class BlockModal extends React.PureComponent {
   handleClick = () => {
     this.props.onClose();
     this.props.onConfirm(this.props.account);
-  }
+  };
 
   handleSecondary = () => {
     this.props.onClose();
     this.props.onBlockAndReport(this.props.account);
-  }
+  };
 
   handleCancel = () => {
     this.props.onClose();
-  }
+  };
 
   setRef = (c) => {
     this.button = c;
-  }
+  };
 
   render () {
     const { account } = this.props;
@@ -101,3 +99,5 @@ class BlockModal extends React.PureComponent {
   }
 
 }
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(BlockModal));
diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.jsx
index 077ce7b35..fe55c963a 100644
--- a/app/javascript/mastodon/features/ui/components/boost_modal.js
+++ b/app/javascript/mastodon/features/ui/components/boost_modal.jsx
@@ -38,8 +38,6 @@ const mapDispatchToProps = dispatch => {
   };
 };
 
-export default @connect(mapStateToProps, mapDispatchToProps)
-@injectIntl
 class BoostModal extends ImmutablePureComponent {
 
   static contextTypes = {
@@ -62,7 +60,7 @@ class BoostModal extends ImmutablePureComponent {
   handleReblog = () => {
     this.props.onReblog(this.props.status, this.props.privacy);
     this.props.onClose();
-  }
+  };
 
   handleAccountClick = (e) => {
     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
@@ -70,7 +68,7 @@ class BoostModal extends ImmutablePureComponent {
       this.props.onClose();
       this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
     }
-  }
+  };
 
   _findContainer = () => {
     return document.getElementsByClassName('modal-root__container')[0];
@@ -78,7 +76,7 @@ class BoostModal extends ImmutablePureComponent {
 
   setRef = (c) => {
     this.button = c;
-  }
+  };
 
   render () {
     const { status, privacy, intl } = this.props;
@@ -98,7 +96,7 @@ class BoostModal extends ImmutablePureComponent {
         <div className='boost-modal__container'>
           <div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
             <div className='status__info'>
-              <a href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
+              <a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
                 <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
                 <RelativeTimestamp timestamp={status.get('created_at')} />
               </a>
@@ -140,3 +138,5 @@ class BoostModal extends ImmutablePureComponent {
   }
 
 }
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal));
diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.jsx
index a60ace35b..1b10a218b 100644
--- a/app/javascript/mastodon/features/ui/components/bundle.js
+++ b/app/javascript/mastodon/features/ui/components/bundle.jsx
@@ -15,7 +15,7 @@ class Bundle extends React.PureComponent {
     onFetch: PropTypes.func,
     onFetchSuccess: PropTypes.func,
     onFetchFail: PropTypes.func,
-  }
+  };
 
   static defaultProps = {
     loading: emptyComponent,
@@ -24,14 +24,14 @@ class Bundle extends React.PureComponent {
     onFetch: noop,
     onFetchSuccess: noop,
     onFetchFail: noop,
-  }
+  };
 
-  static cache = new Map
+  static cache = new Map;
 
   state = {
     mod: undefined,
     forceRender: false,
-  }
+  };
 
   componentWillMount() {
     this.load(this.props);
@@ -83,7 +83,7 @@ class Bundle extends React.PureComponent {
         this.setState({ mod: null });
         onFetchFail(error);
       });
-  }
+  };
 
   render() {
     const { loading: Loading, error: Error, children, renderDelay } = this.props;
diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.jsx
index dfe970ad0..dc9b0e539 100644
--- a/app/javascript/mastodon/features/ui/components/bundle_column_error.js
+++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.jsx
@@ -31,7 +31,7 @@ class GIF extends React.PureComponent {
     if (!animate) {
       this.setState({ hovering: true });
     }
-  }
+  };
 
   handleMouseLeave = () => {
     const { animate } = this.props;
@@ -39,7 +39,7 @@ class GIF extends React.PureComponent {
     if (!animate) {
       this.setState({ hovering: false });
     }
-  }
+  };
 
   render () {
     const { src, staticSrc, className, animate } = this.props;
@@ -75,7 +75,7 @@ class CopyButton extends React.PureComponent {
     navigator.clipboard.writeText(value);
     this.setState({ copied: true });
     this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
-  }
+  };
 
   componentWillUnmount () {
     if (this.timeout) clearTimeout(this.timeout);
@@ -92,7 +92,6 @@ class CopyButton extends React.PureComponent {
 
 }
 
-export default @injectIntl
 class BundleColumnError extends React.PureComponent {
 
   static propTypes = {
@@ -113,7 +112,7 @@ class BundleColumnError extends React.PureComponent {
     if (onRetry) {
       onRetry();
     }
-  }
+  };
 
   render () {
     const { errorType, multiColumn, stacktrace } = this.props;
@@ -160,3 +159,5 @@ class BundleColumnError extends React.PureComponent {
   }
 
 }
+
+export default injectIntl(BundleColumnError);
diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.jsx
index f9365b95b..d79d0ca4a 100644
--- a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js
+++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.jsx
@@ -16,11 +16,11 @@ class BundleModalError extends React.PureComponent {
     onRetry: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
-  }
+  };
 
   handleRetry = () => {
     this.props.onRetry();
-  }
+  };
 
   render () {
     const { onClose, intl: { formatMessage } } = this.props;
diff --git a/app/javascript/mastodon/features/ui/components/column.js b/app/javascript/mastodon/features/ui/components/column.jsx
index 15538ea38..7bc2f7e00 100644
--- a/app/javascript/mastodon/features/ui/components/column.js
+++ b/app/javascript/mastodon/features/ui/components/column.jsx
@@ -23,7 +23,7 @@ export default class Column extends React.PureComponent {
     }
 
     this._interruptScrollAnimation = scrollTop(scrollable);
-  }
+  };
 
   scrollTop () {
     const scrollable = this.node.querySelector('.scrollable');
@@ -40,11 +40,11 @@ export default class Column extends React.PureComponent {
     if (typeof this._interruptScrollAnimation !== 'undefined') {
       this._interruptScrollAnimation();
     }
-  }, 200)
+  }, 200);
 
   setRef = (c) => {
     this.node = c;
-  }
+  };
 
   render () {
     const { heading, icon, children, active, hideHeadingOnMobile } = this.props;
diff --git a/app/javascript/mastodon/features/ui/components/column_header.js b/app/javascript/mastodon/features/ui/components/column_header.jsx
index b1a36e173..4ceef5957 100644
--- a/app/javascript/mastodon/features/ui/components/column_header.js
+++ b/app/javascript/mastodon/features/ui/components/column_header.jsx
@@ -15,7 +15,7 @@ export default class ColumnHeader extends React.PureComponent {
 
   handleClick = () => {
     this.props.onClick();
-  }
+  };
 
   render () {
     const { icon, type, active, columnHeaderId } = this.props;
diff --git a/app/javascript/mastodon/features/ui/components/column_link.js b/app/javascript/mastodon/features/ui/components/column_link.jsx
index 8eebbf526..8eebbf526 100644
--- a/app/javascript/mastodon/features/ui/components/column_link.js
+++ b/app/javascript/mastodon/features/ui/components/column_link.jsx
diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.jsx
index e5ed22584..e5ed22584 100644
--- a/app/javascript/mastodon/features/ui/components/column_loading.js
+++ b/app/javascript/mastodon/features/ui/components/column_loading.jsx
diff --git a/app/javascript/mastodon/features/ui/components/column_subheading.js b/app/javascript/mastodon/features/ui/components/column_subheading.jsx
index 8160c4aa3..8160c4aa3 100644
--- a/app/javascript/mastodon/features/ui/components/column_subheading.js
+++ b/app/javascript/mastodon/features/ui/components/column_subheading.jsx
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.jsx
index e7def800e..1dd6e34e8 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.jsx
@@ -57,7 +57,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
 
   state = {
     renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
-  }
+  };
 
   componentDidMount() {
     if (!this.props.singleColumn) {
@@ -111,7 +111,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
 
   handleLayoutChange = (e) => {
     this.setState({ renderComposePanel: !e.matches });
-  }
+  };
 
   handleWheel = () => {
     if (typeof this._interruptScrollAnimation !== 'function') {
@@ -119,19 +119,19 @@ export default class ColumnsArea extends ImmutablePureComponent {
     }
 
     this._interruptScrollAnimation();
-  }
+  };
 
   setRef = (node) => {
     this.node = node;
-  }
+  };
 
   renderLoading = columnId => () => {
     return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
-  }
+  };
 
   renderError = (props) => {
     return <BundleColumnError multiColumn errorType='network' {...props} />;
-  }
+  };
 
   render () {
     const { columns, children, singleColumn, isModalOpen } = this.props;
diff --git a/app/javascript/mastodon/features/ui/components/compare_history_modal.js b/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx
index ecccc8f7d..1802c167c 100644
--- a/app/javascript/mastodon/features/ui/components/compare_history_modal.js
+++ b/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx
@@ -12,6 +12,7 @@ import RelativeTimestamp from 'mastodon/components/relative_timestamp';
 import MediaAttachments from 'mastodon/components/media_attachments';
 
 const mapStateToProps = (state, { statusId }) => ({
+  language: state.getIn(['statuses', statusId, 'language']),
   versions: state.getIn(['history', statusId, 'items']),
 });
 
@@ -23,18 +24,18 @@ const mapDispatchToProps = dispatch => ({
 
 });
 
-export default @connect(mapStateToProps, mapDispatchToProps)
 class CompareHistoryModal extends React.PureComponent {
 
   static propTypes = {
     onClose: PropTypes.func.isRequired,
     index: PropTypes.number.isRequired,
     statusId: PropTypes.string.isRequired,
+    language: PropTypes.string.isRequired,
     versions: ImmutablePropTypes.list.isRequired,
   };
 
   render () {
-    const { index, versions, onClose } = this.props;
+    const { index, versions, language, onClose } = this.props;
     const currentVersion = versions.get(index);
 
     const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
@@ -65,12 +66,12 @@ class CompareHistoryModal extends React.PureComponent {
           <div className='status__content'>
             {currentVersion.get('spoiler_text').length > 0 && (
               <React.Fragment>
-                <div className='translate' dangerouslySetInnerHTML={spoilerContent} />
+                <div className='translate' dangerouslySetInnerHTML={spoilerContent} lang={language} />
                 <hr />
               </React.Fragment>
             )}
 
-            <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
+            <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} lang={language} />
 
             {!!currentVersion.get('poll') && (
               <div className='poll'>
@@ -82,6 +83,7 @@ class CompareHistoryModal extends React.PureComponent {
                       <span
                         className='poll__option__text translate'
                         dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }}
+                        lang={language}
                       />
                     </li>
                   ))}
@@ -89,7 +91,7 @@ class CompareHistoryModal extends React.PureComponent {
               </div>
             )}
 
-            <MediaAttachments status={currentVersion} />
+            <MediaAttachments status={currentVersion} lang={language} />
           </div>
         </div>
       </div>
@@ -97,3 +99,5 @@ class CompareHistoryModal extends React.PureComponent {
   }
 
 }
+
+export default connect(mapStateToProps, mapDispatchToProps)(CompareHistoryModal);
diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.js b/app/javascript/mastodon/features/ui/components/compose_panel.jsx
index 92d16b5b3..1c6f80ad5 100644
--- a/app/javascript/mastodon/features/ui/components/compose_panel.js
+++ b/app/javascript/mastodon/features/ui/components/compose_panel.jsx
@@ -8,7 +8,6 @@ import LinkFooter from './link_footer';
 import ServerBanner from 'mastodon/components/server_banner';
 import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose';
 
-export default @connect()
 class ComposePanel extends React.PureComponent {
 
   static contextTypes = {
@@ -22,12 +21,12 @@ class ComposePanel extends React.PureComponent {
   onFocus = () => {
     const { dispatch } = this.props;
     dispatch(changeComposing(true));
-  }
+  };
 
   onBlur = () => {
     const { dispatch } = this.props;
     dispatch(changeComposing(false));
-  }
+  };
 
   componentDidMount () {
     const { dispatch } = this.props;
@@ -66,3 +65,5 @@ class ComposePanel extends React.PureComponent {
   }
 
 }
+
+export default connect()(ComposePanel);
diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modal.js b/app/javascript/mastodon/features/ui/components/confirmation_modal.jsx
index 65d97ca16..4437567a1 100644
--- a/app/javascript/mastodon/features/ui/components/confirmation_modal.js
+++ b/app/javascript/mastodon/features/ui/components/confirmation_modal.jsx
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
 import { injectIntl, FormattedMessage } from 'react-intl';
 import Button from '../../../components/button';
 
-export default @injectIntl
 class ConfirmationModal extends React.PureComponent {
 
   static propTypes = {
@@ -30,20 +29,20 @@ class ConfirmationModal extends React.PureComponent {
       this.props.onClose();
     }
     this.props.onConfirm();
-  }
+  };
 
   handleSecondary = () => {
     this.props.onClose();
     this.props.onSecondary();
-  }
+  };
 
   handleCancel = () => {
     this.props.onClose();
-  }
+  };
 
   setRef = (c) => {
     this.button = c;
-  }
+  };
 
   render () {
     const { message, confirm, secondary } = this.props;
@@ -68,3 +67,5 @@ class ConfirmationModal extends React.PureComponent {
   }
 
 }
+
+export default injectIntl(ConfirmationModal);
diff --git a/app/javascript/mastodon/features/ui/components/disabled_account_banner.js b/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx
index 038cc3553..ea11cea44 100644
--- a/app/javascript/mastodon/features/ui/components/disabled_account_banner.js
+++ b/app/javascript/mastodon/features/ui/components/disabled_account_banner.jsx
@@ -28,8 +28,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 });
 
-export default @injectIntl
-@connect(mapStateToProps, mapDispatchToProps)
 class DisabledAccountBanner extends React.PureComponent {
 
   static propTypes = {
@@ -46,7 +44,7 @@ class DisabledAccountBanner extends React.PureComponent {
     this.props.onLogout();
 
     return false;
-  }
+  };
 
   render () {
     const { disabledAcct, movedToAcct } = this.props;
@@ -90,3 +88,5 @@ class DisabledAccountBanner extends React.PureComponent {
   }
 
 }
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(DisabledAccountBanner));
diff --git a/app/javascript/mastodon/features/ui/components/drawer_loading.js b/app/javascript/mastodon/features/ui/components/drawer_loading.jsx
index 08b0d2347..08b0d2347 100644
--- a/app/javascript/mastodon/features/ui/components/drawer_loading.js
+++ b/app/javascript/mastodon/features/ui/components/drawer_loading.jsx
diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.js b/app/javascript/mastodon/features/ui/components/embed_modal.jsx
index 4679c9650..baf6be411 100644
--- a/app/javascript/mastodon/features/ui/components/embed_modal.js
+++ b/app/javascript/mastodon/features/ui/components/embed_modal.jsx
@@ -9,7 +9,6 @@ const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
 });
 
-export default @injectIntl
 class EmbedModal extends ImmutablePureComponent {
 
   static propTypes = {
@@ -17,7 +16,7 @@ class EmbedModal extends ImmutablePureComponent {
     onClose: PropTypes.func.isRequired,
     onError: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
-  }
+  };
 
   state = {
     loading: false,
@@ -48,11 +47,11 @@ class EmbedModal extends ImmutablePureComponent {
 
   setIframeRef = c =>  {
     this.iframe = c;
-  }
+  };
 
   handleTextareaClick = (e) => {
     e.target.select();
-  }
+  };
 
   render () {
     const { intl, onClose } = this.props;
@@ -95,3 +94,5 @@ class EmbedModal extends ImmutablePureComponent {
   }
 
 }
+
+export default injectIntl(EmbedModal);
diff --git a/app/javascript/mastodon/features/ui/components/filter_modal.js b/app/javascript/mastodon/features/ui/components/filter_modal.jsx
index 376db961d..32ebaf7b7 100644
--- a/app/javascript/mastodon/features/ui/components/filter_modal.js
+++ b/app/javascript/mastodon/features/ui/components/filter_modal.jsx
@@ -13,8 +13,6 @@ const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
 });
 
-export default @connect(undefined)
-@injectIntl
 class FilterModal extends ImmutablePureComponent {
 
   static propTypes = {
@@ -132,3 +130,5 @@ class FilterModal extends ImmutablePureComponent {
   }
 
 }
+
+export default connect(injectIntl(FilterModal));
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
index 479f4abd2..11c4c5237 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx
@@ -39,6 +39,7 @@ const mapStateToProps = (state, { id }) => ({
   account: state.getIn(['accounts', me]),
   isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
   description: state.getIn(['compose', 'media_modal', 'description']),
+  lang: state.getIn(['compose', 'language']),
   focusX: state.getIn(['compose', 'media_modal', 'focusX']),
   focusY: state.getIn(['compose', 'media_modal', 'focusY']),
   dirty: state.getIn(['compose', 'media_modal', 'dirty']),
@@ -99,8 +100,6 @@ class ImageLoader extends React.PureComponent {
 
 }
 
-export default @connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })
-@(component => injectIntl(component, { withRef: true }))
 class FocalPointModal extends ImmutablePureComponent {
 
   static propTypes = {
@@ -134,7 +133,7 @@ class FocalPointModal extends ImmutablePureComponent {
 
     this.updatePosition(e);
     this.setState({ dragging: true });
-  }
+  };
 
   handleTouchStart = e => {
     document.addEventListener('touchmove', this.handleMouseMove);
@@ -142,25 +141,25 @@ class FocalPointModal extends ImmutablePureComponent {
 
     this.updatePosition(e);
     this.setState({ dragging: true });
-  }
+  };
 
   handleMouseMove = e => {
     this.updatePosition(e);
-  }
+  };
 
   handleMouseUp = () => {
     document.removeEventListener('mousemove', this.handleMouseMove);
     document.removeEventListener('mouseup', this.handleMouseUp);
 
     this.setState({ dragging: false });
-  }
+  };
 
   handleTouchEnd = () => {
     document.removeEventListener('touchmove', this.handleMouseMove);
     document.removeEventListener('touchend', this.handleTouchEnd);
 
     this.setState({ dragging: false });
-  }
+  };
 
   updatePosition = e => {
     const { x, y } = getPointerPosition(this.node, e);
@@ -168,24 +167,24 @@ class FocalPointModal extends ImmutablePureComponent {
     const focusY   = (y - .5) * -2;
 
     this.props.onChangeFocus(focusX, focusY);
-  }
+  };
 
   handleChange = e => {
     this.props.onChangeDescription(e.target.value);
-  }
+  };
 
   handleKeyDown = (e) => {
     if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
       this.props.onChangeDescription(e.target.value);
       this.handleSubmit(e);
     }
-  }
+  };
 
   handleSubmit = (e) => {
     e.preventDefault();
     e.stopPropagation();
     this.props.onSave(this.props.description, this.props.focusX, this.props.focusY);
-  }
+  };
 
   getCloseConfirmationMessage = () => {
     const { intl, dirty } = this.props;
@@ -198,15 +197,15 @@ class FocalPointModal extends ImmutablePureComponent {
     } else {
       return null;
     }
-  }
+  };
 
   setRef = c => {
     this.node = c;
-  }
+  };
 
   handleTextDetection = () => {
     this._detectText();
-  }
+  };
 
   _detectText = (refreshCache = false) => {
     const { media } = this.props;
@@ -257,24 +256,24 @@ class FocalPointModal extends ImmutablePureComponent {
       console.error(e);
       this.setState({ detecting: false });
     });
-  }
+  };
 
   handleThumbnailChange = e => {
     if (e.target.files.length > 0) {
       this.props.onSelectThumbnail(e.target.files);
     }
-  }
+  };
 
   setFileInputRef = c => {
     this.fileInput = c;
-  }
+  };
 
   handleFileInputClick = () => {
     this.fileInput.click();
-  }
+  };
 
   render () {
-    const { media, intl, account, onClose, isUploadingThumbnail, description, focusX, focusY, dirty, is_changing_upload } = this.props;
+    const { media, intl, account, onClose, isUploadingThumbnail, description, lang, focusX, focusY, dirty, is_changing_upload } = this.props;
     const { dragging, detecting, progress, ocrStatus } = this.state;
     const x = (focusX /  2) + .5;
     const y = (focusY / -2) + .5;
@@ -320,7 +319,7 @@ class FocalPointModal extends ImmutablePureComponent {
               <React.Fragment>
                 <label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
 
-                <Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
+                <Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
 
                 <label>
                   <span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
@@ -349,6 +348,7 @@ class FocalPointModal extends ImmutablePureComponent {
                 id='upload-modal__description'
                 className='setting-text light'
                 value={detecting ? '…' : description}
+                lang={lang}
                 onChange={this.handleChange}
                 onKeyDown={this.handleKeyDown}
                 disabled={detecting || is_changing_upload}
@@ -426,3 +426,7 @@ class FocalPointModal extends ImmutablePureComponent {
   }
 
 }
+
+export default connect(mapStateToProps, mapDispatchToProps, null, {
+  forwardRef: true,
+})(injectIntl(FocalPointModal, { withRef: true }));
diff --git a/app/javascript/mastodon/features/ui/components/follow_requests_column_link.js b/app/javascript/mastodon/features/ui/components/follow_requests_column_link.jsx
index d04d58011..e6ffbdb84 100644
--- a/app/javascript/mastodon/features/ui/components/follow_requests_column_link.js
+++ b/app/javascript/mastodon/features/ui/components/follow_requests_column_link.jsx
@@ -15,8 +15,6 @@ const mapStateToProps = state => ({
   count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
 });
 
-export default @injectIntl
-@connect(mapStateToProps)
 class FollowRequestsColumnLink extends React.Component {
 
   static propTypes = {
@@ -45,3 +43,5 @@ class FollowRequestsColumnLink extends React.Component {
   }
 
 }
+
+export default injectIntl(connect(mapStateToProps)(FollowRequestsColumnLink));
diff --git a/app/javascript/mastodon/features/ui/components/header.js b/app/javascript/mastodon/features/ui/components/header.jsx
index 1384bebda..c14c6faa7 100644
--- a/app/javascript/mastodon/features/ui/components/header.js
+++ b/app/javascript/mastodon/features/ui/components/header.jsx
@@ -22,8 +22,6 @@ const mapDispatchToProps = (dispatch) => ({
   },
 });
 
-export default @connect(null, mapDispatchToProps)
-@withRouter
 class Header extends React.PureComponent {
 
   static contextTypes = {
@@ -85,3 +83,5 @@ class Header extends React.PureComponent {
   }
 
 }
+
+export default withRouter(connect(null, mapDispatchToProps)(Header));
diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.jsx
index dfa0efe49..9093eab28 100644
--- a/app/javascript/mastodon/features/ui/components/image_loader.js
+++ b/app/javascript/mastodon/features/ui/components/image_loader.jsx
@@ -8,16 +8,18 @@ export default class ImageLoader extends PureComponent {
 
   static propTypes = {
     alt: PropTypes.string,
+    lang: PropTypes.string,
     src: PropTypes.string.isRequired,
     previewSrc: PropTypes.string,
     width: PropTypes.number,
     height: PropTypes.number,
     onClick: PropTypes.func,
     zoomButtonHidden: PropTypes.bool,
-  }
+  };
 
   static defaultProps = {
     alt: '',
+    lang: '',
     width: null,
     height: null,
   };
@@ -26,7 +28,7 @@ export default class ImageLoader extends PureComponent {
     loading: true,
     error: false,
     width: null,
-  }
+  };
 
   removers = [];
   canvas = null;
@@ -86,7 +88,7 @@ export default class ImageLoader extends PureComponent {
     image.addEventListener('load', handleLoad);
     image.src = previewSrc;
     this.removers.push(removeEventListeners);
-  })
+  });
 
   clearPreviewCanvas () {
     const { width, height } = this.canvas;
@@ -126,10 +128,10 @@ export default class ImageLoader extends PureComponent {
   setCanvasRef = c => {
     this.canvas = c;
     if (c) this.setState({ width: c.offsetWidth });
-  }
+  };
 
   render () {
-    const { alt, src, width, height, onClick } = this.props;
+    const { alt, lang, src, width, height, onClick } = this.props;
     const { loading } = this.state;
 
     const className = classNames('image-loader', {
@@ -154,6 +156,7 @@ export default class ImageLoader extends PureComponent {
         ) : (
           <ZoomableImage
             alt={alt}
+            lang={lang}
             src={src}
             onClick={onClick}
             width={width}
diff --git a/app/javascript/mastodon/features/ui/components/image_modal.js b/app/javascript/mastodon/features/ui/components/image_modal.jsx
index 7522c3da5..ca93d7b4e 100644
--- a/app/javascript/mastodon/features/ui/components/image_modal.js
+++ b/app/javascript/mastodon/features/ui/components/image_modal.jsx
@@ -9,7 +9,6 @@ const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
 });
 
-export default @injectIntl
 class ImageModal extends React.PureComponent {
 
   static propTypes = {
@@ -57,3 +56,5 @@ class ImageModal extends React.PureComponent {
   }
 
 }
+
+export default injectIntl(ImageModal);
diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.jsx
index 3664a05bf..68ef015ab 100644
--- a/app/javascript/mastodon/features/ui/components/link_footer.js
+++ b/app/javascript/mastodon/features/ui/components/link_footer.jsx
@@ -3,7 +3,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import { Link } from 'react-router-dom';
-import { domain, version, source_url, profile_directory as profileDirectory } from 'mastodon/initial_state';
+import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state';
 import { logOut } from 'mastodon/utils/log_out';
 import { openModal } from 'mastodon/actions/modal';
 import { PERMISSION_INVITE_USERS } from 'mastodon/permissions';
@@ -24,8 +24,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 });
 
-export default @injectIntl
-@connect(null, mapDispatchToProps)
 class LinkFooter extends React.PureComponent {
 
   static contextTypes = {
@@ -44,7 +42,7 @@ class LinkFooter extends React.PureComponent {
     this.props.onLogout();
 
     return false;
-  }
+  };
 
   render () {
     const { signedIn, permissions } = this.context.identity;
@@ -59,21 +57,27 @@ class LinkFooter extends React.PureComponent {
         <p>
           <strong>{domain}</strong>:
           {' '}
-          <Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
+          <Link to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
+          {statusPageUrl && (
+            <>
+              {DividingCircle}
+              <a href={statusPageUrl} target='_blank' rel='noopener'><FormattedMessage id='footer.status' defaultMessage='Status' /></a>
+            </>
+          )}
           {canInvite && (
             <>
               {DividingCircle}
-              <a key='invites' href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
+              <a href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
             </>
           )}
           {canProfileDirectory && (
             <>
               {DividingCircle}
-              <Link key='directory' to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
+              <Link to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
             </>
           )}
           {DividingCircle}
-          <Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
+          <Link to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
         </p>
 
         <p>
@@ -94,3 +98,5 @@ class LinkFooter extends React.PureComponent {
   }
 
 }
+
+export default injectIntl(connect(null, mapDispatchToProps)(LinkFooter));
diff --git a/app/javascript/mastodon/features/ui/components/list_panel.js b/app/javascript/mastodon/features/ui/components/list_panel.jsx
index 2f92a9254..fcff4e37d 100644
--- a/app/javascript/mastodon/features/ui/components/list_panel.js
+++ b/app/javascript/mastodon/features/ui/components/list_panel.jsx
@@ -20,8 +20,6 @@ const mapStateToProps = state => ({
   lists: getOrderedLists(state),
 });
 
-export default @withRouter
-@connect(mapStateToProps)
 class ListPanel extends ImmutablePureComponent {
 
   static propTypes = {
@@ -53,3 +51,5 @@ class ListPanel extends ImmutablePureComponent {
   }
 
 }
+
+export default withRouter(connect(mapStateToProps)(ListPanel));
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.jsx
index 7f50572e7..e8005e67a 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.jsx
@@ -3,6 +3,7 @@ import ReactSwipeableViews from 'react-swipeable-views';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import Video from 'mastodon/features/video';
+import { connect } from 'react-redux';
 import classNames from 'classnames';
 import { defineMessages, injectIntl } from 'react-intl';
 import IconButton from 'mastodon/components/icon_button';
@@ -20,7 +21,10 @@ const messages = defineMessages({
   next: { id: 'lightbox.next', defaultMessage: 'Next' },
 });
 
-export default @injectIntl
+const mapStateToProps = (state, { statusId }) => ({
+  language: state.getIn(['statuses', statusId, 'language']),
+});
+
 class MediaModal extends ImmutablePureComponent {
 
   static propTypes = {
@@ -43,27 +47,27 @@ class MediaModal extends ImmutablePureComponent {
 
   handleSwipe = (index) => {
     this.setState({ index: index % this.props.media.size });
-  }
+  };
 
   handleTransitionEnd = () => {
     this.setState({
       zoomButtonHidden: false,
     });
-  }
+  };
 
   handleNextClick = () => {
     this.setState({
       index: (this.getIndex() + 1) % this.props.media.size,
       zoomButtonHidden: true,
     });
-  }
+  };
 
   handlePrevClick = () => {
     this.setState({
       index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
       zoomButtonHidden: true,
     });
-  }
+  };
 
   handleChangeIndex = (e) => {
     const index = Number(e.currentTarget.getAttribute('data-index'));
@@ -72,7 +76,7 @@ class MediaModal extends ImmutablePureComponent {
       index: index % this.props.media.size,
       zoomButtonHidden: true,
     });
-  }
+  };
 
   handleKeyDown = (e) => {
     switch(e.key) {
@@ -87,7 +91,7 @@ class MediaModal extends ImmutablePureComponent {
       e.stopPropagation();
       break;
     }
-  }
+  };
 
   componentDidMount () {
     window.addEventListener('keydown', this.handleKeyDown, false);
@@ -110,13 +114,13 @@ class MediaModal extends ImmutablePureComponent {
   };
 
   render () {
-    const { media, statusId, intl, onClose } = this.props;
+    const { media, language, statusId, intl, onClose } = this.props;
     const { navigationHidden } = this.state;
 
     const index = this.getIndex();
 
-    const leftNav  = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>;
-    const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav  media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>;
+    const leftNav  = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>;
+    const rightNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav  media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>;
 
     const content = media.map((image) => {
       const width  = image.getIn(['meta', 'original', 'width']) || null;
@@ -130,6 +134,7 @@ class MediaModal extends ImmutablePureComponent {
             width={width}
             height={height}
             alt={image.get('description')}
+            lang={language}
             key={image.get('url')}
             onClick={this.toggleNavigation}
             zoomButtonHidden={this.state.zoomButtonHidden}
@@ -152,6 +157,7 @@ class MediaModal extends ImmutablePureComponent {
             onCloseVideo={onClose}
             detailed
             alt={image.get('description')}
+            lang={language}
             key={image.get('url')}
           />
         );
@@ -163,6 +169,7 @@ class MediaModal extends ImmutablePureComponent {
             height={height}
             key={image.get('preview_url')}
             alt={image.get('description')}
+            lang={language}
             onClick={this.toggleNavigation}
           />
         );
@@ -229,3 +236,5 @@ class MediaModal extends ImmutablePureComponent {
   }
 
 }
+
+export default connect(mapStateToProps, null, null, { forwardRef: true })(injectIntl(MediaModal));
diff --git a/app/javascript/mastodon/features/ui/components/modal_loading.js b/app/javascript/mastodon/features/ui/components/modal_loading.jsx
index f403ca4c9..f403ca4c9 100644
--- a/app/javascript/mastodon/features/ui/components/modal_loading.js
+++ b/app/javascript/mastodon/features/ui/components/modal_loading.jsx
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.jsx
index 6c4aabae5..c252e6caa 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx
@@ -73,23 +73,23 @@ export default class ModalRoot extends React.PureComponent {
       document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
     } else {
       document.body.classList.remove('with-modals--active');
-      document.documentElement.style.marginRight = 0;
+      document.documentElement.style.marginRight = '0';
     }
   }
 
   setBackgroundColor = color => {
     this.setState({ backgroundColor: color });
-  }
+  };
 
   renderLoading = modalId => () => {
     return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
-  }
+  };
 
   renderError = (props) => {
     const { onClose } = this.props;
 
     return <BundleModalError {...props} onClose={onClose} />;
-  }
+  };
 
   handleClose = (ignoreFocus = false) => {
     const { onClose } = this.props;
@@ -102,11 +102,11 @@ export default class ModalRoot extends React.PureComponent {
       // This would be much smoother with react-intl 3+ and `forwardRef`.
     }
     onClose(message, ignoreFocus);
-  }
+  };
 
   setModalRef = (c) => {
     this._modal = c;
-  }
+  };
 
   render () {
     const { type, props, ignoreFocus } = this.props;
diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.jsx
index d8d8e68c3..fa64bf0f4 100644
--- a/app/javascript/mastodon/features/ui/components/mute_modal.js
+++ b/app/javascript/mastodon/features/ui/components/mute_modal.jsx
@@ -43,8 +43,6 @@ const mapDispatchToProps = dispatch => {
   };
 };
 
-export default @connect(mapStateToProps, mapDispatchToProps)
-@injectIntl
 class MuteModal extends React.PureComponent {
 
   static propTypes = {
@@ -65,23 +63,23 @@ class MuteModal extends React.PureComponent {
   handleClick = () => {
     this.props.onClose();
     this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
-  }
+  };
 
   handleCancel = () => {
     this.props.onClose();
-  }
+  };
 
   setRef = (c) => {
     this.button = c;
-  }
+  };
 
   toggleNotifications = () => {
     this.props.onToggleNotifications();
-  }
+  };
 
   changeMuteDuration = (e) => {
     this.props.onChangeMuteDuration(e);
-  }
+  };
 
   render () {
     const { account, notifications, muteDuration, intl } = this.props;
@@ -138,3 +136,5 @@ class MuteModal extends React.PureComponent {
   }
 
 }
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(MuteModal));
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index 9a9309be0..ee1a83cc6 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -18,7 +18,7 @@ const messages = defineMessages({
   explore: { id: 'explore.title', defaultMessage: 'Explore' },
   local: { id: 'tabs_bar.local_timeline', defaultMessage: 'Local' },
   federated: { id: 'tabs_bar.federated_timeline', defaultMessage: 'Federated' },
-  direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
+  direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
   favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
   bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
   lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
@@ -28,7 +28,6 @@ const messages = defineMessages({
   search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
 });
 
-export default @injectIntl
 class NavigationPanel extends React.Component {
 
   static contextTypes = {
@@ -82,8 +81,8 @@ class NavigationPanel extends React.Component {
         {signedIn && (
           <React.Fragment>
             <ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
-            <ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
             <ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
+            <ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
             <ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
 
             <ListPanel />
@@ -105,3 +104,5 @@ class NavigationPanel extends React.Component {
   }
 
 }
+
+export default injectIntl(NavigationPanel);
diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.jsx
index 264da07ce..8b505b8bd 100644
--- a/app/javascript/mastodon/features/ui/components/report_modal.js
+++ b/app/javascript/mastodon/features/ui/components/report_modal.jsx
@@ -30,8 +30,6 @@ const makeMapStateToProps = () => {
   return mapStateToProps;
 };
 
-export default @connect(makeMapStateToProps)
-@injectIntl
 class ReportModal extends ImmutablePureComponent {
 
   static propTypes = {
@@ -95,7 +93,7 @@ class ReportModal extends ImmutablePureComponent {
     } else {
       this.setState({ selectedRuleIds: selectedRuleIds.remove(ruleId) });
     }
-  }
+  };
 
   handleChangeCategory = category => {
     this.setState({ category });
@@ -217,3 +215,5 @@ class ReportModal extends ImmutablePureComponent {
   }
 
 }
+
+export default connect(makeMapStateToProps)(injectIntl(ReportModal));
diff --git a/app/javascript/mastodon/features/ui/components/sign_in_banner.js b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx
index 8bd32edf9..86fcc11b5 100644
--- a/app/javascript/mastodon/features/ui/components/sign_in_banner.js
+++ b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx
@@ -30,7 +30,7 @@ const SignInBanner = () => {
 
   return (
     <div className='sign-in-banner'>
-      <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p>
+      <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
       <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
       {signupButton}
     </div>
diff --git a/app/javascript/mastodon/features/ui/components/upload_area.js b/app/javascript/mastodon/features/ui/components/upload_area.jsx
index 6c423b2c1..035fe7a26 100644
--- a/app/javascript/mastodon/features/ui/components/upload_area.js
+++ b/app/javascript/mastodon/features/ui/components/upload_area.jsx
@@ -22,7 +22,7 @@ export default class UploadArea extends React.PureComponent {
         break;
       }
     }
-  }
+  };
 
   componentDidMount () {
     window.addEventListener('keyup', this.handleKeyUp, false);
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.jsx
index a1533eba0..0e754ccc7 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.jsx
@@ -2,15 +2,21 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import Video from 'mastodon/features/video';
+import { connect } from 'react-redux';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Footer from 'mastodon/features/picture_in_picture/components/footer';
 import { getAverageFromBlurhash } from 'mastodon/blurhash';
 
-export default class VideoModal extends ImmutablePureComponent {
+const mapStateToProps = (state, { statusId }) => ({
+  language: state.getIn(['statuses', statusId, 'language']),
+});
+
+class VideoModal extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
     statusId: PropTypes.string,
+    language: PropTypes.string,
     options: PropTypes.shape({
       startTime: PropTypes.number,
       autoPlay: PropTypes.bool,
@@ -25,7 +31,7 @@ export default class VideoModal extends ImmutablePureComponent {
   }
 
   render () {
-    const { media, statusId, onClose } = this.props;
+    const { media, statusId, language, onClose } = this.props;
     const options = this.props.options || {};
 
     return (
@@ -43,6 +49,7 @@ export default class VideoModal extends ImmutablePureComponent {
             autoFocus
             detailed
             alt={media.get('description')}
+            lang={language}
           />
         </div>
 
@@ -54,3 +61,5 @@ export default class VideoModal extends ImmutablePureComponent {
   }
 
 }
+
+export default connect(mapStateToProps, null, null, { forwardRef: true })(VideoModal);
diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.js b/app/javascript/mastodon/features/ui/components/zoomable_image.jsx
index 1cf263cb9..b41d0fe31 100644
--- a/app/javascript/mastodon/features/ui/components/zoomable_image.js
+++ b/app/javascript/mastodon/features/ui/components/zoomable_image.jsx
@@ -91,21 +91,22 @@ const normalizeWheel = event => {
   };
 };
 
-export default @injectIntl
 class ZoomableImage extends React.PureComponent {
 
   static propTypes = {
     alt: PropTypes.string,
+    lang: PropTypes.string,
     src: PropTypes.string.isRequired,
     width: PropTypes.number,
     height: PropTypes.number,
     onClick: PropTypes.func,
     zoomButtonHidden: PropTypes.bool,
     intl: PropTypes.object.isRequired,
-  }
+  };
 
   static defaultProps = {
     alt: '',
+    lang: '',
     width: null,
     height: null,
   };
@@ -132,7 +133,7 @@ class ZoomableImage extends React.PureComponent {
     dragged: false,
     lockScroll: { x: 0, y: 0 },
     lockTranslate: { x: 0, y: 0 },
-  }
+  };
 
   removers = [];
   container = null;
@@ -212,7 +213,7 @@ class ZoomableImage extends React.PureComponent {
 
     // lock horizontal scroll
     this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelX, this.state.lockScroll.x);
-  }
+  };
 
   mouseDownHandler = e => {
     this.container.style.cursor = 'grabbing';
@@ -228,7 +229,7 @@ class ZoomableImage extends React.PureComponent {
 
     this.image.addEventListener('mousemove', this.mouseMoveHandler);
     this.image.addEventListener('mouseup', this.mouseUpHandler);
-  }
+  };
 
   mouseMoveHandler = e => {
     const dx = e.clientX - this.state.dragPosition.x;
@@ -238,7 +239,7 @@ class ZoomableImage extends React.PureComponent {
     this.container.scrollTop = Math.max(this.state.dragPosition.top - dy, this.state.lockScroll.y);
 
     this.setState({ dragged: true });
-  }
+  };
 
   mouseUpHandler = () => {
     this.container.style.cursor = 'grab';
@@ -246,13 +247,13 @@ class ZoomableImage extends React.PureComponent {
 
     this.image.removeEventListener('mousemove', this.mouseMoveHandler);
     this.image.removeEventListener('mouseup', this.mouseUpHandler);
-  }
+  };
 
   handleTouchStart = e => {
     if (e.touches.length !== 2) return;
 
     this.lastDistance = getDistance(...e.touches);
-  }
+  };
 
   handleTouchMove = e => {
     const { scrollTop, scrollHeight, clientHeight } = this.container;
@@ -275,7 +276,7 @@ class ZoomableImage extends React.PureComponent {
 
     this.lastMidpoint = midpoint;
     this.lastDistance = distance;
-  }
+  };
 
   zoom(nextScale, midpoint) {
     const { scale, zoomMatrix } = this.state;
@@ -314,11 +315,11 @@ class ZoomableImage extends React.PureComponent {
     const handler = this.props.onClick;
     if (handler) handler();
     this.setState({ navigationHidden: !this.state.navigationHidden });
-  }
+  };
 
   handleMouseDown = e => {
     e.preventDefault();
-  }
+  };
 
   initZoomMatrix = () => {
     const { width, height } = this.props;
@@ -350,7 +351,7 @@ class ZoomableImage extends React.PureComponent {
         translateY: translateY,
       },
     });
-  }
+  };
 
   handleZoomClick = e => {
     e.preventDefault();
@@ -392,18 +393,18 @@ class ZoomableImage extends React.PureComponent {
 
     this.container.style.cursor = 'grab';
     this.container.style.removeProperty('user-select');
-  }
+  };
 
   setContainerRef = c => {
     this.container = c;
-  }
+  };
 
   setImageRef = c => {
     this.image = c;
-  }
+  };
 
   render () {
-    const { alt, src, width, height, intl } = this.props;
+    const { alt, lang, src, width, height, intl } = this.props;
     const { scale, lockTranslate } = this.state;
     const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
     const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
@@ -431,6 +432,7 @@ class ZoomableImage extends React.PureComponent {
             ref={this.setImageRef}
             alt={alt}
             title={alt}
+            lang={lang}
             src={src}
             width={width}
             height={height}
@@ -448,3 +450,5 @@ class ZoomableImage extends React.PureComponent {
   }
 
 }
+
+export default injectIntl(ZoomableImage);