about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/elephant-friend.pngbin0 -> 24466 bytes
-rw-r--r--app/assets/images/logo.pngbin20674 -> 7752 bytes
-rw-r--r--app/assets/images/logo.svg2
-rw-r--r--app/assets/javascripts/components/actions/compose.jsx6
-rw-r--r--app/assets/javascripts/components/actions/onboarding.jsx14
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx5
-rw-r--r--app/assets/javascripts/components/features/compose/components/character_counter.jsx13
-rw-r--r--app/assets/javascripts/components/features/ui/components/modal_root.jsx2
-rw-r--r--app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx251
-rw-r--r--app/assets/javascripts/components/locales/fr.jsx22
-rw-r--r--app/assets/javascripts/components/locales/hr.jsx124
-rw-r--r--app/assets/javascripts/components/locales/index.jsx4
-rw-r--r--app/assets/javascripts/components/locales/nl.jsx44
-rw-r--r--app/assets/javascripts/components/locales/no.jsx2
-rw-r--r--app/assets/javascripts/components/locales/pt-br.jsx125
-rw-r--r--app/assets/javascripts/components/locales/pt.jsx191
-rw-r--r--app/assets/javascripts/components/locales/zh-cn.jsx157
-rw-r--r--app/assets/javascripts/components/reducers/settings.jsx2
-rw-r--r--app/assets/stylesheets/admin.scss2
-rw-r--r--app/assets/stylesheets/components.scss239
-rw-r--r--app/assets/stylesheets/forms.scss18
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb4
-rw-r--r--app/controllers/api/v1/statuses_controller.rb8
-rw-r--r--app/controllers/auth/registrations_controller.rb1
-rw-r--r--app/controllers/home_controller.rb1
-rw-r--r--app/controllers/media_controller.rb13
-rw-r--r--app/controllers/well_known/webfinger_controller.rb9
-rw-r--r--app/helpers/instance_helper.rb11
-rw-r--r--app/helpers/settings_helper.rb5
-rw-r--r--app/helpers/site_title_helper.rb7
-rw-r--r--app/lib/atom_serializer.rb14
-rw-r--r--app/mailers/application_mailer.rb1
-rw-r--r--app/mailers/notification_mailer.rb7
-rw-r--r--app/mailers/user_mailer.rb2
-rw-r--r--app/models/domain_block.rb3
-rw-r--r--app/models/favourite.rb6
-rw-r--r--app/models/media_attachment.rb1
-rw-r--r--app/models/mute.rb5
-rw-r--r--app/models/status.rb3
-rw-r--r--app/models/subscription.rb2
-rw-r--r--app/models/user.rb3
-rw-r--r--app/services/account_search_service.rb2
-rw-r--r--app/services/notify_service.rb2
-rw-r--r--app/services/post_status_service.rb7
-rw-r--r--app/services/process_feed_service.rb23
-rw-r--r--app/views/about/more.html.haml4
-rw-r--r--app/views/about/show.html.haml8
-rw-r--r--app/views/about/terms.en.html.haml2
-rw-r--r--app/views/about/terms.no.html.haml2
-rw-r--r--app/views/accounts/show.html.haml4
-rw-r--r--app/views/admin/accounts/show.html.haml5
-rw-r--r--app/views/admin/domain_blocks/index.html.haml14
-rw-r--r--app/views/admin/domain_blocks/new.html.haml14
-rw-r--r--app/views/admin/domain_blocks/show.html.haml12
-rw-r--r--app/views/admin/reports/show.html.haml2
-rw-r--r--app/views/api/oembed/show.json.rabl2
-rw-r--r--app/views/api/v1/instances/show.rabl2
-rw-r--r--app/views/api/v1/statuses/_media.rabl4
-rw-r--r--app/views/home/initial_state.json.rabl9
-rwxr-xr-xapp/views/layouts/application.html.haml3
-rw-r--r--app/views/layouts/mailer.text.erb2
-rw-r--r--app/views/layouts/public.html.haml2
-rw-r--r--app/views/settings/imports/show.html.haml2
-rw-r--r--app/views/settings/preferences/show.html.haml2
-rw-r--r--app/views/shared/_landing_strip.html.haml2
-rw-r--r--app/views/stream_entries/show.html.haml2
-rw-r--r--app/views/user_mailer/confirmation_instructions.ja.html.erb8
-rw-r--r--app/views/user_mailer/confirmation_instructions.ja.text.erb8
-rw-r--r--app/views/user_mailer/reset_password_instructions.ja.html.erb2
-rw-r--r--app/views/user_mailer/reset_password_instructions.ja.text.erb2
70 files changed, 1266 insertions, 216 deletions
diff --git a/app/assets/images/elephant-friend.png b/app/assets/images/elephant-friend.png
new file mode 100644
index 000000000..3c5145ba9
--- /dev/null
+++ b/app/assets/images/elephant-friend.png
Binary files differdiff --git a/app/assets/images/logo.png b/app/assets/images/logo.png
index 3ed93f120..f0c1c46c3 100644
--- a/app/assets/images/logo.png
+++ b/app/assets/images/logo.png
Binary files differdiff --git a/app/assets/images/logo.svg b/app/assets/images/logo.svg
index 52bf86b0e..c233db842 100644
--- a/app/assets/images/logo.svg
+++ b/app/assets/images/logo.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><g fill="#189efc"><path d="M500 0A500 500 0 0 0 0 500a500 500 0 0 0 500 500 500 500 0 0 0 500-500A500 500 0 0 0 500 0zm-2.5 271.1h107.24c-20.56 14.471-27.24 57.064-27.24 78.927v202.145c0 43.726-35.202 78.928-80 78.928s-80-35.202-80-78.928V350.027c0-43.725 35.202-78.927 80-78.927zm-276 48.9c44.798 0 80 35.202 80 78.928v202.144c0 21.863 6.68 64.456 27.24 78.928H221.5c-44.798 0-80-35.202-80-78.928V398.928c0-43.726 35.202-78.928 80-78.928zm550.24 0c44.799 0 80 35.202 80 78.928v202.144c0 43.726-35.201 78.928-80 78.928H664.5c20.56-14.472 27.24-57.065 27.24-78.928V398.928c0-43.726 35.202-78.928 80-78.928z"/><g transform="translate(-2)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(1 0 0 -1 274 951)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(-1 0 0 1 995 0)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#189efc"/></svg>
\ No newline at end of file
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx
index 88e91c356..de75ddabe 100644
--- a/app/assets/javascripts/components/actions/compose.jsx
+++ b/app/assets/javascripts/components/actions/compose.jsx
@@ -73,9 +73,13 @@ export function mentionCompose(account, router) {
 
 export function submitCompose() {
   return function (dispatch, getState) {
+    const status = emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], ''));
+    if (!status || !status.length) {
+      return;
+    }
     dispatch(submitComposeRequest());
     api(getState).post('/api/v1/statuses', {
-      status: emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')),
+      status,
       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/onboarding.jsx b/app/assets/javascripts/components/actions/onboarding.jsx
new file mode 100644
index 000000000..a161c50ef
--- /dev/null
+++ b/app/assets/javascripts/components/actions/onboarding.jsx
@@ -0,0 +1,14 @@
+import { openModal } from './modal';
+import { changeSetting, saveSettings } from './settings';
+
+export function showOnboardingOnce() {
+  return (dispatch, getState) => {
+    const alreadySeen = getState().getIn(['settings', 'onboarded']);
+
+    if (!alreadySeen) {
+      dispatch(openModal('ONBOARDING'));
+      dispatch(changeSetting(['onboarded'], true));
+      dispatch(saveSettings());
+    }
+  };
+};
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index a771d1269..185911861 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -8,6 +8,7 @@ import {
   connectTimeline,
   disconnectTimeline
 } from '../actions/timelines';
+import { showOnboardingOnce } from '../actions/onboarding';
 import { updateNotifications, refreshNotifications } from '../actions/notifications';
 import createBrowserHistory from 'history/lib/createBrowserHistory';
 import {
@@ -56,6 +57,7 @@ import uk from 'react-intl/locale-data/uk';
 import zh from 'react-intl/locale-data/zh';
 import bg from 'react-intl/locale-data/bg';
 import { localeData as zh_hk } from '../locales/zh-hk';
+import pt_br from '../locales/pt-br';
 import getMessagesForLocale from '../locales';
 import { hydrateStore } from '../actions/store';
 import createStream from '../stream';
@@ -78,6 +80,7 @@ addLocaleData([
   ...hu,
   ...ja,
   ...pt,
+  ...pt_br,
   ...nl,
   ...no,
   ...ru,
@@ -134,6 +137,8 @@ const Mastodon = React.createClass({
     if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
       Notification.requestPermission();
     }
+
+    store.dispatch(showOnboardingOnce());
   },
 
   componentWillUnmount () {
diff --git a/app/assets/javascripts/components/features/compose/components/character_counter.jsx b/app/assets/javascripts/components/features/compose/components/character_counter.jsx
index e6b675354..fc64f94a5 100644
--- a/app/assets/javascripts/components/features/compose/components/character_counter.jsx
+++ b/app/assets/javascripts/components/features/compose/components/character_counter.jsx
@@ -9,14 +9,17 @@ const CharacterCounter = React.createClass({
 
   mixins: [PureRenderMixin],
 
+  checkRemainingText (diff) {
+    if (diff <= 0) {
+      return <span style={{ fontSize: '16px', cursor: 'default', color: '#ff5050' }}>{diff}</span>;
+    }
+    return <span style={{ fontSize: '16px', cursor: 'default' }}>{diff}</span>;
+  },
+
   render () {
     const diff = this.props.max - this.props.text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length;
 
-    return (
-      <span style={{ fontSize: '16px', cursor: 'default' }}>
-        {diff}
-      </span>
-    );
+    return this.checkRemainingText(diff);
   }
 
 });
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 a1ed8fd88..ace3e085f 100644
--- a/app/assets/javascripts/components/features/ui/components/modal_root.jsx
+++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
@@ -1,11 +1,13 @@
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import MediaModal from './media_modal';
+import OnboardingModal from './onboarding_modal';
 import VideoModal from './video_modal';
 import BoostModal from './boost_modal';
 import { TransitionMotion, spring } from 'react-motion';
 
 const MODAL_COMPONENTS = {
   'MEDIA': MediaModal,
+  'ONBOARDING': OnboardingModal,
   'VIDEO': VideoModal,
   'BOOST': BoostModal
 };
diff --git a/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx b/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx
new file mode 100644
index 000000000..8d5132ea2
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx
@@ -0,0 +1,251 @@
+import { connect } from 'react-redux';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Permalink from '../../../components/permalink';
+import { TransitionMotion, spring } from 'react-motion';
+import ComposeForm from '../../compose/components/compose_form';
+import Search from '../../compose/components/search';
+import NavigationBar from '../../compose/components/navigation_bar';
+import ColumnHeader from './column_header';
+import Immutable from 'immutable';
+
+const messages = defineMessages({
+  home_title: { id: 'column.home', defaultMessage: 'Home' },
+  notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
+  local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
+  federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' }
+});
+
+const PageOne = ({ acct, domain }) => (
+  <div className='onboarding-modal__page onboarding-modal__page-one'>
+    <div style={{ flex: '0 0 auto' }}>
+      <div className='onboarding-modal__page-one__elephant-friend' />
+    </div>
+
+    <div>
+      <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1>
+      <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a social network that belongs to everyone.' /></p>
+      <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, one of many independent Mastodon instances. Your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }}/></p>
+    </div>
+  </div>
+);
+
+PageOne.propTypes = {
+  acct: React.PropTypes.string.isRequired,
+  domain: React.PropTypes.string.isRequired
+};
+
+const PageTwo = () => (
+  <div className='onboarding-modal__page onboarding-modal__page-two'>
+    <div className='figure non-interactive'>
+      <ComposeForm
+        text='Awoo! #introductions'
+        suggestions={Immutable.List()}
+        mentionedDomains={[]}
+        onChange={() => {}}
+        onSubmit={() => {}}
+        onPaste={() => {}}
+        onPickEmoji={() => {}}
+        onChangeSpoilerText={() => {}}
+        onClearSuggestions={() => {}}
+        onFetchSuggestions={() => {}}
+        onSuggestionSelected={() => {}}
+      />
+    </div>
+
+    <p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p>
+  </div>
+);
+
+const PageThree = ({ me, domain }) => (
+  <div className='onboarding-modal__page onboarding-modal__page-three'>
+    <div className='figure non-interactive'>
+      <Search
+        value=''
+        onChange={() => {}}
+        onSubmit={() => {}}
+        onClear={() => {}}
+        onShow={() => {}}
+      />
+
+      <div className='pseudo-drawer'>
+        <NavigationBar account={me} />
+      </div>
+    </div>
+
+    <p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }}/></p>
+    <p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p>
+  </div>
+);
+
+PageThree.propTypes = {
+  me: ImmutablePropTypes.map.isRequired,
+  domain: React.PropTypes.string.isRequired
+};
+
+const PageFour = ({ domain, intl }) => (
+  <div className='onboarding-modal__page onboarding-modal__page-four'>
+    <div className='onboarding-modal__page-four__columns'>
+      <div className='row'>
+        <div>
+          <div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div>
+          <p><FormattedMessage id='onboarding.page_four.home' defaultMessage='Home timeline shows posts from people you follow'/></p>
+        </div>
+
+        <div>
+          <div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div>
+          <p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='Notifications show when someone interacts with you' /></p>
+        </div>
+      </div>
+
+      <div className='row'>
+        <div>
+          <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div>
+        </div>
+
+        <div>
+          <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div>
+        </div>
+      </div>
+
+      <p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='Federated timeline lists public posts from everyone who people on {domain} follow. Local timeline is the same, but limited to people on {domain}.' values={{ domain }} /></p>
+    </div>
+  </div>
+);
+
+PageFour.propTypes = {
+  domain: React.PropTypes.string.isRequired,
+  intl: React.PropTypes.object.isRequired
+};
+
+const PageSix = ({ admin }) => {
+  let adminSection = '';
+
+  if (admin) {
+    adminSection = (
+      <p>
+        <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />
+        <br />
+        <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage='Please, do not forget to read the {guidelines}!' values={{ guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }}/>
+      </p>
+    );
+  }
+
+  return (
+    <div className='onboarding-modal__page onboarding-modal__page-six'>
+      <h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1>
+      {adminSection}
+      <p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p>
+      <p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms. And now... Bon Appetoot!' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='various mobile apps' /></a> }} /></p>
+    </div>
+  );
+};
+
+PageSix.propTypes = {
+  admin: ImmutablePropTypes.map
+};
+
+const mapStateToProps = state => ({
+  me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
+  admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
+  domain: state.getIn(['meta', 'domain'])
+});
+
+const OnboardingModal = React.createClass({
+
+  propTypes: {
+    onClose: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired,
+    me: ImmutablePropTypes.map.isRequired,
+    domain: React.PropTypes.string.isRequired,
+    admin: ImmutablePropTypes.map
+  },
+
+  getInitialState () {
+    return {
+      currentIndex: 0
+    };
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleSkip (e) {
+    e.preventDefault();
+    this.props.onClose();
+  },
+
+  handleDot (i, e) {
+    e.preventDefault();
+    this.setState({ currentIndex: i });
+  },
+
+  handleNext (maxNum, e) {
+    e.preventDefault();
+
+    if (this.state.currentIndex < maxNum - 1) {
+      this.setState({ currentIndex: this.state.currentIndex + 1 });
+    } else {
+      this.props.onClose();
+    }
+  },
+
+  render () {
+    const { me, admin, domain, intl } = this.props;
+
+    const pages = [
+      <PageOne acct={me.get('acct')} domain={domain} />,
+      <PageTwo />,
+      <PageThree me={me} domain={domain} />,
+      <PageFour domain={domain} intl={intl} />,
+      <PageSix admin={admin} />
+    ];
+
+    const { currentIndex } = this.state;
+    const hasMore = currentIndex < pages.length - 1;
+
+    let nextOrDoneBtn;
+
+    if(hasMore) {
+      nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>;
+    } else {
+      nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.next' defaultMessage='Done' /></a>;
+    }
+
+    const styles = pages.map((page, i) => ({
+      key: i,
+      style: { opacity: spring(i === currentIndex ? 1 : 0) }
+    }));
+
+    return (
+      <div className='modal-root__modal onboarding-modal'>
+        <TransitionMotion styles={styles}>
+          {interpolatedStyles =>
+            <div className='onboarding-modal__pager'>
+              {pages.map((page, i) =>
+                <div key={i} style={{ opacity: interpolatedStyles[i].style.opacity, pointerEvents: i === currentIndex ? 'auto' : 'none' }}>{page}</div>
+              )}
+            </div>
+          }
+        </TransitionMotion>
+
+        <div className='onboarding-modal__paginator'>
+          <div>
+            <a href='#' className='onboarding-modal__skip' onClick={this.handleSkip}><FormattedMessage id='onboarding.skip' defaultMessage='Skip' /></a>
+          </div>
+
+          <div className='onboarding-modal__dots'>
+            {pages.map((_, i) => <div key={i} onClick={this.handleDot.bind(null, i)} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)}
+          </div>
+
+          <div>
+            {nextOrDoneBtn}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+});
+
+export default connect(mapStateToProps)(injectIntl(OnboardingModal));
diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx
index 0a1dd38ae..73a34ee4f 100644
--- a/app/assets/javascripts/components/locales/fr.jsx
+++ b/app/assets/javascripts/components/locales/fr.jsx
@@ -99,7 +99,7 @@ const fr = {
   "notifications.column_settings.mention": "Mentions :",
   "notifications.column_settings.reblog": "Partages :",
   "notifications.clear": "Nettoyer",
-  "notifications.clear_confirmation": "Voulez-vous vraiment nettoyer toutes vos notifications ?",
+  "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
   "notifications.settings": "Paramètres de la colonne",
   "privacy.public.short": "Public",
   "privacy.public.long": "Afficher dans les fils publics",
@@ -123,7 +123,25 @@ const fr = {
   "report.heading": "Nouveau signalement",
   "report.placeholder": "Commentaires additionnels",
   "report.submit": "Envoyer",
-  "report.target": "Signalement"
+  "report.target": "Signalement",
+  "onboarding.next": "Suivant",
+  "onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
+  "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.",
+  "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d'utilisateur⋅trice complet est {handle}",
+  "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.",
+  "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateurs⋅trices et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu'un qui n'est pas sur cette instance, utilisez son nom d'utilisateur⋅trice complet.",
+  "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d'autres préférences.",
+  "onboarding.page_four.home": "L'Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez",
+  "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu'un interagit avec vous",
+  "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateurs⋅trices suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateurs⋅trices de {domain}.",
+  "onboarding.page_six.almost_done": "Nous y sommes presque...",
+  "onboarding.page_six.admin": "L'administrateur⋅trice de votre instance est {admin}",
+  "onboarding.page_six.read_guidelines": "S'il vous plaît, n'oubliez pas de lire les {guidelines} !",
+  "onboarding.page_six.guidelines": "règles de la communauté",
+  "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.",
+  "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant... Bon Appetoot!",
+  "onboarding.page_six.various_app": "applications mobiles",
+  "onboarding.skip": "Passer",
 };
 
 export default fr;
diff --git a/app/assets/javascripts/components/locales/hr.jsx b/app/assets/javascripts/components/locales/hr.jsx
new file mode 100644
index 000000000..c26e2cc29
--- /dev/null
+++ b/app/assets/javascripts/components/locales/hr.jsx
@@ -0,0 +1,124 @@
+/**
+hrvatski jezik
+ */
+const hr = {
+  "account.block": "Blokiraj @{name}",
+  "account.disclaimer": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.",
+  "account.edit_profile": "Uredi profil",
+  "account.follow": "Slijedi",
+  "account.followers": "Sljedbenici",
+  "account.follows_you": "te slijedi",
+  "account.follows": "Slijedi",
+  "account.mention": "Spomeni @{name}",
+  "account.mute": "Utišaj @{name}",
+  "account.posts": "Postovi",
+  "account.report": "Prijavi @{name}",
+  "account.requested": "Čeka pristanak",
+  "account.unblock": "Deblokiraj @{name}",
+  "account.unfollow": "Prestani slijediti",
+  "account.unmute": "Poništi utišavanje @{name}",
+  "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
+  "column_back_button.label": "Natrag",
+  "column.blocks": "Blokirani korisnici",
+  "column.community": "Lokalni timeline",
+  "column.favourites": "Favoriti",
+  "column.follow_requests": "Zahtjevi za slijeđenje",
+  "column.home": "Dom",
+  "column.notifications": "Notifikacije",
+  "column.public": "Federalni timeline",
+  "compose_form.placeholder": "Što ti je na umu?",
+  "compose_form.privacy_disclaimer": "Tvoj privatni status će biti dostavljen spomenutim korisnicima na {domains}. Vjeruješ li {domainsCount, plural, one {that server} drugim {those servers}}? Privatnost postova radi samo na Mastodon instancama. Ako {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, neće biti indikacije da je tvoj post privatan, i mogao bit biti boosted ili biti učinjen vidljivim na drugi način neželjenim primateljima.",
+  "compose_form.publish": "Toot",
+  "compose_form.sensitive": "Označi media sadržaj kao osjetljiv",
+  "compose_form.spoiler_placeholder": "Upozorenje o sadržaju",
+  "compose_form.spoiler": "Sakrij text iza upozorenja",
+  "emoji_button.label": "Umetni smajlije",
+  "empty_column.community": "Lokalni timeline je prazan. Napiši nešto javno kako bi pokrenuo stvari!",
+  "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.",
+  "empty_column.home.public_timeline": "javni timeline",
+  "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.",
+  "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.",
+  "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio",
+  "follow_request.authorize": "Authoriziraj",
+  "follow_request.reject": "Odbij",
+  "getting_started.apps": "Dostupne su razne aplikacije",
+  "getting_started.heading": "Počnimo",
+  "getting_started.open_source_notice": "Mastodon je softver otvorenog koda. Možeš pridonijeti ili prijaviti probleme na GitHubu  {github}. {apps}.",
+  "home.column_settings.advanced": "Napredno",
+  "home.column_settings.basic": "Osnovno",
+  "home.column_settings.filter_regex": "Filtriraj s regularnim izrazima",
+  "home.column_settings.show_reblogs": "Pokaži boosts",
+  "home.column_settings.show_replies": "Pokaži odgovore",
+  "home.settings": "Postavke Stupca",
+  "lightbox.close": "Zatvori",
+  "loading_indicator.label": "Učitavam...",
+  "media_gallery.toggle_visible": "Preklopi vidljivost",
+  "missing_indicator.label": "Nije nađen",
+  "navigation_bar.blocks": "Blokirani korisnici",
+  "navigation_bar.community_timeline": "Lokalni timeline",
+  "navigation_bar.edit_profile": "Uredi profil",
+  "navigation_bar.favourites": "Favoriti",
+  "navigation_bar.follow_requests": "Zahtjevi za sljeđenje",
+  "navigation_bar.info": "Proširena informacija",
+  "navigation_bar.logout": "Odjavi se",
+  "navigation_bar.preferences": "Postavke",
+  "navigation_bar.public_timeline": "Federalni timeline",
+  "notification.favourite": "{name} je lajkao tvoj status",
+  "notification.follow": "{name} te sada slijedi",
+  "notification.reblog": "{name} je boosted tvoj status",
+  "notifications.clear_confirmation": "Želiš li zaista obrisati sve svoje notifikacije?",
+  "notifications.clear": "Očisti notifikacije",
+  "notifications.column_settings.alert": "Desktop notifikacije",
+  "notifications.column_settings.favourite": "Favoriti:",
+  "notifications.column_settings.follow": "Novi sljedbenici:",
+  "notifications.column_settings.mention": "Spominjanja:",
+  "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.show": "Prikaži u stupcu",
+  "notifications.column_settings.sound": "Sviraj zvuk",
+  "notifications.settings": "Postavke rubrike",
+  "privacy.change": "Podesi status privatnosti",
+  "privacy.direct.long": "Prikaži samo spomenutim korisnicima",
+  "privacy.direct.short": "Direktno",
+  "privacy.private.long": "Prikaži samo sljedbenicima",
+  "privacy.private.short": "Privatno",
+  "privacy.public.long": "Postaj na javne timeline",
+  "privacy.public.short": "Javno",
+  "privacy.unlisted.long": "Ne prikazuj u javnim timelines",
+  "privacy.unlisted.short": "Unlisted",
+  "reply_indicator.cancel": "Otkaži",
+  "report.heading": "Nova prijava",
+  "report.placeholder": "Dodatni komentari",
+  "report.submit": "Pošalji",
+  "report.target": "Prijavljivanje",
+  "search_results.total": "{count} {count, plural, one {result} other {results}}",
+  "search.placeholder": "Traži",
+  "search.status_by": "Status od {name}",
+  "status.delete": "Obriši",
+  "status.favourite": "Označi omiljenim",
+  "status.load_more": "Učitaj više",
+  "status.media_hidden": "Sakriven media sadržaj",
+  "status.mention": "Spomeni @{name}",
+  "status.open": "Proširi ovaj status",
+  "status.reblog": "Boost",
+  "status.reblogged_by": "{name} boosted",
+  "status.reply": "Odgovori",
+  "status.report": "Prijavi @{name}",
+  "status.sensitive_toggle": "Klikni da bi vidio",
+  "status.sensitive_warning": "Osjetljiv sadržaj",
+  "status.show_less": "Pokaži manje",
+  "status.show_more": "Pokaži više",
+  "tabs_bar.compose": "Sastavi",
+  "tabs_bar.federated_timeline": "Federalni",
+  "tabs_bar.home": "Dom",
+  "tabs_bar.local_timeline": "Lokalno",
+  "tabs_bar.notifications": "Notifikacije",
+  "upload_area.title": "Povuci & spusti kako bi uploadao",
+  "upload_button.label": "Dodaj media",
+  "upload_form.undo": "Poništi",
+  "upload_progress.label": "Uploadam...",
+  "video_player.toggle_sound": "Toggle zvuk",
+  "video_player.toggle_visible": "Preklopi vidljivost",
+  "video_player.expand": "Proširi video",
+};
+
+export default hr;
diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx
index 7525022b1..7abb315da 100644
--- a/app/assets/javascripts/components/locales/index.jsx
+++ b/app/assets/javascripts/components/locales/index.jsx
@@ -1,11 +1,13 @@
 import en from './en';
 import de from './de';
 import es from './es';
+import hr from './hr';
 import hu from './hu';
 import fr from './fr';
 import nl from './nl';
 import no from './no';
 import pt from './pt';
+import pt_br from './pt-br';
 import uk from './uk';
 import fi from './fi';
 import eo from './eo';
@@ -18,11 +20,13 @@ const locales = {
   en,
   de,
   es,
+  hr,
   hu,
   fr,
   nl,
   no,
   pt,
+  'pt-BR': pt_br,
   uk,
   fi,
   eo,
diff --git a/app/assets/javascripts/components/locales/nl.jsx b/app/assets/javascripts/components/locales/nl.jsx
index 8fc3a422f..533bc2aa5 100644
--- a/app/assets/javascripts/components/locales/nl.jsx
+++ b/app/assets/javascripts/components/locales/nl.jsx
@@ -22,47 +22,69 @@ const nl = {
   "account.followers": "Volgers",
   "account.follows_you": "Volgt jou",
   "account.requested": "Wacht op goedkeuring",
+  "account.mute": "@{name} negeren",
+  "account.unmute": "@{name} niet meer negeren",
+  "account.report": "Report @{name}",
   "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.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent. Voer hiervoor het e-mailachtige adres in het zoekveld in.",
+  "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 toots wilt vermelden.",
   "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
-  "column.home": "Thuis",
+  "column.home": "Jouw tijdlijn",
   "column.community": "Lokale tijdlijn",
-  "column.public": "Federatietijdlijn",
+  "column.public": "Globale tijdlijn",
   "column.notifications": "Meldingen",
   "tabs_bar.compose": "Schrijven",
-  "tabs_bar.home": "Thuis",
+  "tabs_bar.home": "Jouw tijdlijn",
   "tabs_bar.mentions": "Vermeldingen",
-  "tabs_bar.public": "Federatietijdlijn",
+  "tabs_bar.public": "Globale tijdlijn",
   "tabs_bar.notifications": "Meldingen",
   "compose_form.placeholder": "Waar ben je mee bezig?",
   "compose_form.publish": "Toot",
   "compose_form.sensitive": "Media als gevoelig markeren",
   "compose_form.spoiler": "Tekst achter waarschuwing verbergen",
+  "compose_form.spoiler_placeholder": "Waarschuwingstekst",
   "compose_form.private": "Als privé markeren",
-  "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.privacy_disclaimer": "Jouw privétoot wordt afgeleverd aan de vermelde gebruikers op {domains}. Vertrouw jij {domainsCount, plural, one {that server} andere {those servers}}? Het privé plaatsen van toots werkt alleen op Mastodon-servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er niet aangegeven dat de toot besloten is, waardoor het kan worden geboost of op een andere manier zichtbaar wordt gemaakt voor mensen waarvoor het niet was bedoeld.",
   "compose_form.unlisted": "Niet op openbare tijdlijnen tonen",
   "navigation_bar.edit_profile": "Profiel bewerken",
   "navigation_bar.preferences": "Voorkeuren",
   "navigation_bar.community_timeline": "Lokale tijdlijn",
-  "navigation_bar.public_timeline": "Federatietijdlijn",
+  "navigation_bar.public_timeline": "Globale tijdlijn",
+  "navigation_bar.follow_requests": "Volgverzoeken",
+  "navigation_bar.info": "Uitgebreide informatie",
+  "navigation_bar.blocks": "Geblokkeerde gebruikers",
+  "navigation_bar.mutes": "Genegeerde gebruikers",
   "navigation_bar.logout": "Afmelden",
   "reply_indicator.cancel": "Annuleren",
   "search.placeholder": "Zoeken",
   "search.account": "Account",
   "search.hashtag": "Hashtag",
+  "search_results.total": "{count} {count, plural, one {resultaat} other {resultaten}}",
   "upload_button.label": "Media toevoegen",
   "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.follow": "{name} volgt jou nu",
+  "notification.favourite": "{name} markeerde jouw toot als favoriet",
+  "notification.reblog": "{name} boostte jouw toot",
   "notification.mention": "{name} vermeldde jou",
+  "notifications.clear_confirmation": "Weet je zeker dat je al jouw meldingen wilt verwijderen?",
+  "notifications.clear": "Meldingen verwijderen",
   "notifications.column_settings.alert": "Desktopmeldingen",
   "notifications.column_settings.show": "In kolom tonen",
   "notifications.column_settings.follow": "Nieuwe volgers:",
   "notifications.column_settings.favourite": "Favorieten:",
   "notifications.column_settings.mention": "Vermeldingen:",
   "notifications.column_settings.reblog": "Boosts:",
+  "notifications.column_settings.sound": "Geluid afspelen",
+  "notifications.settings": "Kolom-instellingen",
+  "privacy.change": "Privacy toot aanpassen",
+  "privacy.direct.long": "Toot alleen naar vermelde gebruikers",
+  "privacy.direct.short": "Direct",
+  "privacy.private.long": "Toot alleen naar jouw volgers",
+  "privacy.private.short": "Privé",
+  "privacy.public.long": "Toot naar openbare tijdlijnen",
+  "privacy.public.short": "Openbaar",
+  "privacy.unlisted.long": "Niet op openbare tijdlijnen weergeven",
+  "privacy.unlisted.short": "Minder openbaar",
 };
 
 export default nl;
diff --git a/app/assets/javascripts/components/locales/no.jsx b/app/assets/javascripts/components/locales/no.jsx
index 43715fb5c..c89c5ede6 100644
--- a/app/assets/javascripts/components/locales/no.jsx
+++ b/app/assets/javascripts/components/locales/no.jsx
@@ -33,7 +33,7 @@ const no = {
   "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
   "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
   "empty_column.home.public_timeline": "en offentlig tidslinje",
-  "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",                
+  "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
   "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
   "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
   "follow_request.authorize": "Autorisér",
diff --git a/app/assets/javascripts/components/locales/pt-br.jsx b/app/assets/javascripts/components/locales/pt-br.jsx
new file mode 100644
index 000000000..724c5f1ce
--- /dev/null
+++ b/app/assets/javascripts/components/locales/pt-br.jsx
@@ -0,0 +1,125 @@
+const pt_br = {
+  "account.block": "Bloquear @{name}",
+  "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.",
+  "account.edit_profile": "Editar perfil",
+  "account.follow": "Seguir",
+  "account.followers": "Seguidores",
+  "account.follows_you": "É teu seguidor",
+  "account.follows": "Segue",
+  "account.mention": "Mencionar @{name}",
+  "account.mute": "Silenciar @{name}",
+  "account.posts": "Posts",
+  "account.report": "Denunciar @{name}",
+  "account.requested": "A aguardar aprovação",
+  "account.unblock": "Não bloquear @{name}",
+  "account.unfollow": "Deixar de seguir",
+  "account.unmute": "Não silenciar @{name}",
+  "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
+  "column_back_button.label": "Voltar",
+  "column.blocks": "Utilizadores Bloqueados",
+  "column.community": "Local",
+  "column.favourites": "Favoritos",
+  "column.follow_requests": "Seguidores Pendentes",
+  "column.home": "Home",
+  "column.mutes": "Utilizadores silenciados",
+  "column.notifications": "Notificações",
+  "column.public": "Global",
+  "compose_form.placeholder": "Em que estás a pensar?",
+  "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
+  "compose_form.publish": "Publicar",
+  "compose_form.sensitive": "Marcar media como conteúdo sensível",
+  "compose_form.spoiler_placeholder": "Aviso de conteúdo",
+  "compose_form.spoiler": "Esconder texto com aviso",
+  "emoji_button.label": "Inserir Emoji",
+  "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
+  "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
+  "empty_column.home.public_timeline": "global",
+  "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
+  "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
+  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
+  "follow_request.authorize": "Autorizar",
+  "follow_request.reject": "Rejeitar",
+  "getting_started.apps": "Existem várias aplicações disponíveis",
+  "getting_started.heading": "Primeiros passos",
+  "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.",
+  "home.column_settings.advanced": "Avançado",
+  "home.column_settings.basic": "Básico",
+  "home.column_settings.filter_regex": "Filtrar com uma expressão regular",
+  "home.column_settings.show_reblogs": "Mostrar as partilhas",
+  "home.column_settings.show_replies": "Mostrar as respostas",
+  "home.settings": "Parâmetros da listagem Home",
+  "lightbox.close": "Fechar",
+  "loading_indicator.label": "Carregando...",
+  "media_gallery.toggle_visible": "Esconder/Mostrar",
+  "missing_indicator.label": "Não encontrado",
+  "navigation_bar.blocks": "Utilizadores bloqueados",
+  "navigation_bar.community_timeline": "Local",
+  "navigation_bar.edit_profile": "Editar perfil",
+  "navigation_bar.favourites": "Favoritos",
+  "navigation_bar.follow_requests": "Seguidores pendentes",
+  "navigation_bar.info": "Mais informações",
+  "navigation_bar.logout": "Sair",
+  "navigation_bar.mutes": "Utilizadores silenciados",
+  "navigation_bar.preferences": "Preferências",
+  "navigation_bar.public_timeline": "Global",
+  "notification.favourite": "{name} adicionou o teu post aos favoritos",
+  "notification.follow": "{name} seguiu-te",
+  "notification.mention": "{name} mencionou-te",
+  "notification.reblog": "{name} partilhou o teu post",
+  "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
+  "notifications.clear": "Limpar notificações",
+  "notifications.column_settings.alert": "Notificações no computador",
+  "notifications.column_settings.favourite": "Favoritos:",
+  "notifications.column_settings.follow": "Novos seguidores:",
+  "notifications.column_settings.mention": "Menções:",
+  "notifications.column_settings.reblog": "Partilhas:",
+  "notifications.column_settings.show": "Mostrar nas colunas",
+  "notifications.column_settings.sound": "Reproduzir som",
+  "notifications.settings": "Parâmetros da listagem de Notificações",
+  "privacy.change": "Ajustar a privacidade da mensagem",
+  "privacy.direct.long": "Apenas para utilizadores mencionados",
+  "privacy.direct.short": "Directo",
+  "privacy.private.long": "Apenas para os seguidores",
+  "privacy.private.short": "Privado",
+  "privacy.public.long": "Publicar em todos os feeds",
+  "privacy.public.short": "Público",
+  "privacy.unlisted.long": "Não publicar nos feeds públicos",
+  "privacy.unlisted.short": "Não listar",
+  "reply_indicator.cancel": "Cancelar",
+  "report.heading": "Nova denúncia",
+  "report.placeholder": "Comentários adicionais",
+  "report.submit": "Enviar",
+  "report.target": "Denunciar",
+  "search_results.total": "{count} {count, plural, one {resultado} other {resultados}}",
+  "search.placeholder": "Pesquisar",
+  "search.status_by": "Post de {name}",
+  "status.delete": "Eliminar",
+  "status.favourite": "Adicionar aos favoritos",
+  "status.load_more": "Carregar mais",
+  "status.media_hidden": "Media escondida",
+  "status.mention": "Mencionar @{name}",
+  "status.open": "Expandir",
+  "status.reblog": "Partilhar",
+  "status.reblogged_by": "{name} partilhou",
+  "status.reply": "Responder",
+  "status.report": "Denúnciar @{name}",
+  "status.sensitive_toggle": "Clique para ver",
+  "status.sensitive_warning": "Conteúdo sensível",
+  "status.show_less": "Mostrar menos",
+  "status.show_more": "Mostrar mais",
+  "tabs_bar.compose": "Criar",
+  "tabs_bar.federated_timeline": "Global",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notificações",
+  "upload_area.title": "Arraste e solte para enviar",
+  "upload_button.label": "Adicionar media",
+  "upload_form.undo": "Anular",
+  "upload_progress.label": "A gravar...",
+  "video_player.toggle_sound": "Ligar/Desligar som",
+  "video_player.toggle_visible": "Ligar/Desligar vídeo",
+  "video_player.expand": "Expandir vídeo",
+  "video_player.video_error": "Não é possível ver o vídeo",
+};
+
+export default pt_br;
diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx
index cd345a585..88729c94c 100644
--- a/app/assets/javascripts/components/locales/pt.jsx
+++ b/app/assets/javascripts/components/locales/pt.jsx
@@ -1,128 +1,125 @@
 const pt = {
-  "column_back_button.label": "Voltar",
-  "lightbox.close": "Fechar",
-  "loading_indicator.label": "Carregando...",
-  "status.mention": "Mencionar @{name}",
-  "status.delete": "Eliminar",
-  "status.reply": "Responder",
-  "status.reblog": "Partilhar",
-  "status.favourite": "Adicionar aos favoritos",
-  "status.reblogged_by": "{name} partilhou",
-  "status.sensitive_warning": "Conteúdo sensível",
-  "status.sensitive_toggle": "Clique para ver",
-  "status.show_more": "Mostrar mais",
-  "status.show_less": "Mostrar menos",
-  "status.open": "Expandir",
-  "status.report": "Reportar @{name}",
-  "status.load_more": "Carregar mais",
-  "status.media_hidden": "Media escondida",
-  "video_player.toggle_sound": "Ligar/Desligar som",
-  "video_player.toggle_visible": "Ligar/Desligar vídeo",
-  "account.mention": "Mencionar @{name}",
-  "account.edit_profile": "Editar perfil",
-  "account.unblock": "Não bloquear @{name}",
-  "account.unfollow": "Não seguir",
   "account.block": "Bloquear @{name}",
-  "account.mute": "Mute",
-  "account.unmute": "Remover Mute",
+  "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.",
+  "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
-  "account.posts": "Posts",
-  "account.follows": "Segue",
   "account.followers": "Seguidores",
   "account.follows_you": "É teu seguidor",
+  "account.follows": "Segue",
+  "account.mention": "Mencionar @{name}",
+  "account.mute": "Silenciar @{name}",
+  "account.posts": "Posts",
+  "account.report": "Denunciar @{name}",
   "account.requested": "A aguardar aprovação",
-  "account.report": "Denunciar",
-  "account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.",
-  "getting_started.heading": "Primeiros passos",
-  "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.",
-  "getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.",
-  "getting_started.about_developer": "Pode seguir o developer deste projecto em Gargron@mastodon.social",
-  "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.",
-  "column.home": "Home",
-  "column.community": "Local",
-  "column.public": "Global",
-  "column.notifications": "Notificações",
+  "account.unblock": "Não bloquear @{name}",
+  "account.unfollow": "Deixar de seguir",
+  "account.unmute": "Não silenciar @{name}",
+  "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
+  "column_back_button.label": "Voltar",
   "column.blocks": "Utilizadores Bloqueados",
+  "column.community": "Local",
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores Pendentes",
-  "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
-  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
-  "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
-  "empty_column.home.public_timeline": "global",
-  "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
-  "empty_column.hashtag": "Não existe qualquer conteúdo com essa hashtag",
-  "tabs_bar.compose": "Criar",
-  "tabs_bar.home": "Home",
-  "tabs_bar.mentions": "Menções",
-  "tabs_bar.public": "Público",
-  "tabs_bar.notifications": "Notificações",
-  "tabs_bar.local_timeline": "Local",
-  "tabs_bar.federated_timeline": "Global",
+  "column.home": "Home",
+  "column.mutes": "Utilizadores silenciados",
+  "column.notifications": "Notificações",
+  "column.public": "Global",
   "compose_form.placeholder": "Em que estás a pensar?",
+  "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
   "compose_form.publish": "Publicar",
   "compose_form.sensitive": "Marcar media como conteúdo sensível",
+  "compose_form.spoiler_placeholder": "Aviso de conteúdo",
   "compose_form.spoiler": "Esconder texto com aviso",
-  "compose_form.spoiler_placeholder": "Aviso",
-  "compose_form.private": "Tornar privado",
-  "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
-  "compose_form.unlisted": "Não mostrar na listagem pública",
   "emoji_button.label": "Inserir Emoji",
-  "navigation_bar.edit_profile": "Editar perfil",
-  "navigation_bar.preferences": "Preferências",
-  "navigation_bar.community_timeline": "Local",
-  "navigation_bar.public_timeline": "Global",
+  "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
+  "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
+  "empty_column.home.public_timeline": "global",
+  "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
+  "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
+  "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
+  "follow_request.authorize": "Autorizar",
+  "follow_request.reject": "Rejeitar",
+  "getting_started.apps": "Existem várias aplicações disponíveis",
+  "getting_started.heading": "Primeiros passos",
+  "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.",
+  "home.column_settings.advanced": "Avançado",
+  "home.column_settings.basic": "Básico",
+  "home.column_settings.filter_regex": "Filtrar com uma expressão regular",
+  "home.column_settings.show_reblogs": "Mostrar as partilhas",
+  "home.column_settings.show_replies": "Mostrar as respostas",
+  "home.settings": "Parâmetros da listagem Home",
+  "lightbox.close": "Fechar",
+  "loading_indicator.label": "Carregando...",
+  "media_gallery.toggle_visible": "Esconder/Mostrar",
+  "missing_indicator.label": "Não encontrado",
   "navigation_bar.blocks": "Utilizadores bloqueados",
+  "navigation_bar.community_timeline": "Local",
+  "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
+  "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.logout": "Sair",
-  "navigation_bar.follow_requests": "Seguidores pendentes",
-  "reply_indicator.cancel": "Cancelar",
-  "search.placeholder": "Pesquisar",
-  "search.account": "Conta",
-  "search.hashtag": "Hashtag",
-  "search_results.total": "{count} {count, plural, one {resultado} other {resultados}}",
-  "search.status_by": "Post de {name}",
-  "upload_button.label": "Adicionar media",
-  "upload_form.undo": "Anular",
-  "upload_progress.label": "A gravar…",
-  "upload_area.title": "Arraste e solte para enviar",
-  "notification.follow": "{name} seguiu-te",
+  "navigation_bar.mutes": "Utilizadores silenciados",
+  "navigation_bar.preferences": "Preferências",
+  "navigation_bar.public_timeline": "Global",
   "notification.favourite": "{name} adicionou o teu post aos favoritos",
-  "notification.reblog": "{name} partilhou o teu post",
+  "notification.follow": "{name} seguiu-te",
   "notification.mention": "{name} mencionou-te",
+  "notification.reblog": "{name} partilhou o teu post",
+  "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
+  "notifications.clear": "Limpar notificações",
   "notifications.column_settings.alert": "Notificações no computador",
-  "notifications.column_settings.show": "Mostrar nas colunas",
-  "notifications.column_settings.sound": "Reproduzir som",
-  "notifications.column_settings.follow": "Novos seguidores:",
   "notifications.column_settings.favourite": "Favoritos:",
+  "notifications.column_settings.follow": "Novos seguidores:",
   "notifications.column_settings.mention": "Menções:",
   "notifications.column_settings.reblog": "Partilhas:",
-  "notifications.clear": "Limpar notificações",
-  "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
-  "notifications.settings": "Parâmetros da lista de Notificações",
-  "privacy.public.short": "Público",
+  "notifications.column_settings.show": "Mostrar nas colunas",
+  "notifications.column_settings.sound": "Reproduzir som",
+  "notifications.settings": "Parâmetros da listagem de Notificações",
+  "privacy.change": "Ajustar a privacidade da mensagem",
+  "privacy.direct.long": "Apenas para utilizadores mencionados",
+  "privacy.direct.short": "Directo",
+  "privacy.private.long": "Apenas para os seguidores",
+  "privacy.private.short": "Privado",
   "privacy.public.long": "Publicar em todos os feeds",
-  "privacy.unlisted.short": "Não listar",
+  "privacy.public.short": "Público",
   "privacy.unlisted.long": "Não publicar nos feeds públicos",
-  "privacy.private.short": "Privado",
-  "privacy.private.long": "Apenas para os seguidores",
-  "privacy.direct.short": "Directo",
-  "privacy.direct.long": "Apenas para utilizadores mencionados",
-  "privacy.change": "Ajustar a privacidade da mensagem",
-  "media_gallery.toggle_visible": "Modificar a visibilidade",
-  "missing_indicator.label": "Não encontrado",
-  "follow_request.authorize": "Autorizar",
-  "follow_request.reject": "Rejeitar",
-  "home.settings": "Parâmetros da coluna Home",
-  "home.column_settings.basic": "Básico",
-  "home.column_settings.show_reblogs": "Mostrar as partilhas",
-  "home.column_settings.show_replies": "Mostrar as respostas",
-  "home.column_settings.advanced": "Avançadas",
-  "home.column_settings.filter_regex": "Filtrar com uma expressão regular",
-  "report.heading": "Nova denuncia",
+  "privacy.unlisted.short": "Não listar",
+  "reply_indicator.cancel": "Cancelar",
+  "report.heading": "Nova denúncia",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
-  "report.target": "Denunciar"
+  "report.target": "Denunciar",
+  "search_results.total": "{count} {count, plural, one {resultado} other {resultados}}",
+  "search.placeholder": "Pesquisar",
+  "search.status_by": "Post de {name}",
+  "status.delete": "Eliminar",
+  "status.favourite": "Adicionar aos favoritos",
+  "status.load_more": "Carregar mais",
+  "status.media_hidden": "Media escondida",
+  "status.mention": "Mencionar @{name}",
+  "status.open": "Expandir",
+  "status.reblog": "Partilhar",
+  "status.reblogged_by": "{name} partilhou",
+  "status.reply": "Responder",
+  "status.report": "Denúnciar @{name}",
+  "status.sensitive_toggle": "Clique para ver",
+  "status.sensitive_warning": "Conteúdo sensível",
+  "status.show_less": "Mostrar menos",
+  "status.show_more": "Mostrar mais",
+  "tabs_bar.compose": "Criar",
+  "tabs_bar.federated_timeline": "Global",
+  "tabs_bar.home": "Home",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notificações",
+  "upload_area.title": "Arraste e solte para enviar",
+  "upload_button.label": "Adicionar media",
+  "upload_form.undo": "Anular",
+  "upload_progress.label": "A gravar...",
+  "video_player.toggle_sound": "Ligar/Desligar som",
+  "video_player.toggle_visible": "Ligar/Desligar vídeo",
+  "video_player.expand": "Expandir vídeo",
+  "video_player.video_error": "Não é possível ver o vídeo",
 };
 
 export default pt;
diff --git a/app/assets/javascripts/components/locales/zh-cn.jsx b/app/assets/javascripts/components/locales/zh-cn.jsx
new file mode 100644
index 000000000..67baa02a2
--- /dev/null
+++ b/app/assets/javascripts/components/locales/zh-cn.jsx
@@ -0,0 +1,157 @@
+import zh from 'react-intl/locale-data/zh';
+
+const localeData = zh.reduce(function (acc, localeData) {
+  if (localeData.locale === "zh-Hans-CN") {
+    // rename the locale "zh-Hans-CN" as "zh-CN"
+    // (match the code usually used in Accepted-Language header)
+    acc.push(Object.assign({},
+      localeData,
+      {
+        "locale": "zh-CN",
+        "parentLocale": "zh-Hans-CN",
+      }
+    ));
+  }
+  return acc;
+}, []);
+
+export { localeData as localeData };
+
+const zh_cn = {
+  "account.block": "屏蔽 @{name}",
+  "account.disclaimer": "由于这个账户处于另一个服务站,实际数字会比这个更多。",
+  "account.edit_profile": "修改个人资料",
+  "account.follow": "关注",
+  "account.followers": "关注的人",
+  "account.follows_you": "关注你",
+  "account.follows": "正在关注",
+  "account.mention": "提及 @{name}",
+  "account.mute": "将 @{name} 静音",
+  "account.posts": "嘟文",
+  "account.report": "举报 @{name}",
+  "account.requested": "等候审批",
+  "account.unblock": "解除对 @{name} 的屏蔽",
+  "account.unfollow": "取消关注",
+  "account.unmute": "取消 @{name} 的静音",
+  "boost_modal.combo": "如你想在下次路过时显示,请按{combo},",
+  "column_back_button.label": "返回",
+  "column.blocks": "屏蔽用户",
+  "column.community": "本站时间轴",
+  // intentional departure from existing "推文" translation for posts:
+  // "推文" refers to "推特", the official translation for Twitter.
+  // Currently using a semi-phonetic translation "嘟", which refers
+  // to train horn sounds, for "toot".
+  "column.favourites": "赞过的嘟文",
+  "column.follow_requests": "关注请求",
+  "column.home": "主页",
+  "column.notifications": "通知",
+  "column.public": "跨站公共时间轴",
+  "compose_form.placeholder": "在想啥?",
+  "compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任 {domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务站} other {之中有些不是 Mastodon 服务站}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。",
+  "compose_form.private": "标示为“只有关注你的人能看”",
+  // Going "toot-toot!" here below.
+  "compose_form.publish": "嘟嘟!",
+  "compose_form.sensitive": "将媒体文件标示为“敏感内容”",
+  "compose_form.spoiler_placeholder": "敏感内容",
+  "compose_form.spoiler": "将部份文本藏于警告消息之后",
+  "compose_form.unlisted": "请勿在公共时间轴显示",
+  "emoji_button.label": "加入表情符号",
+  "empty_column.community": "本站时间轴暂时未有内容,快贴文来抢头香啊!",
+  "empty_column.hashtag": "这个标签暂时未有内容。",
+  "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
+  "empty_column.home.public_timeline": "公共时间轴",
+  "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
+  "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
+  "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务站的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
+  "follow_request.authorize": "批准",
+  "follow_request.reject": "拒绝",
+  "getting_started.about_addressing": "只要你知道一位用户的用户名称和域名,你可以用“@用户名称@域名”的格式在搜索栏寻找该用户。",
+  "getting_started.about_shortcuts": "只要该用户是在你现在的服务站开立,你可以直接输入用户𠱷搜索。同样的规则适用于在嘟文提及别的用户。",
+  "getting_started.apps": "手机或桌面应用程序",
+  "getting_started.heading": "开始使用",
+  "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。你亦可透过{apps}阅读 Mastodon 上的消息。",
+  "home.column_settings.advanced": "高端",
+  "home.column_settings.basic": "基本",
+  "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤",
+  "home.column_settings.show_reblogs": "显示被转的嘟文",
+  "home.column_settings.show_replies": "显示回应嘟文",
+  "home.settings": "字段设置",
+  "lightbox.close": "关闭",
+  "loading_indicator.label": "加载中...",
+  "media_gallery.toggle_visible": "打开或关上",
+  "missing_indicator.label": "找不到内容",
+  "navigation_bar.blocks": "被屏蔽的用户",
+  "navigation_bar.community_timeline": "本站时间轴",
+  "navigation_bar.edit_profile": "修改个人资料",
+  "navigation_bar.favourites": "赞的内容",
+  "navigation_bar.follow_requests": "关注请求",
+  "navigation_bar.info": "关于本服务站",
+  "navigation_bar.logout": "注销",
+  // intentional departure from https://github.com/tootsuite/mastodon/blob/f864fee1/config/locales/zh-CN.yml#L126:
+  // clashes for settings/preferences
+  "navigation_bar.preferences": "首选项",
+  "navigation_bar.public_timeline": "跨站公共时间轴",
+  "notification.favourite": "{name} 赞你的嘟文",
+  "notification.follow": "{name} 开始关注你",
+  "notification.mention": "{name} 提及你",
+  "notification.reblog": "{name} 转嘟你的嘟文",
+  "notifications.clear_confirmation": "你确定要清空通知纪录吗?",
+  "notifications.clear": "清空通知纪录",
+  "notifications.column_settings.alert": "显示桌面通知",
+  "notifications.column_settings.favourite": "赞你的嘟文:",
+  "notifications.column_settings.follow": "关注你:",
+  "notifications.column_settings.mention": "提及你:",
+  "notifications.column_settings.reblog": "转你的嘟文:",
+  "notifications.column_settings.show": "在通知栏显示",
+  "notifications.column_settings.sound": "播放音效",
+  "notifications.settings": "字段设置",
+  "privacy.change": "调整隐私设置",
+  "privacy.direct.long": "只有提及的用户能看到",
+  "privacy.direct.short": "私人消息",
+  "privacy.private.long": "只有关注你用户能看到",
+  "privacy.private.short": "关注者",
+  "privacy.public.long": "在公共时间轴显示",
+  "privacy.public.short": "公共",
+  "privacy.unlisted.long": "公开,但不在公共时间轴显示",
+  "privacy.unlisted.short": "公开",
+  "reply_indicator.cancel": "取消",
+  "report.heading": "举报",
+  "report.placeholder": "额外消息",
+  "report.submit": "提交",
+  "report.target": "Reporting",
+  "search_results.total": "{count} 项结果",
+  "search.account": "用户",
+  "search.hashtag": "标签",
+  "search.placeholder": "搜索",
+  "search.status_by": "按{name}搜索嘟文",
+  "status.delete": "删除",
+  "status.favourite": "赞",
+  "status.load_more": "加载更多",
+  "status.media_hidden": "隐藏媒体内容",
+  "status.mention": "提及 @{name}",
+  "status.open": "展开嘟文",
+  "status.reblog": "转嘟",
+  "status.reblogged_by": "{name} 转嘟",
+  "status.reply": "回应",
+  "status.report": "举报 @{name}",
+  "status.sensitive_toggle": "点击显示",
+  "status.sensitive_warning": "敏感内容",
+  "status.show_less": "减少显示",
+  "status.show_more": "显示更多",
+  "tabs_bar.compose": "撰写",
+  "tabs_bar.federated_timeline": "跨站",
+  "tabs_bar.home": "主页",
+  "tabs_bar.local_timeline": "本站",
+  "tabs_bar.mentions": "提及",
+  "tabs_bar.notifications": "通知",
+  "tabs_bar.public": "跨站公共时间轴",
+  "upload_area.title": "将文件拖放至此上传",
+  "upload_button.label": "上传媒体文件",
+  "upload_form.undo": "还原",
+  "upload_progress.label": "上传中……",
+  "video_player.expand": "展开影片",
+  "video_player.toggle_sound": "开关音效",
+  "video_player.toggle_visible": "打开或关上",
+};
+
+export default zh_cn;
diff --git a/app/assets/javascripts/components/reducers/settings.jsx b/app/assets/javascripts/components/reducers/settings.jsx
index 8acc3faca..820af99ed 100644
--- a/app/assets/javascripts/components/reducers/settings.jsx
+++ b/app/assets/javascripts/components/reducers/settings.jsx
@@ -3,6 +3,8 @@ import { STORE_HYDRATE } from '../actions/store';
 import Immutable from 'immutable';
 
 const initialState = Immutable.Map({
+  onboarded: false,
+
   home: Immutable.Map({
     shows: Immutable.Map({
       reblog: true,
diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss
index e27b88e5f..2916b915b 100644
--- a/app/assets/stylesheets/admin.scss
+++ b/app/assets/stylesheets/admin.scss
@@ -120,10 +120,12 @@
   @media screen and (max-width: 600px) {
     display: block;
     overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
 
     .sidebar-wrapper, .content-wrapper {
       flex: 0 0 auto;
       height: auto;
+      overflow: initial;
     }
 
     .sidebar {
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 8bd35819a..6f407a6d5 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -932,6 +932,12 @@ a.status__content__spoiler-link {
   }
 }
 
+.pseudo-drawer {
+  background: lighten($color1, 13%);
+  font-size: 13px;
+  text-align: left;
+}
+
 .drawer__header {
   flex: 0 0 auto;
   font-size: 16px;
@@ -2018,6 +2024,7 @@ button.icon-button.active i.fa-retweet {
 .modal-root__modal {
   pointer-events: auto;
   display: flex;
+  z-index: 9999;
 }
 
 .media-modal {
@@ -2031,6 +2038,237 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.onboarding-modal {
+  background: $color2;
+  color: $color1;
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.onboarding-modal__pager {
+  height: 80vh;
+  width: 80vw;
+  max-width: 520px;
+  max-height: 420px;
+  position: relative;
+
+  & > div {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    box-sizing: border-box;
+    padding: 25px;
+    display: none;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    display: flex;
+    opacity: 0;
+    user-select: text;
+  }
+}
+
+@media screen and (max-width: 550px) {
+  .onboarding-modal {
+    width: 100%;
+    height: 100%;
+    border-radius: 0;
+  }
+
+  .onboarding-modal__pager {
+    width: 100%;
+    height: auto;
+    max-width: none;
+    max-height: none;
+    flex: 1 1 auto;
+  }
+}
+
+.onboarding-modal__paginator {
+  flex: 0 0 auto;
+  background: darken($color2, 8%);
+  display: flex;
+  padding: 25px;
+
+  & > div {
+    min-width: 33px;
+  }
+
+  a {
+    color: darken($color2, 34%);
+    text-decoration: none;
+    font-size: 14px;
+    font-weight: 500;
+
+    &:hover, &:focus, &:active {
+      color: darken($color2, 38%);
+    }
+
+    &.onboarding-modal__done, &.onboarding-modal__next {
+      color: $color4;
+    }
+  }
+}
+
+.onboarding-modal__dots {
+  flex: 1 1 auto;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.onboarding-modal__dot {
+  width: 14px;
+  height: 14px;
+  border-radius: 14px;
+  background: darken($color2, 16%);
+  margin: 0 3px;
+  cursor: pointer;
+
+  &:hover {
+    background: darken($color2, 18%);
+  }
+
+  &.active {
+    cursor: default;
+    background: darken($color2, 24%);
+  }
+}
+
+.onboarding-modal__page {
+  cursor: default;
+  line-height: 21px;
+
+  h1 {
+    font-size: 18px;
+    font-weight: 500;
+    color: $color1;
+    margin-bottom: 20px;
+  }
+
+  a {
+    color: $color4;
+
+    &:hover, &:focus, &:active {
+      color: lighten($color4, 4%);
+    }
+  }
+
+  p {
+    font-size: 16px;
+    color: lighten($color1, 8%);
+    margin-top: 10px;
+    margin-bottom: 10px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    strong {
+      font-weight: 500;
+      background: $color1;
+      color: $color2;
+      border-radius: 4px;
+      font-size: 14px;
+      padding: 3px 6px;
+    }
+  }
+}
+
+.onboarding-modal__page-one {
+  display: flex;
+}
+
+.onboarding-modal__page-one__elephant-friend {
+  background: image-url('elephant-friend.png') no-repeat 0 0;
+  width: 147px;
+  height: 160px;
+  margin-right: 10px;
+}
+
+.onboarding-modal__page-two,
+.onboarding-modal__page-three,
+.onboarding-modal__page-four,
+.onboarding-modal__page-five {
+  p {
+    text-align: left;
+  }
+
+  .figure {
+    background: darken($color1, 8%);
+    color: $color2;
+    margin-bottom: 20px;
+    border-radius: 4px;
+    padding: 10px;
+    text-align: center;
+    font-size: 14px;
+    box-shadow: 1px 2px 6px rgba($color8, 0.3);
+
+    .onboarding-modal__image {
+      border-radius: 4px;
+      margin-bottom: 10px;
+    }
+
+    &.non-interactive {
+      pointer-events: none;
+      text-align: left;
+    }
+  }
+}
+
+.onboarding-modal__page-four__columns {
+  .row {
+    display: flex;
+    margin-bottom: 20px;
+
+    & > div {
+      flex: 1 1 0;
+      margin: 0 10px;
+
+      &:first-child {
+        margin-left: 0;
+      }
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+      p {
+        text-align: center;
+      }
+    }
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .column-header {
+    color: $color5;
+  }
+}
+
+.onboarding-modal__image {
+  border-radius: 8px;
+  width: 70vw;
+  max-width: 450px;
+  max-height: auto;
+  display: block;
+  margin: auto;
+  margin-bottom: 20px;
+}
+
+.onboard-sliders {
+  display: inline-block;
+  max-width: 30px;
+  max-height: auto;
+  margin-left: 10px;
+}
+
 .boost-modal {
   background: lighten($color2, 8%);
   color: $color1;
@@ -2043,6 +2281,7 @@ button.icon-button.active i.fa-retweet {
 }
 
 .boost-modal__container {
+  overflow-x: scroll;
   padding: 10px;
 
   .status {
diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss
index e5e8697a0..c6a8b5b02 100644
--- a/app/assets/stylesheets/forms.scss
+++ b/app/assets/stylesheets/forms.scss
@@ -42,7 +42,7 @@ code {
     }
   }
 
-  .input.file, .input.select {
+  .input.file, .input.select, .input.radio_buttons {
     padding: 15px 0;
     margin-bottom: 0;
 
@@ -59,6 +59,15 @@ code {
     margin-bottom: 25px;
   }
 
+  .input.radio_buttons .radio label {
+    margin-bottom: 5px;
+    font-family: inherit;
+    font-size: 14px;
+    color: white;
+    display: block;
+    width: auto;
+  }
+
   .input.boolean {
     margin-bottom: 5px;
 
@@ -72,7 +81,8 @@ code {
 
     label.checkbox {
       position: relative;
-	    padding-left: 25px;
+      padding-left: 25px;
+      flex: 1 1 auto;
     }
 
     input[type=checkbox] {
@@ -182,6 +192,10 @@ code {
       }
     }
   }
+
+  select {
+    font-size: 16px;
+  }
 }
 
 .flash-message {
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 5d146d946..1932dc6a8 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -15,7 +15,7 @@ module Admin
 
       if @domain_block.save
         DomainBlockWorker.perform_async(@domain_block.id)
-        redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_block.created_msg')
+        redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
       else
         render action: :new
       end
@@ -28,7 +28,7 @@ module Admin
     def destroy
       @domain_block = DomainBlock.find(params[:id])
       UnblockDomainService.new.call(@domain_block, resource_params[:retroactive])
-      redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_block.destroyed_msg')
+      redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
     end
 
     private
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 1976ce330..b0e26918e 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -77,9 +77,9 @@ class Api::V1::StatusesController < ApiController
   end
 
   def unreblog
-    reblog         = Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first!
-    @status        = reblog.reblog
-    @reblogged_map = { @status.id => false }
+    reblog       = Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first!
+    @status      = reblog.reblog
+    @reblogs_map = { @status.id => false }
 
     RemovalWorker.perform_async(reblog.id)
 
@@ -93,7 +93,7 @@ class Api::V1::StatusesController < ApiController
 
   def unfavourite
     @status         = Status.find(params[:id])
-    @favourited_map = { @status.id => false }
+    @favourites_map = { @status.id => false }
 
     UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
 
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index f8050afb5..dd30be32a 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -10,6 +10,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   def build_resource(hash = nil)
     super(hash)
+    resource.locale = I18n.locale
     resource.build_account if resource.account.nil?
   end
 
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 2d1cf74f0..0a25b52aa 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -7,6 +7,7 @@ class HomeController < ApplicationController
     @body_classes           = 'app-body'
     @token                  = find_or_create_access_token.token
     @web_settings           = Web::Setting.find_by(user: current_user)&.data || {}
+    @admin                  = Account.find_local(Setting.site_contact_username)
     @streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
   end
 
diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index 488c4f944..fa1daf012 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -1,16 +1,19 @@
 # frozen_string_literal: true
 
 class MediaController < ApplicationController
-  before_action :set_media_attachment
+  before_action :verify_permitted_status
 
   def show
-    redirect_to @media_attachment.file.url(:original)
+    redirect_to media_attachment.file.url(:original)
   end
 
   private
 
-  def set_media_attachment
-    @media_attachment = MediaAttachment.where.not(status_id: nil).find_by!(shortcode: params[:id])
-    raise ActiveRecord::RecordNotFound unless @media_attachment.status.permitted?(current_account)
+  def media_attachment
+    MediaAttachment.attached.find_by!(shortcode: params[:id])
+  end
+
+  def verify_permitted_status
+    raise ActiveRecord::RecordNotFound unless media_attachment.status.permitted?(current_account)
   end
 end
diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb
index 1a8ef5f90..4a521d102 100644
--- a/app/controllers/well_known/webfinger_controller.rb
+++ b/app/controllers/well_known/webfinger_controller.rb
@@ -8,8 +8,13 @@ module WellKnown
       @magic_key = pem_to_magic_key(@account.keypair.public_key)
 
       respond_to do |format|
-        format.xml  { render content_type: 'application/xrd+xml' }
-        format.json { render content_type: 'application/jrd+json' }
+        format.any(:json, :html) do
+          render formats: :json, content_type: 'application/jrd+json'
+        end
+
+        format.xml do
+          render content_type: 'application/xrd+xml'
+        end
       end
     rescue ActiveRecord::RecordNotFound
       head 404
diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb
new file mode 100644
index 000000000..a1c3c3521
--- /dev/null
+++ b/app/helpers/instance_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module InstanceHelper
+  def site_title
+    Setting.site_title.to_s
+  end
+
+  def site_hostname
+    Rails.configuration.x.local_domain
+  end
+end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index cf7b9b381..01900b87f 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -7,17 +7,20 @@ module SettingsHelper
     es: 'Español',
     eo: 'Esperanto',
     fr: 'Français',
-    it: 'Italiano',
+    hr: 'Hrvatski',
     hu: 'Magyar',
+    it: 'Italiano',
     nl: 'Nederlands',
     no: 'Norsk',
     pt: 'Português',
+    'pt-BR': 'Português do Brasil',
     fi: 'Suomi',
     ru: 'Русский',
     uk: 'Українська',
     ja: '日本語',
     'zh-CN': '简体中文',
     'zh-HK': '繁體中文(香港)',
+    'zh-TW': '繁體中文(臺灣)',
     bg: 'Български',
   }.freeze
 
diff --git a/app/helpers/site_title_helper.rb b/app/helpers/site_title_helper.rb
deleted file mode 100644
index d2caa9203..000000000
--- a/app/helpers/site_title_helper.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module SiteTitleHelper
-  def site_title
-    Setting.site_title.to_s
-  end
-end
diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb
index 180b9bb82..5aeb7b4f9 100644
--- a/app/lib/atom_serializer.rb
+++ b/app/lib/atom_serializer.rb
@@ -7,7 +7,7 @@ class AtomSerializer
     def render(element)
       document = Ox::Document.new(version: '1.0')
       document << element
-      ('<?xml version="1.0"?>' + Ox.dump(element)).force_encoding('UTF-8')
+      ('<?xml version="1.0"?>' + Ox.dump(element, effort: :tolerant)).force_encoding('UTF-8')
     end
   end
 
@@ -311,11 +311,15 @@ class AtomSerializer
 
   def append_element(parent, name, content = nil, attributes = {})
     element = Ox::Element.new(name)
-    attributes.each { |k, v| element[k] = v.to_s }
-    element << content.to_s unless content.nil?
+    attributes.each { |k, v| element[k] = sanitize_str(v) }
+    element << sanitize_str(content) unless content.nil?
     parent  << element
   end
 
+  def sanitize_str(raw_str)
+    raw_str.to_s
+  end
+
   def add_namespaces(parent)
     parent['xmlns']          = TagManager::XMLNS
     parent['xmlns:thr']      = TagManager::THR_XMLNS
@@ -327,8 +331,8 @@ class AtomSerializer
   end
 
   def serialize_status_attributes(entry, status)
-    append_element(entry, 'summary', status.spoiler_text) if status.spoiler_text?
-    append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html')
+    append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
+    append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html', 'xml:lang': status.language)
 
     status.mentions.each do |mentioned|
       append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account))
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 0d9f10a08..e5dbfeeda 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -3,4 +3,5 @@
 class ApplicationMailer < ActionMailer::Base
   default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' }
   layout 'mailer'
+  helper :instance
 end
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index bf4c16e43..a163edd3c 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -59,7 +59,12 @@ class NotificationMailer < ApplicationMailer
     return if @notifications.empty?
 
     I18n.with_locale(@me.user.locale || I18n.default_locale) do
-      mail to: @me.user.email, subject: I18n.t('notification_mailer.digest.subject', count: @notifications.size)
+      mail to: @me.user.email,
+        subject: I18n.t(
+          :subject,
+          scope: [:notification_mailer, :digest],
+          count: @notifications.size
+        )
     end
   end
 end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 64ca92a3a..6abf9c9ca 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -4,6 +4,8 @@ class UserMailer < Devise::Mailer
   default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' }
   layout 'mailer'
 
+  helper :instance
+
   def confirmation_instructions(user, token, _opts = {})
     @resource = user
     @token    = token
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 89c81f766..baf5c3973 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -7,6 +7,9 @@ class DomainBlock < ApplicationRecord
 
   validates :domain, presence: true, uniqueness: true
 
+  has_many :accounts, foreign_key: :domain, primary_key: :domain
+  delegate :count, to: :accounts, prefix: true
+
   def self.blocked?(domain)
     where(domain: domain, severity: :suspend).exists?
   end
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index 41d06e734..32d54476b 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -3,14 +3,14 @@
 class Favourite < ApplicationRecord
   include Paginable
 
-  belongs_to :account, inverse_of: :favourites
-  belongs_to :status,  inverse_of: :favourites, counter_cache: true
+  belongs_to :account, inverse_of: :favourites, required: true
+  belongs_to :status,  inverse_of: :favourites, counter_cache: true, required: true
 
   has_one :notification, as: :activity, dependent: :destroy
 
   validates :status_id, uniqueness: { scope: :account_id }
 
   before_validation do
-    self.status = status.reblog if status.reblog?
+    self.status = status.reblog if status&.reblog?
   end
 end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 818190214..85e82e12b 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -33,6 +33,7 @@ class MediaAttachment < ApplicationRecord
 
   validates :account, presence: true
 
+  scope :attached, -> { where.not(status_id: nil) }
   scope :local, -> { where(remote_url: '') }
   default_scope { order('id asc') }
 
diff --git a/app/models/mute.rb b/app/models/mute.rb
index 875d030e9..0cf17be4f 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -3,10 +3,9 @@
 class Mute < ApplicationRecord
   include Paginable
 
-  belongs_to :account
-  belongs_to :target_account, class_name: 'Account'
+  belongs_to :account, required: true
+  belongs_to :target_account, class_name: 'Account', required: true
 
-  validates :account, :target_account, presence: true
   validates :account_id, uniqueness: { scope: :target_account_id }
 
   after_create :remove_blocking_cache
diff --git a/app/models/status.rb b/app/models/status.rb
index 5393acfcc..c05a3386f 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -10,7 +10,7 @@ class Status < ApplicationRecord
 
   belongs_to :application, class_name: 'Doorkeeper::Application'
 
-  belongs_to :account, inverse_of: :statuses, counter_cache: true
+  belongs_to :account, inverse_of: :statuses, counter_cache: true, required: true
   belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account'
 
   belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies
@@ -26,7 +26,6 @@ class Status < ApplicationRecord
   has_one :notification, as: :activity, dependent: :destroy
   has_one :preview_card, dependent: :destroy
 
-  validates :account, presence: true
   validates :uri, uniqueness: true, unless: 'local?'
   validates :text, presence: true, unless: 'reblog?'
   validates_with StatusLengthValidator
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index 497cabb09..63553e9fe 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -4,7 +4,7 @@ class Subscription < ApplicationRecord
   MIN_EXPIRATION = 3600 * 24 * 7
   MAX_EXPIRATION = 3600 * 24 * 30
 
-  belongs_to :account
+  belongs_to :account, required: true
 
   validates :callback_url, presence: true
   validates :callback_url, uniqueness: { scope: :account_id }
diff --git a/app/models/user.rb b/app/models/user.rb
index 27a38674e..a59d843d4 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -9,10 +9,9 @@ class User < ApplicationRecord
          otp_secret_encryption_key: ENV['OTP_SECRET'],
          otp_number_of_backup_codes: 10
 
-  belongs_to :account, inverse_of: :user
+  belongs_to :account, inverse_of: :user, required: true
   accepts_nested_attributes_for :account
 
-  validates :account, presence: true
   validates :locale, inclusion: I18n.available_locales.map(&:to_s), unless: 'locale.nil?'
   validates :email, email: true
 
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index a9cb85500..58a23d978 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -41,7 +41,7 @@ class AccountSearchService < BaseService
   end
 
   def query_username
-    @_query_username ||= split_query_string.first
+    @_query_username ||= split_query_string.first || ''
   end
 
   def query_domain
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index ffeee5fcf..00f7cbd00 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -6,7 +6,7 @@ class NotifyService < BaseService
     @activity     = activity
     @notification = Notification.new(account: @recipient, activity: @activity)
 
-    return if blocked? || recipient.user.nil?
+    return if recipient.user.nil? || blocked?
 
     create_notification
     send_email if email_enabled?
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 221aa42a3..00af28edd 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -19,6 +19,7 @@ class PostStatusService < BaseService
                                       sensitive: options[:sensitive],
                                       spoiler_text: options[:spoiler_text] || '',
                                       visibility: options[:visibility],
+                                      language: detect_language(text),
                                       application: options[:application])
 
     attach_media(status, media)
@@ -35,7 +36,7 @@ class PostStatusService < BaseService
   private
 
   def validate_media!(media_ids)
-    return if media_ids.nil? || !media_ids.is_a?(Enumerable)
+    return if media_ids.blank? || !media_ids.is_a?(Enumerable)
 
     raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4
 
@@ -51,6 +52,10 @@ class PostStatusService < BaseService
     media.update(status_id: status.id)
   end
 
+  def detect_language(text)
+    WhatLanguage.new(:all).language_iso(text) || 'en'
+  end
+
   def process_mentions_service
     @process_mentions_service ||= ProcessMentionsService.new
   end
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index fa0633b27..98d92f630 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -119,6 +119,7 @@ class ProcessFeedService < BaseService
         spoiler_text: content_warning(entry),
         created_at: published(entry),
         reply: thread?(entry),
+        language: content_language(entry),
         visibility: visibility_scope(entry)
       )
 
@@ -161,13 +162,7 @@ class ProcessFeedService < BaseService
       xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link|
         next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type']
 
-        url = Addressable::URI.parse(link['href'])
-
-        mentioned_account = if TagManager.instance.web_domain?(url.host)
-                              Account.find_local(url.path.gsub('/users/', ''))
-                            else
-                              Account.find_by(url: link['href']) || FetchRemoteAccountService.new.call(link['href'])
-                            end
+        mentioned_account = account_from_href(link['href'])
 
         next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id)
 
@@ -178,6 +173,16 @@ class ProcessFeedService < BaseService
       end
     end
 
+    def account_from_href(href)
+      url = Addressable::URI.parse(href)
+
+      if TagManager.instance.web_domain?(url.host)
+        Account.find_local(url.path.gsub('/users/', ''))
+      else
+        Account.find_by(uri: href) || Account.find_by(url: href) || FetchRemoteAccountService.new.call(href)
+      end
+    end
+
     def hashtags_from_xml(parent, xml)
       tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?)
       ProcessHashtagsService.new.call(parent, tags)
@@ -234,6 +239,10 @@ class ProcessFeedService < BaseService
       xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content
     end
 
+    def content_language(xml = @xml)
+      xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en'
+    end
+
     def content_warning(xml = @xml)
       xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || ''
     end
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 418c98247..84b29912c 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -1,11 +1,11 @@
 - content_for :page_title do
-  #{Rails.configuration.x.local_domain}
+  = site_hostname
 
 .wrapper.thicc
   .sidebar-layout
     .main
       .panel
-        %h2= Rails.configuration.x.local_domain
+        %h2= site_hostname
 
         - unless @instance_presenter.site_description.blank?
           %p= @instance_presenter.site_description.html_safe
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 39686b531..49ad03557 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -2,13 +2,13 @@
   = javascript_include_tag 'application_public', integrity: true
 
 - content_for :page_title do
-  = Rails.configuration.x.local_domain
+  = site_hostname
 
 - content_for :header_tags do
   %meta{ property: 'og:site_name', content: site_title }/
   %meta{ property: 'og:type', content: 'website' }/
-  %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/
-  %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.blank? ? t('about.about_mastodon') : @instance_presenter.site_description) }/
+  %meta{ property: 'og:title', content: site_hostname }/
+  %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon')) }/
   %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/
   %meta{ property: 'og:image:width', content: '400' }/
   %meta{ property: 'og:image:height', content: '400' }/
@@ -72,7 +72,7 @@
           = t 'about.features.api'
 
   - unless @instance_presenter.site_description.blank?
-    %h3= t('about.description_headline', domain: Rails.configuration.x.local_domain)
+    %h3= t('about.description_headline', domain: site_hostname)
     %p= @instance_presenter.site_description.html_safe
 
   .actions
diff --git a/app/views/about/terms.en.html.haml b/app/views/about/terms.en.html.haml
index e1766ca16..7e0fb94c2 100644
--- a/app/views/about/terms.en.html.haml
+++ b/app/views/about/terms.en.html.haml
@@ -1,5 +1,5 @@
 - content_for :page_title do
-  #{Rails.configuration.x.local_domain} Terms of Service and Privacy Policy
+  #{site_hostname} Terms of Service and Privacy Policy
 
 .wrapper
   %h2 Privacy Policy
diff --git a/app/views/about/terms.no.html.haml b/app/views/about/terms.no.html.haml
index 32ec57ed1..46f62950d 100644
--- a/app/views/about/terms.no.html.haml
+++ b/app/views/about/terms.no.html.haml
@@ -1,5 +1,5 @@
 - content_for :page_title do
-  #{Rails.configuration.x.local_domain} Personvern og villkår for bruk av nettstedet
+  #{site_hostname} Personvern og villkår for bruk av nettstedet
 
 .wrapper
   %h2 Personvernserklæring
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 9a70fd16f..b01f3c4e3 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -7,7 +7,7 @@
 
   %meta{ property: 'og:site_name', content: site_title }/
   %meta{ property: 'og:type', content: 'profile' }/
-  %meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
+  %meta{ property: 'og:title', content: "#{@account.username} on #{site_hostname}" }/
   %meta{ property: 'og:description', content: @account.note }/
   %meta{ property: 'og:image', content: full_asset_url(@account.avatar.url(:original)) }/
   %meta{ property: 'og:image:width', content: '120' }/
@@ -18,7 +18,7 @@
   = render partial: 'shared/landing_strip', locals: { account: @account }
 
 .h-feed
-  %data.p-name{ value: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
+  %data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
 
   = render 'header', account: @account
 
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 07dcc7f46..7609868e6 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -61,8 +61,9 @@
         = surround '(', ')' do
           = number_to_human_size @account.media_attachments.sum('file_file_size')
 
-%div{ style: 'float: right' }
-  = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
+- if @account.local?
+  %div{ style: 'float: right' }
+    = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button'
 
 %div{ style: 'float: left' }
   - if @account.silenced?
diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml
index da9a07bbc..bdef4294e 100644
--- a/app/views/admin/domain_blocks/index.html.haml
+++ b/app/views/admin/domain_blocks/index.html.haml
@@ -1,24 +1,24 @@
 - content_for :page_title do
-  = t('admin.domain_block.title')
+  = t('admin.domain_blocks.title')
 
 %table.table
   %thead
     %tr
-      %th= t('admin.domain_block.domain')
-      %th= t('admin.domain_block.severity')
-      %th= t('admin.domain_block.reject_media')
+      %th= t('admin.domain_blocks.domain')
+      %th= t('admin.domain_blocks.severity')
+      %th= t('admin.domain_blocks.reject_media')
       %th
   %tbody
     - @blocks.each do |block|
       %tr
         %td
           %samp= block.domain
-        %td= t("admin.domain_block.severities.#{block.severity}")
+        %td= t("admin.domain_blocks.severities.#{block.severity}")
         %td
           - if block.reject_media? || block.suspend?
             %i.fa.fa-check
         %td
-          = table_link_to 'undo', t('admin.domain_block.undo'), admin_domain_block_path(block)
+          = table_link_to 'undo', t('admin.domain_blocks.undo'), admin_domain_block_path(block)
 
 = paginate @blocks
-= link_to t('admin.domain_block.add_new'), new_admin_domain_block_path, class: 'button'
+= link_to t('admin.domain_blocks.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 603faeb55..38fa90169 100644
--- a/app/views/admin/domain_blocks/new.html.haml
+++ b/app/views/admin/domain_blocks/new.html.haml
@@ -1,17 +1,17 @@
 - content_for :page_title do
-  = t('admin.domain_block.new.title')
+  = t('.title')
 
 = simple_form_for @domain_block, url: admin_domain_blocks_path do |f|
   = render 'shared/error_messages', object: @domain_block
 
-  %p.hint= t('admin.domain_block.new.hint')
+  %p.hint= t('.hint')
 
-  = 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}") }
+  = f.input :domain, placeholder: t('admin.domain_blocks.domain')
+  = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t(".severity.#{type}") }
 
-  %p.hint= t('admin.domain_block.new.severity.desc_html')
+  %p.hint= t('.severity.desc_html')
 
-  = f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_block.reject_media'), hint: I18n.t('admin.domain_block.reject_media_hint')
+  = f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
 
   .actions
-    = f.button :button, t('admin.domain_block.new.create'), type: :submit
+    = f.button :button, t('.create'), type: :submit
diff --git a/app/views/admin/domain_blocks/show.html.haml b/app/views/admin/domain_blocks/show.html.haml
index bf9011c52..70dfef9b2 100644
--- a/app/views/admin/domain_blocks/show.html.haml
+++ b/app/views/admin/domain_blocks/show.html.haml
@@ -1,9 +1,15 @@
 - content_for :page_title do
-  = t('admin.domain_block.show.title', domain: @domain_block.domain)
+  = t('admin.domain_blocks.show.title', domain: @domain_block.domain)
 
 = simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f|
 
-  = f.input :retroactive, as: :boolean, wrapper: :with_label, label: I18n.t("admin.domain_block.show.retroactive.#{@domain_block.severity}"), hint: I18n.t('admin.domain_block.show.affected_accounts', count: Account.where(domain: @domain_block.domain).count)
+  = f.input :retroactive,
+    as: :boolean,
+    wrapper: :with_label,
+    label: t(".retroactive.#{@domain_block.severity}"),
+    hint: t(:affected_accounts,
+      scope: [:admin, :domain_blocks, :show],
+      count: @domain_block.accounts_count)
 
   .actions
-    = f.button :button, t('admin.domain_block.show.undo'), type: :submit
+    = f.button :button, t('.undo'), type: :submit
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 5391d99a8..aa144170d 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -12,7 +12,7 @@
 %p
   %strong= t('admin.reports.comment.label')
   \:
-  = @report.comment.presence || t('reports.comment.none')
+  = @report.comment.presence || t('admin.reports.comment.none')
 
 - unless @report.statuses.empty?
   %hr/
diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl
index 311c02dad..11dcec538 100644
--- a/app/views/api/oembed/show.json.rabl
+++ b/app/views/api/oembed/show.json.rabl
@@ -6,7 +6,7 @@ node(:version) { '1.0' }
 node(:title, &:title)
 node(:author_name) { |entry| entry.account.display_name.blank? ? entry.account.username : entry.account.display_name }
 node(:author_url) { |entry| account_url(entry.account) }
-node(:provider_name) { Rails.configuration.x.local_domain }
+node(:provider_name) { site_hostname }
 node(:provider_url) { root_url }
 node(:cache_age) { 86_400 }
 node(:html) { |entry| "<iframe src=\"#{embed_account_stream_entry_url(entry.account, entry)}\" style=\"width: 100%; overflow: hidden\" frameborder=\"0\" width=\"#{@width}\" height=\"#{@height}\" scrolling=\"no\"></iframe>" }
diff --git a/app/views/api/v1/instances/show.rabl b/app/views/api/v1/instances/show.rabl
index 88eb08a9e..f5598fde3 100644
--- a/app/views/api/v1/instances/show.rabl
+++ b/app/views/api/v1/instances/show.rabl
@@ -1,6 +1,6 @@
 object false
 
-node(:uri)         { Rails.configuration.x.local_domain }
+node(:uri)         { site_hostname }
 node(:title)       { Setting.site_title }
 node(:description) { Setting.site_description }
 node(:email)       { Setting.site_contact_email }
diff --git a/app/views/api/v1/statuses/_media.rabl b/app/views/api/v1/statuses/_media.rabl
index 80d80ea05..2f56c6d07 100644
--- a/app/views/api/v1/statuses/_media.rabl
+++ b/app/views/api/v1/statuses/_media.rabl
@@ -1,5 +1,5 @@
 attributes :id, :remote_url, :type
 
-node(:url)         { |media| media.file.blank? ? media.remote_url : full_asset_url(media.file.url(:original)) }
-node(:preview_url) { |media| media.file.blank? ? media.remote_url : full_asset_url(media.file.url(:small)) }
+node(:url)         { |media| full_asset_url(media.file.url(:original)) }
+node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
 node(:text_url)    { |media| media.local? ? medium_url(media) : nil }
diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl
index 9f94e6141..104049387 100644
--- a/app/views/home/initial_state.json.rabl
+++ b/app/views/home/initial_state.json.rabl
@@ -5,7 +5,9 @@ node(:meta) do
     streaming_api_base_url: @streaming_api_base_url,
     access_token: @token,
     locale: I18n.locale,
+    domain: site_hostname,
     me: current_account.id,
+    admin: @admin.try(:id),
     boost_modal: current_account.user.setting_boost_modal,
   }
 end
@@ -18,9 +20,10 @@ node(:compose) do
 end
 
 node(:accounts) do
-  {
-    current_account.id => partial('api/v1/accounts/show', object: current_account),
-  }
+  store = {}
+  store[current_account.id] = partial('api/v1/accounts/show', object: current_account)
+  store[@admin.id] = partial('api/v1/accounts/show', object: @admin) unless @admin.nil?
+  store
 end
 
 node(:settings) { @web_settings }
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index a27c3de95..688deaebd 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -6,6 +6,7 @@
     %meta{'http-equiv' => 'X-UA-Compatible', :content => 'IE=edge'}/
 
     %link{:rel => "apple-touch-icon", :sizes => "180x180", :href => "/apple-touch-icon.png"}/
+    %link{:rel => "mask-icon", :href => "/mask-icon.svg", :color => "#2B90D9"}/
     %link{:rel => "manifest", :href => "/manifest.json"}/
     %meta{:name => "msapplication-config", :content => "/browserconfig.xml"}/
     %meta{:name => "theme-color", :content => "#282c37"}/
@@ -13,7 +14,7 @@
 
     %title<
       - if content_for?(:page_title)
-        = yield(:page_title)
+        = yield(:page_title).strip
         = ' - '
       = site_title
 
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
index 21bf444c3..cdb284de8 100644
--- a/app/views/layouts/mailer.text.erb
+++ b/app/views/layouts/mailer.text.erb
@@ -1,5 +1,5 @@
 <%= yield %>
 ---
 
-<%= t('application_mailer.signature', instance: Rails.configuration.x.local_domain) %>
+<%= t('application_mailer.signature', instance: site_hostname) %>
 <%= t('application_mailer.settings', link: settings_preferences_url) %>
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index fdde0a681..556102f53 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -4,7 +4,7 @@
 - content_for :content do
   .container= yield
   .footer
-    %span.domain= link_to Rails.configuration.x.local_domain, root_path
+    %span.domain= link_to site_hostname, root_path
     %span.powered-by
       = t('generic.powered_by', link: link_to('Mastodon', 'https://github.com/tootsuite/mastodon')).html_safe
 
diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml
index 8502913dc..991dd4e94 100644
--- a/app/views/settings/imports/show.html.haml
+++ b/app/views/settings/imports/show.html.haml
@@ -4,7 +4,7 @@
 %p.hint= t('imports.preface')
 
 = simple_form_for @import, url: settings_import_path do |f|
-  = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }
+  = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
   = f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data')
 
   .actions
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index e819429b6..4f4326763 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -7,7 +7,7 @@
   .fields-group
     = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }
 
-    = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| I18n.t("statuses.visibilities.#{visibility}") }, required: false
+    = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| I18n.t("statuses.visibilities.#{visibility}") }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 
   .fields-group
     = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml
index 3536c5ca8..02e694418 100644
--- a/app/views/shared/_landing_strip.html.haml
+++ b/app/views/shared/_landing_strip.html.haml
@@ -1,5 +1,5 @@
 .landing-strip
   = t('landing_strip_html',
     name: content_tag(:span, display_name(account), class: :emojify),
-    domain: Rails.configuration.x.local_domain,
+    domain: site_hostname,
     sign_up_path: new_user_registration_path)
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index dea5e9d40..31efa26c4 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -4,7 +4,7 @@
 
   %meta{ property: 'og:site_name', content: site_title }/
   %meta{ property: 'og:type', content: 'article' }/
-  %meta{ property: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
+  %meta{ property: 'og:title', content: "#{@account.username} on #{site_hostname}" }/
 
   = render 'stream_entries/og_description', activity: @stream_entry.activity
   = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account
diff --git a/app/views/user_mailer/confirmation_instructions.ja.html.erb b/app/views/user_mailer/confirmation_instructions.ja.html.erb
index bbb44b2cc..1232f94b4 100644
--- a/app/views/user_mailer/confirmation_instructions.ja.html.erb
+++ b/app/views/user_mailer/confirmation_instructions.ja.html.erb
@@ -1,5 +1,11 @@
 <p>ようこそ<%= @resource.email %>さん</p>
 
-<p>以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください</p>
+<p><%= @instance %>にアカウントが作成されました。</p>
+
+<p>以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください。</p>
 
 <p><%= link_to 'メールアドレスの確認', confirmation_url(@resource, confirmation_token: @token) %></p>
+
+<p>また、インスタンスの<%= link_to '利用規約', terms_url %>についてもご確認ください。</p>
+
+<p><%= @instance %> チーム</p>
diff --git a/app/views/user_mailer/confirmation_instructions.ja.text.erb b/app/views/user_mailer/confirmation_instructions.ja.text.erb
index ad8abee2d..99868ba8a 100644
--- a/app/views/user_mailer/confirmation_instructions.ja.text.erb
+++ b/app/views/user_mailer/confirmation_instructions.ja.text.erb
@@ -1,5 +1,11 @@
 ようこそ<%= @resource.email %>さん
 
-以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください
+<%= @instance %>にアカウントが作成されました。
+
+以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください。
 
 <%= confirmation_url(@resource, confirmation_token: @token) %>
+
+また、インスタンスの<%= link_to '利用規約', terms_url %>についてもご確認ください。
+
+<%= @instance %> チーム
diff --git a/app/views/user_mailer/reset_password_instructions.ja.html.erb b/app/views/user_mailer/reset_password_instructions.ja.html.erb
index 156758ef5..d0d7203f4 100644
--- a/app/views/user_mailer/reset_password_instructions.ja.html.erb
+++ b/app/views/user_mailer/reset_password_instructions.ja.html.erb
@@ -4,5 +4,5 @@
 
 <p><%= link_to 'パスワードを変更', edit_password_url(@resource, reset_password_token: @token) %></p>
 
-<p>このメールに見に覚えのない場合は無視してください。</p>
+<p>このメールに身に覚えのない場合は無視してください。</p>
 <p>上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。</p>
diff --git a/app/views/user_mailer/reset_password_instructions.ja.text.erb b/app/views/user_mailer/reset_password_instructions.ja.text.erb
index 5fb0eba04..9ed607b58 100644
--- a/app/views/user_mailer/reset_password_instructions.ja.text.erb
+++ b/app/views/user_mailer/reset_password_instructions.ja.text.erb
@@ -4,5 +4,5 @@ Mastodonアカウントのパスワードの変更がリクエストされまし
 
 <%= edit_password_url(@resource, reset_password_token: @token) %>
 
-このメールに見に覚えのない場合は無視してください。
+このメールに身に覚えのない場合は無視してください。
 上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。