about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.env.production.sample1
-rw-r--r--.gitignore4
-rw-r--r--app/assets/javascripts/components/actions/compose.jsx5
-rw-r--r--app/assets/javascripts/components/actions/reports.jsx10
-rw-r--r--app/assets/javascripts/components/components/extended_video_player.jsx34
-rw-r--r--app/assets/javascripts/components/components/icon_button.jsx28
-rw-r--r--app/assets/javascripts/components/components/media_gallery.jsx8
-rw-r--r--app/assets/javascripts/components/components/status.jsx3
-rw-r--r--app/assets/javascripts/components/components/status_content.jsx2
-rw-r--r--app/assets/javascripts/components/components/video_player.jsx41
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx2
-rw-r--r--app/assets/javascripts/components/containers/status_container.jsx4
-rw-r--r--app/assets/javascripts/components/features/notifications/components/column_settings.jsx3
-rw-r--r--app/assets/javascripts/components/features/report/index.jsx7
-rw-r--r--app/assets/javascripts/components/features/status/components/detailed_status.jsx5
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx6
-rw-r--r--app/assets/javascripts/components/features/ui/components/media_modal.jsx2
-rw-r--r--app/assets/javascripts/components/features/ui/components/modal_root.jsx2
-rw-r--r--app/assets/javascripts/components/features/ui/components/video_modal.jsx47
-rw-r--r--app/assets/javascripts/components/features/ui/index.jsx2
-rw-r--r--app/assets/javascripts/components/locales/en.jsx5
-rw-r--r--app/assets/javascripts/components/locales/index.jsx2
-rw-r--r--app/assets/javascripts/components/locales/nl.jsx68
-rw-r--r--app/assets/javascripts/components/reducers/compose.jsx1
-rw-r--r--app/assets/javascripts/components/reducers/reports.jsx5
-rw-r--r--app/assets/stylesheets/components.scss12
-rw-r--r--app/assets/stylesheets/stream_entries.scss1
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/lib/formatter.rb3
-rw-r--r--app/validators/email_validator.rb (renamed from app/lib/email_validator.rb)0
-rw-r--r--app/validators/status_length_validator.rb (renamed from app/lib/status_length_validator.rb)0
-rw-r--r--app/validators/url_validator.rb (renamed from app/lib/url_validator.rb)0
-rw-r--r--app/views/about/terms.en.html.haml2
-rw-r--r--app/views/admin/accounts/index.html.haml36
-rw-r--r--app/views/admin/accounts/show.html.haml38
-rw-r--r--app/views/admin/domain_blocks/index.html.haml8
-rw-r--r--app/views/admin/domain_blocks/new.html.haml16
-rw-r--r--app/views/admin/pubsubhubbub/index.html.haml12
-rw-r--r--app/views/admin/reports/index.html.haml18
-rw-r--r--app/views/admin/reports/show.html.haml16
-rw-r--r--config/application.rb6
-rw-r--r--config/environments/production.rb3
-rw-r--r--config/initializers/paperclip.rb2
-rw-r--r--config/locales/bg.yml16
-rw-r--r--config/locales/devise.hr.yml6
-rw-r--r--config/locales/devise.nl.yml59
-rw-r--r--config/locales/doorkeeper.fr.yml19
-rw-r--r--config/locales/doorkeeper.nl.yml114
-rw-r--r--config/locales/en.yml177
-rw-r--r--config/locales/es.yml32
-rw-r--r--config/locales/fr.yml181
-rw-r--r--config/locales/ja.yml119
-rw-r--r--config/locales/nl.yml165
-rw-r--r--config/locales/ru.yml6
-rw-r--r--config/locales/simple_form.en.yml3
-rw-r--r--config/locales/simple_form.fr.yml2
-rw-r--r--config/locales/simple_form.ja.yml6
-rw-r--r--config/locales/simple_form.nl.yml46
-rw-r--r--config/locales/simple_form.no.yml6
-rw-r--r--config/navigation.rb12
-rw-r--r--config/settings.yml2
-rw-r--r--spec/spec_helper.rb4
62 files changed, 1114 insertions, 332 deletions
diff --git a/.env.production.sample b/.env.production.sample
index 97bba5e3f..7c82c014e 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -35,6 +35,7 @@ SMTP_PORT=587
 SMTP_LOGIN=
 SMTP_PASSWORD=
 SMTP_FROM_ADDRESS=notifications@example.com
+#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
 #SMTP_AUTH_METHOD=plain
 #SMTP_OPENSSL_VERIFY_MODE=peer
 #SMTP_ENABLE_STARTTLS_AUTO=true
diff --git a/.gitignore b/.gitignore
index b671bcb96..5c95f7806 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,7 @@ config/deploy/*
 
 # Ignore IDE files
 .vscode/
+
+# Ignore postgres + redis volume optionally created by docker-compose
+postgres
+redis
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx
index 1b3cc60dc..88e91c356 100644
--- a/app/assets/javascripts/components/actions/compose.jsx
+++ b/app/assets/javascripts/components/actions/compose.jsx
@@ -2,6 +2,8 @@ import api from '../api';
 
 import { updateTimeline } from './timelines';
 
+import * as emojione from 'emojione';
+
 export const COMPOSE_CHANGE          = 'COMPOSE_CHANGE';
 export const COMPOSE_SUBMIT_REQUEST  = 'COMPOSE_SUBMIT_REQUEST';
 export const COMPOSE_SUBMIT_SUCCESS  = 'COMPOSE_SUBMIT_SUCCESS';
@@ -72,9 +74,8 @@ export function mentionCompose(account, router) {
 export function submitCompose() {
   return function (dispatch, getState) {
     dispatch(submitComposeRequest());
-
     api(getState).post('/api/v1/statuses', {
-      status: getState().getIn(['compose', 'text'], ''),
+      status: emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')),
       in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
       media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
       sensitive: getState().getIn(['compose', 'sensitive']),
diff --git a/app/assets/javascripts/components/actions/reports.jsx b/app/assets/javascripts/components/actions/reports.jsx
index 2c1245dc4..094670d62 100644
--- a/app/assets/javascripts/components/actions/reports.jsx
+++ b/app/assets/javascripts/components/actions/reports.jsx
@@ -7,7 +7,8 @@ export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
 export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
 export const REPORT_SUBMIT_FAIL    = 'REPORT_SUBMIT_FAIL';
 
-export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
+export const REPORT_STATUS_TOGGLE  = 'REPORT_STATUS_TOGGLE';
+export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
 
 export function initReport(account, status) {
   return {
@@ -62,3 +63,10 @@ export function submitReportFail(error) {
     error
   };
 };
+
+export function changeReportComment(comment) {
+  return {
+    type: REPORT_COMMENT_CHANGE,
+    comment
+  };
+};
diff --git a/app/assets/javascripts/components/components/extended_video_player.jsx b/app/assets/javascripts/components/components/extended_video_player.jsx
index 66e5dee16..a64515583 100644
--- a/app/assets/javascripts/components/components/extended_video_player.jsx
+++ b/app/assets/javascripts/components/components/extended_video_player.jsx
@@ -3,15 +3,43 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
 const ExtendedVideoPlayer = React.createClass({
 
   propTypes: {
-    src: React.PropTypes.string.isRequired
+    src: React.PropTypes.string.isRequired,
+    time: React.PropTypes.number,
+    controls: React.PropTypes.bool.isRequired,
+    muted: React.PropTypes.bool.isRequired
   },
 
   mixins: [PureRenderMixin],
 
+  handleLoadedData () {
+    if (this.props.time) {
+      this.video.currentTime = this.props.time;
+    }
+  },
+
+  componentDidMount () {
+    this.video.addEventListener('loadeddata', this.handleLoadedData);
+  },
+
+  componentWillUnmount () {
+    this.video.removeEventListener('loadeddata', this.handleLoadedData);
+  },
+
+  setRef (c) {
+    this.video = c;
+  },
+
   render () {
     return (
-      <div>
-        <video src={this.props.src} autoPlay muted loop />
+      <div className='extended-video-player'>
+        <video
+          ref={this.setRef}
+          src={this.props.src}
+          autoPlay
+          muted={this.props.muted}
+          controls={this.props.controls}
+          loop={!this.props.controls}
+        />
       </div>
     );
   },
diff --git a/app/assets/javascripts/components/components/icon_button.jsx b/app/assets/javascripts/components/components/icon_button.jsx
index 33835f9a0..0c683db5d 100644
--- a/app/assets/javascripts/components/components/icon_button.jsx
+++ b/app/assets/javascripts/components/components/icon_button.jsx
@@ -13,7 +13,8 @@ const IconButton = React.createClass({
     activeStyle: React.PropTypes.object,
     disabled: React.PropTypes.bool,
     inverted: React.PropTypes.bool,
-    animate: React.PropTypes.bool
+    animate: React.PropTypes.bool,
+    overlay: React.PropTypes.bool
   },
 
   getDefaultProps () {
@@ -21,7 +22,8 @@ const IconButton = React.createClass({
       size: 18,
       active: false,
       disabled: false,
-      animate: false
+      animate: false,
+      overlay: false
     };
   },
 
@@ -39,7 +41,7 @@ const IconButton = React.createClass({
     let style = {
       fontSize: `${this.props.size}px`,
       width: `${this.props.size * 1.28571429}px`,
-      height: `${this.props.size}px`,
+      height: `${this.props.size * 1.28571429}px`,
       lineHeight: `${this.props.size}px`,
       ...this.props.style
     };
@@ -48,13 +50,31 @@ const IconButton = React.createClass({
       style = { ...style, ...this.props.activeStyle };
     }
 
+    const classes = ['icon-button'];
+
+    if (this.props.active) {
+      classes.push('active');
+    }
+
+    if (this.props.disabled) {
+      classes.push('disabled');
+    }
+
+    if (this.props.inverted) {
+      classes.push('inverted');
+    }
+
+    if (this.props.overlay) {
+      classes.push('overlayed');
+    }
+
     return (
       <Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
         {({ rotate }) =>
           <button
             aria-label={this.props.title}
             title={this.props.title}
-            className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''} ${this.props.inverted ? 'inverted' : ''}`}
+            className={classes.join(' ')}
             onClick={this.handleClick}
             style={style}>
             <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx
index 72b5e977f..10b7d525b 100644
--- a/app/assets/javascripts/components/components/media_gallery.jsx
+++ b/app/assets/javascripts/components/components/media_gallery.jsx
@@ -39,8 +39,8 @@ const spoilerSubSpanStyle = {
 
 const spoilerButtonStyle = {
   position: 'absolute',
-  top: '6px',
-  left: '8px',
+  top: '4px',
+  left: '4px',
   zIndex: '100'
 };
 
@@ -232,8 +232,8 @@ const MediaGallery = React.createClass({
 
     return (
       <div style={{ ...outerStyle, height: `${this.props.height}px` }}>
-        <div style={spoilerButtonStyle}>
-          <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} />
+        <div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}>
+          <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
         </div>
 
         {children}
diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx
index c4d5f829b..d2d2aaf20 100644
--- a/app/assets/javascripts/components/components/status.jsx
+++ b/app/assets/javascripts/components/components/status.jsx
@@ -25,6 +25,7 @@ const Status = React.createClass({
     onReblog: React.PropTypes.func,
     onDelete: React.PropTypes.func,
     onOpenMedia: React.PropTypes.func,
+    onOpenVideo: React.PropTypes.func,
     onBlock: React.PropTypes.func,
     me: React.PropTypes.number,
     boostModal: React.PropTypes.bool,
@@ -76,7 +77,7 @@ const Status = React.createClass({
 
     if (status.get('media_attachments').size > 0 && !this.props.muted) {
       if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
-        media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />;
+        media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
       } else {
         media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />;
       }
diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx
index 9cf03bb32..c7eefcaf5 100644
--- a/app/assets/javascripts/components/components/status_content.jsx
+++ b/app/assets/javascripts/components/components/status_content.jsx
@@ -92,7 +92,7 @@ const StatusContent = React.createClass({
     const { status } = this.props;
     const { hidden } = this.state;
 
-    const content = { __html: emojify(status.get('content')).replace(/\n/g, '') };
+    const content = { __html: emojify(status.get('content')) };
     const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
     const directionStyle = { direction: 'ltr' };
 
diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx
index ab21ca9cd..dce276c75 100644
--- a/app/assets/javascripts/components/components/video_player.jsx
+++ b/app/assets/javascripts/components/components/video_player.jsx
@@ -6,7 +6,8 @@ import { isIOS } from '../is_mobile';
 
 const messages = defineMessages({
   toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
-  toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }
+  toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
+  expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }
 });
 
 const videoStyle = {
@@ -21,8 +22,8 @@ const videoStyle = {
 
 const muteStyle = {
   position: 'absolute',
-  top: '10px',
-  right: '10px',
+  top: '4px',
+  right: '4px',
   color: 'white',
   textShadow: "0px 1px 1px black, 1px 0px 1px black",
   opacity: '0.8',
@@ -54,8 +55,17 @@ const spoilerSubSpanStyle = {
 
 const spoilerButtonStyle = {
   position: 'absolute',
-  top: '6px',
-  left: '8px',
+  top: '4px',
+  left: '4px',
+  color: 'white',
+  textShadow: "0px 1px 1px black, 1px 0px 1px black",
+  zIndex: '100'
+};
+
+const expandButtonStyle = {
+  position: 'absolute',
+  bottom: '4px',
+  right: '4px',
   color: 'white',
   textShadow: "0px 1px 1px black, 1px 0px 1px black",
   zIndex: '100'
@@ -68,7 +78,8 @@ const VideoPlayer = React.createClass({
     height: React.PropTypes.number,
     sensitive: React.PropTypes.bool,
     intl: React.PropTypes.object.isRequired,
-    autoplay: React.PropTypes.bool
+    autoplay: React.PropTypes.bool,
+    onOpenVideo: React.PropTypes.func.isRequired
   },
 
   getDefaultProps () {
@@ -116,6 +127,11 @@ const VideoPlayer = React.createClass({
     });
   },
 
+  handleExpand () {
+    this.video.pause();
+    this.props.onOpenVideo(this.props.media, this.video.currentTime);
+  },
+
   setRef (c) {
     this.video = c;
   },
@@ -154,8 +170,14 @@ const VideoPlayer = React.createClass({
     const { media, intl, width, height, sensitive, autoplay } = this.props;
 
     let spoilerButton = (
-      <div style={spoilerButtonStyle} >
-        <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
+      <div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
+        <IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
+      </div>
+    );
+
+    let expandButton = (
+      <div style={expandButtonStyle} >
+        <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
       </div>
     );
 
@@ -164,7 +186,7 @@ const VideoPlayer = React.createClass({
     if (this.state.hasAudio) {
       muteButton = (
         <div style={muteStyle}>
-          <IconButton title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
+          <IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
         </div>
       );
     }
@@ -202,6 +224,7 @@ const VideoPlayer = React.createClass({
       <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
         {spoilerButton}
         {muteButton}
+        {expandButton}
         <video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
       </div>
     );
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index b9086de42..5cd727822 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -48,6 +48,7 @@ import fr from 'react-intl/locale-data/fr';
 import hu from 'react-intl/locale-data/hu';
 import ja from 'react-intl/locale-data/ja';
 import pt from 'react-intl/locale-data/pt';
+import nl from 'react-intl/locale-data/nl';
 import no from 'react-intl/locale-data/no';
 import ru from 'react-intl/locale-data/ru';
 import uk from 'react-intl/locale-data/uk';
@@ -76,6 +77,7 @@ addLocaleData([
   ...hu,
   ...ja,
   ...pt,
+  ...nl,
   ...no,
   ...ru,
   ...uk,
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
index fedf80fbf..f704ac722 100644
--- a/app/assets/javascripts/components/containers/status_container.jsx
+++ b/app/assets/javascripts/components/containers/status_container.jsx
@@ -75,6 +75,10 @@ const mapDispatchToProps = (dispatch) => ({
     dispatch(openModal('MEDIA', { media, index }));
   },
 
+  onOpenVideo (media, time) {
+    dispatch(openModal('VIDEO', { media, time }));
+  },
+
   onBlock (account) {
     dispatch(blockAccount(account.get('id')));
   },
diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
index 4e5fe1263..2edf98292 100644
--- a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
@@ -27,8 +27,9 @@ const ColumnSettings = React.createClass({
 
   propTypes: {
     settings: ImmutablePropTypes.map.isRequired,
+    intl: React.PropTypes.object.isRequired,
     onChange: React.PropTypes.func.isRequired,
-    onSave: React.PropTypes.func.isRequired
+    onSave: React.PropTypes.func.isRequired,
   },
 
   mixins: [PureRenderMixin],
diff --git a/app/assets/javascripts/components/features/report/index.jsx b/app/assets/javascripts/components/features/report/index.jsx
index 3177d28b1..fc8e543c5 100644
--- a/app/assets/javascripts/components/features/report/index.jsx
+++ b/app/assets/javascripts/components/features/report/index.jsx
@@ -47,7 +47,7 @@ const Report = React.createClass({
   propTypes: {
     isSubmitting: React.PropTypes.bool,
     account: ImmutablePropTypes.map,
-    statusIds: ImmutablePropTypes.list.isRequired,
+    statusIds: ImmutablePropTypes.orderedSet.isRequired,
     comment: React.PropTypes.string.isRequired,
     dispatch: React.PropTypes.func.isRequired,
     intl: React.PropTypes.object.isRequired
@@ -94,7 +94,8 @@ const Report = React.createClass({
     return (
       <Column heading={intl.formatMessage(messages.heading)} icon='flag'>
         <ColumnBackButtonSlim />
-        <div className='report' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}>
+
+        <div className='report scrollable' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}>
           <div className='report__target' style={{ flex: '0 0 auto', padding: '10px' }}>
             <FormattedMessage id='report.target' defaultMessage='Reporting' />
             <strong>{account.get('acct')}</strong>
@@ -106,7 +107,7 @@ const Report = React.createClass({
             </div>
           </div>
 
-          <div style={{ flex: '0 0 160px', padding: '10px' }}>
+          <div style={{ flex: '0 0 100px', padding: '10px' }}>
             <textarea
               className='report__textarea'
               placeholder={intl.formatMessage(messages.placeholder)}
diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
index 2da57252e..ceafc1a32 100644
--- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx
+++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
@@ -17,7 +17,8 @@ const DetailedStatus = React.createClass({
 
   propTypes: {
     status: ImmutablePropTypes.map.isRequired,
-    onOpenMedia: React.PropTypes.func.isRequired
+    onOpenMedia: React.PropTypes.func.isRequired,
+    onOpenVideo: React.PropTypes.func.isRequired,
   },
 
   mixins: [PureRenderMixin],
@@ -39,7 +40,7 @@ const DetailedStatus = React.createClass({
 
     if (status.get('media_attachments').size > 0) {
       if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
-        media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />;
+        media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
       } else {
         media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;
       }
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 48fea658d..7ead68807 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -112,6 +112,10 @@ const Status = React.createClass({
     this.props.dispatch(openModal('MEDIA', { media, index }));
   },
 
+  handleOpenVideo (media, time) {
+    this.props.dispatch(openModal('VIDEO', { media, time }));
+  },
+
   handleReport (status) {
     this.props.dispatch(initReport(status.get('account'), status));
   },
@@ -151,7 +155,7 @@ const Status = React.createClass({
           <div className='scrollable'>
             {ancestors}
 
-            <DetailedStatus status={status} me={me} onOpenMedia={this.handleOpenMedia} />
+            <DetailedStatus status={status} me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} />
             <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} />
 
             {descendants}
diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx
index 35eb2cb0c..130f48b46 100644
--- a/app/assets/javascripts/components/features/ui/components/media_modal.jsx
+++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx
@@ -111,7 +111,7 @@ const MediaModal = React.createClass({
     if (attachment.get('type') === 'image') {
       content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />;
     } else if (attachment.get('type') === 'gifv') {
-      content = <ExtendedVideoPlayer src={url} />;
+      content = <ExtendedVideoPlayer src={url} muted={true} controls={false} />;
     }
 
     return (
diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
index e7ac02dde..74eb50039 100644
--- a/app/assets/javascripts/components/features/ui/components/modal_root.jsx
+++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
@@ -1,10 +1,12 @@
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import MediaModal from './media_modal';
+import VideoModal from './video_modal';
 import BoostModal from './boost_modal';
 import { TransitionMotion, spring } from 'react-motion';
 
 const MODAL_COMPONENTS = {
   'MEDIA': MediaModal,
+  'VIDEO': VideoModal,
   'BOOST': BoostModal
 };
 
diff --git a/app/assets/javascripts/components/features/ui/components/video_modal.jsx b/app/assets/javascripts/components/features/ui/components/video_modal.jsx
new file mode 100644
index 000000000..1c3519bd3
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/video_modal.jsx
@@ -0,0 +1,47 @@
+import LoadingIndicator from '../../../components/loading_indicator';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ExtendedVideoPlayer from '../../../components/extended_video_player';
+import { defineMessages, injectIntl } from 'react-intl';
+import IconButton from '../../../components/icon_button';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' }
+});
+
+const closeStyle = {
+  position: 'absolute',
+  zIndex: '100',
+  top: '4px',
+  right: '4px'
+};
+
+const VideoModal = React.createClass({
+
+  propTypes: {
+    media: ImmutablePropTypes.map.isRequired,
+    time: React.PropTypes.number,
+    onClose: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { media, intl, time, onClose } = this.props;
+
+    const url = media.get('url');
+
+    return (
+      <div className='modal-root__modal media-modal'>
+        <div>
+          <div style={closeStyle}><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div>
+          <ExtendedVideoPlayer src={url} muted={false} controls={true} time={time} />
+        </div>
+      </div>
+    );
+  }
+
+});
+
+export default injectIntl(VideoModal);
diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx
index 89fb82568..5c7cc6ef4 100644
--- a/app/assets/javascripts/components/features/ui/index.jsx
+++ b/app/assets/javascripts/components/features/ui/index.jsx
@@ -47,7 +47,7 @@ const UI = React.createClass({
       this.dragTargets.push(e.target);
     }
 
-    if (e.dataTransfer && e.dataTransfer.files.length > 0) {
+    if (e.dataTransfer && e.dataTransfer.items.length > 0) {
       this.setState({ draggingOver: true });
     }
   },
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index 740caef9d..47e23d983 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -5,7 +5,7 @@
  *   1. to add your new string here; and
  *   2. to remove old strings that are no longer needed; and
  *   3. to sort the strings by the key.
- #   4. To rename the `en` const name and export default name to match your locale.
+ *   4. To rename the `en` const name and export default name to match your locale.
  * Thanks!
  */
 const en = {
@@ -47,7 +47,7 @@ const en = {
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
   "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
   "follow_request.authorize": "Authorize",
-  "follow_request.reject": "Rejec",
+  "follow_request.reject": "Reject",
   "getting_started.apps": "Various apps are available",
   "getting_started.heading": "Getting started",
   "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
@@ -125,6 +125,7 @@ const en = {
   "upload_progress.label": "Uploading...",
   "video_player.toggle_sound": "Toggle sound",
   "video_player.toggle_visible": "Toggle visibility",
+  "video_player.expand": "Expand video",
 };
 
 export default en;
diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx
index f14568a3d..7525022b1 100644
--- a/app/assets/javascripts/components/locales/index.jsx
+++ b/app/assets/javascripts/components/locales/index.jsx
@@ -3,6 +3,7 @@ import de from './de';
 import es from './es';
 import hu from './hu';
 import fr from './fr';
+import nl from './nl';
 import no from './no';
 import pt from './pt';
 import uk from './uk';
@@ -19,6 +20,7 @@ const locales = {
   es,
   hu,
   fr,
+  nl,
   no,
   pt,
   uk,
diff --git a/app/assets/javascripts/components/locales/nl.jsx b/app/assets/javascripts/components/locales/nl.jsx
new file mode 100644
index 000000000..cc80854fc
--- /dev/null
+++ b/app/assets/javascripts/components/locales/nl.jsx
@@ -0,0 +1,68 @@
+const nl = {
+  "column_back_button.label": "terug",
+  "lightbox.close": "Sluiten",
+  "loading_indicator.label": "Laden...",
+  "status.mention": "Vermeld @{name}",
+  "status.delete": "Verwijder",
+  "status.reply": "Reageer",
+  "status.reblog": "Boost",
+  "status.favourite": "Favoriet",
+  "status.reblogged_by": "{name} boostte",
+  "status.sensitive_warning": "Gevoelige inhoud",
+  "status.sensitive_toggle": "Klik om te zien",
+  "video_player.toggle_sound": "Geluid omschakelen",
+  "account.mention": "Vermeld @{name}",
+  "account.edit_profile": "Bewerk profiel",
+  "account.unblock": "Deblokkeer @{name}",
+  "account.unfollow": "Ontvolg",
+  "account.block": "Blokkeer @{name}",
+  "account.follow": "Volg",
+  "account.posts": "Berichten",
+  "account.follows": "Volgt",
+  "account.followers": "Volgers",
+  "account.follows_you": "Volgt jou",
+  "account.requested": "Wacht op goedkeuring",
+  "getting_started.heading": "Beginnen",
+  "getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent, door het e-mailachtige adres in het zoekscherm in te voeren.",
+  "getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.",
+  "getting_started.open_source_notice": "Mastodon is open source software. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
+  "column.home": "Thuis",
+  "column.community": "Lokale tijdlijn",
+  "column.public": "Federatietijdlijn",
+  "column.notifications": "Meldingen",
+  "tabs_bar.compose": "Schrijven",
+  "tabs_bar.home": "Thuis",
+  "tabs_bar.mentions": "Vermeldingen",
+  "tabs_bar.public": "Federatietijdlijn",
+  "tabs_bar.notifications": "Meldingen",
+  "compose_form.placeholder": "Waar ben je mee bezig?",
+  "compose_form.publish": "Toot",
+  "compose_form.sensitive": "Markeer media als gevoelig",
+  "compose_form.spoiler": "Verberg tekst achter waarschuwing",
+  "compose_form.private": "Mark als priv",
+  "compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op  {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Priv plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
+  "compose_form.unlisted": "Niet tonen op openbare tijdlijnen",
+  "navigation_bar.edit_profile": "Bewerk profiel",
+  "navigation_bar.preferences": "Voorkeuren",
+  "navigation_bar.community_timeline": "Lokale tijdlijn",
+  "navigation_bar.public_timeline": "Federatietijdlijn",
+  "navigation_bar.logout": "Uitloggen",
+  "reply_indicator.cancel": "Annuleren",
+  "search.placeholder": "Zoeken",
+  "search.account": "Account",
+  "search.hashtag": "Hashtag",
+  "upload_button.label": "Toevoegen media",
+  "upload_form.undo": "Ongedaan maken",
+  "notification.follow": "{name} volgde jou",
+  "notification.favourite": "{name} markeerde je status als favoriet",
+  "notification.reblog": "{name} boostte je status",
+  "notification.mention": "{name} vermeldde jou",
+  "notifications.column_settings.alert": "Desktopmeldingen",
+  "notifications.column_settings.show": "Tonen in kolom",
+  "notifications.column_settings.follow": "Nieuwe volgers:",
+  "notifications.column_settings.favourite": "Favoriten:",
+  "notifications.column_settings.mention": "Vermeldingen:",
+  "notifications.column_settings.reblog": "Boosts:",
+};
+
+export default nl;
diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx
index 86974239b..7349cc351 100644
--- a/app/assets/javascripts/components/reducers/compose.jsx
+++ b/app/assets/javascripts/components/reducers/compose.jsx
@@ -67,6 +67,7 @@ function clearAll(state) {
     map.set('is_submitting', false);
     map.set('in_reply_to', null);
     map.set('privacy', state.get('default_privacy'));
+    map.set('sensitive', false);
     map.update('media_attachments', list => list.clear());
   });
 };
diff --git a/app/assets/javascripts/components/reducers/reports.jsx b/app/assets/javascripts/components/reducers/reports.jsx
index e1cce1c5f..eab004377 100644
--- a/app/assets/javascripts/components/reducers/reports.jsx
+++ b/app/assets/javascripts/components/reducers/reports.jsx
@@ -4,7 +4,8 @@ import {
   REPORT_SUBMIT_SUCCESS,
   REPORT_SUBMIT_FAIL,
   REPORT_CANCEL,
-  REPORT_STATUS_TOGGLE
+  REPORT_STATUS_TOGGLE,
+  REPORT_COMMENT_CHANGE
 } from '../actions/reports';
 import Immutable from 'immutable';
 
@@ -39,6 +40,8 @@ export default function reports(state = initialState, action) {
 
       return set.remove(action.statusId);
     });
+  case REPORT_COMMENT_CHANGE:
+    return state.setIn(['new', 'comment'], action.comment);
   case REPORT_SUBMIT_REQUEST:
     return state.setIn(['new', 'isSubmitting'], true);
   case REPORT_SUBMIT_FAIL:
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index b135d27c9..1c363ec15 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -112,6 +112,18 @@
       color: $color3;
     }
   }
+
+  &.overlayed {
+    box-sizing: content-box;
+    background: rgba($color8, 0.6);
+    color: rgba($color5, 0.7);
+    border-radius: 4px;
+    padding: 2px;
+
+    &:hover {
+      background: rgba($color8, 0.9);
+    }
+  }
 }
 
 .text-icon-button {
diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss
index 4a6dc6aa4..7bd180c15 100644
--- a/app/assets/stylesheets/stream_entries.scss
+++ b/app/assets/stylesheets/stream_entries.scss
@@ -218,6 +218,7 @@
       margin-top: 8px;
       height: 300px;
       overflow: hidden;
+      position: relative;
 
       video {
         position: relative;
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 3162e565f..cf7b9b381 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -9,6 +9,7 @@ module SettingsHelper
     fr: 'Français',
     it: 'Italiano',
     hu: 'Magyar',
+    nl: 'Nederlands',
     no: 'Norsk',
     pt: 'Português',
     fi: 'Suomi',
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index b6d371ed2..64b1f86d4 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -15,6 +15,7 @@ class Formatter
     html = status.text
     html = encode(html)
     html = simple_format(html, {}, sanitize: false)
+    html = html.gsub(/\n/, '')
     html = link_urls(html)
     html = link_mentions(html, status.mentions)
     html = link_hashtags(html)
@@ -95,6 +96,6 @@ class Formatter
   end
 
   def mention_html(match, account)
-    "#{match.split('@').first}<span class=\"h-card\"><a href=\"#{TagManager.instance.url_for(account)}\" class=\"u-url mention\">@#{account.username}</a></span>"
+    "#{match.split('@').first}<span class=\"h-card\"><a href=\"#{TagManager.instance.url_for(account)}\" class=\"u-url mention\">@<span>#{account.username}</span></a></span>"
   end
 end
diff --git a/app/lib/email_validator.rb b/app/validators/email_validator.rb
index 06e9375f6..06e9375f6 100644
--- a/app/lib/email_validator.rb
+++ b/app/validators/email_validator.rb
diff --git a/app/lib/status_length_validator.rb b/app/validators/status_length_validator.rb
index 55135a598..55135a598 100644
--- a/app/lib/status_length_validator.rb
+++ b/app/validators/status_length_validator.rb
diff --git a/app/lib/url_validator.rb b/app/validators/url_validator.rb
index 4a5c4ef3f..4a5c4ef3f 100644
--- a/app/lib/url_validator.rb
+++ b/app/validators/url_validator.rb
diff --git a/app/views/about/terms.en.html.haml b/app/views/about/terms.en.html.haml
index 9fb318053..e1766ca16 100644
--- a/app/views/about/terms.en.html.haml
+++ b/app/views/about/terms.en.html.haml
@@ -51,7 +51,7 @@
   %h3#coppa Children's Online Privacy Protection Act Compliance
 
   %p
-    Our site, products and services are all directed to people who are at least 13 years old or older. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA
+    Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA
     = surround '(', '),' do
       = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act'
     do not use this site.
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 4d636601e..bd1eb7ecd 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -1,30 +1,30 @@
 - content_for :page_title do
-  Accounts
+  = t('admin.accounts.title')
 
 .filters
   .filter-subset
-    %strong Location
+    %strong= t('admin.accounts.location.title')
     %ul
-      %li= filter_link_to 'All', local: nil, remote: nil
-      %li= filter_link_to 'Local', local: '1', remote: nil
-      %li= filter_link_to 'Remote', remote: '1', local: nil
+      %li= filter_link_to t('admin.accounts.location.all'), local: nil, remote: nil
+      %li= filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil
+      %li= filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil
   .filter-subset
-    %strong Moderation
+    %strong= t('admin.accounts.moderation.title')
     %ul
-      %li= filter_link_to 'All', silenced: nil, suspended: nil
-      %li= filter_link_to 'Silenced', silenced: '1'
-      %li= filter_link_to 'Suspended', suspended: '1'
+      %li= filter_link_to t('admin.accounts.moderation.all'), silenced: nil, suspended: nil
+      %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1'
+      %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1'
   .filter-subset
-    %strong Order
+    %strong= t('admin.accounts.order.title')
     %ul
-      %li= filter_link_to 'Alphabetic', recent: nil
-      %li= filter_link_to 'Most recent', recent: '1'
+      %li= filter_link_to t('admin.accounts.order.alphabetic'), recent: nil
+      %li= filter_link_to t('admin.accounts.order.most_recent'), recent: '1'
 
 %table.table
   %thead
     %tr
-      %th Username
-      %th Domain
+      %th= t('admin.accounts.username')
+      %th= t('admin.accounts.domain')
       %th= fa_icon 'paper-plane-o'
       %th
   %tbody
@@ -36,14 +36,14 @@
             = link_to account.domain, admin_accounts_path(by_domain: account.domain)
         %td
           - if account.local?
-            Local
+            = t('admin.accounts.location.local')
           - elsif account.subscribed?
             %i.fa.fa-check
           - else
             %i.fa.fa-times
         %td
-          = table_link_to 'circle', 'Web', web_path("accounts/#{account.id}")
-          = table_link_to 'globe', 'Public', TagManager.instance.url_for(account)
-          = table_link_to 'pencil', 'Edit', admin_account_path(account.id)
+          = 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)
+          = table_link_to 'pencil', t('admin.accounts.edit'), admin_account_path(account.id)
 
 = paginate @accounts
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 22901aed1..6d2a4d123 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -4,24 +4,24 @@
 %table.table
   %tbody
     %tr
-      %th Username
+      %th= t('admin.accounts.username')
       %td= @account.username
     %tr
-      %th Domain
+      %th= t('admin.accounts.domain')
       %td= @account.domain
     %tr
-      %th Display name
+      %th= t('admin.accounts.display_name')
       %td= @account.display_name
 
     - if @account.local?
       %tr
-        %th E-mail
+        %th= t('admin.accounts.email')
         %td= @account.user.email
       %tr
-        %th Most recent IP
+        %th= t('admin.accounts.most_recent_ip')
         %td= @account.user.current_sign_in_ip
       %tr
-        %th Most recent activity
+        %th= t('admin.accounts.most_recent_activity')
         %td
           - if @account.user.current_sign_in_at
             = l @account.user.current_sign_in_at
@@ -29,44 +29,44 @@
             Never
     - else
       %tr
-        %th Profile URL
+        %th= t('admin.accounts.profile_url')
         %td= link_to @account.url
       %tr
-        %th Feed URL
+        %th= t('admin.accounts.feed_url')
         %td= link_to @account.remote_url
       %tr
-        %th PuSH subscription expires
+        %th= t('admin.accounts.push_subscription_expires')
         %td
           - if @account.subscribed?
             = l @account.subscription_expires_at
           - else
-            Not subscribed
+            = t('admin.accounts.not_subscribed')
       %tr
-        %th Salmon URL
+        %th= t('admin.accounts.salmon_url')
         %td= link_to @account.salmon_url
 
     %tr
-      %th Follows
+      %th= t('admin.accounts.follows')
       %td= @account.following_count
     %tr
-      %th Followers
+      %th= t('admin.accounts.followers')
       %td= @account.followers_count
     %tr
-      %th Statuses
+      %th= t('admin.accounts.statuses')
       %td= @account.statuses_count
     %tr
-      %th Media attachments
+      %th= t('admin.accounts.media_attachments')
       %td
         = @account.media_attachments.count
         = surround '(', ')' do
           = number_to_human_size @account.media_attachments.sum('file_file_size')
 
 - if @account.silenced?
-  = link_to 'Undo silence', admin_account_silence_path(@account.id), method: :delete, class: 'button'
+  = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button'
 - else
-  = link_to 'Silence', admin_account_silence_path(@account.id), method: :post, class: 'button'
+  = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button'
 
 - if @account.suspended?
-  = link_to 'Undo suspension', admin_account_suspension_path(@account.id), method: :delete, class: 'button'
+  = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button'
 - else
-  = link_to 'Perform full suspension', admin_account_suspension_path(@account.id), method: :post, data: { confirm: 'Are you sure?' }, class: 'button'
+  = 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'
diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml
index fe6ff683f..6f4ba9b57 100644
--- a/app/views/admin/domain_blocks/index.html.haml
+++ b/app/views/admin/domain_blocks/index.html.haml
@@ -1,11 +1,11 @@
 - content_for :page_title do
-  Domain Blocks
+  = t('admin.domain_block.title')
 
 %table.table
   %thead
     %tr
-      %th Domain
-      %th Severity
+      %th= t('admin.domain_block.domain')
+      %th= t('admin.domain_block.severity')
   %tbody
     - @blocks.each do |block|
       %tr
@@ -14,4 +14,4 @@
         %td= block.severity
 
 = paginate @blocks
-= link_to 'Add new', new_admin_domain_block_path, class: 'button'
+= link_to t('admin.domain_block.add_new'), new_admin_domain_block_path, class: 'button'
diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml
index fbd39d6cf..53aab21ff 100644
--- a/app/views/admin/domain_blocks/new.html.haml
+++ b/app/views/admin/domain_blocks/new.html.haml
@@ -1,18 +1,14 @@
 - content_for :page_title do
-  New domain block
+  = t('admin.domain_block.new.title')
 
 = simple_form_for @domain_block, url: admin_domain_blocks_path do |f|
   = render 'shared/error_messages', object: @domain_block
 
-  %p.hint The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts.
+  %p.hint= t('admin.domain_block.new.hint')
 
-  = f.input :domain, placeholder: 'Domain'
-  = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false
+  = f.input :domain, placeholder: t('admin.domain_block.domain')
+  = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("admin.domain_block.new.severity.#{type}") }
 
-  %p.hint
-    %strong Silence
-    will make the account's posts invisible to anyone who isn't following them.
-    %strong Suspend
-    will remove all of the account's content, media, and profile data.
+  %p.hint= t('admin.domain_block.new.severity.desc_html')
   .actions
-    = f.button :button, 'Create block', type: :submit
+    = f.button :button, t('admin.domain_block.new.create'), type: :submit
diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml
index dcbb11c11..6b1d1ba4a 100644
--- a/app/views/admin/pubsubhubbub/index.html.haml
+++ b/app/views/admin/pubsubhubbub/index.html.haml
@@ -1,14 +1,14 @@
 - content_for :page_title do
-  PubSubHubbub
+  = t('admin.pubsubhubbub.title')
 
 %table.table
   %thead
     %tr
-      %th Topic
-      %th Callback URL
-      %th Confirmed
-      %th Expires in
-      %th Last delivery
+      %th= t('admin.pubsubhubbub.topic')
+      %th= t('admin.pubsubhubbub.callback_url')
+      %th= t('admin.pubsubhubbub.confirmed')
+      %th= t('admin.pubsubhubbub.expires_in')
+      %th= t('admin.pubsubhubbub.last_delivery')
   %tbody
     - @subscriptions.each do |subscription|
       %tr
diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml
index 68dc07016..d5deec8f6 100644
--- a/app/views/admin/reports/index.html.haml
+++ b/app/views/admin/reports/index.html.haml
@@ -1,12 +1,12 @@
 - content_for :page_title do
-  = t('reports.reports')
+  = t('admin.reports.title')
 
 .filters
   .filter-subset
-    %strong= t('reports.status')
+    %strong= t('admin.reports.status')
     %ul
-      %li= filter_link_to t('reports.unresolved'), action_taken: nil
-      %li= filter_link_to t('reports.resolved'), action_taken: '1'
+      %li= filter_link_to t('admin.reports.unresolved'), action_taken: nil
+      %li= filter_link_to t('admin.reports.resolved'), action_taken: '1'
 
 = form_tag do
 
@@ -14,10 +14,10 @@
     %thead
       %tr
         %th
-        %th= t('reports.id')
-        %th= t('reports.target')
-        %th= t('reports.reported_by')
-        %th= t('reports.comment.label')
+        %th= t('admin.reports.id')
+        %th= t('admin.reports.target')
+        %th= t('admin.reports.reported_by')
+        %th= t('admin.reports.comment.label')
         %th
     %tbody
       - @reports.each do |report|
@@ -27,6 +27,6 @@
           %td= link_to report.target_account.acct, admin_account_path(report.target_account.id)
           %td= link_to report.account.acct, admin_account_path(report.account.id)
           %td= truncate(report.comment, length: 30, separator: ' ')
-          %td= table_link_to 'circle', t('reports.view'), admin_report_path(report)
+          %td= table_link_to 'circle', t('admin.reports.view'), admin_report_path(report)
 
 = paginate @reports
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index ecbb98482..a7430f396 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -1,16 +1,16 @@
 - content_for :page_title do
-  = t('reports.report', id: @report.id)
+  = t('admin.reports.report', id: @report.id)
 
 .report-accounts
   .report-accounts__item
-    %strong= t('reports.reported_account')
+    %strong= t('admin.reports.reported_account')
     = render partial: 'authorize_follow/card', locals: { account: @report.target_account }
   .report-accounts__item
-    %strong= t('reports.reported_by')
+    %strong= t('admin.reports.reported_by')
     = render partial: 'authorize_follow/card', locals: { account: @report.account }
 
 %p
-  %strong= t('reports.comment.label')
+  %strong= t('admin.reports.comment.label')
   \:
   = @report.comment.presence || t('reports.comment.none')
 
@@ -22,7 +22,7 @@
       .activity-stream.activity-stream-headless
         .entry= render partial: 'stream_entries/simple_status', locals: { status: status }
       .report-status__actions
-        = link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: t('reports.delete') do
+        = link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: t('admin.reports.delete') do
           = fa_icon 'trash'
 
 - if !@report.action_taken?
@@ -30,10 +30,10 @@
 
   %div{ style: 'overflow: hidden' }
     %div{ style: 'float: right' }
-      = link_to t('reports.silence_account'), silence_admin_report_path(@report), method: :post, class: 'button'
-      = link_to t('reports.suspend_account'), suspend_admin_report_path(@report), method: :post, class: 'button'
+      = link_to t('admin.reports.silence_account'), silence_admin_report_path(@report), method: :post, class: 'button'
+      = link_to t('admin.reports.suspend_account'), suspend_admin_report_path(@report), method: :post, class: 'button'
     %div{ style: 'float: left' }
-      = link_to t('reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button'
+      = link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button'
 - elsif !@report.action_taken_by_account.nil?
   %hr/
 
diff --git a/config/application.rb b/config/application.rb
index 959385802..f5c276862 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -24,7 +24,6 @@ module Mastodon
 
     # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
     # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
-
     config.i18n.available_locales = [
       :en,
       :bg,
@@ -36,6 +35,7 @@ module Mastodon
       :it,
       :hu,
       :ja,
+      :nl,
       :no,
       :pt,
       :ru,
@@ -63,7 +63,9 @@ module Mastodon
     config.middleware.use Rack::Attack
     config.middleware.use Rack::Deflater
 
-    config.browserify_rails.commandline_options = '--transform [ babelify --presets [ es2015 react ] ] --extension=".jsx"'
+    config.browserify_rails.source_map_environments << 'development'
+    config.browserify_rails.commandline_options   = '--transform [ babelify --presets [ es2015 react ] ] --extension=".jsx"'
+    config.browserify_rails.evaluate_node_modules = true
 
     config.to_prepare do
       Doorkeeper::AuthorizationsController.layout 'public'
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 37a10fd4d..0b0ae29c8 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -104,7 +104,8 @@ Rails.application.configure do
     :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true,
   }
 
-  config.action_mailer.delivery_method = :smtp
+  config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym
+
 
   config.react.variant = :production
 
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 2a2ef8fea..c58f1b06c 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -4,7 +4,7 @@ Paperclip.options[:read_timeout] = 60
 
 Paperclip.interpolates :filename do |attachment, style|
   return attachment.original_filename if style == :original
-  [basename(attachment, style), extension(attachment, style)].delete_if(&:empty?).join('.')
+  [basename(attachment, style), content_type_extension(attachment, style)].delete_if(&:empty?).join('.')
 end
 
 if ENV['S3_ENABLED'] == 'true'
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index a8687f3ca..bceb66ca5 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -58,7 +58,7 @@ bg:
   authorize_follow:
     error: Възникна грешка в откриването на потребителя
     follow: Последвай
-    prompt_html: '(<strong>%{self}</strong>), молбата ти беше изпратена до:'
+    prompt_html: "(<strong>%{self}</strong>), молбата ти беше изпратена до:"
     title: Последвай %{acct}
   datetime:
     distance_in_words:
@@ -94,6 +94,10 @@ bg:
       following: Списък на последователите
     upload: Качване
   landing_strip_html: <strong>%{name}</strong> е потребител от <strong>%{domain}</strong>. Можеш да ги следваш, или да контактуваш с тях, ако имаш акаунт където и да е из федерираната вселена на Mastodon. Ако нямаш акаунт, можеш да си <a href="%{sign_up_path}">създадеш ето тук</a>.
+  media_attachments:
+    validations:
+      images_and_video: Не мога да прикача видеоклип към публикация, която вече съдържа изображения
+      too_many: Не мога да прикача повече от 4 файла
   notification_mailer:
     digest:
       body: 'Ето кратко резюме на нещата, които се случиха от последното ти посещение в %{instance} на %{since}:'
@@ -114,8 +118,8 @@ bg:
       body: "%{name} помоли за разрешение да те последва"
       subject: 'Чакащ последовател: %{name}'
     mention:
-      body: '%{name} те спомена в:'
-      subject: '%{name} те спомена'
+      body: "%{name} те спомена в:"
+      subject: "%{name} те спомена"
     reblog:
       body: 'Твоята публикация беше споделена от %{name}:'
       subject: "%{name} сподели публикацията ти"
@@ -156,14 +160,10 @@ bg:
     disable: Деактивирай
     enable: Активирай
     instructions_html: "<strong>Сканирай този QR код с Google Authenticator или подобно приложение от своя телефон</strong>. Oтсега нататък, това приложение ще генерира код, който ще трябва да въвеждаш при всяко влизане."
-    plaintext_secret_html: "Тайна в обикновен текст: <samp>%{secret}</samp>"
+    plaintext_secret_html: 'Тайна в обикновен текст: <samp>%{secret}</samp>'
     warning: Ако не можеш да настроиш приложението за удостверяване сега, избери "Деактивирай". В противен случай, няма да можеш да влезеш в акаунта си.
   users:
     invalid_email: E-mail адресът е невалиден
     invalid_otp_token: Невалиден код
   will_paginate:
     page_gap: "&hellip;"
-  media_attachments:
-    validations:
-      too_many: Не мога да прикача повече от 4 файла
-      images_and_video: Не мога да прикача видеоклип към публикация, която вече съдържа изображения
diff --git a/config/locales/devise.hr.yml b/config/locales/devise.hr.yml
index 352641ca7..40e0effcf 100644
--- a/config/locales/devise.hr.yml
+++ b/config/locales/devise.hr.yml
@@ -2,15 +2,15 @@
 hr:
   devise:
     confirmations:
-      confirmed: Tvoja email adresa je uspješno potvrđena.
-      send_instructions: Primit ćeš email sa uputama kako potvrditi  svoju email adresu za nekoliko minuta.
-      send_paranoid_instructions: Ako tvoja email adresa postoji u našoj bazi podataka, primit ćeš email sa uputama kako ju potvrditi za nekoliko minuta.
       already_authenticated: Već si prijavljen.
+      confirmed: Tvoja email adresa je uspješno potvrđena.
       inactive: Tvoj račun još nije aktiviran.
       invalid: Nevaljan %{authentication_keys} ili lozinka.
       last_attempt: Imaš još jedan pokušaj prije no što ti se račun zaključa.
       locked: Tvoj račun je zaključan.
       not_found_in_database: Nevaljani %{authentication_keys} ili lozinka.
+      send_instructions: Primit ćeš email sa uputama kako potvrditi  svoju email adresu za nekoliko minuta.
+      send_paranoid_instructions: Ako tvoja email adresa postoji u našoj bazi podataka, primit ćeš email sa uputama kako ju potvrditi za nekoliko minuta.
       timeout: Tvoja sesija je istekla. Molimo te, prijavi se ponovo kako bi nastavio.
       unauthenticated: Moraš se registrirati ili prijaviti prije no što nastaviš.
       unconfirmed: Moraš potvrditi svoju email adresu prije no što nastaviš.
diff --git a/config/locales/devise.nl.yml b/config/locales/devise.nl.yml
new file mode 100644
index 000000000..9057a6775
--- /dev/null
+++ b/config/locales/devise.nl.yml
@@ -0,0 +1,59 @@
+---
+nl:
+  devise:
+    confirmations:
+      confirmed: Je account is bevestigd.
+      send_instructions: Je ontvangt via e-mail instructies hoe je je account kan bevestigen.
+      send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je account kan bevestigen.
+    failure:
+      already_authenticated: Je bent al ingelogd.
+      inactive: Je account is nog niet geactiveerd.
+      invalid: Ongeldig e-mail of wachtwoord.
+      invalid_token: Invalide authenticiteit token.
+      last_attempt: Je hebt nog een poging over voordat je account wordt geblokkeerd.
+      locked: Je account is gelocked.
+      not_found_in_database: Ongeldig e-mail of wachtwoord.
+      timeout: Je sessie is verlopen, log a.u.b. opnieuw in.
+      unauthenticated: Je dient in te loggen of je in te schrijven.
+      unconfirmed: Je dient eerst je account te bevestigen.
+    mailer:
+      confirmation_instructions:
+        subject: Bevestiging mailadres
+      reset_password_instructions:
+        subject: Wachtwoord resetten
+      unlock_instructions:
+        subject: Unlock instructies
+    omniauth_callbacks:
+      failure: Kon je niet aanmelden met je %{kind} account, omdat "%{reason}".
+      success: Successvol aangemeld met je %{kind} account.
+    passwords:
+      no_token: Je kan deze pagina niet benaderen zonder een "wachtwoord reset e-mail"
+      send_instructions: Je ontvangt via e-mail instructies hoe je je wachtwoord moet resetten.
+      send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je wachtwoord moet resetten.
+      updated: Je wachtwoord is gewijzigd. Je bent nu ingelogd.
+      updated_not_active: Je wachtwoord is gewijzigd.
+    registrations:
+      destroyed: Je account is verwijderd, wellicht tot ziens!
+      signed_up: Je bent ingeschreven.
+      signed_up_but_inactive: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat je account nog niet geactiveerd is.
+      signed_up_but_locked: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat je account geblokkeerd is.
+      signed_up_but_unconfirmed: Je ontvangt via e-mail instructies hoe je je account kunt activeren.
+      update_needs_confirmation: Je hebt je e-mailadres succesvol gewijzigd, maar we moeten je nieuwe mailadres nog verifiëren. Controleer je e-mail en klik op de link in de mail om je mailadres te verifiëren.
+      updated: Je account gegevens zijn opgeslagen.
+    sessions:
+      signed_in: Je bent succesvol ingelogd.
+      signed_out: Je bent succesvol uitgelogd.
+    unlocks:
+      send_instructions: Je ontvangt via e-mail instructies hoe je je account kan unlocken.
+      send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je account kan unlocken.
+      unlocked: Je account is ge-unlocked. Je kan nu weer inloggen.
+  errors:
+    messages:
+      already_confirmed: is reeds bevestigd
+      confirmation_period_expired: moet worden bevestigd binnen %{period}, probeer het a.u.b. nog een keer
+      expired: is verlopen, vraag een nieuwe aan
+      not_found: niet gevonden
+      not_locked: is niet gesloten
+      not_saved:
+        one: '1 fout blokkeerde het opslaan van deze %{resource}:'
+        other: "%{count} fouten blokkeerden het opslaan van deze %{resource}:"
diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml
index edfc71336..842a96ac8 100644
--- a/config/locales/doorkeeper.fr.yml
+++ b/config/locales/doorkeeper.fr.yml
@@ -13,7 +13,16 @@ fr:
         name: Nom
         redirect_uri: L'URL de redirection
     errors:
+      messages:
+        record_invalid: Données invalides
       models:
+        account:
+          attributes:
+            note:
+              too_long: Description trop longue
+            username:
+              blank: Identifiant vide
+              taken: Identifiant déjà pris
         doorkeeper/application:
           attributes:
             redirect_uri:
@@ -21,23 +30,17 @@ fr:
               invalid_uri: doit être une URL valide.
               relative_uri: doit être une URL absolue.
               secured_uri: doit être une URL HTTP/SSL.
-        account:
-          attributes:
-            username:
-              blank: Identifiant vide
         user:
           attributes:
             email:
-              taken: Email pris
-              invalid: Email invalide
               blank: Email vide
+              invalid: Email invalide
+              taken: Email pris
             password:
               blank: Mot de passe vide
               too_short: Mot de passe trop court
             password_confirmation:
               confirmation: Le mot de passe ne correspond pas
-      messages:
-        record_invalid: Données invalides
   doorkeeper:
     applications:
       buttons:
diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml
new file mode 100644
index 000000000..91e62dc0c
--- /dev/null
+++ b/config/locales/doorkeeper.nl.yml
@@ -0,0 +1,114 @@
+---
+nl:
+  activerecord:
+    attributes:
+      doorkeeper/application:
+        name: Naam
+        redirect_uri: Redirect URI
+        scopes: Scopes
+    errors:
+      models:
+        doorkeeper/application:
+          attributes:
+            redirect_uri:
+              fragment_present: kan geen fragment bevatten.
+              invalid_uri: moet een geldige URI zijn.
+              relative_uri: moet een absolute URI zijn.
+              secured_uri: moet een HTTPS/SSL URI zijn.
+  doorkeeper:
+    applications:
+      buttons:
+        authorize: Autoriseren
+        cancel: Annuleren
+        destroy: Verwijderen
+        edit: Bewerken
+        submit: Opslaan
+      confirmations:
+        destroy: Weet je het zeker?
+      edit:
+        title: Bewerk applicatie
+      form:
+        error: Oops! Controleer het formulier op fouten
+      help:
+        native_redirect_uri: Gebruik %{native_redirect_uri} voor lokale tests
+        redirect_uri: 'Gebruik één regel per URI. '
+        scopes: Scheid scopes met spaties. Laat leeg om de standaard scopes te gebruiken.
+      index:
+        callback_url: Callback URL
+        name: Naam
+        new: Nieuwe applicatie
+        title: Jouw applicaties
+      new:
+        title: Nieuwe applicatie
+      show:
+        actions: Acties
+        application_id: Applicatie Id
+        callback_urls: Callback urls
+        scopes: Scopes
+        secret: Secret
+        title: 'Applicatie: %{name}'
+    authorizations:
+      buttons:
+        authorize: Autoriseren
+        deny: Weigeren
+      error:
+        title: Er is een fout opgetreden
+      new:
+        able_to: Deze applicatie zal in staat zijn om
+        prompt: "%{client_name} autoriseren om uw account te gebruiken?"
+        title: Autorisatie vereist
+      show:
+        title: Autorisatie code
+    authorized_applications:
+      buttons:
+        revoke: Intrekken
+      confirmations:
+        revoke: Weet je het zeker?
+      index:
+        application: Applicatie
+        created_at: Aangemaakt op
+        date_format: "%d-%m-%Y %H:%M:%S"
+        title: Jouw geautoriseerde applicaties
+    errors:
+      messages:
+        access_denied: De resource eigenaar of autorisatie-server weigerde het verzoek.
+        credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.
+        invalid_client: Client verificatie is mislukt door onbekende klant, geen client authenticatie opgegeven, of een niet-ondersteunde authenticatie methode.
+        invalid_grant: De verstrekte autorisatie is ongeldig, verlopen, ingetrokken, komt niet overeen met de redirect uri die is opgegeven, of werd uitgegeven aan een andere klant.
+        invalid_redirect_uri: De opgegeven redirect uri is niet geldig.
+        invalid_request: Het verzoek mist een vereiste parameter, bevat een niet-ondersteunde parameter waarde of is anderszins onjuist.
+        invalid_resource_owner: De verstrekte resource eigenaar gegevens zijn niet geldig of de resource eigenaar kan niet worden gevonden
+        invalid_scope: De opgevraagde scope is niet geldig, onbekend of onjuist.
+        invalid_token:
+          expired: Het toegangstoken is verlopen
+          revoked: Het toegangstoken is geweigerd
+          unknown: Het toegangstoken is ongeldig
+        resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.
+        server_error: De autorisatieserver is een onverwachte voorwaarde tegengekomen die het verzoek verhinderd.
+        temporarily_unavailable: De autorisatieserver is momenteel niet in staat het verzoek te behandelen als gevolg van een tijdelijke overbelasting of onderhoud aan de server.
+        unauthorized_client: De client is niet bevoegd om dit verzoek met deze methode uit te voeren.
+        unsupported_grant_type: Het type autorisatie is niet ondersteund door de autorisatieserver
+        unsupported_response_type: De autorisatieserver ondersteund dit response type niet
+    flash:
+      applications:
+        create:
+          notice: Applicatie aangemaakt.
+        destroy:
+          notice: Applicatie verwijderd.
+        update:
+          notice: Applicatie bewerkt.
+      authorized_applications:
+        destroy:
+          notice: Applicatie ingetrokken.
+    layouts:
+      admin:
+        nav:
+          applications: Applicaties
+          home: Home
+          oauth2_provider: OAuth2 Provider
+      application:
+        title: OAuth autorisatie vereist
+    scopes:
+      follow: volg, blokkeer, deblokkeer en stop volgen accounts
+      read: lees je accountgegevens
+      write: plaatsen namens jou
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6e32d2c38..9c2708923 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -39,6 +39,108 @@ en:
     posts: Posts
     remote_follow: Remote follow
     unfollow: Unfollow
+  admin:
+    accounts:
+      are_you_sure: Are you sure?
+      display_name: Display name
+      domain: Domain
+      edit: Edit
+      email: E-mail
+      feed_url: Feed URL
+      followers: Followers
+      follows: Follows
+      location:
+        all: All
+        local: Local
+        remote: Remote
+        title: Location
+      media_attachments: Media attachments
+      moderation:
+        all: All
+        silenced: Silenced
+        suspended: Suspended
+        title: Moderation
+      most_recent_activity: Most recent activity
+      most_recent_ip: Most recent IP
+      not_subscribed: Not subscribed
+      order:
+        alphabetic: Alphabetic
+        most_recent: Most recent
+        title: Order
+      perform_full_suspension: Perform full suspension
+      profile_url: Profile URL
+      public: Public
+      push_subscription_expires: PuSH subscription expires
+      salmon_url: Salmon URL
+      silence: Silence
+      statuses: Statuses
+      title: Accounts
+      undo_silenced: Undo silence
+      undo_suspension: Undo suspension
+      username: Username
+      web: Web
+    domain_block:
+      add_new: Add new
+      domain: Domain
+      new:
+        create: Create block
+        hint: The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts.
+        severity:
+          desc_html: "<strong>Silence</strong> will make the account's posts invisible to anyone who isn't following them. <strong>Suspend</strong> will remove all of the account's content, media, and profile data."
+          silence: Silence
+          suspend: Suspend
+        title: New domain block
+      severity: Severity
+      title: Domain Blocks
+    pubsubhubbub:
+      callback_url: Callback URL
+      confirmed: Confirmed
+      expires_in: Expires in
+      last_delivery: Last delivery
+      title: PubSubHubbub
+      topic: Topic
+    reports:
+      comment:
+        label: Comment
+        none: None
+      delete: Delete
+      id: ID
+      mark_as_resolved: Mark as resolved
+      report: 'Report #%{id}'
+      reported_account: Reported account
+      reported_by: Reported by
+      resolved: Resolved
+      silence_account: Silence account
+      status: Status
+      suspend_account: Suspend account
+      target: Target
+      title: Reports
+      unresolved: Unresolved
+      view: View
+    settings:
+      click_to_edit: Click to edit
+      contact_information:
+        email: Enter a public e-mail address
+        label: Contact information
+        username: Enter a username
+      registrations:
+        closed_message:
+          desc_html: Displayed on frontpage when registrations are closed<br>You can use HTML tags
+          title: Closed registration message
+        open:
+          disabled: Disabled
+          enabled: Enabled
+          title: Open registration
+      setting: Setting
+      site_description:
+        desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br>You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
+        title: Site description
+      site_description_extended:
+        desc_html: Displayed on extended information page<br>You can use HTML tags
+        title: Extended site description
+      site_title: Site title
+      title: Site Settings
+    title: Administration
   application_mailer:
     settings: 'Change e-mail preferences: %{link}'
     signature: Mastodon notifications from %{instance}
@@ -74,11 +176,17 @@ en:
       x_minutes: "%{count}m"
       x_months: "%{count}mo"
       x_seconds: "%{count}s"
+  errors:
+    '404': The page you were looking for doesn't exist.
+    '410': The page you were looking for doesn't exist anymore.
+    '422':
+      content: Security verification failed. Are you blocking cookies?
+      title: Security verification failed
   exports:
     blocks: You block
-    mutes: You mute
     csv: CSV
     follows: You follow
+    mutes: You mute
     storage: Media storage
   generic:
     changes_saved_msg: Changes successfully saved!
@@ -134,6 +242,24 @@ en:
     missing_resource: Could not find the required redirect URL for your account
     proceed: Proceed to follow
     prompt: 'You are going to follow:'
+  reports:
+    comment:
+      label: Comment
+      none: None
+    delete: Delete
+    id: ID
+    mark_as_resolved: Mark as resolved
+    report: 'Report #%{id}'
+    reported_account: Reported account
+    reported_by: Reported by
+    reports: Reports
+    resolved: Resolved
+    silence_account: Silence account
+    status: Status
+    suspend_account: Suspend account
+    target: Target
+    unresolved: Unresolved
+    view: View
   settings:
     authorized_apps: Authorized apps
     back: Back to Mastodon
@@ -174,52 +300,3 @@ en:
     invalid_otp_token: Invalid two-factor code
   will_paginate:
     page_gap: "&hellip;"
-  errors:
-      404: The page you were looking for doesn't exist.
-      410: The page you were looking for doesn't exist anymore.
-      422:
-        title: Security verification failed
-        content: Security verification failed. Are you blocking cookies?
-  reports:
-    reports: Reports
-    status: Status
-    unresolved: Unresolved
-    resolved: Resolved
-    id: ID
-    target: Target
-    reported_by: Reported by
-    comment:
-      label: Comment
-      none: None
-    view: View
-    report: "Report #%{id}"
-    delete: Delete
-    reported_account: Reported account
-    reported_by: Signalé par
-    silence_account: Silence account
-    suspend_account: Suspend account
-    mark_as_resolved: Mark as resolved
-  admin:
-    settings:
-      title: Site Settings
-      setting: Setting
-      click_to_edit: Click to edit
-      contact_information:
-        label: Contact information
-        username: Enter a username
-        email: Enter a public e-mail address
-      site_title: Site title
-      site_description:
-        title: Site description
-        desc_html: "Displayed as a paragraph on the frontpage and used as a meta tag.<br>You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>."
-      site_description_extended:
-        title: Extended site description
-        desc_html: "Displayed on extended information page<br>You can use HTML tags"
-      registrations:
-        open:
-          title: Open registration
-          enabled: Enabled
-          disabled: Disabled
-        closed_message:
-          title: Closed registration message
-          desc_html: "Displayed on frontpage when registrations are closed<br>You can use HTML tags"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index da4270e9e..3b0181f63 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -7,14 +7,14 @@ es:
     business_email: 'Correo de negocios:'
     closed_registrations: Los registros están actualmente cerrados en esta instancia.
     contact: Contacto
-    description_headline: ¿Qué es %{domain}?
+    description_headline: "¿Qué es %{domain}?"
     domain_count_after: otras instancias
-    domain_count_before: Conectado a 
+    domain_count_before: Conectado a
     features:
       api: API pública para aplicaciones y servicios
       blocks: Moderación de contenido
       characters: 500 caracteres por publicación
-      chronology: Las historias son cronológicas 
+      chronology: Las historias son cronológicas
       ethics: 'Diseño etico: sin anuncios, sin rastreos'
       gifv: Videos cortos y GIFV
       privacy: Configuraciones de privacidad ajustables
@@ -33,7 +33,7 @@ es:
     follow: Seguir
     followers: Seguidores
     following: Siguiendo
-    nothing_here: ¡No hay nada aquí!
+    nothing_here: "¡No hay nada aquí!"
     people_followed_by: Usuarios a quien %{name} sigue
     people_who_follow: Usuarios que siguen a %{name}
     posts: Toots
@@ -47,8 +47,8 @@ es:
     invalid_url: La URL proporcionada es incorrecta
   auth:
     change_password: Cambiar contraseña
-    didnt_get_confirmation: ¿No recibió el correo de confirmación?
-    forgot_password: ¿Olvidaste tu contraseña?
+    didnt_get_confirmation: "¿No recibió el correo de confirmación?"
+    forgot_password: "¿Olvidaste tu contraseña?"
     login: Iniciar sesión
     logout: Cerrar sesión
     register: Registrarse
@@ -80,12 +80,12 @@ es:
     follows: Personas que sigues
     storage: Almacenamiento
   generic:
-    changes_saved_msg: ¡Cambios guardados con éxito!
+    changes_saved_msg: "¡Cambios guardados con éxito!"
     powered_by: powered by %{link}
     save_changes: Guardar cambios
     validation_errors:
-      one: ¡Algo no está bien! Por favor, revisa el error
-      other: ¡Algo no está bien! Por favor, revise %{count} errores más abajo
+      one: "¡Algo no está bien! Por favor, revisa el error"
+      other: "¡Algo no está bien! Por favor, revise %{count} errores más abajo"
   imports:
     preface: Puedes importar ciertos datos, como todas las personas que estás siguiendo o bloqueando en tu cuenta en esta instancia, desde archivos exportados de otra instancia.
     success: Sus datos se han cargado correctamente y serán procesados en brevedad
@@ -94,13 +94,17 @@ es:
       following: Lista de seguidos
     upload: Cargar
   landing_strip_html: <strong>%{name}</strong> es un usuario en <strong>%{domain}</strong>. Puedes seguirlo(a) o interactuar con el o ella si tienes una cuenta en cualquier parte del fediverse. Si no tienes una, puedes <a href="%{sign_up_path}">registrar aquí</a>.
+  media_attachments:
+    validations:
+      images_and_video: No se puede adjuntar un video a un estado que ya contenga imágenes
+      too_many: No se pueden adjuntar más de 4 archivos
   notification_mailer:
     digest:
       body: 'Un resumen de lo que te perdiste en %{instance} desde tu última visita el %{since}:'
       mention: "%{name} te ha mencionado en:"
       new_followers_summary:
-        one: ¡Hurra!. Alguien más te ha comenzado a seguir
-        other: ¡Genial!. Te han seguido %{count} nuevas personas
+        one: "¡Hurra!. Alguien más te ha comenzado a seguir"
+        other: "¡Genial!. Te han seguido %{count} nuevas personas"
       subject:
         one: "1 nueva notificación desde tu última visita \U0001F418"
         other: "%{count} nuevas notificaciones desde tu última visita \U0001F418"
@@ -117,7 +121,7 @@ es:
       body: 'Fuiste mencionado por %{name} en:'
       subject: Fuiste mencionado por %{name}
     reblog:
-      body: '%{name} ha retooteado tu estado'
+      body: "%{name} ha retooteado tu estado"
       subject: "%{name} ha retooteado tu estado"
   pagination:
     next: Próximo
@@ -161,7 +165,3 @@ es:
   users:
     invalid_email: La dirección de correo es incorrecta
     invalid_otp_token: Código de dos factores incorrecto
-  media_attachments:
-    validations:
-      too_many: No se pueden adjuntar más de 4 archivos
-      images_and_video: No se puede adjuntar un video a un estado que ya contenga imágenes
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 0ab756377..9e590f100 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -15,7 +15,7 @@ fr:
       blocks: Outils complets de bloquage et masquage
       characters: 500 caractères par post
       chronology: Fil chronologique
-      ethics: 'Pas de pubs, pas de pistage'
+      ethics: Pas de pubs, pas de pistage
       gifv: Partage de vidéos et de GIFs
       privacy: Réglages de confidentialité au niveau des posts
       public: Fils publics
@@ -39,6 +39,108 @@ fr:
     posts: Statuts
     remote_follow: Suivre à distance
     unfollow: Ne plus suivre
+  admin:
+    accounts:
+      are_you_sure: Êtes-vous certain ?
+      display_name: Nom affiché
+      domain: Domaine
+      edit: Éditer
+      email: Courriel
+      feed_url: URL du flux
+      followers: Abonné⋅es
+      follows: Abonnements
+      location:
+        all: Tous
+        local: Local
+        remote: Distant
+        title: Situation
+      media_attachments: Fichiers médias
+      moderation:
+        all: Tous
+        silenced: Muets
+        suspended: Suspendus
+        title: Modération
+      most_recent_activity: Dernière activité
+      most_recent_ip: Adresse IP la plus récente
+      not_subscribed: Non abonné
+      order:
+        alphabetic: Alphabétique
+        most_recent: Plus récent
+        title: Tri
+      perform_full_suspension: Effectuer une suspension complète
+      profile_url: URL du profil
+      public: Public
+      push_subscription_expires: Expiration de l'abonnement PuSH
+      salmon_url: URL Salmon
+      silence: Rendre muet
+      statuses: Statuts
+      title: Comptes
+      undo_silenced: Annuler la mu
+      undo_suspension: Annuler la suspension
+      username: Nom d'utilisateur
+      web: Web
+    domain_block:
+      add_new: Ajouter
+      domain: Domaine
+      new:
+        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>Suspend</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil."
+          silence: Muet
+          suspend: Suspendre
+        title: Nouveau blocage de domaine
+      severity: Séverité
+      title: Blocage de domaines
+    pubsubhubbub:
+      callback_url: URL de rappel
+      confirmed: Confirmé
+      expires_in: Expire dans
+      last_delivery: Dernière livraison
+      title: PubSubHubbub
+      topic: Sujet
+    reports:
+      comment:
+        label: Commentaire
+        none: Aucun
+      delete: Supprimer
+      id: ID
+      mark_as_resolved: Marqué comme résolu
+      report: 'Signalement #%{id}'
+      reported_account: Compte signalé
+      reported_by: Signalé par
+      resolved: Résolus
+      silence_account: Rendre le compte muet
+      status: Statut
+      suspend_account: Suspendre le compte
+      target: Cible
+      title: Signalements
+      unresolved: Non résolus
+      view: Voir
+    settings:
+      click_to_edit: Cliquez pour éditer
+      contact_information:
+        email: Entrez une adresse courriel publique
+        label: Informations de contact
+        username: Entrez un nom d'utilisateur
+      registrations:
+        closed_message:
+          desc_html: Affiché sur la page d'accueil lorsque les inscriptions sont fermées<br>Vous pouvez utiliser des balises HTML
+          title: Message de fermeture des inscriptions
+        open:
+          disabled: Désactivées
+          enabled: Activées
+          title: Inscriptions
+      setting: Paramètre
+      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>.
+        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
+        title: Description étendue du site
+      site_title: Titre du site
+      title: Paramètres du site
+    title: Administration
   application_mailer:
     settings: 'Changer les préférences courriel : %{link}'
     signature: Notifications de Mastodon depuis %{instance}
@@ -80,6 +182,12 @@ fr:
       x_minutes: "%{count}min"
       x_months: "%{count}mois"
       x_seconds: "%{count}s"
+  errors:
+    '404': La page que vous recherchez n'existe pas.
+    '410': La page que vous recherchez n'existe plus.
+    '422':
+      content: Vérification de sécurité échouée. Bloquez-vous les cookies ?
+      title: Vérification de sécurité échouée
   exports:
     blocks: Vous bloquez
     csv: CSV
@@ -103,7 +211,7 @@ fr:
   notification_mailer:
     digest:
       body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :'
-      mention: '%{name} vous a mentionné⋅e'
+      mention: "%{name} vous a mentionné⋅e"
       new_followers_summary:
         one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi !
         other: Vous avez %{count} nouveaux abonné⋅es ! Incroyable !
@@ -133,6 +241,24 @@ fr:
     missing_resource: L'URL de redirection n'a pas pu être trouvée
     proceed: Continuez pour suivre
     prompt: 'Vous allez suivre :'
+  reports:
+    comment:
+      label: Commentaire
+      none: Aucun
+    delete: Supprimer
+    id: ID
+    mark_as_resolved: Marqué comme résolu
+    report: 'Signalement #%{id}'
+    reported_account: Compte signalé
+    reported_by: Signalé par
+    reports: Signalements
+    resolved: Résolus
+    silence_account: Rendre le compte muet
+    status: Statut
+    suspend_account: Suspendre le compte
+    target: Cible
+    unresolved: Non résolus
+    view: Voir
   settings:
     authorized_apps: Applications autorisées
     back: Retour vers Mastodon
@@ -156,7 +282,7 @@ fr:
     sensitive_content: Contenu sensible
   time:
     formats:
-      default: '%d %b %Y, %H:%M'
+      default: "%d %b %Y, %H:%M"
   two_factor_auth:
     description_html: Si vous activez <strong>l'identification à deux facteurs</strong>, vous devrez être en possession de votre téléphone afin de générer un code de connexion.
     disable: Désactiver
@@ -169,52 +295,3 @@ fr:
     invalid_otp_token: Le code d'authentification à deux facteurs est invalide
   will_paginate:
     page_gap: "&hellip;"
-  errors:
-    404: La page que vous recherchez n'existe pas.
-    410: La page que vous recherchez n'existe plus.
-    422:
-      title: Vérification de sécurité échouée
-      content: Vérification de sécurité échouée. Bloquez-vous les cookies ?
-  reports:
-    reports: Signalements
-    status: Statut
-    unresolved: Non résolus
-    resolved: Résolus
-    id: ID
-    target: Cible
-    reported_by: Signalé par
-    comment:
-      label: Commentaire
-      none: Aucun
-    view: Voir
-    report: "Signalement #%{id}"
-    delete: Supprimer
-    reported_account: Compte signalé
-    reported_by: Signalé par
-    silence_account: Rendre le compte muet
-    suspend_account: Suspendre le compte
-    mark_as_resolved: Marqué comme résolu
-  admin:
-    settings:
-      title: Paramètres du site
-      setting: Paramètre
-      click_to_edit: Cliquez pour éditer
-      contact_information:
-        label: Informations de contact
-        username: Entrez un nom d'utilisateur
-        email: Entrez une adresse courriel publique
-      site_title: Titre du site
-      site_description:
-        title: Description du site
-        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>."
-      site_description_extended:
-        title: Description étendue du site
-        desc_html: "Affichée sur la page d'informations complémentaires du site<br>Vous pouvez utiliser des balises HTML"
-      registrations:
-        open:
-          title: Inscriptions
-          enabled: Activées
-          disabled: Désactivées
-        closed_message:
-          title: Message de fermeture des inscriptions
-          desc_html: "Affiché sur la page d'accueil lorsque les inscriptions sont fermées<br>Vous pouvez utiliser des balises HTML"
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index cf2086780..c1df73b7c 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -7,7 +7,7 @@ ja:
     business_email: 'ビジネスメールアドレス:'
     closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。
     contact: 連絡先
-    description_headline: '%{domain} とは?'
+    description_headline: "%{domain} とは?"
     domain_count_after: 個のインスタンス
     domain_count_before: 接続中
     features:
@@ -34,14 +34,35 @@ ja:
     followers: フォロワー
     following: フォロー中
     nothing_here: 何もありません
-    people_followed_by: '%{name} さんをフォロー中のアカウント'
-    people_who_follow: '%{name} さんがフォロー中のアカウント'
+    people_followed_by: "%{name} さんをフォロー中のアカウント"
+    people_who_follow: "%{name} さんがフォロー中のアカウント"
     posts: 投稿
     remote_follow: リモートフォロー
     unfollow: フォロー解除
+  admin:
+    settings:
+      click_to_edit: クリックして編集
+      contact_information:
+        email: 公開するメールアドレスを入力
+        label: 連絡先情報
+        username: ユーザー名を入力
+      registrations:
+        open:
+          disabled: 無効
+          enabled: 有効
+          title: 新規登録を受け付ける
+      setting: 設定
+      site_description:
+        desc_html: トップページへの表示と meta タグに使用されます。<br>HTMLタグ、特に<code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>が利用可能です。
+        title: サイトの説明文
+      site_description_extended:
+        desc_html: インスタンスについてのページに表示されます。<br>HTMLタグが利用可能です。
+        title: サイトの詳細な説明
+      site_title: サイトのタイトル
+      title: サイト設定
   application_mailer:
     settings: 'メール設定の変更: %{link}'
-    signature: 'Mastodon %{instance} インスタンスからの通知'
+    signature: Mastodon %{instance} インスタンスからの通知
     view: 'View:'
   applications:
     invalid_url: URLが無効です
@@ -59,7 +80,7 @@ ja:
     error: 残念ながら、リモートアカウントにエラーが発生しました。
     follow: フォロー
     prompt_html: 'あなた(<strong>%{self}</strong>)は以下のアカウントのフォローをリクエストしました:'
-    title: '%{acct} をフォロー'
+    title: "%{acct} をフォロー"
   datetime:
     distance_in_words:
       about_x_hours: "%{count}時間"
@@ -74,11 +95,17 @@ ja:
       x_minutes: "%{count}分"
       x_months: "%{count}月"
       x_seconds: "%{count}秒"
+  errors:
+    '404': お探しのページは見つかりませんでした。
+    '410': お探しのページはもう存在しません。
+    '422':
+      content: セキュリティ認証に失敗しました。Cookieをブロックしていませんか?
+      title: セキュリティ認証に失敗
   exports:
     blocks: ブロック
-    mutes: ミュート
     csv: CSV
     follows: フォロー
+    mutes: ミュート
     storage: メディア
   generic:
     changes_saved_msg: 正常に変更されました
@@ -102,11 +129,11 @@ ja:
       too_many: 追加できるファイルは4つまでです。
   notification_mailer:
     digest:
-      body: '%{instance} での最後のログインからの出来事:'
+      body: "%{instance} での最後のログインからの出来事:"
       mention: "%{name} さんがあなたに返信しました:"
       new_followers_summary:
         one: 新たなフォロワーを獲得しました!
-        other: '%{count} 人の新たなフォロワーを獲得しました!'
+        other: "%{count} 人の新たなフォロワーを獲得しました!"
       subject:
         one: "新しい1件の通知 \U0001F418"
         other: "新しい%{count}件の通知 \U0001F418"
@@ -118,13 +145,13 @@ ja:
       subject: "%{name} さんにフォローされています"
     follow_request:
       body: "%{name} さんがあなたにフォローをリクエストしました。"
-      subject: '%{name} さんからのフォローリクエスト'
+      subject: "%{name} さんからのフォローリクエスト"
     mention:
-      body: '%{name} さんから返信がありました:'
-      subject: '%{name} さんに返信されました'
+      body: "%{name} さんから返信がありました:"
+      subject: "%{name} さんに返信されました"
     reblog:
       body: 'あなたのトゥートが %{name} さんにブーストされました:'
-      subject: "あなたのトゥートが %{name} さんにブーストされました"
+      subject: あなたのトゥートが %{name} さんにブーストされました
   pagination:
     next: 次
     prev: 前
@@ -134,6 +161,24 @@ ja:
     missing_resource: リダイレクト先が見つかりませんでした
     proceed: フォローする
     prompt: 'フォローしようとしています:'
+  reports:
+    comment:
+      label: コメント
+      none: なし
+    delete: 削除
+    id: ID
+    mark_as_resolved: 解決する
+    report: '通報 #%{id}'
+    reported_account: 通報されているユーザー
+    reported_by: 通報者
+    reports: 通報
+    resolved: 解決済み
+    silence_account: ユーザーをサイレンスする
+    status: 現状
+    suspend_account: ユーザーを停止する
+    target: 通報されているユーザー
+    unresolved: 未決
+    view: 見る
   settings:
     authorized_apps: 認証済みアプリ
     back: 戻る
@@ -145,7 +190,7 @@ ja:
     two_factor_auth: 二段階認証
   statuses:
     open_in_web: Webで開く
-    over_character_limit: '上限は %{max}文字までです'
+    over_character_limit: 上限は %{max}文字までです
     show_more: もっと見る
     visibilities:
       private: Private - フォロワーだけに見せる
@@ -160,7 +205,7 @@ ja:
       default: "%Y年%m月%d日 %H:%M"
   two_factor_auth:
     code_hint: 確認するには認証アプリで表示されたコードを入力してください
-    description_html: <strong>二段階認証</strong>を有効にするとログイン時、電話でコードを受け取る必要があります。
+    description_html: "<strong>二段階認証</strong>を有効にするとログイン時、電話でコードを受け取る必要があります。"
     disable: 無効
     enable: 有効
     enabled_success: 二段階認証が有効になりました
@@ -174,49 +219,3 @@ ja:
     invalid_otp_token: 二段階認証コードが間違っています
   will_paginate:
     page_gap: "&hellip;"
-  errors:
-      404: お探しのページは見つかりませんでした。
-      410: お探しのページはもう存在しません。
-      422:
-        title: セキュリティ認証に失敗
-        content: セキュリティ認証に失敗しました。Cookieをブロックしていませんか?
-  reports:
-    reports: 通報
-    status: 現状
-    unresolved: 未決
-    resolved: 解決済み
-    id: ID
-    target: 通報されているユーザー
-    reported_by: 通報者
-    comment:
-      label: コメント
-      none: なし
-    view: 見る
-    report: "通報 #%{id}"
-    delete: 削除
-    reported_account: 通報されているユーザー
-    reported_by: 通報者
-    silence_account: ユーザーをサイレンスする
-    suspend_account: ユーザーを停止する
-    mark_as_resolved: 解決する
-  admin:
-    settings:
-      title: サイト設定
-      setting: 設定
-      click_to_edit: クリックして編集
-      contact_information:
-        label: 連絡先情報
-        username: ユーザー名を入力
-        email: 公開するメールアドレスを入力
-      site_title: サイトのタイトル
-      site_description:
-        title: サイトの説明文
-        desc_html: "トップページへの表示と meta タグに使用されます。<br>HTMLタグ、特に<code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>が利用可能です。"
-      site_description_extended:
-        title: サイトの詳細な説明
-        desc_html: "インスタンスについてのページに表示されます。<br>HTMLタグが利用可能です。"
-      registrations:
-        open:
-          title: 新規登録を受け付ける
-          enabled: 有効
-          disabled: 無効
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
new file mode 100644
index 000000000..22fed2285
--- /dev/null
+++ b/config/locales/nl.yml
@@ -0,0 +1,165 @@
+---
+nl:
+  about:
+    about_mastodon: Mastodon is een <em>vrije, gratis, open-source</em> sociaal netwerk. E <em>gedecentraliseerd</em> alternatief voor commerciële platforms, het voorkomt de risico's van een enkel bedrijf dat jouw communicatie monopoliseert. Kies een server die je vertrouwt &mdash; welke je ook kiest, je kunt met iedere ander communiceren. Iedereen kan een eigen Mastodon server draaien en naadloos deelnemen in het <em>sociale netwerk</em>.
+    about_this: Over deze server
+    apps: Apps
+    business_email: 'Zakelijke e-mailadres:'
+    closed_registrations: Registrateren op deze server is momenteel uitgeschakeld.
+    contact: Contact
+    description_headline: Wat is %{domain}?
+    domain_count_after: andere servers
+    domain_count_before: Verbonden met
+    features:
+      api: Open API voor apps en services
+      blocks: Rijke blokkeer- en dempingshulpmiddelen
+      characters: 500 tekens per bericht
+      chronology: Tijdlijnen zijn chronologisch
+      ethics: 'Ethisch design: geen ads, geen spionage'
+      gifv: GIFV sets en korte video's
+      privacy: Granulaire, privacy instellingen per bericht
+      public: Openbare tijdlijnen
+    features_headline: Wat maak Mastodon anders
+    get_started: Beginnen
+    links: Links
+    other_instances: Andere servers
+    source_code: Source code
+    status_count_after: statussen
+    status_count_before: Wie schreef
+    terms: Voorw
+    user_count_after: gebruikers
+    user_count_before: Thuis naar
+  accounts:
+    follow: Volg
+    followers: Volgens
+    following: Volgend
+    nothing_here: Hier is niets!
+    people_followed_by: Mensen die %{name} volgt
+    people_who_follow: Mensen die %{name} volgen
+    posts: Berichten
+    remote_follow: Externe volg
+    unfollow: Ontvolgen
+  application_mailer:
+    settings: 'Wijzigen e-mailvoorkeuren: %{link}'
+    signature: Mastodon meldingen van %{instance}
+    view: 'Bekijk:'
+  applications:
+    invalid_url: De opgegevens URL is ongeldig
+  auth:
+    change_password: Inloggegevens
+    didnt_get_confirmation: Ontving je geen bevestigingsinstructies?
+    forgot_password: Wachtwoord vergeten?
+    login: Inloggen
+    logout: Uitloggen
+    register: Registreren
+    resend_confirmation: Herstuur de bevestigingsinstructies
+    reset_password: Herstel wachtwoord
+    set_new_password: Instellen nieuw wachtwoord
+  authorize_follow:
+    error: Helaas, er was een fout bij het opzoeken van het externe account
+    follow: Volgen
+    prompt_html: 'Je (<strong>%{self}</strong>) hebt volgen aangevraagd:'
+    title: Volg %{acct}
+  datetime:
+    distance_in_words:
+      about_x_hours: "%{count}u"
+      about_x_months: "%{count}ma"
+      about_x_years: "%{count}j"
+      almost_x_years: "%{count}j"
+      half_a_minute: Net
+      less_than_x_minutes: "%{count}m"
+      less_than_x_seconds: Net
+      over_x_years: "%{count}j"
+      x_days: "%{count}d"
+      x_minutes: "%{count}m"
+      x_months: "%{count}ma"
+      x_seconds: "%{count}s"
+  exports:
+    blocks: Je blokkeert
+    csv: CSV
+    follows: Je volgt
+    storage: Media-opslag
+  generic:
+    changes_saved_msg: Wijzigingen succesvol opgeslagen!
+    powered_by: powered by %{link}
+    save_changes: Wijziginen opslaan
+    validation_errors:
+      one: Er is iets niet helemaal goed! Bekijk onderstaande fout
+      other: Er is iets niet helemaal goed! Bekijk onderstaande %{count} fouten
+  imports:
+    preface: Je kunt bepaalde gegevens, zoals de mensen die je volgt of blokkeert, importeren voor je account op deze server, als ze zijn geëxporteerd op een andere server.
+    success: Je gegevens zijn succesvol ge-upload en wordt binnenkort verwerkt
+    types:
+      blocking: Blokkadelijst
+      following: Volglijst
+    upload: Uploaden
+  landing_strip_html: <strong>%{name}</strong> is een gebruiker op <strong>%{domain}</strong>. Je kunt deze volgen of ermee interacteren als je ergens in deze fediverse een account hebt. Als he dat niet hebt, kun je je <a href="%{sign_up_path}">hier aanmelden</a>.
+  notification_mailer:
+    digest:
+      body: 'Hier is een korte samenvatting van wat je hebt gemist op %{instance} sinds je laatste bezoek op %{since}:'
+      mention: "%{name} vermeldde je in:"
+      new_followers_summary:
+        one: Je hebt een nieuwe volger! Hoera!
+        other: Je hebt %{count} nieuwe volgers! Prachtig!
+      subject:
+        one: "1 nieuwe melding sinds je laatste bezoek \U0001F418"
+        other: "%{count} nieuwe meldingen sinds je laatste bezoek \U0001F418"
+    favourite:
+      body: 'Je status werd als favoriet gemarkeerd door  %{name}:'
+      subject: "%{name} markeerde je status als favouriet"
+    follow:
+      body: "%{name} volgt je nu!"
+      subject: "%{name} volgt je nu"
+    follow_request:
+      body: "%{name} wil je graag volgend"
+      subject: 'Volgen in afwachting: %{name}'
+    mention:
+      body: 'Je werd door %{name} vermeld in:'
+      subject: Je werd vermeld door %{name}
+    reblog:
+      body: 'Je status werd geboost door %{name}:'
+      subject: "%{name} booste je status"
+  pagination:
+    next: Volgende
+    prev: Vorige
+  remote_follow:
+    acct: Geef je gebruikersnaam@domein op waarvandaan je wilt volgen
+    missing_resource: Kon geen de vereiste doorverwijszings-URL voor je account niet vinden
+    proceed: Ga door om te volgen
+    prompt: 'Je gaat volgen:'
+  settings:
+    authorized_apps: Geautoriseerde
+    back: Terug naar Mastodon
+    edit_profile: Bewerk profiel
+    export: Gegevensexport
+    import: Import
+    preferences: Voorkeuren
+    settings: Instellingen
+    two_factor_auth: Twe-factor authenticatie
+  statuses:
+    open_in_web: Openen in web
+    over_character_limit: Tekenlimiet van %{max} overschreden
+    show_more: Toon meer
+    visibilities:
+      private: Toon alleen aan volgers
+      public: Openbaar
+      unlisted: Openbaar, maar niet tonen op openbare tijdlijn
+  stream_entries:
+    click_to_show: Klik om te tonen
+    reblogged: boostte
+    sensitive_content: Gevoelige inhoud
+  time:
+    formats:
+      default: "%b %d, %J, %U:%M"
+  two_factor_auth:
+    description_html: Als je <strong>twee-factor authenticatie</strong> instelt, kun je alleen inloggen als je je mobiele telefoon bij je hebt, waarmee je de in te voeren tokens genereert.
+    disable: Uitschakelen
+    enable: Inschakelen
+    instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op je mobiele telefoon</strong>. Van nu af aan creëert deze app tokens die je bij inloggen moet invoeren."
+    plaintext_secret_html: 'Gewone-tekst geheim: <samp>%{secret}</samp>'
+    warning: Als je nu geen authenticator app kunt installeren, moet je "Uitschakelen" kiezen of je kunt niet meer inloggen.
+  users:
+    invalid_email: Het e-mailadres is ongeldig
+    invalid_otp_token: Ongeldige twe-factor code
+  will_paginate:
+    page_gap: "&hellip;"
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 0c2725855..ca73dd454 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -112,7 +112,7 @@ ru:
       subject: "%{name} теперь подписан(а) на Вас"
     follow_request:
       body: "%{name} запросил Вас о подписке"
-      subject: '%{name} хочет подписаться на Вас'
+      subject: "%{name} хочет подписаться на Вас"
     mention:
       body: 'Вас упомянул(а) %{name} в:'
       subject: Вы были упомянуты %{name}
@@ -152,15 +152,15 @@ ru:
     formats:
       default: "%b %d, %Y, %H:%M"
   two_factor_auth:
+    code_hint: Для подтверждения введите код, сгенерированный приложением аутентификатора
     description_html: При включении <strong>двухфакторной аутентификации</strong>, вход потребует от Вас использования Вашего телефона, который сгенерирует входные токены.
     disable: Отключить
     enable: Включить
     instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа."
     manual_instructions: 'Если Вы не можете отсканировать QR-код и хотите ввести его вручную, секрет представлен здесь открытым текстом:'
     plaintext_secret_html: 'Секрет открытым текстом: <samp>%{secret}</samp>'
-    code_hint: 'Для подтверждения введите код, сгенерированный приложением аутентификатора'
     setup: Настроить
-    warning: 'Если сейчас у Вас не получается настроить аутентификатор, нажмите "отключить", иначе Вы не сможете войти!'
+    warning: Если сейчас у Вас не получается настроить аутентификатор, нажмите "отключить", иначе Вы не сможете войти!
   users:
     invalid_email: Введенный e-mail неверен
     invalid_otp_token: Введен неверный код
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 6b6657a98..74649da51 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -26,10 +26,11 @@ en:
         note: Bio
         otp_attempt: Two-factor code
         password: Password
+        setting_boost_modal: Show confirmation dialog before boosting
         setting_default_privacy: Post privacy
+        severity: Severity
         type: Import type
         username: Username
-        setting_boost_modal: Show confirmation dialog before boosting
       interactions:
         must_be_follower: Block notifications from non-followers
         must_be_following: Block notifications from people you don't follow
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index afcaeae8f..ae4975143 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -26,7 +26,9 @@ fr:
         note: Présentation
         otp_attempt: Code d'identification à deux facteurs
         password: Mot de passe
+        setting_boost_modal: Afficher un dialogue de confirmation avant de partager
         setting_default_privacy: Confidentialité des statuts
+        severity: Séverité
         type: Type d'import
         username: Identifiant
       interactions:
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index bdf8e0f54..103001b7e 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -26,10 +26,10 @@ ja:
         note: プロフィール
         otp_attempt: 二段階認証コード
         password: パスワード
+        setting_boost_modal: ブーストする前に確認ダイアログを表示する
         setting_default_privacy: 投稿の公開範囲
         type: インポートする項目
         username: ユーザー名
-        setting_boost_modal: ブーストする前に確認ダイアログを表示する
       interactions:
         must_be_follower: フォロワー以外からの通知をブロック
         must_be_following: フォローしていないユーザーからの通知をブロック
@@ -40,8 +40,8 @@ ja:
         follow_request: フォローリクエストを受けた時にメールで通知する
         mention: 返信が来た時にメールで通知する
         reblog: トゥートがブーストされた時にメールで通知する
-    'no': 'いいえ'
+    'no': いいえ
     required:
       mark: "*"
       text: 必須
-    'yes': 'はい'
+    'yes': はい
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
new file mode 100644
index 000000000..5bc38a87b
--- /dev/null
+++ b/config/locales/simple_form.nl.yml
@@ -0,0 +1,46 @@
+---
+nl:
+  simple_form:
+    hints:
+      defaults:
+        avatar: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 120x120px
+        display_name: Maximaal 30 tekens
+        header: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 700x335px
+        locked: Vereist dat je handmatig volgers accepteert en stelt standaard plaatsen berichten privacy in op alleen-volgers
+        note: Maximaal 160 characters
+      imports:
+        data: CSV file geëxporteerd van een andere Mastodon server
+    labels:
+      defaults:
+        avatar: Avatar
+        confirm_new_password: Bevestig nieuw wachtwoord
+        confirm_password: Bevestig wachtwoord
+        current_password: Huidige wachtwoord
+        data: Gegevens
+        display_name: Weergavenaam
+        email: E-mailadres
+        header: Kop
+        locale: Taal
+        locked: Maak account besloten
+        new_password: Nieuwe wachtwoord
+        note: Bio
+        otp_attempt: Twee-factor code
+        password: Wachtwoord
+        setting_default_privacy: Berichten privacy
+        type: Import type
+        username: gebruikersnaam
+      interactions:
+        must_be_follower: Blokkeermeldingen van niet-volgers
+        must_be_following: Blokkeer meldingen van mensen die je niet volgt
+      notification_emails:
+        digest: Verstuur samenvattingse-mails
+        favourite: Verstuur een e-mail wanneer iemand je status als favoriet markeert
+        follow: Verstuur een e-mail wanneer iemand je volgt
+        follow_request: Verstuur een e-mail wanneer iemand je wil volgen
+        mention: Verstuur een e-mail wanneer iemand je vermeld
+        reblog: Verstuur een e-mail wanneer iemand je status boost
+    'no': 'Nee'
+    required:
+      mark: "*"
+      text: vereist
+    'yes': 'Ja'
diff --git a/config/locales/simple_form.no.yml b/config/locales/simple_form.no.yml
index a4e43b1a1..22df43e73 100644
--- a/config/locales/simple_form.no.yml
+++ b/config/locales/simple_form.no.yml
@@ -26,10 +26,10 @@
         note: Biografi
         otp_attempt: To-faktor kode
         password: Passord
+        setting_boost_modal: Vis bekreftelsesdialog før reblogging
         setting_default_privacy: Leserettigheter for poster
         type: Importeringstype
         username: Brukernavn
-        setting_boost_modal: Vis bekreftelsesdialog før reblogging 
       interactions:
         must_be_follower: Blokker varslinger fra ikke-følgere
         must_be_following: Blokker varslinger fra folk du ikke følger
@@ -40,8 +40,8 @@
         follow_request: Send e-post når noen ber om å få følge deg
         mention: Send e-post når noen nevner deg
         reblog: Send e-post når noen reblogger din status
-    'no': 'Nei'
+    'no': Nei
     required:
       mark: "*"
       text: påkrevd
-    'yes': 'Ja'
+    'yes': Ja
diff --git a/config/navigation.rb b/config/navigation.rb
index b92b87202..3d5ba1741 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -14,14 +14,14 @@ SimpleNavigation::Configuration.run do |navigation|
       settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
     end
 
-    primary.item :admin, safe_join([fa_icon('cogs fw'), 'Administration']), admin_reports_url, if: proc { current_user.admin? } do |admin|
-      admin.item :reports, safe_join([fa_icon('flag fw'), 'Reports']), admin_reports_url, highlights_on: %r{/admin/reports}
-      admin.item :accounts, safe_join([fa_icon('users fw'), 'Accounts']), admin_accounts_url, highlights_on: %r{/admin/accounts}
-      admin.item :pubsubhubbubs, safe_join([fa_icon('paper-plane-o fw'), 'PubSubHubbub']), admin_pubsubhubbub_index_url
-      admin.item :domain_blocks, safe_join([fa_icon('lock fw'), 'Domain Blocks']), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}
+    primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_reports_url, if: proc { current_user.admin? } do |admin|
+      admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
+      admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts}
+      admin.item :pubsubhubbubs, safe_join([fa_icon('paper-plane-o fw'), t('admin.pubsubhubbub.title')]), admin_pubsubhubbub_index_url
+      admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_block.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}
       admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url
       admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url
-      admin.item :settings, safe_join([fa_icon('cogs fw'), 'Site Settings']), admin_settings_url
+      admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_url
     end
 
     primary.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' }
diff --git a/config/settings.yml b/config/settings.yml
index d364120db..04213fd0b 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -14,7 +14,7 @@ defaults: &defaults
   site_contact_email: ''
   open_registrations: true
   closed_registrations_message: ''
-  boost_modal: true
+  boost_modal: false
   notification_emails:
     follow: false
     reblog: false
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index a5dce977d..7038efce1 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,7 +1,9 @@
 require 'simplecov'
 
 SimpleCov.start 'rails' do
-  add_group "Services", "app/services"
+  add_group 'Services', 'app/services'
+  add_group 'Presenters', 'app/presenters'
+  add_group 'Validators', 'app/validators'
 end
 
 RSpec.configure do |config|